diff --git a/.github/workflows/Formatter.yml b/.github/workflows/Formatter.yml new file mode 100644 index 00000000..0155d9b6 --- /dev/null +++ b/.github/workflows/Formatter.yml @@ -0,0 +1,14 @@ +name: Format suggestions +on: + pull_request: + # this argument is not required if you don't use the `suggestion-label` input + types: [ opened, reopened, synchronize, labeled, unlabeled ] +jobs: + code-style: + runs-on: ubuntu-latest + steps: + - uses: julia-actions/julia-format@v3 + with: + version: '1' # Set `version` to '1.0.54' if you need to use JuliaFormatter.jl v1.0.54 (default: '1') + suggestion-label: 'format-suggest' # leave this unset or empty to show suggestions for all PRs + diff --git a/Project.toml b/Project.toml index 870ca401..b4883650 100644 --- a/Project.toml +++ b/Project.toml @@ -1,33 +1,33 @@ -name = "FMIFlux" -uuid = "fabad875-0d53-4e47-9446-963b74cae21f" -version = "0.13.0" - -[deps] -Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" -DifferentiableEigen = "73a20539-4e65-4dcb-a56d-dc20f210a01b" -FMIImport = "9fcbc62e-52a0-44e9-a616-1359a0008194" -FMISensitivity = "3e748fe5-cd7f-4615-8419-3159287187d2" -Flux = "587475ba-b771-5e3f-ad9e-33799f191a9c" -Optim = "429524aa-4258-5aef-a3af-852621145aeb" -OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" -Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" -Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" -ThreadPools = "b189fb0b-2eb5-4ed4-bc0c-d34c51242431" - -[weakdeps] -JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" - -[extensions] -JLD2Ext = ["JLD2"] - -[compat] -Colors = "0.12" -DifferentiableEigen = "0.2.0" -FMIImport = "1.0.0" -FMISensitivity = "0.2.0" -Flux = "0.9 - 0.14" -Optim = "1.6" -OrdinaryDiffEq = "6.0" -Statistics = "1" -ThreadPools = "2.1" -julia = "1.6" \ No newline at end of file +name = "FMIFlux" +uuid = "fabad875-0d53-4e47-9446-963b74cae21f" +version = "0.13.0" + +[deps] +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +DifferentiableEigen = "73a20539-4e65-4dcb-a56d-dc20f210a01b" +FMIImport = "9fcbc62e-52a0-44e9-a616-1359a0008194" +FMISensitivity = "3e748fe5-cd7f-4615-8419-3159287187d2" +Flux = "587475ba-b771-5e3f-ad9e-33799f191a9c" +Optim = "429524aa-4258-5aef-a3af-852621145aeb" +OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +ThreadPools = "b189fb0b-2eb5-4ed4-bc0c-d34c51242431" + +[weakdeps] +JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" + +[extensions] +JLD2Ext = ["JLD2"] + +[compat] +Colors = "0.12" +DifferentiableEigen = "0.2.0" +FMIImport = "1.0.6" +FMISensitivity = "0.2.0" +Flux = "0.9 - 0.14" +Optim = "1.6" +OrdinaryDiffEq = "6.0" +Statistics = "1" +ThreadPools = "2.1" +julia = "1.6" diff --git a/README.md b/README.md index bd44bd2e..3dd541dd 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ You can evaluate FMUs inside of your loss function. [![Run PkgEval](https://github.com/ThummeTo/FMIFlux.jl/actions/workflows/Eval.yml/badge.svg)](https://github.com/ThummeTo/FMIFlux.jl/actions/workflows/Eval.yml) [![Coverage](https://codecov.io/gh/ThummeTo/FMIFlux.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/ThummeTo/FMIFlux.jl) [![ColPrac: Contributor's Guide on Collaborative Practices for Community Packages](https://img.shields.io/badge/ColPrac-Contributor's%20Guide-blueviolet)](https://github.com/SciML/ColPrac) -[![FMIFlux Downloads](https://shields.io/endpoint?url=https://pkgs.genieframework.com/api/v1/badge/FMIFlux)](https://pkgs.genieframework.com?packages=FMIFlux) +[![SciML Code Style](https://img.shields.io/static/v1?label=code%20style&message=SciML&color=9558b2&labelColor=389826)](https://github.com/SciML/SciMLStyle) ## How can I use FMIFlux.jl? diff --git a/docs/make.jl b/docs/make.jl index 512a306f..8d49af6a 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -3,36 +3,38 @@ # Licensed under the MIT license. See LICENSE file in the project root for details. # -import Pkg; Pkg.develop(path=joinpath(@__DIR__,"../../FMIFlux.jl")) +import Pkg; +Pkg.develop(path = joinpath(@__DIR__, "../../FMIFlux.jl")); using Documenter, FMIFlux using Documenter: GitHubActions -makedocs(sitename="FMIFlux.jl", - format = Documenter.HTML( - collapselevel = 1, - sidebar_sitename = false, - edit_link = nothing, - size_threshold_ignore = [joinpath("examples","juliacon_2023.md")] - ), - warnonly=true, - pages= Any[ - "Introduction" => "index.md" - "Examples" => [ - "Overview" => "examples/overview.md" - "Simple CS-NeuralFMU" => "examples/simple_hybrid_CS.md" - "Simple ME-NeuralFMU" => "examples/simple_hybrid_ME.md" - "Growing Horizon ME-NeuralFMU" => "examples/growing_horizon_ME.md" - "JuliaCon 2023" => "examples/juliacon_2023.md" - "MDPI 2022" => "examples/mdpi_2022.md" - "Modelica Conference 2021" => "examples/modelica_conference_2021.md" - "Pluto Workshops" => "examples/workshops.md" - ] - "FAQ" => "faq.md" - "Library Functions" => "library.md" - "Related Publication" => "related.md" - "Contents" => "contents.md" - ] - ) +makedocs( + sitename = "FMIFlux.jl", + format = Documenter.HTML( + collapselevel = 1, + sidebar_sitename = false, + edit_link = nothing, + size_threshold_ignore = [joinpath("examples", "juliacon_2023.md")], + ), + warnonly = true, + pages = Any[ + "Introduction" => "index.md" + "Examples" => [ + "Overview" => "examples/overview.md" + "Simple CS-NeuralFMU" => "examples/simple_hybrid_CS.md" + "Simple ME-NeuralFMU" => "examples/simple_hybrid_ME.md" + "Growing Horizon ME-NeuralFMU" => "examples/growing_horizon_ME.md" + "JuliaCon 2023" => "examples/juliacon_2023.md" + "MDPI 2022" => "examples/mdpi_2022.md" + "Modelica Conference 2021" => "examples/modelica_conference_2021.md" + "Pluto Workshops" => "examples/workshops.md" + ] + "FAQ" => "faq.md" + "Library Functions" => "library.md" + "Related Publication" => "related.md" + "Contents" => "contents.md" + ], +) function deployConfig() github_repository = get(ENV, "GITHUB_REPOSITORY", "") @@ -44,4 +46,8 @@ function deployConfig() return GitHubActions(github_repository, github_event_name, github_ref) end -deploydocs(repo = "github.com/ThummeTo/FMIFlux.jl.git", devbranch = "main", deploy_config = deployConfig()) +deploydocs( + repo = "github.com/ThummeTo/FMIFlux.jl.git", + devbranch = "main", + deploy_config = deployConfig(), +) diff --git a/examples/jupyter-src/.gitignore b/examples/jupyter-src/.gitignore index ae9c2156..9448d154 100644 --- a/examples/jupyter-src/.gitignore +++ b/examples/jupyter-src/.gitignore @@ -1,2 +1,3 @@ params/ -*.png \ No newline at end of file +*.png +*.gif \ No newline at end of file diff --git a/examples/jupyter-src/juliacon_2023.ipynb b/examples/jupyter-src/juliacon_2023.ipynb index fc7ca792..5269cb0c 100644 --- a/examples/jupyter-src/juliacon_2023.ipynb +++ b/examples/jupyter-src/juliacon_2023.ipynb @@ -25,7 +25,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -65,7 +65,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -74,7 +74,7 @@ "using FMIFlux # for NeuralFMUs\n", "using FMIZoo # a collection of demo models, including the VLDM\n", "using FMIFlux.Flux # Machine Learning in Julia\n", - "using DifferentialEquations # for picking a NeuralFMU solver\n", + "using DifferentialEquations: Tsit5 # for picking a NeuralFMU solver\n", "\n", "import JLD2 # data format for saving/loading parameters\n", "\n", @@ -91,7 +91,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -108,7 +108,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -372,7 +372,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -408,7 +408,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": {}, "outputs": [], "source": [ @@ -489,7 +489,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "metadata": {}, "outputs": [], "source": [ @@ -552,7 +552,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": {}, "outputs": [], "source": [ @@ -737,7 +737,7 @@ " params = FMIFlux.params(neuralFMU)\n", "\n", " # initialize the scheduler, keywords are passed to the NeuralFMU\n", - " initialize!(scheduler; parameters=data.params, p=params[1], showProgress=showProgress)\n", + " FMIFlux.initialize!(scheduler; parameters=data.params, p=params[1], showProgress=showProgress)\n", " \n", " # initialize Adam optimizer with our hyperparameters\n", " optim = Adam(ETA, (BETA1, BETA2))\n", @@ -747,16 +747,16 @@ " neuralFMU, # the neural FMU including the parameters to train\n", " Iterators.repeated((), steps), # an iterator repeating `steps` times\n", " optim; # the optimizer to train\n", - " gradient=:ForwardDiff, # currently, only ForwardDiff leads to good results for multi-event systems\n", - " chunk_size=32, # ForwardDiff chunk_size (=number of parameter estimations per run)\n", - " cb=() -> update!(scheduler), # update the scheduler after every step \n", + " gradient=:ReverseDiff, # ForwardDiff leads to good results for multi-event systems\n", + " chunk_size=32, # ForwardDiff chunk_size (=number of parameter estimations per run) - only if ForwardDiff is used\n", + " cb=() -> FMIFlux.update!(scheduler), # update the scheduler after every step \n", " proceed_on_assert=true) # proceed, even if assertions are thrown, with the next step\n", " \n", " # the default execution mode\n", " singleInstanceMode(fmu, false)\n", "\n", " # save our result parameters\n", - " fmiSaveParameters(neuralFMU, joinpath(@__DIR__, \"params\", \"$(ind).jld2\"))\n", + " FMIFlux.saveParameters(neuralFMU, joinpath(@__DIR__, \"params\", \"$(ind).jld2\"))\n", " \n", " # simulate the NeuralFMU on a validation trajectory\n", " resultNFMU = neuralFMU(x0, (data_validation.consumption_t[1], data_validation.consumption_t[end]); parameters=data_validation.params, showProgress=showProgress, maxiters=1e7, saveat=data_validation.consumption_t)\n", @@ -817,11 +817,11 @@ "neuralFMU = build_NFMU(fmu)\n", "\n", "# load parameters from hyperparameter optimization\n", - "loadParameters(neuralFMU, joinpath(@__DIR__, \"juliacon_2023.jld2\"))\n", + "FMIFlux.loadParameters(neuralFMU, joinpath(@__DIR__, \"juliacon_2023.jld2\"))\n", "\n", "# simulate and plot the NeuralFMU\n", - "resultNFMU = neuralFMU(x0, (tStart, tStop); parameters=data.params, showProgress=showProgress, saveat=tSave) \n", - "resultFMU = fmiSimulate(fmu, (tStart, tStop); parameters=data.params, showProgress=showProgress, saveat=tSave) \n", + "resultNFMU = neuralFMU(x0, (tStart, tStop); parameters=data.params, showProgress=showProgress, saveat=tSave) \n", + "resultFMU = simulate(fmu, (tStart, tStop); parameters=data.params, showProgress=showProgress, saveat=tSave) \n", "\n", "# plot the NeuralFMU, original FMU and data (cumulative consumption)\n", "fig = plot(resultNFMU; stateIndices=6:6, stateEvents=false, timeEvents=false, label=\"NeuralFMU\", ylabel=\"cumulative consumption [m/s]\")\n", @@ -929,7 +929,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "metadata": {}, "outputs": [], "source": [ @@ -957,7 +957,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 34, "metadata": {}, "outputs": [], "source": [ diff --git a/examples/jupyter-src/juliacon_2023_distributedhyperopt.jl b/examples/jupyter-src/juliacon_2023_distributedhyperopt.jl index 7c7053c3..4b56bc1b 100644 --- a/examples/jupyter-src/juliacon_2023_distributedhyperopt.jl +++ b/examples/jupyter-src/juliacon_2023_distributedhyperopt.jl @@ -13,34 +13,48 @@ using DistributedHyperOpt # add via `add "https://github.com/ThummeTo/Distribu # ENV["JULIA_DEBUG"] = "DistributedHyperOpt" nprocs() -workers = addprocs(5) +workers = addprocs(5) @everywhere include(joinpath(@__DIR__, "workshop_module.jl")) # creating paths for log files (logs), parameter sets (params) and hyperparameter plots (plots) -for dir ∈ ("logs", "params", "plots") +for dir ∈ ("logs", "params", "plots") path = joinpath(@__DIR__, dir) @info "Creating (if not already) path: $(path)" mkpath(path) -end - -beta1 = 1.0 .- exp10.(LinRange(-4,-1,4)) -beta2 = 1.0 .- exp10.(LinRange(-6,-1,6)) - -sampler = DistributedHyperOpt.Hyperband(;R=81, η=3, ressourceScale=1.0/81.0*NODE_Training.data.cumconsumption_t[end]) -optimization = DistributedHyperOpt.Optimization(NODE_Training.train!, - DistributedHyperOpt.Parameter("eta", (1e-5, 1e-2); type=:Log, samples=7, round_digits=5), - DistributedHyperOpt.Parameter("beta1", beta1), - DistributedHyperOpt.Parameter("beta2", beta2), - DistributedHyperOpt.Parameter("batchDur", (0.5, 20.0); samples=40, round_digits=1), - DistributedHyperOpt.Parameter("lastWeight", (0.1, 1.0); samples=10, round_digits=1), - DistributedHyperOpt.Parameter("schedulerID", [:Random, :Sequential, :LossAccumulation]), - DistributedHyperOpt.Parameter("loss", [:MSE, :MAE]) ) -DistributedHyperOpt.optimize(optimization; - sampler=sampler, - plot=true, - plot_ressources=true, - save_plot=joinpath(@__DIR__, "plots", "hyperoptim.png"), - redirect_worker_io_dir=joinpath(@__DIR__, "logs")) - -Plots.plot(optimization; size=(1024, 1024), ressources=true) +end + +beta1 = 1.0 .- exp10.(LinRange(-4, -1, 4)) +beta2 = 1.0 .- exp10.(LinRange(-6, -1, 6)) + +sampler = DistributedHyperOpt.Hyperband(; + R = 81, + η = 3, + ressourceScale = 1.0 / 81.0 * NODE_Training.data.cumconsumption_t[end], +) +optimization = DistributedHyperOpt.Optimization( + NODE_Training.train!, + DistributedHyperOpt.Parameter( + "eta", + (1e-5, 1e-2); + type = :Log, + samples = 7, + round_digits = 5, + ), + DistributedHyperOpt.Parameter("beta1", beta1), + DistributedHyperOpt.Parameter("beta2", beta2), + DistributedHyperOpt.Parameter("batchDur", (0.5, 20.0); samples = 40, round_digits = 1), + DistributedHyperOpt.Parameter("lastWeight", (0.1, 1.0); samples = 10, round_digits = 1), + DistributedHyperOpt.Parameter("schedulerID", [:Random, :Sequential, :LossAccumulation]), + DistributedHyperOpt.Parameter("loss", [:MSE, :MAE]), +) +DistributedHyperOpt.optimize( + optimization; + sampler = sampler, + plot = true, + plot_ressources = true, + save_plot = joinpath(@__DIR__, "plots", "hyperoptim.png"), + redirect_worker_io_dir = joinpath(@__DIR__, "logs"), +) + +Plots.plot(optimization; size = (1024, 1024), ressources = true) minimum, minimizer, ressource = DistributedHyperOpt.results(optimization) diff --git a/examples/jupyter-src/juliacon_2023_helpers.jl b/examples/jupyter-src/juliacon_2023_helpers.jl index 2f398f8a..471b6b88 100644 --- a/examples/jupyter-src/juliacon_2023_helpers.jl +++ b/examples/jupyter-src/juliacon_2023_helpers.jl @@ -11,76 +11,124 @@ import FMI: FMUSolution import FMIZoo: VLDM, VLDM_Data function singleInstanceMode(fmu::FMU2, mode::Bool) - if mode + if mode # switch to a more efficient execution configuration, allocate only a single FMU instance, see: # https://thummeto.github.io/FMI.jl/dev/features/#Execution-Configuration fmu.executionConfig = FMI.FMIImport.FMU_EXECUTION_CONFIGURATION_NOTHING - c, _ = FMIFlux.prepareSolveFMU(fmu, nothing, fmu.type; instantiate=true, setup=true, data.params, x0=x0) + c, _ = FMIFlux.prepareSolveFMU( + fmu, + nothing, + fmu.type; + instantiate = true, + setup = true, + parameters = data.params, + x0 = x0, + ) else - c = FMI.getCurrentComponent(fmu) + c = FMI.getCurrentInstance(fmu) # switch back to the default execution configuration, allocate a new FMU instance for every run, see: # https://thummeto.github.io/FMI.jl/dev/features/#Execution-Configuration fmu.executionConfig = FMI.FMIImport.FMU_EXECUTION_CONFIGURATION_NO_RESET - FMIFlux.finishSolveFMU(fmu, c; freeInstance=false, terminate=true) + FMIFlux.finishSolveFMU(fmu, c; freeInstance = false, terminate = true) end return nothing end function dataIndexForTime(t::Real) - return 1+round(Int, t/dt) + return 1 + round(Int, t / dt) end -function plotEnhancements(neuralFMU::NeuralFMU, fmu::FMU2, data::FMIZoo.VLDM_Data; reductionFactor::Int=10, mov_avg::Int=100, filename=nothing) +function plotEnhancements( + neuralFMU::NeuralFMU, + fmu::FMU2, + data::FMIZoo.VLDM_Data; + reductionFactor::Int = 10, + mov_avg::Int = 100, + filename = nothing, +) colorMin = 0 colorMax = 0 okregion = 0 - label="" + label = "" tStart = data.consumption_t[1] tStop = data.consumption_t[end] x0 = FMIZoo.getStateVector(data, tStart) - resultNFMU = neuralFMU(x0, (tStart, tStop); parameters=data.params, showProgress=false, recordValues=:derivatives, saveat=data.consumption_t) - resultFMU = fmiSimulate(fmu, (tStart, tStop); parameters=data.params, showProgress=false, recordValues=:derivatives, saveat=data.consumption_t) + resultNFMU = neuralFMU( + x0, + (tStart, tStop); + parameters = data.params, + showProgress = false, + recordValues = :derivatives, + saveat = data.consumption_t, + ) + resultFMU = simulate( + fmu, + (tStart, tStop); + parameters = data.params, + showProgress = false, + recordValues = :derivatives, + saveat = data.consumption_t, + ) # Finite differences for acceleration - dt = data.consumption_t[2]-data.consumption_t[1] + dt = data.consumption_t[2] - data.consumption_t[1] acceleration_val = (data.speed_val[2:end] - data.speed_val[1:end-1]) / dt acceleration_val = [acceleration_val..., 0.0] acceleration_dev = (data.speed_dev[2:end] - data.speed_dev[1:end-1]) / dt acceleration_dev = [acceleration_dev..., 0.0] - ANNInputs = fmiGetSolutionValue(resultNFMU, :derivatives) # collect([0.0, 0.0, 0.0, data.speed_val[i], acceleration_val[i], data.consumption_val[i]] for i in 1:length(data.consumption_t)) - ANNInputs = collect([ANNInputs[1][i], ANNInputs[2][i], ANNInputs[3][i], ANNInputs[4][i], ANNInputs[5][i], ANNInputs[6][i]] for i in 1:length(ANNInputs[1])) - - ANNOutputs = fmiGetSolutionDerivative(resultNFMU, 5:6; isIndex=true) - ANNOutputs = collect([ANNOutputs[1][i], ANNOutputs[2][i]] for i in 1:length(ANNOutputs[1])) + ANNInputs = getValue(resultNFMU, :derivatives) # collect([0.0, 0.0, 0.0, data.speed_val[i], acceleration_val[i], data.consumption_val[i]] for i in 1:length(data.consumption_t)) + ANNInputs = collect( + [ + ANNInputs[1][i], + ANNInputs[2][i], + ANNInputs[3][i], + ANNInputs[4][i], + ANNInputs[5][i], + ANNInputs[6][i], + ] for i = 1:length(ANNInputs[1]) + ) - FMUOutputs = fmiGetSolutionDerivative(resultFMU, 5:6; isIndex=true) - FMUOutputs = collect([FMUOutputs[1][i], FMUOutputs[2][i]] for i in 1:length(FMUOutputs[1])) + ANNOutputs = getStateDerivative(resultNFMU, 5:6; isIndex = true) + ANNOutputs = + collect([ANNOutputs[1][i], ANNOutputs[2][i]] for i = 1:length(ANNOutputs[1])) - ANN_consumption = collect(o[2] for o in ANNOutputs) + FMUOutputs = getStateDerivative(resultFMU, 5:6; isIndex = true) + FMUOutputs = + collect([FMUOutputs[1][i], FMUOutputs[2][i]] for i = 1:length(FMUOutputs[1])) + + ANN_consumption = collect(o[2] for o in ANNOutputs) ANN_error = ANN_consumption - data.consumption_val - ANN_error = collect(ANN_error[i] > 0.0 ? max(0.0, ANN_error[i]-data.consumption_dev[i]) : min(0.0, ANN_error[i]+data.consumption_dev[i]) for i in 1:length(data.consumption_t)) + ANN_error = collect( + ANN_error[i] > 0.0 ? max(0.0, ANN_error[i] - data.consumption_dev[i]) : + min(0.0, ANN_error[i] + data.consumption_dev[i]) for + i = 1:length(data.consumption_t) + ) - FMU_consumption = collect(o[2] for o in FMUOutputs) + FMU_consumption = collect(o[2] for o in FMUOutputs) FMU_error = FMU_consumption - data.consumption_val - FMU_error = collect(FMU_error[i] > 0.0 ? max(0.0, FMU_error[i]-data.consumption_dev[i]) : min(0.0, FMU_error[i]+data.consumption_dev[i]) for i in 1:length(data.consumption_t)) - - colorMin=-231.0 - colorMax=231.0 - + FMU_error = collect( + FMU_error[i] > 0.0 ? max(0.0, FMU_error[i] - data.consumption_dev[i]) : + min(0.0, FMU_error[i] + data.consumption_dev[i]) for + i = 1:length(data.consumption_t) + ) + + colorMin = -231.0 + colorMax = 231.0 + FMU_error = movavg(FMU_error, mov_avg) ANN_error = movavg(ANN_error, mov_avg) - + ANN_error = ANN_error .- FMU_error ANNInput_vel = collect(o[4] for o in ANNInputs) ANNInput_acc = collect(o[5] for o in ANNInputs) ANNInput_con = collect(o[6] for o in ANNInputs) - + _max = max(ANN_error...) _min = min(ANN_error...) - neutral = 0.5 + neutral = 0.5 if _max > colorMax @warn "max value ($(_max)) is larger than colorMax ($(colorMax)) - values will be cut" @@ -90,72 +138,150 @@ function plotEnhancements(neuralFMU::NeuralFMU, fmu::FMU2, data::FMIZoo.VLDM_Dat @warn "min value ($(_min)) is smaller than colorMin ($(colorMin)) - values will be cut" end - anim = @animate for ang in 0:5:360 - l = Plots.@layout [Plots.grid(3,1) r{0.85w}] - fig = Plots.plot(layout=l, size=(1600,800), left_margin = 10Plots.mm, right_margin = 10Plots.mm, bottom_margin = 10Plots.mm) - + anim = @animate for ang = 0:5:360 + l = Plots.@layout [Plots.grid(3, 1) r{0.85w}] + fig = Plots.plot( + layout = l, + size = (1600, 800), + left_margin = 10Plots.mm, + right_margin = 10Plots.mm, + bottom_margin = 10Plots.mm, + ) + colorgrad = cgrad([:green, :white, :red], [0.0, 0.5, 1.0]) # , scale = :log) - - scatter!(fig[1], ANNInput_vel[1:reductionFactor:end], ANNInput_acc[1:reductionFactor:end], - xlabel="velocity [m/s]", ylabel="acceleration [m/s^2]", - color=colorgrad, zcolor=ANN_error[1:reductionFactor:end], label=:none, colorbar=:none) # - - scatter!(fig[2], ANNInput_acc[1:reductionFactor:end], ANNInput_con[1:reductionFactor:end], - xlabel="acceleration [m/s^2]", ylabel="consumption [W]", - color=colorgrad, zcolor=ANN_error[1:reductionFactor:end], label=:none, colorbar=:none) # - - scatter!(fig[3], ANNInput_vel[1:reductionFactor:end], ANNInput_con[1:reductionFactor:end], - xlabel="velocity [m/s]", ylabel="consumption [W]", - color=colorgrad, zcolor=ANN_error[1:reductionFactor:end], label=:none, colorbar=:none) # - - scatter!(fig[4], ANNInput_vel[1:reductionFactor:end], ANNInput_acc[1:reductionFactor:end], ANNInput_con[1:reductionFactor:end], - xlabel="velocity [m/s]", ylabel="acceleration [m/s^2]", zlabel="consumption [W]", - color=colorgrad, zcolor=ANN_error[1:reductionFactor:end], markersize=8, label=:none, camera=(ang,20), colorbar_title=" \n\n\n\n" * L"ΔMAE" * " (smoothed)") - + + scatter!( + fig[1], + ANNInput_vel[1:reductionFactor:end], + ANNInput_acc[1:reductionFactor:end], + xlabel = "velocity [m/s]", + ylabel = "acceleration [m/s^2]", + color = colorgrad, + zcolor = ANN_error[1:reductionFactor:end], + label = :none, + colorbar = :none, + ) # + + scatter!( + fig[2], + ANNInput_acc[1:reductionFactor:end], + ANNInput_con[1:reductionFactor:end], + xlabel = "acceleration [m/s^2]", + ylabel = "consumption [W]", + color = colorgrad, + zcolor = ANN_error[1:reductionFactor:end], + label = :none, + colorbar = :none, + ) # + + scatter!( + fig[3], + ANNInput_vel[1:reductionFactor:end], + ANNInput_con[1:reductionFactor:end], + xlabel = "velocity [m/s]", + ylabel = "consumption [W]", + color = colorgrad, + zcolor = ANN_error[1:reductionFactor:end], + label = :none, + colorbar = :none, + ) # + + scatter!( + fig[4], + ANNInput_vel[1:reductionFactor:end], + ANNInput_acc[1:reductionFactor:end], + ANNInput_con[1:reductionFactor:end], + xlabel = "velocity [m/s]", + ylabel = "acceleration [m/s^2]", + zlabel = "consumption [W]", + color = colorgrad, + zcolor = ANN_error[1:reductionFactor:end], + markersize = 8, + label = :none, + camera = (ang, 20), + colorbar_title = " \n\n\n\n" * L"ΔMAE" * " (smoothed)", + ) + # draw invisible dummys to scale colorbar to fixed size - for i in 1:3 - scatter!(fig[i], [0.0,0.0], [0.0,0.0], - color=colorgrad, zcolor=[colorMin, colorMax], - markersize=0, label=:none) + for i = 1:3 + scatter!( + fig[i], + [0.0, 0.0], + [0.0, 0.0], + color = colorgrad, + zcolor = [colorMin, colorMax], + markersize = 0, + label = :none, + ) end - for i in 4:4 - scatter!(fig[i], [0.0,0.0], [0.0,0.0], [0.0,0.0], - color=colorgrad, zcolor=[colorMin, colorMax], - markersize=0, label=:none) + for i = 4:4 + scatter!( + fig[i], + [0.0, 0.0], + [0.0, 0.0], + [0.0, 0.0], + color = colorgrad, + zcolor = [colorMin, colorMax], + markersize = 0, + label = :none, + ) end end if !isnothing(filename) - return gif(anim, filename; fps=10) + return gif(anim, filename; fps = 10) else - return gif(anim; fps=10) + return gif(anim; fps = 10) end end -function plotCumulativeConsumption(solutionNFMU::FMUSolution, solutionFMU::FMUSolution, data::FMIZoo.VLDM_Data; range=(0.0,1.0), filename=nothing) +function plotCumulativeConsumption( + solutionNFMU::FMUSolution, + solutionFMU::FMUSolution, + data::FMIZoo.VLDM_Data; + range = (0.0, 1.0), + filename = nothing, +) len = length(data.consumption_t) - steps = (1+round(Int, range[1]*len)):(round(Int, range[end]*len)) + steps = (1+round(Int, range[1] * len)):(round(Int, range[end] * len)) - t = data.consumption_t - nfmu_val = fmiGetSolutionState(solutionNFMU, 6; isIndex=true) - fmu_val = fmiGetSolutionState(solutionFMU, 6; isIndex=true) + t = data.consumption_t + nfmu_val = getState(solutionNFMU, 6; isIndex = true) + fmu_val = getState(solutionFMU, 6; isIndex = true) data_val = data.cumconsumption_val data_dev = data.cumconsumption_dev - + mse_nfmu = FMIFlux.Losses.mse_dev(nfmu_val, data_val, data_dev) - mse_fmu = FMIFlux.Losses.mse_dev(fmu_val, data_val, data_dev) - + mse_fmu = FMIFlux.Losses.mse_dev(fmu_val, data_val, data_dev) + mae_nfmu = FMIFlux.Losses.mae_dev(nfmu_val, data_val, data_dev) - mae_fmu = FMIFlux.Losses.mae_dev(fmu_val, data_val, data_dev) - + mae_fmu = FMIFlux.Losses.mae_dev(fmu_val, data_val, data_dev) + max_nfmu = FMIFlux.Losses.max_dev(nfmu_val, data_val, data_dev) - max_fmu = FMIFlux.Losses.max_dev(fmu_val, data_val, data_dev) - - fig = plot(xlabel=L"t[s]", ylabel=L"x_6 [Ws]", dpi=600) - plot!(fig, t[steps], data_val[steps]; label="Data", ribbon=data_dev, fillalpha=0.3) - plot!(fig, t[steps], fmu_val[steps]; label="FMU [ MSE:$(roundToLength(mse_fmu,10)) | MAE:$(roundToLength(mae_fmu,10)) | MAX:$(roundToLength(max_fmu,10)) ]") - plot!(fig, t[steps], nfmu_val[steps]; label="NeuralFMU [ MSE:$(roundToLength(mse_nfmu,10)) | MAE:$(roundToLength(mae_nfmu,10)) | MAX:$(roundToLength(max_nfmu,10)) ]") + max_fmu = FMIFlux.Losses.max_dev(fmu_val, data_val, data_dev) + + fig = plot(xlabel = L"t[s]", ylabel = L"x_6 [Ws]", dpi = 600) + plot!( + fig, + t[steps], + data_val[steps]; + label = "Data", + ribbon = data_dev, + fillalpha = 0.3, + ) + plot!( + fig, + t[steps], + fmu_val[steps]; + label = "FMU [ MSE:$(roundToLength(mse_fmu,10)) | MAE:$(roundToLength(mae_fmu,10)) | MAX:$(roundToLength(max_fmu,10)) ]", + ) + plot!( + fig, + t[steps], + nfmu_val[steps]; + label = "NeuralFMU [ MSE:$(roundToLength(mse_nfmu,10)) | MAE:$(roundToLength(mae_nfmu,10)) | MAX:$(roundToLength(max_nfmu,10)) ]", + ) if !isnothing(filename) savefig(fig, filename) @@ -164,18 +290,31 @@ function plotCumulativeConsumption(solutionNFMU::FMUSolution, solutionFMU::FMUSo return fig end -function simPlotCumulativeConsumption(cycle::Symbol, filename=nothing; kwargs...) +function simPlotCumulativeConsumption(cycle::Symbol, filename = nothing; kwargs...) d = FMIZoo.VLDM(cycle) tStart = d.consumption_t[1] tStop = d.consumption_t[end] tSave = d.consumption_t - - resultNFMU = neuralFMU(x0, (tStart, tStop); parameters=d.params, showProgress=false, saveat=tSave, maxiters=1e7) - resultFMU = fmiSimulate(fmu, (tStart, tStop), parameters=d.params, showProgress=false, saveat=tSave) + + resultNFMU = neuralFMU( + x0, + (tStart, tStop); + parameters = d.params, + showProgress = false, + saveat = tSave, + maxiters = 1e7, + ) + resultFMU = simulate( + fmu, + (tStart, tStop), + parameters = d.params, + showProgress = false, + saveat = tSave, + ) fig = plotCumulativeConsumption(resultNFMU, resultFMU, d, kwargs...) if !isnothing(filename) savefig(fig, filename) end return fig -end \ No newline at end of file +end diff --git a/examples/pluto-src/SciMLUsingFMUs/SciMLUsingFMUs.jl b/examples/pluto-src/SciMLUsingFMUs/SciMLUsingFMUs.jl index d59c76e9..c9abffd7 100644 --- a/examples/pluto-src/SciMLUsingFMUs/SciMLUsingFMUs.jl +++ b/examples/pluto-src/SciMLUsingFMUs/SciMLUsingFMUs.jl @@ -7,7 +7,14 @@ using InteractiveUtils # This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error). macro bind(def, element) quote - local iv = try Base.loaded_modules[Base.PkgId(Base.UUID("6e696c72-6542-2067-7265-42206c756150"), "AbstractPlutoDingetjes")].Bonds.initial_value catch; b -> missing; end + local iv = try + Base.loaded_modules[Base.PkgId( + Base.UUID("6e696c72-6542-2067-7265-42206c756150"), + "AbstractPlutoDingetjes", + )].Bonds.initial_value + catch + b -> missing + end local el = $(esc(element)) global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : iv(el) el @@ -39,7 +46,7 @@ using ProgressLogging: @withprogress, @logprogress, @progressid, uuid4 using BenchmarkTools # default benchmarking library # ╔═╡ 45c4b9dd-0b04-43ae-a715-cd120c571424 -using Plots +using Plots # ╔═╡ 1470df0f-40e1-45d5-a4cc-519cc3b28fb8 md""" @@ -185,146 +192,199 @@ data_train = FMIZoo.RobotRR(:train) # ╔═╡ 33223393-bfb9-4e9a-8ea6-a3ab6e2f22aa begin - -# define the printing messages used at different places in this notebook -LIVE_RESULTS_MESSAGE = md"""ℹ️ Live plotting is disabled to safe performance. Checkbox `Plot Results`.""" -LIVE_TRAIN_MESSAGE = md"""ℹ️ Live training is disabled to safe performance. Checkbox `Start Training`.""" -BENCHMARK_MESSAGE = md"""ℹ️ Live benchmarks are disabled to safe performance. Checkbox `Start Benchmark`.""" -HIDDEN_CODE_MESSAGE = md"""> 👻 Hidden Code | You probably want to skip this code section on the first run.""" - -import FMI.FMIImport.FMICore: hasCurrentComponent, getCurrentComponent, FMU2Solution -import Random - -function fmiSingleInstanceMode!(fmu::FMU2, - mode::Bool, - params=FMIZoo.getParameter(data_train, 0.0; friction=false), - x0=FMIZoo.getState(data_train, 0.0)) - - fmu.executionConfig = deepcopy(FMU2_EXECUTION_CONFIGURATION_NO_RESET) - - # for this model, state events are generated but don't need to be handled, - # we can skip that to gain performance - fmu.executionConfig.handleStateEvents = false - - fmu.executionConfig.loggingOn = false - #fmu.executionConfig.externalCallbacks = true - - if mode - # switch to a more efficient execution configuration, allocate only a single FMU instance, see: - # https://thummeto.github.io/FMI.jl/dev/features/#Execution-Configuration - fmu.executionConfig.terminate = true - fmu.executionConfig.instantiate = false - fmu.executionConfig.reset = true - fmu.executionConfig.setup = true - fmu.executionConfig.freeInstance = false - c, _ = FMIFlux.prepareSolveFMU(fmu, nothing, fmu.type, true, # instantiate - false, # free - true, # terminate - true, # reset - true, # setup - params; x0=x0) - else - if !hasCurrentComponent(fmu) - return nothing - end - c = getCurrentComponent(fmu) - # switch back to the default execution configuration, allocate a new FMU instance for every run, see: - # https://thummeto.github.io/FMI.jl/dev/features/#Execution-Configuration - fmu.executionConfig.terminate = true - fmu.executionConfig.instantiate = true - fmu.executionConfig.reset = true - fmu.executionConfig.setup = true - fmu.executionConfig.freeInstance = true - FMIFlux.finishSolveFMU(fmu, c, true, # free - true) # terminate + + # define the printing messages used at different places in this notebook + LIVE_RESULTS_MESSAGE = + md"""ℹ️ Live plotting is disabled to safe performance. Checkbox `Plot Results`.""" + LIVE_TRAIN_MESSAGE = + md"""ℹ️ Live training is disabled to safe performance. Checkbox `Start Training`.""" + BENCHMARK_MESSAGE = + md"""ℹ️ Live benchmarks are disabled to safe performance. Checkbox `Start Benchmark`.""" + HIDDEN_CODE_MESSAGE = + md"""> 👻 Hidden Code | You probably want to skip this code section on the first run.""" + + import FMI.FMIImport.FMICore: hasCurrentComponent, getCurrentComponent, FMU2Solution + import Random + + function fmiSingleInstanceMode!( + fmu::FMU2, + mode::Bool, + params = FMIZoo.getParameter(data_train, 0.0; friction = false), + x0 = FMIZoo.getState(data_train, 0.0), + ) + + fmu.executionConfig = deepcopy(FMU2_EXECUTION_CONFIGURATION_NO_RESET) + + # for this model, state events are generated but don't need to be handled, + # we can skip that to gain performance + fmu.executionConfig.handleStateEvents = false + + fmu.executionConfig.loggingOn = false + #fmu.executionConfig.externalCallbacks = true + + if mode + # switch to a more efficient execution configuration, allocate only a single FMU instance, see: + # https://thummeto.github.io/FMI.jl/dev/features/#Execution-Configuration + fmu.executionConfig.terminate = true + fmu.executionConfig.instantiate = false + fmu.executionConfig.reset = true + fmu.executionConfig.setup = true + fmu.executionConfig.freeInstance = false + c, _ = FMIFlux.prepareSolveFMU( + fmu, + nothing, + fmu.type, + true, # instantiate + false, # free + true, # terminate + true, # reset + true, # setup + params; + x0 = x0, + ) + else + if !hasCurrentComponent(fmu) + return nothing + end + c = getCurrentComponent(fmu) + # switch back to the default execution configuration, allocate a new FMU instance for every run, see: + # https://thummeto.github.io/FMI.jl/dev/features/#Execution-Configuration + fmu.executionConfig.terminate = true + fmu.executionConfig.instantiate = true + fmu.executionConfig.reset = true + fmu.executionConfig.setup = true + fmu.executionConfig.freeInstance = true + FMIFlux.finishSolveFMU( + fmu, + c, + true, # free + true, + ) # terminate + end + return nothing end - return nothing -end - function prepareSolveFMU(fmu, parameters) - FMIFlux.prepareSolveFMU(fmu, nothing, fmu.type, - fmu.executionConfig.instantiate, - fmu.executionConfig.freeInstance, - fmu.executionConfig.terminate, - fmu.executionConfig.reset, - fmu.executionConfig.setup, - parameters) - end - -function dividePath(values) - last_value = values[1] - paths = [] - path = [] - for j in 1:length(values) - if values[j] == 1.0 - push!(path, j) - end - - if values[j] == 0.0 && last_value != 0.0 - push!(path, j) - push!(paths, path) - path = [] - end - - last_value = values[j] - end - if length(path) > 0 - push!(paths, path) - end - return paths -end + function prepareSolveFMU(fmu, parameters) + FMIFlux.prepareSolveFMU( + fmu, + nothing, + fmu.type, + fmu.executionConfig.instantiate, + fmu.executionConfig.freeInstance, + fmu.executionConfig.terminate, + fmu.executionConfig.reset, + fmu.executionConfig.setup, + parameters, + ) + end -function plotRobot(solution::FMU2Solution, t::Real) - x = solution.states(t) - a1 = x[5] - a2 = x[3] - - dt = 0.01 - i = 1+round(Integer, t/dt) - v = solution.values.saveval[i] - - l1 = 0.2 - l2 = 0.1 - - margin = 0.05 - scale = 1500 - fig = plot(; title="Time $(round(t; digits=1))s", - size=(round(Integer, (2*margin+l1+l2)*scale), round(Integer, (l1+l2+2*margin)*scale)), - xlims=(-margin, l1+l2+margin), ylims=(-l1-margin, l2+margin), legend=:bottomleft) - - p0 = [0.0, 0.0] - p1 = p0 .+ [cos(a1)*l1, sin(a1)*l1] - p2 = p1 .+ [cos(a1+a2)*l2, sin(a1+a2)*l2] - - f_norm = collect(v[3] for v in solution.values.saveval) - - paths = dividePath(f_norm) - drawing = collect(v[1:2] for v in solution.values.saveval) - for path in paths - plot!(fig, collect(v[1] for v in drawing[path]), collect(v[2] for v in drawing[path]), label=:none, color=:black, style=:dot) - end - - paths = dividePath(f_norm[1:i]) - drawing_is = collect(v[4:5] for v in solution.values.saveval)[1:i] - for path in paths - plot!(fig, collect(v[1] for v in drawing_is[path]), collect(v[2] for v in drawing_is[path]), label=:none, color=:green, width=2) - end - - plot!(fig, [p0[1], p1[1]], [p0[2], p1[2]], label=:none, width=3, color=:blue) - plot!(fig, [p1[1], p2[1]], [p1[2], p2[2]], label=:none, width=3, color=:blue) - - scatter!(fig, [p0[1]], [p0[2]], label="R1 | α1=$(round(a1; digits=3)) rad", color=:red) - scatter!(fig, [p1[1]], [p1[2]], label="R2 | α2=$(round(a2; digits=3)) rad", color=:purple) - - scatter!(fig, [v[1]], [v[2]], label="TCP | F=$(v[3]) N", color=:orange) -end + function dividePath(values) + last_value = values[1] + paths = [] + path = [] + for j = 1:length(values) + if values[j] == 1.0 + push!(path, j) + end -HIDDEN_CODE_MESSAGE + if values[j] == 0.0 && last_value != 0.0 + push!(path, j) + push!(paths, path) + path = [] + end + + last_value = values[j] + end + if length(path) > 0 + push!(paths, path) + end + return paths + end + + function plotRobot(solution::FMU2Solution, t::Real) + x = solution.states(t) + a1 = x[5] + a2 = x[3] + + dt = 0.01 + i = 1 + round(Integer, t / dt) + v = solution.values.saveval[i] + + l1 = 0.2 + l2 = 0.1 + + margin = 0.05 + scale = 1500 + fig = plot(; + title = "Time $(round(t; digits=1))s", + size = ( + round(Integer, (2 * margin + l1 + l2) * scale), + round(Integer, (l1 + l2 + 2 * margin) * scale), + ), + xlims = (-margin, l1 + l2 + margin), + ylims = (-l1 - margin, l2 + margin), + legend = :bottomleft, + ) + + p0 = [0.0, 0.0] + p1 = p0 .+ [cos(a1) * l1, sin(a1) * l1] + p2 = p1 .+ [cos(a1 + a2) * l2, sin(a1 + a2) * l2] + + f_norm = collect(v[3] for v in solution.values.saveval) + + paths = dividePath(f_norm) + drawing = collect(v[1:2] for v in solution.values.saveval) + for path in paths + plot!( + fig, + collect(v[1] for v in drawing[path]), + collect(v[2] for v in drawing[path]), + label = :none, + color = :black, + style = :dot, + ) + end + + paths = dividePath(f_norm[1:i]) + drawing_is = collect(v[4:5] for v in solution.values.saveval)[1:i] + for path in paths + plot!( + fig, + collect(v[1] for v in drawing_is[path]), + collect(v[2] for v in drawing_is[path]), + label = :none, + color = :green, + width = 2, + ) + end + + plot!(fig, [p0[1], p1[1]], [p0[2], p1[2]], label = :none, width = 3, color = :blue) + plot!(fig, [p1[1], p2[1]], [p1[2], p2[2]], label = :none, width = 3, color = :blue) + + scatter!( + fig, + [p0[1]], + [p0[2]], + label = "R1 | α1=$(round(a1; digits=3)) rad", + color = :red, + ) + scatter!( + fig, + [p1[1]], + [p1[2]], + label = "R2 | α2=$(round(a2; digits=3)) rad", + color = :purple, + ) + + scatter!(fig, [v[1]], [v[2]], label = "TCP | F=$(v[3]) N", color = :orange) + end + + HIDDEN_CODE_MESSAGE end # begin # ╔═╡ 92ad1a99-4ad9-4b69-b6f3-84aab49db54f -@bind t_train_plot Slider(0.0:0.1:data_train.t[end], default=data_train.t[1]) +@bind t_train_plot Slider(0.0:0.1:data_train.t[end], default = data_train.t[1]) # ╔═╡ f111e772-a340-4217-9b63-e7715f773b2c md""" @@ -367,7 +427,7 @@ The parameter array only contains the path to the training data file, the trajec """ # ╔═╡ 8f8f91cc-9a92-4182-8f18-098ae3e2c553 -parameters = FMIZoo.getParameter(data_train, tStart; friction=false) +parameters = FMIZoo.getParameter(data_train, tStart; friction = false) # ╔═╡ 8d93a1ed-28a9-4a77-9ac2-5564be3729a5 md""" @@ -380,7 +440,7 @@ To check whether the hybrid model was not only able to *imitate*, but *understan data_validation = FMIZoo.RobotRR(:validate) # ╔═╡ dbde2da3-e3dc-4b78-8f69-554018533d35 -@bind t_validate_plot Slider(0.0:0.1:data_validation.t[end], default=data_validation.t[1]) +@bind t_validate_plot Slider(0.0:0.1:data_validation.t[end], default = data_validation.t[1]) # ╔═╡ 6a8b98c9-e51a-4f1c-a3ea-cc452b9616b7 md""" @@ -408,35 +468,47 @@ The SCARA simulation model is called `RobotRR` for `Robot Rotational Rotational` # load the FMU named `RobotRR` from the FMIZoo # the FMU was exported from Dymola (version 2023x) # load the FMU in mode `model-exchange` (ME) -fmu = fmiLoad("RobotRR", "Dymola", "2023x"; type=:ME) +fmu = fmiLoad("RobotRR", "Dymola", "2023x"; type = :ME) # ╔═╡ c228eb10-d694-46aa-b952-01d824879287 begin -# We activate the single instance mode, so only one FMU instance gets allocated and is reused again an again. -fmiSingleInstanceMode!(fmu, true) - -using FMI.FMIImport: fmi2StringToValueReference - -# declare some model identifiers (inside of the FMU) -STATE_I1 = fmu.modelDescription.stateValueReferences[2] -STATE_I2 = fmu.modelDescription.stateValueReferences[1] -STATE_A1 = fmi2StringToValueReference(fmu, "rRPositionControl_Elasticity.rr1.rotational1.revolute1.phi") -STATE_A2 = fmi2StringToValueReference(fmu,"rRPositionControl_Elasticity.rr1.rotational2.revolute1.phi") -STATE_dA1 = fmi2StringToValueReference(fmu,"rRPositionControl_Elasticity.rr1.rotational1.revolute1.w") -STATE_dA2 = fmi2StringToValueReference(fmu,"rRPositionControl_Elasticity.rr1.rotational2.revolute1.w") - -DER_ddA2 = fmu.modelDescription.derivativeValueReferences[4] -DER_ddA1 = fmu.modelDescription.derivativeValueReferences[6] - -VAR_TCP_PX = fmi2StringToValueReference(fmu,"rRPositionControl_Elasticity.tCP.p_x") -VAR_TCP_PY = fmi2StringToValueReference(fmu,"rRPositionControl_Elasticity.tCP.p_y") -VAR_TCP_VX = fmi2StringToValueReference(fmu,"rRPositionControl_Elasticity.tCP.v_x") -VAR_TCP_VY = fmi2StringToValueReference(fmu,"rRPositionControl_Elasticity.tCP.v_y") -VAR_TCP_F = fmi2StringToValueReference(fmu, "combiTimeTable.y[3]") - -HIDDEN_CODE_MESSAGE - + # We activate the single instance mode, so only one FMU instance gets allocated and is reused again an again. + fmiSingleInstanceMode!(fmu, true) + + using FMI.FMIImport: fmi2StringToValueReference + + # declare some model identifiers (inside of the FMU) + STATE_I1 = fmu.modelDescription.stateValueReferences[2] + STATE_I2 = fmu.modelDescription.stateValueReferences[1] + STATE_A1 = fmi2StringToValueReference( + fmu, + "rRPositionControl_Elasticity.rr1.rotational1.revolute1.phi", + ) + STATE_A2 = fmi2StringToValueReference( + fmu, + "rRPositionControl_Elasticity.rr1.rotational2.revolute1.phi", + ) + STATE_dA1 = fmi2StringToValueReference( + fmu, + "rRPositionControl_Elasticity.rr1.rotational1.revolute1.w", + ) + STATE_dA2 = fmi2StringToValueReference( + fmu, + "rRPositionControl_Elasticity.rr1.rotational2.revolute1.w", + ) + + DER_ddA2 = fmu.modelDescription.derivativeValueReferences[4] + DER_ddA1 = fmu.modelDescription.derivativeValueReferences[6] + + VAR_TCP_PX = fmi2StringToValueReference(fmu, "rRPositionControl_Elasticity.tCP.p_x") + VAR_TCP_PY = fmi2StringToValueReference(fmu, "rRPositionControl_Elasticity.tCP.p_y") + VAR_TCP_VX = fmi2StringToValueReference(fmu, "rRPositionControl_Elasticity.tCP.v_x") + VAR_TCP_VY = fmi2StringToValueReference(fmu, "rRPositionControl_Elasticity.tCP.v_y") + VAR_TCP_F = fmi2StringToValueReference(fmu, "combiTimeTable.y[3]") + + HIDDEN_CODE_MESSAGE + end # ╔═╡ 16ffc610-3c21-40f7-afca-e9da806ea626 @@ -480,11 +552,17 @@ Let's define an array of values we want to be recorded during the first simulati """ # ╔═╡ 0c9493c4-322e-41a0-9ec7-2e2c54ae1373 -recordValues = [DER_ddA2, DER_ddA1, # mechanical accelerations - STATE_A2, STATE_A1, # mechanical angles - VAR_TCP_PX, VAR_TCP_PY, # tool-center-point x and y - VAR_TCP_VX, VAR_TCP_VY, # tool-center-point velocity x and y - VAR_TCP_F] # normal force pen on paper +recordValues = [ + DER_ddA2, + DER_ddA1, # mechanical accelerations + STATE_A2, + STATE_A1, # mechanical angles + VAR_TCP_PX, + VAR_TCP_PY, # tool-center-point x and y + VAR_TCP_VX, + VAR_TCP_VY, # tool-center-point velocity x and y + VAR_TCP_F, +] # normal force pen on paper # ╔═╡ 325c3032-4c78-4408-b86e-d9aa4cfc3187 md""" @@ -492,12 +570,14 @@ Let's simulate the FMU using `fmiSimulate`. In the solution object, different in """ # ╔═╡ 25e55d1c-388f-469d-99e6-2683c0508693 -sol_fmu_train = fmiSimulate(fmu, # our FMU - (tStart, tStop); # sim. from tStart to tStop - solver=solver, # use the Tsit5 solver - parameters=parameters, # the word "train" - saveat=tSave, # saving points for the sol. - recordValues=recordValues) # values to record +sol_fmu_train = fmiSimulate( + fmu, # our FMU + (tStart, tStop); # sim. from tStart to tStop + solver = solver, # use the Tsit5 solver + parameters = parameters, # the word "train" + saveat = tSave, # saving points for the sol. + recordValues = recordValues, +) # values to record # ╔═╡ 74c519c9-0eef-4798-acff-b11044bb4bf1 md""" @@ -533,10 +613,20 @@ Which signals are used for `y_refs`, can be selected: """ # ╔═╡ b42bf3d8-e70c-485c-89b3-158eb25d8b25 -@bind CHOOSE_y_refs MultiCheckBox([STATE_A1 => "Angle Joint 1", STATE_A2 => "Angle Joint 2", STATE_dA1 => "Angular velocity Joint 1", STATE_dA2 => "Angular velocity Joint 2", VAR_TCP_PX => "TCP position x", VAR_TCP_PY => "TCP position y", VAR_TCP_VX => "TCP velocity x", VAR_TCP_VY => "TCP velocity y", VAR_TCP_F => "TCP (normal) force z"]) +@bind CHOOSE_y_refs MultiCheckBox([ + STATE_A1 => "Angle Joint 1", + STATE_A2 => "Angle Joint 2", + STATE_dA1 => "Angular velocity Joint 1", + STATE_dA2 => "Angular velocity Joint 2", + VAR_TCP_PX => "TCP position x", + VAR_TCP_PY => "TCP position y", + VAR_TCP_VX => "TCP velocity x", + VAR_TCP_VY => "TCP velocity y", + VAR_TCP_F => "TCP (normal) force z", +]) # ╔═╡ 2e08df84-a468-4e99-a277-e2813dfeae5c -model = Chain(x -> fmu(; x=x, dx_refs=:all, y_refs=CHOOSE_y_refs)) +model = Chain(x -> fmu(; x = x, dx_refs = :all, y_refs = CHOOSE_y_refs)) # ╔═╡ c446ed22-3b23-487d-801e-c23742f81047 md""" @@ -544,16 +634,16 @@ Let's pick a state `x1` one second after simulation start to determine sensitivi """ # ╔═╡ fc3d7989-ac10-4a82-8777-eeecd354a7d0 -x1 = FMIZoo.getState(data_train, tStart+1.0) +x1 = FMIZoo.getState(data_train, tStart + 1.0) # ╔═╡ f4e66f76-76ff-4e21-b4b5-c1ecfd846329 -begin - using FMIFlux.FMISensitivity.ReverseDiff - using FMIFlux.FMISensitivity.ForwardDiff - - prepareSolveFMU(fmu, parameters) - jac_rwd = ReverseDiff.jacobian(x -> model(x), x1); - A_rwd = jac_rwd[1:length(x1), :] +begin + using FMIFlux.FMISensitivity.ReverseDiff + using FMIFlux.FMISensitivity.ForwardDiff + + prepareSolveFMU(fmu, parameters) + jac_rwd = ReverseDiff.jacobian(x -> model(x), x1) + A_rwd = jac_rwd[1:length(x1), :] end # ╔═╡ 0a7955e7-7c1a-4396-9613-f8583195c0a8 @@ -562,8 +652,8 @@ Depending on how many signals you select, the output of the FMU-layer is extende """ # ╔═╡ 4912d9c9-d68d-4afd-9961-5d8315884f75 -begin - dx_y = model(x1) +begin + dx_y = model(x1) end # ╔═╡ 19942162-cd4e-487c-8073-ea6b262d299d @@ -601,7 +691,7 @@ We can determine further Jacobians for FMUs, for example the Jacobian $C = \frac # ╔═╡ ac0afa6c-b6ec-4577-aeb6-10d1ec63fa41 begin - C_rwd = jac_rwd[length(x1)+1:end, :] + C_rwd = jac_rwd[length(x1)+1:end, :] end # ╔═╡ 5e9cb956-d5ea-4462-a649-b133a77929b0 @@ -617,12 +707,12 @@ The amount of selected signals has influence on the computational performance of # ╔═╡ 476a1ed7-c865-4878-a948-da73d3c81070 begin - CHOOSE_y_refs; - - md""" - 🎬 **Start Benchmark** $(@bind BENCHMARK CheckBox()) - (benchmarking takes around 10 seconds) - """ + CHOOSE_y_refs + + md""" + 🎬 **Start Benchmark** $(@bind BENCHMARK CheckBox()) + (benchmarking takes around 10 seconds) + """ end # ╔═╡ 0b6b4f6d-be09-42f3-bc2c-5f17a8a9ab0e @@ -632,11 +722,11 @@ The current timing and allocations for inference are: # ╔═╡ a1aca180-d561-42a3-8d12-88f5a3721aae begin - if BENCHMARK - @btime model(x1) - else - BENCHMARK_MESSAGE - end + if BENCHMARK + @btime model(x1) + else + BENCHMARK_MESSAGE + end end # ╔═╡ 3bc2b859-d7b1-4b79-88df-8fb517a6929d @@ -646,16 +736,16 @@ Gradient and Jacobian computation takes a little longer of course. We use revers # ╔═╡ a501d998-6fd6-496f-9718-3340c42b08a6 begin - if BENCHMARK - prepareSolveFMU(fmu, parameters) - function ben_rwd(x) - return ReverseDiff.jacobian(model, x + rand(6)*1e-12); - end - @btime ben_rwd(x1) - #nothing - else - BENCHMARK_MESSAGE - end + if BENCHMARK + prepareSolveFMU(fmu, parameters) + function ben_rwd(x) + return ReverseDiff.jacobian(model, x + rand(6) * 1e-12) + end + @btime ben_rwd(x1) + #nothing + else + BENCHMARK_MESSAGE + end end # ╔═╡ 83a2122d-56da-4a80-8c10-615a8f76c4c1 @@ -665,16 +755,16 @@ Further, forward-mode automatic differentiation is available too via `ForwardDif # ╔═╡ e342be7e-0806-4f72-9e32-6d74ed3ed3f2 begin - if BENCHMARK - prepareSolveFMU(fmu, parameters) - function ben_fwd(x) - return ForwardDiff.jacobian(model, x + rand(6)*1e-12); - end - @btime ben_fwd(x1) # second run for "benchmarking" - #nothing - else - BENCHMARK_MESSAGE - end + if BENCHMARK + prepareSolveFMU(fmu, parameters) + function ben_fwd(x) + return ForwardDiff.jacobian(model, x + rand(6) * 1e-12) + end + @btime ben_fwd(x1) # second run for "benchmarking" + #nothing + else + BENCHMARK_MESSAGE + end end # ╔═╡ eaf37128-0377-42b6-aa81-58f0a815276b @@ -695,16 +785,20 @@ We simplify the ANN to a single nonlinear activation function. Let's see what's """ # ╔═╡ 51c200c9-0de3-4e50-8884-49fe06158560 -begin - fig_pre_post1 = plot(layout=grid(1,2,widths=(1/4, 3/4)), xlabel="t [s]", legend=:bottomright) - - plot!(fig_pre_post1[1], data_train.t, data_train.da1, label=:none, xlims=(0.0,0.1)) - plot!(fig_pre_post1[1], data_train.t, tanh.(data_train.da1), label=:none) - - plot!(fig_pre_post1[2], data_train.t, data_train.da1, label="dα1") - plot!(fig_pre_post1[2], data_train.t, tanh.(data_train.da1), label="tanh(dα1)") - - fig_pre_post1 +begin + fig_pre_post1 = plot( + layout = grid(1, 2, widths = (1 / 4, 3 / 4)), + xlabel = "t [s]", + legend = :bottomright, + ) + + plot!(fig_pre_post1[1], data_train.t, data_train.da1, label = :none, xlims = (0.0, 0.1)) + plot!(fig_pre_post1[1], data_train.t, tanh.(data_train.da1), label = :none) + + plot!(fig_pre_post1[2], data_train.t, data_train.da1, label = "dα1") + plot!(fig_pre_post1[2], data_train.t, tanh.(data_train.da1), label = "tanh(dα1)") + + fig_pre_post1 end # ╔═╡ 0dadd112-3132-4491-9f02-f43cf00aa1f9 @@ -715,7 +809,7 @@ We can add shift (=addition) and scale (=multiplication) operations before and a """ # ╔═╡ bf6bf640-54bc-44ef-bd4d-b98e934d416e -@bind PRE_POST_SHIFT Slider(-1:0.1:1.0, default=0.0) +@bind PRE_POST_SHIFT Slider(-1:0.1:1.0, default = 0.0) # ╔═╡ 5c2308d9-6d04-4b38-af3b-6241da3b6871 md""" @@ -723,7 +817,7 @@ Change the `shift` value $(PRE_POST_SHIFT): """ # ╔═╡ 007d6d95-ad85-4804-9651-9ac3703d3b40 -@bind PRE_POST_SCALE Slider(0.1:0.1:2.0, default=1.0) +@bind PRE_POST_SCALE Slider(0.1:0.1:2.0, default = 1.0) # ╔═╡ 639889b3-b9f2-4a3c-999d-332851768fd7 md""" @@ -731,21 +825,38 @@ Change the `scale` value $(PRE_POST_SCALE): """ # ╔═╡ ed1887df-5079-4367-ab04-9d02a1d6f366 -begin - fun_pre = ShiftScale([PRE_POST_SHIFT], [PRE_POST_SCALE]) - fun_post = ScaleShift(fun_pre) - - fig_pre_post2 = plot(;layout=grid(1,2,widths=(1/4, 3/4)), xlabel="t [s]") - - plot!(fig_pre_post2[2], data_train.t, data_train.da1, label=:none, title="Shift: $(round(PRE_POST_SHIFT; digits=1)) | Scale: $(round(PRE_POST_SCALE; digits=1))", legend=:bottomright) - plot!(fig_pre_post2[2], data_train.t, tanh.(data_train.da1), label=:none) - plot!(fig_pre_post2[2], data_train.t, fun_post(tanh.(fun_pre(data_train.da1))), label=:none) - - plot!(fig_pre_post2[1], data_train.t, data_train.da1, label="dα1", xlims=(0.0, 0.1)) - plot!(fig_pre_post2[1], data_train.t, tanh.(data_train.da1), label="tanh(dα1)") - plot!(fig_pre_post2[1], data_train.t, fun_post(tanh.(fun_pre(data_train.da1))), label="post(tanh(pre(dα1)))") - - fig_pre_post2 +begin + fun_pre = ShiftScale([PRE_POST_SHIFT], [PRE_POST_SCALE]) + fun_post = ScaleShift(fun_pre) + + fig_pre_post2 = plot(; layout = grid(1, 2, widths = (1 / 4, 3 / 4)), xlabel = "t [s]") + + plot!( + fig_pre_post2[2], + data_train.t, + data_train.da1, + label = :none, + title = "Shift: $(round(PRE_POST_SHIFT; digits=1)) | Scale: $(round(PRE_POST_SCALE; digits=1))", + legend = :bottomright, + ) + plot!(fig_pre_post2[2], data_train.t, tanh.(data_train.da1), label = :none) + plot!( + fig_pre_post2[2], + data_train.t, + fun_post(tanh.(fun_pre(data_train.da1))), + label = :none, + ) + + plot!(fig_pre_post2[1], data_train.t, data_train.da1, label = "dα1", xlims = (0.0, 0.1)) + plot!(fig_pre_post2[1], data_train.t, tanh.(data_train.da1), label = "tanh(dα1)") + plot!( + fig_pre_post2[1], + data_train.t, + fun_post(tanh.(fun_pre(data_train.da1))), + label = "post(tanh(pre(dα1)))", + ) + + fig_pre_post2 end # ╔═╡ 0b0c4650-2ce1-4879-9acd-81c16d06700e @@ -776,7 +887,7 @@ The good news is, you don't have to decide this beforehand. This is something th """ # ╔═╡ cbae6aa4-1338-428c-86aa-61d3304e33ed -@bind GATE_INIT_FMU Slider(0.0:0.1:1.0, default=1.0) +@bind GATE_INIT_FMU Slider(0.0:0.1:1.0, default = 1.0) # ╔═╡ 2fa1821b-aaec-4de4-bfb4-89560790dc39 md""" @@ -784,7 +895,7 @@ Change the opening of the **FMU gate** $(GATE_INIT_FMU) for dα1: """ # ╔═╡ 8c56acd6-94d3-4cbc-bc29-d249740268a0 -@bind GATE_INIT_ANN Slider(0.0:0.1:1.0, default=0.0) +@bind GATE_INIT_ANN Slider(0.0:0.1:1.0, default = 0.0) # ╔═╡ 9b52a65a-f20c-4387-aaca-5292a92fb639 md""" @@ -797,40 +908,49 @@ The FMU gate value for dα1 is $(GATE_INIT_FMU) and the ANN gate value is $(GATE """ # ╔═╡ 5a399a9b-32d9-4f93-a41f-8f16a4b102dc -begin - function build_model_gates() - Random.seed!(123) - - cache = CacheLayer() # allocate a cache layer - cacheRetrieve = CacheRetrieveLayer(cache) # allocate a cache retrieve layer, link it to the cache layer - - # we have two signals (acceleration, consumption) and two sources (ANN, FMU), so four gates: - # (1) acceleration from FMU (gate=1.0 | open) - # (2) consumption from FMU (gate=1.0 | open) - # (3) acceleration from ANN (gate=0.0 | closed) - # (4) consumption from ANN (gate=0.0 | closed) - # the accelerations [1,3] and consumptions [2,4] are paired - gates = ScaleSum([GATE_INIT_FMU, GATE_INIT_ANN], [[1,2]]) # gates with sum - - # setup the neural FMU topology - model_gates = Flux.f64(Chain(dx -> cache(dx), # cache `dx` - Dense(1, 16, tanh), - Dense(16, 1, tanh), # pre-process `dx` - dx -> cacheRetrieve(1, dx), # dynamics FMU | dynamics ANN - gates)) # stack together - - model_input = collect([v] for v in data_train.da1) - model_output = collect(model_gates(inp) for inp in model_input) - ANN_output = collect(model_gates[2:3](inp) for inp in model_input) - - fig = plot(; ylims=(-3,1), legend=:bottomright) - plot!(fig, data_train.t, collect(v[1] for v in model_input), label="dα1 of FMU") - plot!(fig, data_train.t, collect(v[1] for v in ANN_output), label="dα1 of ANN") - plot!(fig, data_train.t, collect(v[1] for v in model_output), label="dα1 of neural FMU") - - return fig - end - build_model_gates() +begin + function build_model_gates() + Random.seed!(123) + + cache = CacheLayer() # allocate a cache layer + cacheRetrieve = CacheRetrieveLayer(cache) # allocate a cache retrieve layer, link it to the cache layer + + # we have two signals (acceleration, consumption) and two sources (ANN, FMU), so four gates: + # (1) acceleration from FMU (gate=1.0 | open) + # (2) consumption from FMU (gate=1.0 | open) + # (3) acceleration from ANN (gate=0.0 | closed) + # (4) consumption from ANN (gate=0.0 | closed) + # the accelerations [1,3] and consumptions [2,4] are paired + gates = ScaleSum([GATE_INIT_FMU, GATE_INIT_ANN], [[1, 2]]) # gates with sum + + # setup the neural FMU topology + model_gates = Flux.f64( + Chain( + dx -> cache(dx), # cache `dx` + Dense(1, 16, tanh), + Dense(16, 1, tanh), # pre-process `dx` + dx -> cacheRetrieve(1, dx), # dynamics FMU | dynamics ANN + gates, + ), + ) # stack together + + model_input = collect([v] for v in data_train.da1) + model_output = collect(model_gates(inp) for inp in model_input) + ANN_output = collect(model_gates[2:3](inp) for inp in model_input) + + fig = plot(; ylims = (-3, 1), legend = :bottomright) + plot!(fig, data_train.t, collect(v[1] for v in model_input), label = "dα1 of FMU") + plot!(fig, data_train.t, collect(v[1] for v in ANN_output), label = "dα1 of ANN") + plot!( + fig, + data_train.t, + collect(v[1] for v in model_output), + label = "dα1 of neural FMU", + ) + + return fig + end + build_model_gates() end # ╔═╡ fd1cebf1-5ccc-4bc5-99d4-1eaa30e9762e @@ -887,7 +1007,17 @@ Pick additional ANN layer inputs: """ # ╔═╡ d240c95c-5aba-4b47-ab8d-2f9c0eb854cd -@bind y_refs MultiCheckBox([STATE_A2 => "Angle Joint 2", STATE_A1 => "Angle Joint 1", STATE_dA1 => "Angular velocity Joint 1", STATE_dA2 => "Angular velocity Joint 2", VAR_TCP_PX => "TCP position x", VAR_TCP_PY => "TCP position y", VAR_TCP_VX => "TCP velocity x", VAR_TCP_VY => "TCP velocity y", VAR_TCP_F => "TCP (normal) force z"]) +@bind y_refs MultiCheckBox([ + STATE_A2 => "Angle Joint 2", + STATE_A1 => "Angle Joint 1", + STATE_dA1 => "Angular velocity Joint 1", + STATE_dA2 => "Angular velocity Joint 2", + VAR_TCP_PX => "TCP position x", + VAR_TCP_PY => "TCP position y", + VAR_TCP_VX => "TCP velocity x", + VAR_TCP_VY => "TCP velocity y", + VAR_TCP_F => "TCP (normal) force z", +]) # ╔═╡ 06937575-9ab1-41cd-960c-7eef3e8cae7f md""" @@ -905,7 +1035,7 @@ The ANN shall have $(@bind NUM_LAYERS Select([2, 3, 4])) layers with a width of """ # ╔═╡ 53e971d8-bf43-41cc-ac2b-20dceaa78667 -@bind GATES_INIT Slider(0.0:0.1:1.0, default=0.0) +@bind GATES_INIT Slider(0.0:0.1:1.0, default = 0.0) # ╔═╡ 68d57a23-68c3-418c-9c6f-32bdf8cafceb md""" @@ -924,50 +1054,57 @@ Our final neural FMU topology looks like this: """ # ╔═╡ 84215a73-1ab0-416d-a9db-6b29cd4f5d2a -begin - -function build_topology(gates_init, add_y_refs, nl, lw) - - ANN_input_Vars = [recordValues[1:2]..., add_y_refs...] - ANN_input_Vals = fmiGetSolutionValue(sol_fmu_train, ANN_input_Vars) - ANN_input_Idcs = [4, 6] - for i in 1:length(add_y_refs) - push!(ANN_input_Idcs, i+6) - end - - # pre- and post-processing - preProcess = ShiftScale(ANN_input_Vals) # we put in the derivatives recorded above, FMIFlux shift and scales so we have a data mean of 0 and a standard deviation of 1 - #preProcess.scale[:] *= 0.1 # add some additional "buffer" - postProcess = ScaleShift(preProcess; indices=[1,2]) # initialize the postProcess as inverse of the preProcess, but only take indices 1 and 2 - - # cache - cache = CacheLayer() # allocate a cache layer - cacheRetrieve = CacheRetrieveLayer(cache) # allocate a cache retrieve layer, link it to the cache layer - - gates = ScaleSum([1.0-gates_init, 1.0-gates_init, gates_init, gates_init], [[1,3], [2,4]]) # gates with sum - - ANN_layers = [] - push!(ANN_layers, Dense(2+length(add_y_refs), lw, tanh)) # first layer - for i in 3:nl - push!(ANN_layers, Dense(lw, lw, tanh)) - end - push!(ANN_layers, Dense(lw, 2, tanh)) # last layer - - model = Flux.f64(Chain(x -> fmu(; x=x, dx_refs=:all, y_refs=add_y_refs), - dxy -> cache(dxy), # cache `dx` - dxy -> dxy[ANN_input_Idcs], - preProcess, - ANN_layers..., - postProcess, - dx -> cacheRetrieve(4, 6, dx), # dynamics FMU | dynamics ANN - gates, # compute resulting dx from ANN + FMU - dx -> cacheRetrieve(1:3, dx[1], 5, dx[2]))) - - return model - -end +begin + + function build_topology(gates_init, add_y_refs, nl, lw) + + ANN_input_Vars = [recordValues[1:2]..., add_y_refs...] + ANN_input_Vals = fmiGetSolutionValue(sol_fmu_train, ANN_input_Vars) + ANN_input_Idcs = [4, 6] + for i = 1:length(add_y_refs) + push!(ANN_input_Idcs, i + 6) + end + + # pre- and post-processing + preProcess = ShiftScale(ANN_input_Vals) # we put in the derivatives recorded above, FMIFlux shift and scales so we have a data mean of 0 and a standard deviation of 1 + #preProcess.scale[:] *= 0.1 # add some additional "buffer" + postProcess = ScaleShift(preProcess; indices = [1, 2]) # initialize the postProcess as inverse of the preProcess, but only take indices 1 and 2 -HIDDEN_CODE_MESSAGE + # cache + cache = CacheLayer() # allocate a cache layer + cacheRetrieve = CacheRetrieveLayer(cache) # allocate a cache retrieve layer, link it to the cache layer + + gates = ScaleSum( + [1.0 - gates_init, 1.0 - gates_init, gates_init, gates_init], + [[1, 3], [2, 4]], + ) # gates with sum + + ANN_layers = [] + push!(ANN_layers, Dense(2 + length(add_y_refs), lw, tanh)) # first layer + for i = 3:nl + push!(ANN_layers, Dense(lw, lw, tanh)) + end + push!(ANN_layers, Dense(lw, 2, tanh)) # last layer + + model = Flux.f64( + Chain( + x -> fmu(; x = x, dx_refs = :all, y_refs = add_y_refs), + dxy -> cache(dxy), # cache `dx` + dxy -> dxy[ANN_input_Idcs], + preProcess, + ANN_layers..., + postProcess, + dx -> cacheRetrieve(4, 6, dx), # dynamics FMU | dynamics ANN + gates, # compute resulting dx from ANN + FMU + dx -> cacheRetrieve(1:3, dx[1], 5, dx[2]), + ), + ) + + return model + + end + + HIDDEN_CODE_MESSAGE end @@ -1023,21 +1160,21 @@ Different loss functions are thinkable here. Two quantities that should be consi function loss(solution::FMU2Solution, data::FMIZoo.RobotRR_Data) # determine the start/end indices `ts` and `te` (sampled with 100Hz) - dt = 0.01 - ts = 1+round(Integer, solution.states.t[1] / dt) - te = 1+round(Integer, solution.states.t[end] / dt) - + dt = 0.01 + ts = 1 + round(Integer, solution.states.t[1] / dt) + te = 1 + round(Integer, solution.states.t[end] / dt) + # retrieve simulation data from neural FMU ("where we are") and data from measurements ("where we want to be") i1_value = fmiGetSolutionState(solution, STATE_I1) i2_value = fmiGetSolutionState(solution, STATE_I2) i1_data = data.i1[ts:te] i2_data = data.i2[ts:te] - # accumulate our loss value + # accumulate our loss value Δvalue = 0.0 Δvalue += FMIFlux.Losses.mae(i1_value, i1_data) Δvalue += FMIFlux.Losses.mae(i2_value, i2_data) - + return Δvalue end @@ -1062,13 +1199,18 @@ $(@bind MODE Select([:train => "Training", :demo => "Demo (pre-trained)"])) # ╔═╡ f9d35cfd-4ae5-4dcd-94d9-02aefc99bdfb begin - using JLD2 - - if MODE == :train - final_model = build_topology(GATES_INIT, y_refs, NUM_LAYERS, LAYERS_WIDTH) - elseif MODE == :demo - final_model = build_topology(0.2, [STATE_A2, STATE_A1, VAR_TCP_VX, VAR_TCP_VY, VAR_TCP_F], 3, 32) - end + using JLD2 + + if MODE == :train + final_model = build_topology(GATES_INIT, y_refs, NUM_LAYERS, LAYERS_WIDTH) + elseif MODE == :demo + final_model = build_topology( + 0.2, + [STATE_A2, STATE_A1, VAR_TCP_VX, VAR_TCP_VY, VAR_TCP_F], + 3, + 32, + ) + end end # ╔═╡ f741b213-a20d-423a-a382-75cae1123f2c @@ -1076,143 +1218,159 @@ final_model(x0) # ╔═╡ 91473bef-bc23-43ed-9989-34e62166d455 begin - neuralFMU = ME_NeuralFMU( - fmu, # the FMU used in the neural FMU + neuralFMU = ME_NeuralFMU( + fmu, # the FMU used in the neural FMU final_model, # the model we specified above - (tStart, tStop),# start and stop time for solving - solver; # the solver (Tsit5) - saveat=tSave) # time points to save the solution at + (tStart, tStop),# start and stop time for solving + solver; # the solver (Tsit5) + saveat = tSave, + ) # time points to save the solution at end # ╔═╡ 404ca10f-d944-4a9f-addb-05efebb4f159 begin - import Downloads - demo_path = Downloads.download("https://github.com/ThummeTo/FMIFlux.jl/blob/main/examples/pluto-src/SciMLUsingFMUs/src/20000.jld2?raw=true") - - # in demo mode, we load parameters from a pre-trained model - if MODE == :demo - fmiLoadParameters(neuralFMU, demo_path) - end - - HIDDEN_CODE_MESSAGE + import Downloads + demo_path = Downloads.download( + "https://github.com/ThummeTo/FMIFlux.jl/blob/main/examples/pluto-src/SciMLUsingFMUs/src/20000.jld2?raw=true", + ) + + # in demo mode, we load parameters from a pre-trained model + if MODE == :demo + fmiLoadParameters(neuralFMU, demo_path) + end + + HIDDEN_CODE_MESSAGE end # ╔═╡ e8bae97d-9f90-47d2-9263-dc8fc065c3d0 begin - neuralFMU; - y_refs; - NUM_LAYERS; - LAYERS_WIDTH; - GATES_INIT; - ETA; - BATCHDUR; - MODE; - - if MODE == :train - md"""⚠️ The roughly estimated training time is **$(round(Integer, STEPS*10*BATCHDUR + 0.6/BATCHDUR)) seconds** (Windows, i7 @ 3.6GHz). Training might be faster if the system is less stiff than expected. Once you started training by clicking on `Start Training`, training can't be terminated easily. - -🎬 **Start Training** $(@bind LIVE_TRAIN CheckBox()) - """ - else - LIVE_TRAIN = false - md"""ℹ️ No training in demo mode. Please continue with plotting results. - """ - end + neuralFMU + y_refs + NUM_LAYERS + LAYERS_WIDTH + GATES_INIT + ETA + BATCHDUR + MODE + + if MODE == :train + md"""⚠️ The roughly estimated training time is **$(round(Integer, STEPS*10*BATCHDUR + 0.6/BATCHDUR)) seconds** (Windows, i7 @ 3.6GHz). Training might be faster if the system is less stiff than expected. Once you started training by clicking on `Start Training`, training can't be terminated easily. + + 🎬 **Start Training** $(@bind LIVE_TRAIN CheckBox()) + """ + else + LIVE_TRAIN = false + md"""ℹ️ No training in demo mode. Please continue with plotting results. + """ + end end # ╔═╡ 2dce68a7-27ec-4ffc-afba-87af4f1cb630 begin - -function train(eta, batchdur, steps) - - if steps == 0 - return md"""⚠️ Number of training steps is `0`, no training.""" - end - - prepareSolveFMU(fmu, parameters) - - train_t = data_train.t - train_data = collect([data_train.i2[i], data_train.i1[i]] for i in 1:length(train_t)) - - #@info - @info "Started batching ..." - - batch = batchDataSolution(neuralFMU, # our neural FMU model - t -> FMIZoo.getState(data_train, t), # a function returning a start state for a given time point `t`, to determine start states for batch elements - train_t, # data time points - train_data; # data cumulative consumption - batchDuration=batchdur, # duration of one batch element - indicesModel=[1,2], # model indices to train on (1 and 2 equal the `electrical current` states) - plot=false, # don't show intermediate plots (try this outside of Pluto) - showProgress=false, - parameters=parameters) - - @info "... batching finished!" - - # a random element scheduler - scheduler = RandomScheduler(neuralFMU, batch; applyStep=1, plotStep=0) - - lossFct = (solution::FMU2Solution) -> loss(solution, data_train) - - maxiters = round(Int, 1e5*batchdur) - - _loss = p -> FMIFlux.Losses.loss(neuralFMU, # the neural FMU to simulate - batch; # the batch to take an element from - p=p, # the neural FMU training parameters (given as input) - lossFct=lossFct, # our custom loss function - batchIndex=scheduler.elementIndex, # the index of the batch element to take, determined by the chosen scheduler - logLoss=true, # log losses after every evaluation - showProgress=false, - parameters=parameters, - maxiters=maxiters) - - params = FMIFlux.params(neuralFMU) - - FMIFlux.initialize!(scheduler; p=params[1], showProgress=false, parameters=parameters, print=false) - - BETA1 = 0.9 - BETA2 = 0.999 - optim = Adam(eta, (BETA1, BETA2)) - - @info "Started training ..." - - @withprogress name="iterating" begin - iteration = 0 - function cb() - iteration += 1 - @logprogress iteration/steps - FMIFlux.update!(scheduler; print=false) - nothing - end - - FMIFlux.train!(_loss, # the loss function for training - neuralFMU, # the parameters to train - Iterators.repeated((), steps), # an iterator repeating `steps` times - optim; # the optimizer to train - gradient=:ReverseDiff, # use ReverseDiff, because it's much faster! - cb=cb, # update the scheduler after every step - proceed_on_assert=true) # go on if a training steps fails (e.g. because of instability) - end - - @info "... training finished!" -end -HIDDEN_CODE_MESSAGE + function train(eta, batchdur, steps) + + if steps == 0 + return md"""⚠️ Number of training steps is `0`, no training.""" + end + + prepareSolveFMU(fmu, parameters) + + train_t = data_train.t + train_data = collect([data_train.i2[i], data_train.i1[i]] for i = 1:length(train_t)) + + #@info + @info "Started batching ..." + + batch = batchDataSolution( + neuralFMU, # our neural FMU model + t -> FMIZoo.getState(data_train, t), # a function returning a start state for a given time point `t`, to determine start states for batch elements + train_t, # data time points + train_data; # data cumulative consumption + batchDuration = batchdur, # duration of one batch element + indicesModel = [1, 2], # model indices to train on (1 and 2 equal the `electrical current` states) + plot = false, # don't show intermediate plots (try this outside of Pluto) + showProgress = false, + parameters = parameters, + ) + + @info "... batching finished!" + + # a random element scheduler + scheduler = RandomScheduler(neuralFMU, batch; applyStep = 1, plotStep = 0) + + lossFct = (solution::FMU2Solution) -> loss(solution, data_train) + + maxiters = round(Int, 1e5 * batchdur) + + _loss = + p -> FMIFlux.Losses.loss( + neuralFMU, # the neural FMU to simulate + batch; # the batch to take an element from + p = p, # the neural FMU training parameters (given as input) + lossFct = lossFct, # our custom loss function + batchIndex = scheduler.elementIndex, # the index of the batch element to take, determined by the chosen scheduler + logLoss = true, # log losses after every evaluation + showProgress = false, + parameters = parameters, + maxiters = maxiters, + ) + + params = FMIFlux.params(neuralFMU) + + FMIFlux.initialize!( + scheduler; + p = params[1], + showProgress = false, + parameters = parameters, + print = false, + ) + + BETA1 = 0.9 + BETA2 = 0.999 + optim = Adam(eta, (BETA1, BETA2)) + + @info "Started training ..." + + @withprogress name = "iterating" begin + iteration = 0 + function cb() + iteration += 1 + @logprogress iteration / steps + FMIFlux.update!(scheduler; print = false) + nothing + end + + FMIFlux.train!( + _loss, # the loss function for training + neuralFMU, # the parameters to train + Iterators.repeated((), steps), # an iterator repeating `steps` times + optim; # the optimizer to train + gradient = :ReverseDiff, # use ReverseDiff, because it's much faster! + cb = cb, # update the scheduler after every step + proceed_on_assert = true, + ) # go on if a training steps fails (e.g. because of instability) + end + + @info "... training finished!" + end + + HIDDEN_CODE_MESSAGE end # ╔═╡ c3f5704b-8e98-4c46-be7a-18ab4f139458 let - if MODE == :train - if LIVE_TRAIN - train(ETA, BATCHDUR, STEPS) - else - LIVE_TRAIN_MESSAGE - end - else - md"""ℹ️ No training in demo mode. Please continue with plotting results. - """ - end + if MODE == :train + if LIVE_TRAIN + train(ETA, BATCHDUR, STEPS) + else + LIVE_TRAIN_MESSAGE + end + else + md"""ℹ️ No training in demo mode. Please continue with plotting results. + """ + end end # ╔═╡ 1a608bc8-7264-4dd3-a4e7-0e39128a8375 @@ -1230,56 +1388,60 @@ Let's check out the *training* results of the freshly trained neural FMU. """ # ╔═╡ 51eeb67f-a984-486a-ab8a-a2541966fa72 -begin - neuralFMU; - MODE; - LIVE_TRAIN; - md""" - 🎬 **Plot results** $(@bind LIVE_RESULTS CheckBox()) - """ +begin + neuralFMU + MODE + LIVE_TRAIN + md""" + 🎬 **Plot results** $(@bind LIVE_RESULTS CheckBox()) + """ end # ╔═╡ 27458e32-5891-4afc-af8e-7afdf7e81cc6 begin -function plotPaths!(fig, t, x, N; color=:black, label=:none, kwargs...) - paths = [] - path = nothing - lastN = N[1] - for i in 1:length(N) - if N[i] == 0.0 - if lastN == 1.0 - push!(path, (t[i], x[i]) ) - push!(paths, path) + function plotPaths!(fig, t, x, N; color = :black, label = :none, kwargs...) + paths = [] + path = nothing + lastN = N[1] + for i = 1:length(N) + if N[i] == 0.0 + if lastN == 1.0 + push!(path, (t[i], x[i])) + push!(paths, path) + end end - end - if N[i] == 1.0 - if lastN == 0.0 - path = [] + if N[i] == 1.0 + if lastN == 0.0 + path = [] + end + push!(path, (t[i], x[i])) end - push!(path, (t[i], x[i]) ) + + lastN = N[i] + end + if length(path) > 0 + push!(paths, path) end - lastN = N[i] - end - if length(path) > 0 - push!(paths, path) - end + isfirst = true + for path in paths + plot!( + fig, + collect(v[1] for v in path), + collect(v[2] for v in path); + label = isfirst ? label : :none, + color = color, + kwargs..., + ) + isfirst = false + end - isfirst = true - for path in paths - plot!(fig, collect(v[1] for v in path), collect(v[2] for v in path); - label=isfirst ? label : :none, - color=color, - kwargs...) - isfirst = false + return fig end - return fig -end - -HIDDEN_CODE_MESSAGE + HIDDEN_CODE_MESSAGE end @@ -1291,15 +1453,24 @@ Simulating the FMU (training data): # ╔═╡ 5dd491a4-a8cd-4baf-96f7-7a0b850bb26c begin - fmu_train = fmiSimulate(fmu, (data_train.t[1], data_train.t[end]); x0=x0, - parameters=Dict{String, Any}("fileName" => data_train.params["fileName"]), - recordValues=["rRPositionControl_Elasticity.tCP.p_x", - "rRPositionControl_Elasticity.tCP.p_y", - "rRPositionControl_Elasticity.tCP.N", - "rRPositionControl_Elasticity.tCP.a_x", - "rRPositionControl_Elasticity.tCP.a_y"], - showProgress=true, maxiters=1e6, saveat=data_train.t, solver=Tsit5()); - nothing + fmu_train = fmiSimulate( + fmu, + (data_train.t[1], data_train.t[end]); + x0 = x0, + parameters = Dict{String,Any}("fileName" => data_train.params["fileName"]), + recordValues = [ + "rRPositionControl_Elasticity.tCP.p_x", + "rRPositionControl_Elasticity.tCP.p_y", + "rRPositionControl_Elasticity.tCP.N", + "rRPositionControl_Elasticity.tCP.a_x", + "rRPositionControl_Elasticity.tCP.a_y", + ], + showProgress = true, + maxiters = 1e6, + saveat = data_train.t, + solver = Tsit5(), + ) + nothing end # ╔═╡ 4f27b6c0-21da-4e26-aaad-ff453c8af3da @@ -1310,46 +1481,75 @@ Simulating the neural FMU (training data): # ╔═╡ 1195a30c-3b48-4bd2-8a3a-f4f74f3cd864 begin - if LIVE_RESULTS - result_train = neuralFMU(x0, (data_train.t[1], data_train.t[end]); - parameters=Dict{String, Any}("fileName" => data_train.params["fileName"]), - recordValues=["rRPositionControl_Elasticity.tCP.p_x", - "rRPositionControl_Elasticity.tCP.p_y", - "rRPositionControl_Elasticity.tCP.N", - "rRPositionControl_Elasticity.tCP.v_x", - "rRPositionControl_Elasticity.tCP.v_y"], - showProgress=true, maxiters=1e6, saveat=data_train.t); - nothing - else - LIVE_RESULTS_MESSAGE - end + if LIVE_RESULTS + result_train = neuralFMU( + x0, + (data_train.t[1], data_train.t[end]); + parameters = Dict{String,Any}("fileName" => data_train.params["fileName"]), + recordValues = [ + "rRPositionControl_Elasticity.tCP.p_x", + "rRPositionControl_Elasticity.tCP.p_y", + "rRPositionControl_Elasticity.tCP.N", + "rRPositionControl_Elasticity.tCP.v_x", + "rRPositionControl_Elasticity.tCP.v_y", + ], + showProgress = true, + maxiters = 1e6, + saveat = data_train.t, + ) + nothing + else + LIVE_RESULTS_MESSAGE + end end # ╔═╡ b0ce7b92-93e0-4715-8324-3bf4ff42a0b3 let - if LIVE_RESULTS - loss_fmu = loss(fmu_train, data_train) - loss_nfmu = loss(result_train, data_train) - - md""" -#### The word `train` -The loss function value of the FMU on training data is $(round(loss_fmu; digits=6)), of the neural FMU it is $(round(loss_nfmu; digits=6)). The neural FMU is about $(round(loss_fmu/loss_nfmu; digits=1)) times more accurate. -""" - else - LIVE_RESULTS_MESSAGE - end + if LIVE_RESULTS + loss_fmu = loss(fmu_train, data_train) + loss_nfmu = loss(result_train, data_train) + + md""" + #### The word `train` + The loss function value of the FMU on training data is $(round(loss_fmu; digits=6)), of the neural FMU it is $(round(loss_nfmu; digits=6)). The neural FMU is about $(round(loss_fmu/loss_nfmu; digits=1)) times more accurate. + """ + else + LIVE_RESULTS_MESSAGE + end end # ╔═╡ 919419fe-35de-44bb-89e4-8f8688bee962 let - if LIVE_RESULTS - fig = plot(; dpi=300, size=(200*3,60*3)) - plotPaths!(fig, data_train.tcp_px, data_train.tcp_py, data_train.tcp_norm_f, label="Data", color=:black, style=:dash) - plotPaths!(fig, collect(v[1] for v in fmu_train.values.saveval), collect(v[2] for v in fmu_train.values.saveval), collect(v[3] for v in fmu_train.values.saveval), label="FMU", color=:orange) - plotPaths!(fig, collect(v[1] for v in result_train.values.saveval), collect(v[2] for v in result_train.values.saveval), collect(v[3] for v in result_train.values.saveval), label="Neural FMU", color=:blue) - else - LIVE_RESULTS_MESSAGE - end + if LIVE_RESULTS + fig = plot(; dpi = 300, size = (200 * 3, 60 * 3)) + plotPaths!( + fig, + data_train.tcp_px, + data_train.tcp_py, + data_train.tcp_norm_f, + label = "Data", + color = :black, + style = :dash, + ) + plotPaths!( + fig, + collect(v[1] for v in fmu_train.values.saveval), + collect(v[2] for v in fmu_train.values.saveval), + collect(v[3] for v in fmu_train.values.saveval), + label = "FMU", + color = :orange, + ) + plotPaths!( + fig, + collect(v[1] for v in result_train.values.saveval), + collect(v[2] for v in result_train.values.saveval), + collect(v[3] for v in result_train.values.saveval), + label = "Neural FMU", + color = :blue, + ) + else + LIVE_RESULTS_MESSAGE + end end # ╔═╡ ed25a535-ca2f-4cd2-b0af-188e9699f1c3 @@ -1359,14 +1559,41 @@ md""" # ╔═╡ 2918daf2-6499-4019-a04b-8c3419ee1ab7 let - if LIVE_RESULTS - fig = plot(; dpi=300, size=(40*10,40*10), xlims=(0.165, 0.205), ylims=(-0.035, 0.005)) - plotPaths!(fig, data_train.tcp_px, data_train.tcp_py, data_train.tcp_norm_f, label="Data", color=:black, style=:dash) - plotPaths!(fig, collect(v[1] for v in fmu_train.values.saveval), collect(v[2] for v in fmu_train.values.saveval), collect(v[3] for v in fmu_train.values.saveval), label="FMU", color=:orange) - plotPaths!(fig, collect(v[1] for v in result_train.values.saveval), collect(v[2] for v in result_train.values.saveval), collect(v[3] for v in result_train.values.saveval), label="Neural FMU", color=:blue) - else - LIVE_RESULTS_MESSAGE - end + if LIVE_RESULTS + fig = plot(; + dpi = 300, + size = (40 * 10, 40 * 10), + xlims = (0.165, 0.205), + ylims = (-0.035, 0.005), + ) + plotPaths!( + fig, + data_train.tcp_px, + data_train.tcp_py, + data_train.tcp_norm_f, + label = "Data", + color = :black, + style = :dash, + ) + plotPaths!( + fig, + collect(v[1] for v in fmu_train.values.saveval), + collect(v[2] for v in fmu_train.values.saveval), + collect(v[3] for v in fmu_train.values.saveval), + label = "FMU", + color = :orange, + ) + plotPaths!( + fig, + collect(v[1] for v in result_train.values.saveval), + collect(v[2] for v in result_train.values.saveval), + collect(v[3] for v in result_train.values.saveval), + label = "Neural FMU", + color = :blue, + ) + else + LIVE_RESULTS_MESSAGE + end end # ╔═╡ d798a5d0-3017-4eab-9cdf-ee85d63dfc49 @@ -1376,14 +1603,41 @@ md""" # ╔═╡ 048e39c3-a3d9-4e6b-b050-1fd5a919e4ae let - if LIVE_RESULTS - fig = plot(; dpi=300, size=(50*10,40*10), xlims=(0.245, 0.295), ylims=(-0.04, 0.0)) - plotPaths!(fig, data_train.tcp_px, data_train.tcp_py, data_train.tcp_norm_f, label="Data", color=:black, style=:dash) - plotPaths!(fig, collect(v[1] for v in fmu_train.values.saveval), collect(v[2] for v in fmu_train.values.saveval), collect(v[3] for v in fmu_train.values.saveval), label="FMU", color=:orange) - plotPaths!(fig, collect(v[1] for v in result_train.values.saveval), collect(v[2] for v in result_train.values.saveval), collect(v[3] for v in result_train.values.saveval), label="Neural FMU", color=:blue) - else - LIVE_RESULTS_MESSAGE - end + if LIVE_RESULTS + fig = plot(; + dpi = 300, + size = (50 * 10, 40 * 10), + xlims = (0.245, 0.295), + ylims = (-0.04, 0.0), + ) + plotPaths!( + fig, + data_train.tcp_px, + data_train.tcp_py, + data_train.tcp_norm_f, + label = "Data", + color = :black, + style = :dash, + ) + plotPaths!( + fig, + collect(v[1] for v in fmu_train.values.saveval), + collect(v[2] for v in fmu_train.values.saveval), + collect(v[3] for v in fmu_train.values.saveval), + label = "FMU", + color = :orange, + ) + plotPaths!( + fig, + collect(v[1] for v in result_train.values.saveval), + collect(v[2] for v in result_train.values.saveval), + collect(v[3] for v in result_train.values.saveval), + label = "Neural FMU", + color = :blue, + ) + else + LIVE_RESULTS_MESSAGE + end end # ╔═╡ b489f97d-ee90-48c0-af06-93b66a1f6d2e @@ -1400,13 +1654,22 @@ Simulating the FMU (validation data): # ╔═╡ ea0ede8d-7c2c-4e72-9c96-3260dc8d817d begin - fmu_validation = fmiSimulate(fmu, (data_validation.t[1], data_validation.t[end]); x0=x0, - parameters=Dict{String, Any}("fileName" => data_validation.params["fileName"]), - recordValues=["rRPositionControl_Elasticity.tCP.p_x", - "rRPositionControl_Elasticity.tCP.p_y", - "rRPositionControl_Elasticity.tCP.N"], - showProgress=true, maxiters=1e6, saveat=data_validation.t, solver=Tsit5()); - nothing + fmu_validation = fmiSimulate( + fmu, + (data_validation.t[1], data_validation.t[end]); + x0 = x0, + parameters = Dict{String,Any}("fileName" => data_validation.params["fileName"]), + recordValues = [ + "rRPositionControl_Elasticity.tCP.p_x", + "rRPositionControl_Elasticity.tCP.p_y", + "rRPositionControl_Elasticity.tCP.N", + ], + showProgress = true, + maxiters = 1e6, + saveat = data_validation.t, + solver = Tsit5(), + ) + nothing end # ╔═╡ 35f52dbc-0c0b-495e-8fd4-6edbc6fa811e @@ -1417,43 +1680,72 @@ Simulating the neural FMU (validation data): # ╔═╡ 51aed933-2067-4ea8-9c2f-9d070692ecfc begin - if LIVE_RESULTS - result_validation = neuralFMU(x0, (data_validation.t[1], data_validation.t[end]); - parameters=Dict{String, Any}("fileName" => data_validation.params["fileName"]), - recordValues=["rRPositionControl_Elasticity.tCP.p_x", - "rRPositionControl_Elasticity.tCP.p_y", - "rRPositionControl_Elasticity.tCP.N"], - showProgress=true, maxiters=1e6, saveat=data_validation.t); - nothing - else - LIVE_RESULTS_MESSAGE - end + if LIVE_RESULTS + result_validation = neuralFMU( + x0, + (data_validation.t[1], data_validation.t[end]); + parameters = Dict{String,Any}("fileName" => data_validation.params["fileName"]), + recordValues = [ + "rRPositionControl_Elasticity.tCP.p_x", + "rRPositionControl_Elasticity.tCP.p_y", + "rRPositionControl_Elasticity.tCP.N", + ], + showProgress = true, + maxiters = 1e6, + saveat = data_validation.t, + ) + nothing + else + LIVE_RESULTS_MESSAGE + end end # ╔═╡ 8d9dc86e-f38b-41b1-80c6-b2ab6f488a3a begin - if LIVE_RESULTS - loss_fmu = loss(fmu_validation, data_validation) - loss_nfmu = loss(result_validation, data_validation) - md""" -#### The word `validate` -The loss function value of the FMU on validation data is $(round(loss_fmu; digits=6)), of the neural FMU it is $(round(loss_nfmu; digits=6)). The neural FMU is about $(round(loss_fmu/loss_nfmu; digits=1)) times more accurate. -""" - else - LIVE_RESULTS_MESSAGE - end + if LIVE_RESULTS + loss_fmu = loss(fmu_validation, data_validation) + loss_nfmu = loss(result_validation, data_validation) + md""" + #### The word `validate` + The loss function value of the FMU on validation data is $(round(loss_fmu; digits=6)), of the neural FMU it is $(round(loss_nfmu; digits=6)). The neural FMU is about $(round(loss_fmu/loss_nfmu; digits=1)) times more accurate. + """ + else + LIVE_RESULTS_MESSAGE + end end # ╔═╡ 74ef5a39-1dd7-404a-8baf-caa1021d3054 let - if LIVE_RESULTS - fig = plot(; dpi=300, size=(200*3,40*3)) - plotPaths!(fig, data_validation.tcp_px, data_validation.tcp_py, data_validation.tcp_norm_f, label="Data", color=:black, style=:dash) - plotPaths!(fig, collect(v[1] for v in fmu_validation.values.saveval), collect(v[2] for v in fmu_validation.values.saveval), collect(v[3] for v in fmu_validation.values.saveval), label="FMU", color=:orange) - plotPaths!(fig, collect(v[1] for v in result_validation.values.saveval), collect(v[2] for v in result_validation.values.saveval), collect(v[3] for v in result_validation.values.saveval), label="Neural FMU", color=:blue) - else - LIVE_RESULTS_MESSAGE - end + if LIVE_RESULTS + fig = plot(; dpi = 300, size = (200 * 3, 40 * 3)) + plotPaths!( + fig, + data_validation.tcp_px, + data_validation.tcp_py, + data_validation.tcp_norm_f, + label = "Data", + color = :black, + style = :dash, + ) + plotPaths!( + fig, + collect(v[1] for v in fmu_validation.values.saveval), + collect(v[2] for v in fmu_validation.values.saveval), + collect(v[3] for v in fmu_validation.values.saveval), + label = "FMU", + color = :orange, + ) + plotPaths!( + fig, + collect(v[1] for v in result_validation.values.saveval), + collect(v[2] for v in result_validation.values.saveval), + collect(v[3] for v in result_validation.values.saveval), + label = "Neural FMU", + color = :blue, + ) + else + LIVE_RESULTS_MESSAGE + end end # ╔═╡ 347d209b-9d41-48b0-bee6-0d159caacfa9 @@ -1463,14 +1755,41 @@ md""" # ╔═╡ 05281c4f-dba8-4070-bce3-dc2f1319902e let - if LIVE_RESULTS - fig = plot(; dpi=300, size=(35*10,50*10), xlims=(0.188, 0.223), ylims=(-0.025, 0.025)) - plotPaths!(fig, data_validation.tcp_px, data_validation.tcp_py, data_validation.tcp_norm_f, label="Data", color=:black, style=:dash) - plotPaths!(fig, collect(v[1] for v in fmu_validation.values.saveval), collect(v[2] for v in fmu_validation.values.saveval), collect(v[3] for v in fmu_validation.values.saveval), label="FMU", color=:orange) - plotPaths!(fig, collect(v[1] for v in result_validation.values.saveval), collect(v[2] for v in result_validation.values.saveval), collect(v[3] for v in result_validation.values.saveval), label="Neural FMU", color=:blue) - else - LIVE_RESULTS_MESSAGE - end + if LIVE_RESULTS + fig = plot(; + dpi = 300, + size = (35 * 10, 50 * 10), + xlims = (0.188, 0.223), + ylims = (-0.025, 0.025), + ) + plotPaths!( + fig, + data_validation.tcp_px, + data_validation.tcp_py, + data_validation.tcp_norm_f, + label = "Data", + color = :black, + style = :dash, + ) + plotPaths!( + fig, + collect(v[1] for v in fmu_validation.values.saveval), + collect(v[2] for v in fmu_validation.values.saveval), + collect(v[3] for v in fmu_validation.values.saveval), + label = "FMU", + color = :orange, + ) + plotPaths!( + fig, + collect(v[1] for v in result_validation.values.saveval), + collect(v[2] for v in result_validation.values.saveval), + collect(v[3] for v in result_validation.values.saveval), + label = "Neural FMU", + color = :blue, + ) + else + LIVE_RESULTS_MESSAGE + end end # ╔═╡ 590d7f24-c6b6-4524-b3db-0c93d9963b74 @@ -1480,14 +1799,42 @@ md""" # ╔═╡ 67cfe7c5-8e62-4bf0-996b-19597d5ad5ef let - if LIVE_RESULTS - fig = plot(; dpi=300, size=(25*10,50*10), xlims=(0.245, 0.27), ylims=(-0.025, 0.025), legend=:topleft) - plotPaths!(fig, data_validation.tcp_px, data_validation.tcp_py, data_validation.tcp_norm_f, label="Data", color=:black, style=:dash) - plotPaths!(fig, collect(v[1] for v in fmu_validation.values.saveval), collect(v[2] for v in fmu_validation.values.saveval), collect(v[3] for v in fmu_validation.values.saveval), label="FMU", color=:orange) - plotPaths!(fig, collect(v[1] for v in result_validation.values.saveval), collect(v[2] for v in result_validation.values.saveval), collect(v[3] for v in result_validation.values.saveval), label="Neural FMU", color=:blue) - else - LIVE_RESULTS_MESSAGE - end + if LIVE_RESULTS + fig = plot(; + dpi = 300, + size = (25 * 10, 50 * 10), + xlims = (0.245, 0.27), + ylims = (-0.025, 0.025), + legend = :topleft, + ) + plotPaths!( + fig, + data_validation.tcp_px, + data_validation.tcp_py, + data_validation.tcp_norm_f, + label = "Data", + color = :black, + style = :dash, + ) + plotPaths!( + fig, + collect(v[1] for v in fmu_validation.values.saveval), + collect(v[2] for v in fmu_validation.values.saveval), + collect(v[3] for v in fmu_validation.values.saveval), + label = "FMU", + color = :orange, + ) + plotPaths!( + fig, + collect(v[1] for v in result_validation.values.saveval), + collect(v[2] for v in result_validation.values.saveval), + collect(v[3] for v in result_validation.values.saveval), + label = "Neural FMU", + color = :blue, + ) + else + LIVE_RESULTS_MESSAGE + end end # ╔═╡ e6dc8aab-82c1-4dc9-a1c8-4fe9c137a146 @@ -1497,14 +1844,42 @@ md""" # ╔═╡ dfee214e-bd13-4d4f-af8e-20e0c4e0de9b let - if LIVE_RESULTS - fig = plot(; dpi=300, size=(25*10,30*10), xlims=(0.265, 0.29), ylims=(-0.025, 0.005), legend=:topleft) - plotPaths!(fig, data_validation.tcp_px, data_validation.tcp_py, data_validation.tcp_norm_f, label="Data", color=:black, style=:dash) - plotPaths!(fig, collect(v[1] for v in fmu_validation.values.saveval), collect(v[2] for v in fmu_validation.values.saveval), collect(v[3] for v in fmu_validation.values.saveval), label="FMU", color=:orange) - plotPaths!(fig, collect(v[1] for v in result_validation.values.saveval), collect(v[2] for v in result_validation.values.saveval), collect(v[3] for v in result_validation.values.saveval), label="Neural FMU", color=:blue) - else - LIVE_RESULTS_MESSAGE - end + if LIVE_RESULTS + fig = plot(; + dpi = 300, + size = (25 * 10, 30 * 10), + xlims = (0.265, 0.29), + ylims = (-0.025, 0.005), + legend = :topleft, + ) + plotPaths!( + fig, + data_validation.tcp_px, + data_validation.tcp_py, + data_validation.tcp_norm_f, + label = "Data", + color = :black, + style = :dash, + ) + plotPaths!( + fig, + collect(v[1] for v in fmu_validation.values.saveval), + collect(v[2] for v in fmu_validation.values.saveval), + collect(v[3] for v in fmu_validation.values.saveval), + label = "FMU", + color = :orange, + ) + plotPaths!( + fig, + collect(v[1] for v in result_validation.values.saveval), + collect(v[2] for v in result_validation.values.saveval), + collect(v[3] for v in result_validation.values.saveval), + label = "Neural FMU", + color = :blue, + ) + else + LIVE_RESULTS_MESSAGE + end end # ╔═╡ 88884204-79e4-4412-b861-ebeb5f6f7396 diff --git a/ext/JLD2Ext.jl b/ext/JLD2Ext.jl index 8e53170a..b8ef4b91 100644 --- a/ext/JLD2Ext.jl +++ b/ext/JLD2Ext.jl @@ -6,46 +6,50 @@ module JLD2Ext using FMIFlux, JLD2 +using FMIFlux.Flux -function FMIFlux.saveParameters(nfmu::NeuralFMU, path::String; keyword="parameters") +function FMIFlux.saveParameters(nfmu::NeuralFMU, path::String; keyword = "parameters") params = Flux.params(nfmu) - JLD2.save(path, Dict(keyword=>params[1])) + JLD2.save(path, Dict(keyword => params[1])) end -export saveParameters -function FMIFlux.loadParameters(nfmu::NeuralFMU, path::String; flux_model=nothing, keyword="parameters") +function FMIFlux.loadParameters( + nfmu::NeuralFMU, + path::String; + flux_model = nothing, + keyword = "parameters", +) + + paramsLoad = JLD2.load(path, keyword) - paramsLoad = JLD2.load(path, keyword) - nfmu_params = Flux.params(nfmu) flux_model_params = nothing - if flux_model != nothing + if flux_model != nothing flux_model_params = Flux.params(flux_model) end numParams = length(nfmu_params[1]) l = 1 p = 1 - for i in 1:numParams + for i = 1:numParams nfmu_params[1][i] = paramsLoad[i] - - if flux_model != nothing + + if flux_model != nothing flux_model_params[l][p] = paramsLoad[i] - - p += 1 - + + p += 1 + if p > length(flux_model_params[l]) l += 1 - p = 1 + p = 1 end end end return nothing end -export loadParameters -end # JLD2Ext \ No newline at end of file +end # JLD2Ext diff --git a/src/FMIFlux.jl b/src/FMIFlux.jl index 8e7ae6cb..f599c99b 100644 --- a/src/FMIFlux.jl +++ b/src/FMIFlux.jl @@ -14,7 +14,8 @@ import FMISensitivity.FiniteDiff @debug "Debugging messages enabled for FMIFlux ..." if VERSION < v"1.7.0" - @warn "Training under Julia 1.6 is very slow, please consider using Julia 1.7 or newer." maxlog=1 + @warn "Training under Julia 1.6 is very slow, please consider using Julia 1.7 or newer." maxlog = + 1 end import FMIImport.FMIBase: hasCurrentInstance, getCurrentInstance, unsense @@ -57,8 +58,8 @@ export ME_NeuralFMU, CS_NeuralFMU, NeuralFMU export mse_interpolate, transferParams!, transferFlatParams!, lin_interp # scheduler.jl -export WorstElementScheduler, WorstGrowScheduler, RandomScheduler, SequentialScheduler, LossAccumulationScheduler -export initialize!, update! +export WorstElementScheduler, + WorstGrowScheduler, RandomScheduler, SequentialScheduler, LossAccumulationScheduler # batch.jl export batchDataSolution, batchDataEvaluation diff --git a/src/batch.jl b/src/batch.jl index 54102313..9d08707c 100644 --- a/src/batch.jl +++ b/src/batch.jl @@ -11,15 +11,15 @@ abstract type FMU2BatchElement end mutable struct FMULoss{T} loss::T - step::Integer - time::Real + step::Integer + time::Real - function FMULoss{T}(loss::T, step::Integer=0, time::Real=time()) where {T} + function FMULoss{T}(loss::T, step::Integer = 0, time::Real = time()) where {T} inst = new{T}(loss, step, time) return inst end - function FMULoss(loss, step::Integer=0, time::Real=time()) + function FMULoss(loss, step::Integer = 0, time::Real = time()) loss = unsense(loss) T = typeof(loss) inst = new{T}(loss, step, time) @@ -27,51 +27,51 @@ mutable struct FMULoss{T} end end -function nominalLoss(l::FMULoss{T}) where T <: AbstractArray +function nominalLoss(l::FMULoss{T}) where {T<:AbstractArray} return unsense(sum(l.loss)) end -function nominalLoss(l::FMULoss{T}) where T <: Real +function nominalLoss(l::FMULoss{T}) where {T<:Real} return unsense(l.loss) end -function nominalLoss(::Nothing) +function nominalLoss(::Nothing) return Inf end -function nominalLoss(b::FMU2BatchElement) +function nominalLoss(b::FMU2BatchElement) return nominalLoss(b.loss) end mutable struct FMU2SolutionBatchElement{D} <: FMU2BatchElement - snapshot::Union{FMUSnapshot, Nothing} + snapshot::Union{FMUSnapshot,Nothing} - xStart::Union{Vector{fmi2Real}, Nothing} - xdStart::Union{Vector{D}, Nothing} + xStart::Union{Vector{fmi2Real},Nothing} + xdStart::Union{Vector{D},Nothing} - tStart::fmi2Real - tStop::fmi2Real + tStart::fmi2Real + tStop::fmi2Real # initialState::Union{fmi2FMUstate, Nothing} # initialComponentState::fmi2ComponentState # initialEventInfo::Union{fmi2EventInfo, Nothing} - + loss::FMULoss # the current loss losses::Array{<:FMULoss} # logged losses (if used) step::Integer - saveat::Union{AbstractVector{<:Real}, Nothing} - targets::Union{AbstractArray, Nothing} - - indicesModel + saveat::Union{AbstractVector{<:Real},Nothing} + targets::Union{AbstractArray,Nothing} + + indicesModel::Any solution::FMUSolution scalarLoss::Bool # canGetSetState::Bool - function FMU2SolutionBatchElement{D}(;scalarLoss::Bool=true) where {D} + function FMU2SolutionBatchElement{D}(; scalarLoss::Bool = true) where {D} inst = new() inst.snapshot = nothing @@ -98,27 +98,27 @@ mutable struct FMU2SolutionBatchElement{D} <: FMU2BatchElement end mutable struct FMU2EvaluationBatchElement <: FMU2BatchElement - - tStart::fmi2Real - tStop::fmi2Real + + tStart::fmi2Real + tStop::fmi2Real loss::FMULoss - losses::Array{<:FMULoss} + losses::Array{<:FMULoss} step::Integer - saveat::Union{AbstractVector{<:Real}, Nothing} - targets::Union{AbstractArray, Nothing} - features::Union{AbstractArray, Nothing} + saveat::Union{AbstractVector{<:Real},Nothing} + targets::Union{AbstractArray,Nothing} + features::Union{AbstractArray,Nothing} - indicesModel + indicesModel::Any - result + result::Any scalarLoss::Bool - function FMU2EvaluationBatchElement(;scalarLoss::Bool=true) + function FMU2EvaluationBatchElement(; scalarLoss::Bool = true) inst = new() - + inst.tStart = -Inf inst.tStop = Inf @@ -169,15 +169,22 @@ function copyFMUState!(fmu::FMU2, batchElement::FMU2SolutionBatchElement) return nothing end -function run!(neuralFMU::ME_NeuralFMU, batchElement::FMU2SolutionBatchElement; nextBatchElement=nothing, kwargs...) +function run!( + neuralFMU::ME_NeuralFMU, + batchElement::FMU2SolutionBatchElement; + nextBatchElement = nothing, + kwargs..., +) neuralFMU.customCallbacksAfter = [] neuralFMU.customCallbacksBefore = [] - + # STOP CALLBACK - if !isnothing(nextBatchElement) - stopcb = FunctionCallingCallback((u, t, integrator) -> copyFMUState!(neuralFMU.fmu, nextBatchElement); - funcat=[batchElement.tStop]) + if !isnothing(nextBatchElement) + stopcb = FunctionCallingCallback( + (u, t, integrator) -> copyFMUState!(neuralFMU.fmu, nextBatchElement); + funcat = [batchElement.tStop], + ) push!(neuralFMU.customCallbacksAfter, stopcb) end @@ -185,7 +192,7 @@ function run!(neuralFMU::ME_NeuralFMU, batchElement::FMU2SolutionBatchElement; n readSnapshot = nothing # on first run of the element, there is no snapshot - if isnothing(batchElement.snapshot) + if isnothing(batchElement.snapshot) c = getCurrentInstance(neuralFMU.fmu) batchElement.snapshot = snapshot!(c) writeSnapshot = batchElement.snapshot # needs to be updated, therefore write @@ -194,11 +201,15 @@ function run!(neuralFMU::ME_NeuralFMU, batchElement::FMU2SolutionBatchElement; n end @debug "Running $(batchElement.tStart) with snapshot: $(!isnothing(batchElement.snapshot))..." - - batchElement.solution = neuralFMU(batchElement.xStart, (batchElement.tStart, batchElement.tStop); - readSnapshot=readSnapshot, - writeSnapshot=writeSnapshot, - saveat=batchElement.saveat, kwargs...) + + batchElement.solution = neuralFMU( + batchElement.xStart, + (batchElement.tStart, batchElement.tStop); + readSnapshot = readSnapshot, + writeSnapshot = writeSnapshot, + saveat = batchElement.saveat, + kwargs..., + ) # @assert batchElement.solution.states.t == batchElement.saveat "Batch element simulation failed, missmatch between `states.t` and `saveat`." @@ -206,93 +217,143 @@ function run!(neuralFMU::ME_NeuralFMU, batchElement::FMU2SolutionBatchElement; n neuralFMU.customCallbacksAfter = [] batchElement.step += 1 - + return batchElement.solution end -function run!(model, batchElement::FMU2EvaluationBatchElement, p=nothing) +function run!(model, batchElement::FMU2EvaluationBatchElement, p = nothing) if isnothing(p) # implicite parameter model - batchElement.result = collect(model(f)[batchElement.indicesModel] for f in batchElement.features) + batchElement.result = + collect(model(f)[batchElement.indicesModel] for f in batchElement.features) else # explicite parameter model - batchElement.result = collect(model(p)(f)[batchElement.indicesModel] for f in batchElement.features) + batchElement.result = + collect(model(p)(f)[batchElement.indicesModel] for f in batchElement.features) end end -function plot(batchElement::FMU2SolutionBatchElement; targets::Bool=true, plotkwargs...) +function plot(batchElement::FMU2SolutionBatchElement; targets::Bool = true, plotkwargs...) - fig = Plots.plot(; xlabel="t [s]", plotkwargs...) # , title="loss[$(batchElement.step)] = $(nominalLoss(batchElement.losses[end]))") - for i in 1:length(batchElement.indicesModel) + fig = Plots.plot(; xlabel = "t [s]", plotkwargs...) # , title="loss[$(batchElement.step)] = $(nominalLoss(batchElement.losses[end]))") + for i = 1:length(batchElement.indicesModel) if !isnothing(batchElement.solution) @assert batchElement.solution.states.t == batchElement.saveat "Batch element plotting failed, missmatch between `states.t` and `saveat`." - Plots.plot!(fig, batchElement.solution.states.t, collect(unsense(u[batchElement.indicesModel[i]]) for u in batchElement.solution.states.u), label="Simulation #$(i)") + Plots.plot!( + fig, + batchElement.solution.states.t, + collect( + unsense(u[batchElement.indicesModel[i]]) for + u in batchElement.solution.states.u + ), + label = "Simulation #$(i)", + ) end if targets - Plots.plot!(fig, batchElement.saveat, collect(d[i] for d in batchElement.targets), label="Targets #$(i)") + Plots.plot!( + fig, + batchElement.saveat, + collect(d[i] for d in batchElement.targets), + label = "Targets #$(i)", + ) end end return fig end -function plot(batchElement::FMU2BatchElement; targets::Bool=true, features::Bool=true, plotkwargs...) +function plot( + batchElement::FMU2BatchElement; + targets::Bool = true, + features::Bool = true, + plotkwargs..., +) - fig = Plots.plot(; xlabel="t [s]", plotkwargs...) # , title="loss[$(batchElement.step)] = $(nominalLoss(batchElement.losses[end]))") + fig = Plots.plot(; xlabel = "t [s]", plotkwargs...) # , title="loss[$(batchElement.step)] = $(nominalLoss(batchElement.losses[end]))") if batchElement.features != nothing && features - for i in 1:length(batchElement.features[1]) - Plots.plot!(fig, batchElement.saveat, collect(d[i] for d in batchElement.features), style=:dash, label="Features #$(i)") + for i = 1:length(batchElement.features[1]) + Plots.plot!( + fig, + batchElement.saveat, + collect(d[i] for d in batchElement.features), + style = :dash, + label = "Features #$(i)", + ) end end - for i in 1:length(batchElement.indicesModel) + for i = 1:length(batchElement.indicesModel) if batchElement.result != nothing - Plots.plot!(fig, batchElement.saveat, collect(ForwardDiff.value(u[i]) for u in batchElement.result), label="Evaluation #$(i)") + Plots.plot!( + fig, + batchElement.saveat, + collect(ForwardDiff.value(u[i]) for u in batchElement.result), + label = "Evaluation #$(i)", + ) end - if targets - Plots.plot!(fig, batchElement.saveat, collect(d[i] for d in batchElement.targets), label="Targets #$(i)") + if targets + Plots.plot!( + fig, + batchElement.saveat, + collect(d[i] for d in batchElement.targets), + label = "Targets #$(i)", + ) end end return fig end -function plot(batch::AbstractArray{<:FMU2BatchElement}; plot_mean::Bool=true, plot_shadow::Bool=true, plotkwargs...) +function plot( + batch::AbstractArray{<:FMU2BatchElement}; + plot_mean::Bool = true, + plot_shadow::Bool = true, + plotkwargs..., +) num = length(batch) - xs = 1:num + xs = 1:num ys = collect((nominalLoss(b) != Inf ? nominalLoss(b) : 0.0) for b in batch) - fig = Plots.plot(; xlabel="Batch ID", ylabel="Loss", plotkwargs...) - - if plot_shadow - ys_shadow = collect((length(b.losses) > 1 ? nominalLoss(b.losses[end-1]) : 0.0) for b in batch) - - Plots.bar!(fig, xs, ys_shadow; label="Previous loss", color=:green, bar_width=1.0); + fig = Plots.plot(; xlabel = "Batch ID", ylabel = "Loss", plotkwargs...) + + if plot_shadow + ys_shadow = collect( + (length(b.losses) > 1 ? nominalLoss(b.losses[end-1]) : 0.0) for b in batch + ) + + Plots.bar!( + fig, + xs, + ys_shadow; + label = "Previous loss", + color = :green, + bar_width = 1.0, + ) end - Plots.bar!(fig, xs, ys; label="Current loss", color=:blue, bar_width=0.5); - + Plots.bar!(fig, xs, ys; label = "Current loss", color = :blue, bar_width = 0.5) + if plot_mean avgsum = mean(ys) - Plots.plot!(fig, [1,num], [avgsum, avgsum]; label="mean") + Plots.plot!(fig, [1, num], [avgsum, avgsum]; label = "mean") end - + return fig end function plotLoss(batchElement::FMU2BatchElement; xaxis::Symbol = :steps) @assert length(batchElement.losses) > 0 "Can't plot, no losses!" - - ts = nothing - tlabel = "" + + ts = nothing + tlabel = "" if xaxis == :time ts = collect(l.time for l in batchElement.losses) tlabel = "t [s]" - elseif xaxis == :steps + elseif xaxis == :steps ts = collect(l.step for l in batchElement.losses) tlabel = "steps [/]" else @@ -300,48 +361,53 @@ function plotLoss(batchElement::FMU2BatchElement; xaxis::Symbol = :steps) end ls = collect(l.loss for l in batchElement.losses) - fig = Plots.plot(ts, ls, xlabel=tlabel, ylabel="Loss") + fig = Plots.plot(ts, ls, xlabel = tlabel, ylabel = "Loss") return fig end -function loss!(batchElement::FMU2SolutionBatchElement, lossFct; logLoss::Bool=false) +function loss!(batchElement::FMU2SolutionBatchElement, lossFct; logLoss::Bool = false) loss = 0.0 # will be incremented if hasmethod(lossFct, Tuple{FMUSolution}) loss = lossFct(batchElement.solution) - elseif hasmethod(lossFct, Tuple{FMUSolution, Union{}}) + elseif hasmethod(lossFct, Tuple{FMUSolution,Union{}}) loss = lossFct(batchElement.solution, batchElement.targets) else # hasmethod(lossFct, Tuple{Union{}, Union{}}) if batchElement.solution.success if batchElement.scalarLoss - for i in 1:length(batchElement.indicesModel) + for i = 1:length(batchElement.indicesModel) dataTarget = collect(d[i] for d in batchElement.targets) - modelOutput = collect(u[batchElement.indicesModel[i]] for u in batchElement.solution.states.u) + modelOutput = collect( + u[batchElement.indicesModel[i]] for + u in batchElement.solution.states.u + ) loss += lossFct(modelOutput, dataTarget) end else dataTarget = batchElement.targets - modelOutput = collect(u[batchElement.indicesModel] for u in batchElement.solution.states.u) + modelOutput = collect( + u[batchElement.indicesModel] for u in batchElement.solution.states.u + ) loss = lossFct(modelOutput, dataTarget) end else @warn "Can't compute loss for batch element, because solution is invalid (`success=false`) for batch element\n$(batchElement)." end - + end batchElement.loss.step = batchElement.step batchElement.loss.time = time() batchElement.loss.loss = unsense(loss) - ignore_derivatives() do + ignore_derivatives() do if logLoss push!(batchElement.losses, deepcopy(batchElement.loss)) end @@ -350,12 +416,12 @@ function loss!(batchElement::FMU2SolutionBatchElement, lossFct; logLoss::Bool=fa return loss end -function loss!(batchElement::FMU2EvaluationBatchElement, lossFct; logLoss::Bool=true) +function loss!(batchElement::FMU2EvaluationBatchElement, lossFct; logLoss::Bool = true) loss = 0.0 # will be incremented - + if batchElement.scalarLoss - for i in 1:length(batchElement.indicesModel) + for i = 1:length(batchElement.indicesModel) dataTarget = collect(d[i] for d in batchElement.targets) modelOutput = collect(u[i] for u in batchElement.result) @@ -371,8 +437,8 @@ function loss!(batchElement::FMU2EvaluationBatchElement, lossFct; logLoss::Bool= batchElement.loss.step = batchElement.step batchElement.loss.time = time() batchElement.loss.loss = unsense(loss) - - ignore_derivatives() do + + ignore_derivatives() do if logLoss push!(batchElement.losses, deepcopy(batchElement.loss)) end @@ -381,64 +447,83 @@ function loss!(batchElement::FMU2EvaluationBatchElement, lossFct; logLoss::Bool= return loss end -function _batchDataSolution!(batch::AbstractArray{<:FMIFlux.FMU2SolutionBatchElement}, neuralFMU::NeuralFMU, x0_fun, train_t::AbstractArray{<:AbstractArray{<:Real}}, targets::AbstractArray; kwargs...) +function _batchDataSolution!( + batch::AbstractArray{<:FMIFlux.FMU2SolutionBatchElement}, + neuralFMU::NeuralFMU, + x0_fun, + train_t::AbstractArray{<:AbstractArray{<:Real}}, + targets::AbstractArray; + kwargs..., +) len = length(train_t) - for i in 1:len + for i = 1:len _batchDataSolution!(batch, neuralFMU, x0_fun, train_t[i], targets[i]; kwargs...) end return nothing end -function _batchDataSolution!(batch::AbstractArray{<:FMIFlux.FMU2SolutionBatchElement}, neuralFMU::NeuralFMU, x0_fun, train_t::AbstractArray{<:Real}, targets::AbstractArray; - batchDuration::Real=(train_t[end]-train_t[1]), indicesModel=1:length(targets[1]), plot::Bool=false, scalarLoss::Bool=true) +function _batchDataSolution!( + batch::AbstractArray{<:FMIFlux.FMU2SolutionBatchElement}, + neuralFMU::NeuralFMU, + x0_fun, + train_t::AbstractArray{<:Real}, + targets::AbstractArray; + batchDuration::Real = (train_t[end] - train_t[1]), + indicesModel = 1:length(targets[1]), + plot::Bool = false, + scalarLoss::Bool = true, +) @assert length(train_t) == length(targets) "Timepoints in `train_t` ($(length(train_t))) must match number of `targets` ($(length(targets)))" canGetSetState = canGetSetFMUState(neuralFMU.fmu) if !canGetSetState - logWarning(neuralFMU.fmu, "This FMU can't set/get a FMU state. This is suboptimal for batched training.") + logWarning( + neuralFMU.fmu, + "This FMU can't set/get a FMU state. This is suboptimal for batched training.", + ) end # c, _ = prepareSolveFMU(neuralFMU.fmu, nothing, neuralFMU.fmu.type, nothing, nothing, nothing, nothing, nothing, nothing, neuralFMU.tspan[1], neuralFMU.tspan[end], nothing; handleEvents=FMIFlux.handleEvents) - + # indicesData = 1:1 tStart = train_t[1] - + # iStart = timeToIndex(train_t, tStart) # iStop = timeToIndex(train_t, tStart + batchDuration) - + # startElement = FMIFlux.FMU2SolutionBatchElement(;scalarLoss=scalarLoss) # startElement.tStart = train_t[iStart] # startElement.tStop = train_t[iStop] # startElement.xStart = x0_fun(tStart) - + # startElement.saveat = train_t[iStart:iStop] # startElement.targets = targets[iStart:iStop] - + # startElement.indicesModel = indicesModel # push!(batch, startElement) - - numElements = floor(Integer, (train_t[end]-train_t[1])/batchDuration) + + numElements = floor(Integer, (train_t[end] - train_t[1]) / batchDuration) D = eltype(neuralFMU.fmu.modelDescription.discreteStateValueReferences) - for i in 1:numElements + for i = 1:numElements + + element = FMIFlux.FMU2SolutionBatchElement{D}(; scalarLoss = scalarLoss) - element = FMIFlux.FMU2SolutionBatchElement{D}(;scalarLoss=scalarLoss) - - iStart = FMIFlux.timeToIndex(train_t, tStart + (i-1) * batchDuration) + iStart = FMIFlux.timeToIndex(train_t, tStart + (i - 1) * batchDuration) iStop = FMIFlux.timeToIndex(train_t, tStart + i * batchDuration) element.tStart = train_t[iStart] element.tStop = train_t[iStop] element.xStart = x0_fun(element.tStart) - + element.saveat = train_t[iStart:iStop] element.targets = targets[iStart:iStop] - + element.indicesModel = indicesModel push!(batch, element) @@ -447,28 +532,48 @@ function _batchDataSolution!(batch::AbstractArray{<:FMIFlux.FMU2SolutionBatchEle return nothing end -function batchDataSolution(neuralFMU::NeuralFMU, x0_fun, train_t, targets; - batchDuration::Real=(train_t[end]-train_t[1]), - indicesModel=1:length(targets[1]), - plot::Bool=false, - scalarLoss::Bool=true, - restartAtJump::Bool=true, - solverKwargs...) +function batchDataSolution( + neuralFMU::NeuralFMU, + x0_fun, + train_t, + targets; + batchDuration::Real = (train_t[end] - train_t[1]), + indicesModel = 1:length(targets[1]), + plot::Bool = false, + scalarLoss::Bool = true, + restartAtJump::Bool = true, + solverKwargs..., +) batch = Array{FMIFlux.FMU2SolutionBatchElement,1}() - _batchDataSolution!(batch, neuralFMU, x0_fun, train_t, targets; batchDuration=batchDuration, indicesModel=indicesModel, plot=plot, scalarLoss=scalarLoss) + _batchDataSolution!( + batch, + neuralFMU, + x0_fun, + train_t, + targets; + batchDuration = batchDuration, + indicesModel = indicesModel, + plot = plot, + scalarLoss = scalarLoss, + ) numElements = length(batch) - for i in 1:numElements - - nextBatchElement = nothing + for i = 1:numElements + + nextBatchElement = nothing if i < numElements && batch[i].tStop == batch[i+1].tStart nextBatchElement = batch[i+1] - end - - FMIFlux.run!(neuralFMU, batch[i]; nextBatchElement=nextBatchElement, solverKwargs...) - - if plot + end + + FMIFlux.run!( + neuralFMU, + batch[i]; + nextBatchElement = nextBatchElement, + solverKwargs..., + ) + + if plot fig = FMIFlux.plot(batch[i]) display(fig) end @@ -477,25 +582,33 @@ function batchDataSolution(neuralFMU::NeuralFMU, x0_fun, train_t, targets; return batch end -function batchDataEvaluation(train_t::AbstractArray{<:Real}, targets::AbstractArray, features::Union{AbstractArray, Nothing}=nothing; - batchDuration::Real=(train_t[end]-train_t[1]), indicesModel=1:length(targets[1]), plot::Bool=false, round_digits=3, scalarLoss::Bool=true) +function batchDataEvaluation( + train_t::AbstractArray{<:Real}, + targets::AbstractArray, + features::Union{AbstractArray,Nothing} = nothing; + batchDuration::Real = (train_t[end] - train_t[1]), + indicesModel = 1:length(targets[1]), + plot::Bool = false, + round_digits = 3, + scalarLoss::Bool = true, +) batch = Array{FMIFlux.FMU2EvaluationBatchElement,1}() - + indicesData = 1:1 tStart = train_t[1] - + iStart = timeToIndex(train_t, tStart) iStop = timeToIndex(train_t, tStart + batchDuration) - - startElement = FMIFlux.FMU2EvaluationBatchElement(;scalarLoss=scalarLoss) + + startElement = FMIFlux.FMU2EvaluationBatchElement(; scalarLoss = scalarLoss) startElement.tStart = train_t[iStart] startElement.tStop = train_t[iStop] - + startElement.saveat = train_t[iStart:iStop] startElement.targets = targets[iStart:iStop] - if features != nothing + if features != nothing startElement.features = features[iStart:iStop] else startElement.features = startElement.targets @@ -503,11 +616,11 @@ function batchDataEvaluation(train_t::AbstractArray{<:Real}, targets::AbstractAr startElement.indicesModel = indicesModel push!(batch, startElement) - - for i in 2:floor(Integer, (train_t[end]-train_t[1])/batchDuration) - push!(batch, FMIFlux.FMU2EvaluationBatchElement(;scalarLoss=scalarLoss)) - - iStart = timeToIndex(train_t, tStart + (i-1) * batchDuration) + + for i = 2:floor(Integer, (train_t[end] - train_t[1]) / batchDuration) + push!(batch, FMIFlux.FMU2EvaluationBatchElement(; scalarLoss = scalarLoss)) + + iStart = timeToIndex(train_t, tStart + (i - 1) * batchDuration) iStop = timeToIndex(train_t, tStart + i * batchDuration) batch[i].tStart = train_t[iStart] @@ -515,13 +628,13 @@ function batchDataEvaluation(train_t::AbstractArray{<:Real}, targets::AbstractAr batch[i].saveat = train_t[iStart:iStop] batch[i].targets = targets[iStart:iStop] - if features != nothing + if features != nothing batch[i].features = features[iStart:iStop] else batch[i].features = batch[i].targets end batch[i].indicesModel = indicesModel - + if plot fig = FMIFlux.plot(batch[i-1]) display(fig) diff --git a/src/compatibility_check.jl b/src/compatibility_check.jl index 8311a564..ba15cb2d 100644 --- a/src/compatibility_check.jl +++ b/src/compatibility_check.jl @@ -7,30 +7,45 @@ # https://docs.sciml.ai/SciMLSensitivity/stable/manual/differential_equation_sensitivities/ using FMISensitivity.SciMLSensitivity -function checkSensalgs!(loss, neuralFMU::Union{ME_NeuralFMU, CS_NeuralFMU}; - gradients=(:ReverseDiff, :Zygote, :ForwardDiff), # :FiniteDiff is slow ... - max_msg_len=192, - chunk_size=DEFAULT_CHUNK_SIZE, - OtD_autojacvecs=(false, true, TrackerVJP(), ZygoteVJP(), ReverseDiffVJP(false), ReverseDiffVJP(true)), # EnzymeVJP() deadlocks in the current release xD - OtD_sensealgs=(BacksolveAdjoint, InterpolatingAdjoint, QuadratureAdjoint), - OtD_checkpointings=(true, false), - DtO_sensealgs=(ReverseDiffAdjoint, ForwardDiffSensitivity, TrackerAdjoint), # TrackerAdjoint, ZygoteAdjoint freeze the REPL - multiObjective::Bool=false, - bestof::Int=2, - timeout_seconds::Real=60.0, - gradient_gt::Symbol=:FiniteDiff, - kwargs...) - - params = Flux.params(neuralFMU) +function checkSensalgs!( + loss, + neuralFMU::Union{ME_NeuralFMU,CS_NeuralFMU}; + gradients = (:ReverseDiff, :Zygote, :ForwardDiff), # :FiniteDiff is slow ... + max_msg_len = 192, + chunk_size = DEFAULT_CHUNK_SIZE, + OtD_autojacvecs = ( + false, + true, + TrackerVJP(), + ZygoteVJP(), + ReverseDiffVJP(false), + ReverseDiffVJP(true), + ), # EnzymeVJP() deadlocks in the current release xD + OtD_sensealgs = ( + BacksolveAdjoint, + InterpolatingAdjoint, + QuadratureAdjoint, + GaussAdjoint, + ), + OtD_checkpointings = (true, false), + DtO_sensealgs = (ReverseDiffAdjoint, ForwardDiffSensitivity, TrackerAdjoint), # TrackerAdjoint, ZygoteAdjoint freeze the REPL + multiObjective::Bool = false, + bestof::Int = 2, + timeout_seconds::Real = 60.0, + gradient_gt::Symbol = :FiniteDiff, + kwargs..., +) + + params = Flux.params(neuralFMU) initial_sensalg = neuralFMU.fmu.executionConfig.sensealg best_timing = Inf - best_gradient = nothing + best_gradient = nothing best_sensealg = nothing printstyled("Mode: Ground-Truth ($(gradient_gt)))\n") grads, _ = runGrads(loss, params, gradient_gt, chunk_size, multiObjective) - + # jac = zeros(length(params[1])) # FiniteDiff.finite_difference_gradient!(jac, loss, params[1]) # step = 1e-6 @@ -48,39 +63,57 @@ function checkSensalgs!(loss, neuralFMU::Union{ME_NeuralFMU, CS_NeuralFMU}; grad_gt_val = collect(sum(abs.(grad)) for grad in grads)[1] - printstyled("\tGround Truth: $(grad_gt_val)\n", color=:green) + printstyled("\tGround Truth: $(grad_gt_val)\n", color = :green) @assert grad_gt_val > 0.0 "Loss gradient is zero, grad_gt_val == 0.0" printstyled("Mode: Optimize-then-Discretize\n") for gradient ∈ gradients printstyled("\tGradient: $(gradient)\n") - + for sensealg ∈ OtD_sensealgs printstyled("\t\tSensealg: $(sensealg)\n") for checkpointing ∈ OtD_checkpointings printstyled("\t\t\tCheckpointing: $(checkpointing)\n") - if sensealg == QuadratureAdjoint && checkpointing - printstyled("\t\t\t\tQuadratureAdjoint doesn't implement checkpointing, skipping ...\n") - continue + if sensealg ∈ (QuadratureAdjoint, GaussAdjoint) && checkpointing + printstyled( + "\t\t\t\t$(sensealg) doesn't implement checkpointing, skipping ...\n", + ) + continue end for autojacvec ∈ OtD_autojacvecs printstyled("\t\t\t\tAutojacvec: $(autojacvec)\n") - + if sensealg ∈ (BacksolveAdjoint, InterpolatingAdjoint) - neuralFMU.fmu.executionConfig.sensealg = sensealg(; autojacvec=autojacvec, chunk_size=chunk_size, checkpointing=checkpointing) + neuralFMU.fmu.executionConfig.sensealg = sensealg(; + autojacvec = autojacvec, + chunk_size = chunk_size, + checkpointing = checkpointing, + ) else - neuralFMU.fmu.executionConfig.sensealg = sensealg(; autojacvec=autojacvec, chunk_size=chunk_size) + neuralFMU.fmu.executionConfig.sensealg = + sensealg(; autojacvec = autojacvec, chunk_size = chunk_size) end - call = () -> _tryrun(loss, params, gradient, chunk_size, 5, max_msg_len, multiObjective; timeout_seconds=timeout_seconds, grad_gt_val=grad_gt_val) - for i in 1:bestof + call = + () -> _tryrun( + loss, + params, + gradient, + chunk_size, + 5, + max_msg_len, + multiObjective; + timeout_seconds = timeout_seconds, + grad_gt_val = grad_gt_val, + ) + for i = 1:bestof timing, valid = call() if valid && timing < best_timing best_timing = timing - best_gradient = gradient + best_gradient = gradient best_sensealg = neuralFMU.fmu.executionConfig.sensealg end end @@ -97,18 +130,30 @@ function checkSensalgs!(loss, neuralFMU::Union{ME_NeuralFMU, CS_NeuralFMU}; printstyled("\t\tSensealg: $(sensealg)\n") if sensealg == ForwardDiffSensitivity - neuralFMU.fmu.executionConfig.sensealg = sensealg(; chunk_size=chunk_size, convert_tspan=true) - else + neuralFMU.fmu.executionConfig.sensealg = + sensealg(; chunk_size = chunk_size, convert_tspan = true) + else neuralFMU.fmu.executionConfig.sensealg = sensealg() end - call = () -> _tryrun(loss, params, gradient, chunk_size, 3, max_msg_len, multiObjective; timeout_seconds=timeout_seconds, grad_gt_val=grad_gt_val) - for i in 1:bestof + call = + () -> _tryrun( + loss, + params, + gradient, + chunk_size, + 3, + max_msg_len, + multiObjective; + timeout_seconds = timeout_seconds, + grad_gt_val = grad_gt_val, + ) + for i = 1:bestof timing, valid = call() if valid && timing < best_timing best_timing = timing - best_gradient = gradient + best_gradient = gradient best_sensealg = neuralFMU.fmu.executionConfig.sensealg end end @@ -118,7 +163,10 @@ function checkSensalgs!(loss, neuralFMU::Union{ME_NeuralFMU, CS_NeuralFMU}; neuralFMU.fmu.executionConfig.sensealg = initial_sensalg - printstyled("------------------------------\nBest time: $(best_timing)\nBest gradient: $(best_gradient)\nBest sensealg: $(best_sensealg)\n", color=:blue) + printstyled( + "------------------------------\nBest time: $(best_timing)\nBest gradient: $(best_gradient)\nBest sensealg: $(best_sensealg)\n", + color = :blue, + ) return best_timing, best_gradient, best_sensealg end @@ -133,7 +181,7 @@ function timeout(f, arg, seconds, fail) end try fetch(tsk) - catch _; + catch _ fail end end @@ -146,11 +194,11 @@ function runGrads(loss, params, gradient, chunk_size, multiObjective) dim = loss(params[1]) grads = zeros(Float64, length(params[1]), length(dim)) else - grads = zeros(Float64, length(params[1])) + grads = zeros(Float64, length(params[1])) end computeGradient!(grads, loss, params[1], gradient, chunk_size, multiObjective) - + timing = time() - tstart if length(grads[1]) == 1 @@ -160,26 +208,38 @@ function runGrads(loss, params, gradient, chunk_size, multiObjective) return grads, timing end -function _tryrun(loss, params, gradient, chunk_size, ts, max_msg_len, multiObjective::Bool=false; - print_stdout::Bool=true, print_stderr::Bool=true, timeout_seconds::Real=60.0, grad_gt_val::Real=0.0, reltol=1e-2) +function _tryrun( + loss, + params, + gradient, + chunk_size, + ts, + max_msg_len, + multiObjective::Bool = false; + print_stdout::Bool = true, + print_stderr::Bool = true, + timeout_seconds::Real = 60.0, + grad_gt_val::Real = 0.0, + reltol = 1e-2, +) spacing = "" - for t in ts + for t in ts spacing *= "\t" end - message = "" - color = :black + message = "" + color = :black timing = Inf valid = false original_stdout = stdout original_stderr = stderr - (rd_stdout, wr_stdout) = redirect_stdout(); - (rd_stderr, wr_stderr) = redirect_stderr(); + (rd_stdout, wr_stdout) = redirect_stdout() + (rd_stderr, wr_stderr) = redirect_stderr() try - + #grads, timing = timeout(runGrads, (loss, params, gradient, chunk_size, multiObjective), timeout_seconds, ([Inf], -1.0)) grads, timing = runGrads(loss, params, gradient, chunk_size, multiObjective) @@ -190,19 +250,23 @@ function _tryrun(loss, params, gradient, chunk_size, ts, max_msg_len, multiObjec val = collect(sum(abs.(grad)) for grad in grads)[1] tol = abs(1.0 - val / grad_gt_val) - + if tol > reltol - message = spacing * "WRONG $(round(tol*100;digits=2))% > $(round(reltol*100;digits=2))% | $(round(timing; digits=3))s | GradAbsSum: $(round.(val; digits=6))\n" + message = + spacing * + "WRONG $(round(tol*100;digits=2))% > $(round(reltol*100;digits=2))% | $(round(timing; digits=3))s | GradAbsSum: $(round.(val; digits=6))\n" color = :yellow valid = false else - message = spacing * "SUCCESS | $(round(timing; digits=3))s | GradAbsSum: $(round.(val; digits=6))\n" + message = + spacing * + "SUCCESS $(round(tol*100;digits=2))% <= $(round(reltol*100;digits=2))% | $(round(timing; digits=3))s | GradAbsSum: $(round.(val; digits=6))\n" color = :green valid = true end end - catch e + catch e msg = "$(e)" msg = length(msg) > max_msg_len ? first(msg, max_msg_len) * "..." : msg message = spacing * "$(msg)\n" @@ -218,7 +282,7 @@ function _tryrun(loss, params, gradient, chunk_size, ts, max_msg_len, multiObjec msg = read(rd_stdout, String) if length(msg) > 0 msg = length(msg) > max_msg_len ? first(msg, max_msg_len) * "..." : msg - printstyled(spacing * "STDOUT: $(msg)\n", color=:yellow) + printstyled(spacing * "STDOUT: $(msg)\n", color = :yellow) end end @@ -226,11 +290,11 @@ function _tryrun(loss, params, gradient, chunk_size, ts, max_msg_len, multiObjec msg = read(rd_stderr, String) if length(msg) > 0 msg = length(msg) > max_msg_len ? first(msg, max_msg_len) * "..." : msg - printstyled(spacing * "STDERR: $(msg)\n", color=:yellow) + printstyled(spacing * "STDERR: $(msg)\n", color = :yellow) end end - printstyled(message, color=color) + printstyled(message, color = color) return timing, valid -end \ No newline at end of file +end diff --git a/src/convert.jl b/src/convert.jl index d8528fe5..e74562d7 100644 --- a/src/convert.jl +++ b/src/convert.jl @@ -6,8 +6,8 @@ function is64(model::Flux.Chain) params = Flux.params(model) - for i in 1:length(params) - for j in 1:length(params[i]) + for i = 1:length(params) + for j = 1:length(params[i]) if !isa(params[i][j], Float64) return false end diff --git a/src/deprecated.jl b/src/deprecated.jl index 0dfeeb70..371bf264 100644 --- a/src/deprecated.jl +++ b/src/deprecated.jl @@ -17,28 +17,30 @@ Optional, additional FMU-values can be retrieved by keyword argument `getValueRe Function takes the current system state array ("x") and returns an array with state derivatives ("x dot") and optionally the FMU-values for `getValueReferences`. Setting the FMU time via argument `t` is optional, if not set, the current time of the ODE solver around the NeuralFMU is used. """ -function fmi2EvaluateME(fmu::FMU2, - x::Array{<:Real}, - t,#::Real, - setValueReferences::Union{Array{fmi2ValueReference}, Nothing}=nothing, - setValues::Union{Array{<:Real}, Nothing}=nothing, - getValueReferences::Union{Array{fmi2ValueReference}, Nothing}=nothing) +function fmi2EvaluateME( + fmu::FMU2, + x::Array{<:Real}, + t,#::Real, + setValueReferences::Union{Array{fmi2ValueReference},Nothing} = nothing, + setValues::Union{Array{<:Real},Nothing} = nothing, + getValueReferences::Union{Array{fmi2ValueReference},Nothing} = nothing, +) y = nothing y_refs = getValueReferences u = setValues u_refs = setValueReferences - + if y_refs != nothing y = zeros(length(y_refs)) end - + dx = zeros(length(x)) c = fmu.components[end] - y, dx = c(dx=dx, y=y, y_refs=y_refs, x=x, u=u, u_refs=u_refs, t=t) - + y, dx = c(dx = dx, y = y, y_refs = y_refs, x = x, u = u, u_refs = u_refs, t = t) + return [(dx == nothing ? [] : dx)..., (y == nothing ? [] : y)...] end export fmi2EvaluateME @@ -48,16 +50,15 @@ DEPRECATED: Wrapper. Call ```fmi2EvaluateME``` for more information. """ -function fmiEvaluateME(str::FMI2Struct, - x::Array{<:Real}, - t::Real = (typeof(str) == FMU2 ? str.components[end].t : str.t), - setValueReferences::Union{Array{fmi2ValueReference}, Nothing} = nothing, - setValues::Union{Array{<:Real}, Nothing} = nothing, - getValueReferences::Union{Array{fmi2ValueReference}, Nothing} = nothing ) - fmi2EvaluateME(str, x, t, - setValueReferences, - setValues, - getValueReferences) +function fmiEvaluateME( + str::FMI2Struct, + x::Array{<:Real}, + t::Real = (typeof(str) == FMU2 ? str.components[end].t : str.t), + setValueReferences::Union{Array{fmi2ValueReference},Nothing} = nothing, + setValues::Union{Array{<:Real},Nothing} = nothing, + getValueReferences::Union{Array{fmi2ValueReference},Nothing} = nothing, +) + fmi2EvaluateME(str, x, t, setValueReferences, setValues, getValueReferences) end export fmiEvaluateME @@ -66,11 +67,13 @@ DEPRECATED: Wrapper. Call ```fmi2DoStepCS``` for more information. """ -function fmiDoStepCS(str::FMI2Struct, - dt::Real, - setValueReferences::Array{fmi2ValueReference} = zeros(fmi2ValueReference, 0), - setValues::Array{<:Real} = zeros(Real, 0), - getValueReferences::Array{fmi2ValueReference} = zeros(fmi2ValueReference, 0)) +function fmiDoStepCS( + str::FMI2Struct, + dt::Real, + setValueReferences::Array{fmi2ValueReference} = zeros(fmi2ValueReference, 0), + setValues::Array{<:Real} = zeros(Real, 0), + getValueReferences::Array{fmi2ValueReference} = zeros(fmi2ValueReference, 0), +) fmi2DoStepCS(str, dt, setValueReferences, setValues, getValueReferences) end export fmiDoStepCS @@ -80,9 +83,7 @@ DEPRECATED: Wrapper. Call ```fmi2InputDoStepCSOutput``` for more information. """ -function fmiInputDoStepCSOutput(str::FMI2Struct, - dt::Real, - u::Array{<:Real}) +function fmiInputDoStepCSOutput(str::FMI2Struct, dt::Real, u::Array{<:Real}) fmi2InputDoStepCSOutput(str, dt, u) end export fmiInputDoStepCSOutput @@ -96,11 +97,11 @@ DEPRECATED: Sets all FMU inputs to `u`, performs a ´´´fmi2DoStep´´´ and returns all FMU outputs. """ -function fmi2InputDoStepCSOutput(fmu::FMU2, - dt::Real, - u::Array{<:Real}) - - @assert fmi2IsCoSimulation(fmu) ["fmi2InputDoStepCSOutput(...): As in the name, this function only supports CS-FMUs."] +function fmi2InputDoStepCSOutput(fmu::FMU2, dt::Real, u::Array{<:Real}) + + @assert fmi2IsCoSimulation(fmu) [ + "fmi2InputDoStepCSOutput(...): As in the name, this function only supports CS-FMUs.", + ] # fmi2DoStepCS(fmu, dt, # fmu.modelDescription.inputValueReferences, @@ -112,8 +113,8 @@ function fmi2InputDoStepCSOutput(fmu::FMU2, y = zeros(length(y_refs)) c = fmu.components[end] - - y, _ = c(y=y, y_refs=y_refs, u=u, u_refs=u_refs) + + y, _ = c(y = y, y_refs = y_refs, u = u, u_refs = u_refs) # ignore_derivatives() do # fmi2DoStep(c, dt) @@ -123,11 +124,13 @@ function fmi2InputDoStepCSOutput(fmu::FMU2, end export fmi2InputDoStepCSOutput -function fmi2DoStepCS(fmu::FMU2, +function fmi2DoStepCS( + fmu::FMU2, dt::Real, setValueReferences::Array{fmi2ValueReference} = zeros(fmi2ValueReference, 0), setValues::Array{<:Real} = zeros(Real, 0), - getValueReferences::Array{fmi2ValueReference} = zeros(fmi2ValueReference, 0)) + getValueReferences::Array{fmi2ValueReference} = zeros(fmi2ValueReference, 0), +) y_refs = setValueReferences u_refs = getValueReferences @@ -135,8 +138,8 @@ function fmi2DoStepCS(fmu::FMU2, u = setValues c = fmu.components[end] - - y, _ = c(y=y, y_refs=y_refs, u=u, u_refs=u_refs) + + y, _ = c(y = y, y_refs = y_refs, u = u, u_refs = u_refs) # ignore_derivatives() do # fmi2DoStep(c, dt) diff --git a/src/flux_overload.jl b/src/flux_overload.jl index b3e25e5b..7c66d371 100644 --- a/src/flux_overload.jl +++ b/src/flux_overload.jl @@ -4,4 +4,4 @@ # # feed through -params = Flux.params \ No newline at end of file +params = Flux.params diff --git a/src/hotfixes.jl b/src/hotfixes.jl index c51eecd4..7168ba45 100644 --- a/src/hotfixes.jl +++ b/src/hotfixes.jl @@ -24,24 +24,41 @@ end # https://github.com/SciML/DiffEqBase.jl/blob/c7d949e062d9f382e6ef289d6d28e3c53e7202bc/src/internal_itp.jl#L13 using FMISensitivity.SciMLSensitivity.SciMLBase using FMISensitivity.SciMLSensitivity.DiffEqBase -using FMISensitivity.SciMLSensitivity.DiffEqBase: InternalITP, nextfloat_tdir, prevfloat_tdir, ReturnCode +using FMISensitivity.SciMLSensitivity.DiffEqBase: + InternalITP, nextfloat_tdir, prevfloat_tdir, ReturnCode import FMISensitivity.SciMLSensitivity.SciMLBase: solve -function SciMLBase.solve(prob::IntervalNonlinearProblem{IP, Tuple{T, T2}}, alg::InternalITP, +function SciMLBase.solve( + prob::IntervalNonlinearProblem{IP,Tuple{T,T2}}, + alg::InternalITP, args...; - maxiters = 1000, kwargs...) where {IP, T, T2} + maxiters = 1000, + kwargs..., +) where {IP,T,T2} f = Base.Fix2(prob.f, prob.p) left, right = prob.tspan # a and b fl, fr = f(left), f(right) ϵ = eps(T) if iszero(fl) - return SciMLBase.build_solution(prob, alg, left, fl; - retcode = ReturnCode.ExactSolutionLeft, left = left, - right = right) + return SciMLBase.build_solution( + prob, + alg, + left, + fl; + retcode = ReturnCode.ExactSolutionLeft, + left = left, + right = right, + ) elseif iszero(fr) - return SciMLBase.build_solution(prob, alg, right, fr; - retcode = ReturnCode.ExactSolutionRight, left = left, - right = right) + return SciMLBase.build_solution( + prob, + alg, + right, + fr; + retcode = ReturnCode.ExactSolutionRight, + left = left, + right = right, + ) end #defining variables/cache k1 = T(alg.k1) @@ -96,20 +113,39 @@ function SciMLBase.solve(prob::IntervalNonlinearProblem{IP, Tuple{T, T2}}, alg:: else left = prevfloat_tdir(xp, prob.tspan...) right = xp - return SciMLBase.build_solution(prob, alg, left, f(left); - retcode = ReturnCode.Success, left = left, - right = right) + return SciMLBase.build_solution( + prob, + alg, + left, + f(left); + retcode = ReturnCode.Success, + left = left, + right = right, + ) end i += 1 mid = (left + right) / 2 ϵ_s /= 2 if nextfloat_tdir(left, prob.tspan...) == right - return SciMLBase.build_solution(prob, alg, left, fl; - retcode = ReturnCode.FloatingPointLimit, left = left, - right = right) + return SciMLBase.build_solution( + prob, + alg, + left, + fl; + retcode = ReturnCode.FloatingPointLimit, + left = left, + right = right, + ) end end - return SciMLBase.build_solution(prob, alg, left, fl; retcode = ReturnCode.MaxIters, - left = left, right = right) -end \ No newline at end of file + return SciMLBase.build_solution( + prob, + alg, + left, + fl; + retcode = ReturnCode.MaxIters, + left = left, + right = right, + ) +end diff --git a/src/layers.jl b/src/layers.jl index 3696d126..6982c4be 100644 --- a/src/layers.jl +++ b/src/layers.jl @@ -14,13 +14,17 @@ struct FMUParameterRegistrator{T} fmu::FMU2 p_refs::AbstractArray{<:fmi2ValueReference} p::AbstractArray{T} - - function FMUParameterRegistrator{T}(fmu::FMU2, p_refs::fmi2ValueReferenceFormat, p::AbstractArray{T}) where {T} + + function FMUParameterRegistrator{T}( + fmu::FMU2, + p_refs::fmi2ValueReferenceFormat, + p::AbstractArray{T}, + ) where {T} @assert length(p_refs) == length(p) "`p_refs` and `p` need to be the same length!" p_refs = prepareValueReference(fmu, p_refs) - fmu.default_p_refs = p_refs - fmu.default_p = p + fmu.default_p_refs = p_refs + fmu.default_p = p for c in fmu.instances c.default_p_refs = p_refs c.default_p = p @@ -29,58 +33,62 @@ struct FMUParameterRegistrator{T} return new{T}(fmu, p_refs, p) end - function FMUParameterRegistrator(fmu::FMU2, p_refs::fmi2ValueReferenceFormat, p::AbstractArray{T}) where {T} + function FMUParameterRegistrator( + fmu::FMU2, + p_refs::fmi2ValueReferenceFormat, + p::AbstractArray{T}, + ) where {T} return FMUParameterRegistrator{T}(fmu, p_refs, p) end end export FMUParameterRegistrator function (l::FMUParameterRegistrator)(x) - + l.fmu.default_p_refs = l.p_refs - l.fmu.default_p = l.p + l.fmu.default_p = l.p for c in l.fmu.instances c.default_p_refs = l.p_refs c.default_p = l.p end - + return x end -Flux.@functor FMUParameterRegistrator (p, ) +Flux.@functor FMUParameterRegistrator (p,) ### TimeLayer ### """ A neutral layer that calls a function `fct` with current FMU time as input. """ -struct FMUTimeLayer{F, O} +struct FMUTimeLayer{F,O} fmu::FMU2 fct::F offset::O - function FMUTimeLayer{F, O}(fmu::FMU2, fct::F, offset::O) where {F, O} - return new{F, O}(fmu, fct, offset) + function FMUTimeLayer{F,O}(fmu::FMU2, fct::F, offset::O) where {F,O} + return new{F,O}(fmu, fct, offset) end - function FMUTimeLayer(fmu::FMU2, fct::F, offset::O) where {F, O} - return FMUTimeLayer{F, O}(fmu, fct, offset) + function FMUTimeLayer(fmu::FMU2, fct::F, offset::O) where {F,O} + return FMUTimeLayer{F,O}(fmu, fct, offset) end end export FMUTimeLayer function (l::FMUTimeLayer)(x) - - if hasCurrentInstance(l.fmu) - c = getCurrentInstance(l.fmu) + + if hasCurrentInstance(l.fmu) + c = getCurrentInstance(l.fmu) l.fct(c.default_t + l.offset[1]) end - + return x end -Flux.@functor FMUTimeLayer (offset, ) +Flux.@functor FMUTimeLayer (offset,) ### ParameterRegistrator ### @@ -89,7 +97,7 @@ ToDo. """ struct ParameterRegistrator{T} p::AbstractArray{T} - + function ParameterRegistrator{T}(p::AbstractArray{T}) where {T} return new{T}(p) end @@ -104,23 +112,23 @@ function (l::ParameterRegistrator)(x) return x end -Flux.@functor ParameterRegistrator (p, ) +Flux.@functor ParameterRegistrator (p,) ### SimultaniousZeroCrossing ### """ Forces a simultaniuos zero crossing together with a given value by function. """ -struct SimultaniousZeroCrossing{T, F} +struct SimultaniousZeroCrossing{T,F} m::T # scaling factor fct::F - - function SimultaniousZeroCrossing{T, F}(m::T, fct::F) where {T, F} - return new{T, F}(m, fct) + + function SimultaniousZeroCrossing{T,F}(m::T, fct::F) where {T,F} + return new{T,F}(m, fct) end - function SimultaniousZeroCrossing(m::T, fct::F) where {T, F} - return SimultaniousZeroCrossing{T, F}(m, fct) + function SimultaniousZeroCrossing(m::T, fct::F) where {T,F} + return SimultaniousZeroCrossing{T,F}(m, fct) end end export SimultaniousZeroCrossing @@ -129,7 +137,7 @@ function (l::SimultaniousZeroCrossing)(x) return x * l.m * l.fct() end -Flux.@functor SimultaniousZeroCrossing (m, ) +Flux.@functor SimultaniousZeroCrossing (m,) ### SHIFTSCALE ### @@ -139,7 +147,7 @@ ToDo. struct ShiftScale{T} shift::AbstractArray{T} scale::AbstractArray{T} - + function ShiftScale{T}(shift::AbstractArray{T}, scale::AbstractArray{T}) where {T} inst = new(shift, scale) return inst @@ -150,14 +158,20 @@ struct ShiftScale{T} end # initialize for data array - function ShiftScale(data::AbstractArray{<:AbstractArray{T}}; range::Union{Symbol, UnitRange{<:Integer}}=-1:1) where {T} + function ShiftScale( + data::AbstractArray{<:AbstractArray{T}}; + range::Union{Symbol,UnitRange{<:Integer}} = -1:1, + ) where {T} shift = -mean.(data) scale = nothing if range == :NormalDistribution scale = 1.0 ./ std.(data) elseif isa(range, UnitRange{<:Integer}) - scale = 1.0 ./ (collect(max(d...) for d in data) - collect(min(d...) for d in data)) .* (range[end] - range[1]) + scale = + 1.0 ./ + (collect(max(d...) for d in data) - collect(min(d...) for d in data)) .* + (range[end] - range[1]) else @assert false "Unsupported scaleMode, supported is `:NormalDistribution` or `UnitRange{<:Integer}`" end @@ -170,7 +184,7 @@ export ShiftScale function (l::ShiftScale)(x) x_proc = (x .+ l.shift) .* l.scale - + return x_proc end @@ -184,7 +198,7 @@ ToDo. struct ScaleShift{T} scale::AbstractArray{T} shift::AbstractArray{T} - + function ScaleShift{T}(scale::AbstractArray{T}, shift::AbstractArray{T}) where {T} inst = new(scale, shift) return inst @@ -195,7 +209,7 @@ struct ScaleShift{T} end # init ScaleShift with inverse transformation of a given ShiftScale - function ScaleShift(l::ShiftScale{T}; indices=1:length(l.scale)) where {T} + function ScaleShift(l::ShiftScale{T}; indices = 1:length(l.scale)) where {T} return ScaleShift{T}(1.0 ./ l.scale[indices], -1.0 .* l.shift[indices]) end @@ -210,7 +224,7 @@ export ScaleShift function (l::ScaleShift)(x) x_proc = (x .* l.scale) .+ l.shift - + return x_proc end @@ -220,14 +234,20 @@ Flux.@functor ScaleShift (scale, shift) struct ScaleSum{T} scale::AbstractArray{T} - groups::Union{AbstractVector{<:AbstractVector{<:Integer}}, Nothing} - - function ScaleSum{T}(scale::AbstractArray{T}, groups::Union{AbstractVector{<:AbstractVector{<:Integer}}, Nothing}=nothing) where {T} + groups::Union{AbstractVector{<:AbstractVector{<:Integer}},Nothing} + + function ScaleSum{T}( + scale::AbstractArray{T}, + groups::Union{AbstractVector{<:AbstractVector{<:Integer}},Nothing} = nothing, + ) where {T} inst = new(scale, groups) return inst end - function ScaleSum(scale::AbstractArray{T}, groups::Union{AbstractVector{<:AbstractVector{<:Integer}}, Nothing}=nothing) where {T} + function ScaleSum( + scale::AbstractArray{T}, + groups::Union{AbstractVector{<:AbstractVector{<:Integer}},Nothing} = nothing, + ) where {T} return ScaleSum{T}(scale, groups) end end @@ -243,7 +263,7 @@ function (l::ScaleSum)(x) end end -Flux.@functor ScaleSum (scale, ) +Flux.@functor ScaleSum (scale,) ### CACHE ### @@ -262,7 +282,7 @@ function (l::CacheLayer)(x) tid = Threads.threadid() l.cache[tid] = x - + return x end @@ -270,7 +290,7 @@ end struct CacheRetrieveLayer cacheLayer::CacheLayer - + function CacheRetrieveLayer(cacheLayer::CacheLayer) inst = new(cacheLayer) return inst @@ -282,18 +302,20 @@ function (l::CacheRetrieveLayer)(args...) tid = Threads.threadid() values = zeros(Real, 0) - - for arg in args + + for arg in args if isa(arg, Integer) val = l.cacheLayer.cache[tid][arg] push!(values, val) elseif isa(arg, AbstractArray) && length(arg) == 0 - @warn "Deploying empty arrays `[]` in CacheRetrieveLayer is not necessary anymore, just remove them.\nThis warning is only printed once." maxlog=1 + @warn "Deploying empty arrays `[]` in CacheRetrieveLayer is not necessary anymore, just remove them.\nThis warning is only printed once." maxlog = + 1 # nothing to do here elseif isa(arg, AbstractArray{<:Integer}) && length(arg) == 1 - @warn "Deploying single element arrays `$(arg)` in CacheRetrieveLayer is not necessary anymore, just write `$(arg[1])`.\nThis warning is only printed once." maxlog=1 + @warn "Deploying single element arrays `$(arg)` in CacheRetrieveLayer is not necessary anymore, just write `$(arg[1])`.\nThis warning is only printed once." maxlog = + 1 val = l.cacheLayer.cache[tid][arg] push!(values, val...) diff --git a/src/losses.jl b/src/losses.jl index 7a4c571f..92b9fc8d 100644 --- a/src/losses.jl +++ b/src/losses.jl @@ -13,15 +13,23 @@ mse = Flux.Losses.mse mae = Flux.Losses.mae function last_element_rel(fun, a::AbstractArray, b::AbstractArray, lastElementRatio::Real) - return (1.0-lastElementRatio) * fun(a[1:end-1], b[1:end-1]) + - lastElementRatio * fun(a[ end ], b[ end ]) + return (1.0 - lastElementRatio) * fun(a[1:end-1], b[1:end-1]) + + lastElementRatio * fun(a[end], b[end]) end -function mse_last_element_rel(a::AbstractArray, b::AbstractArray, lastElementRatio::Real=0.25) +function mse_last_element_rel( + a::AbstractArray, + b::AbstractArray, + lastElementRatio::Real = 0.25, +) return last_element_rel(mse, a, b, lastElementRatio) end -function mae_last_element_rel(a::AbstractArray, b::AbstractArray, lastElementRatio::Real=0.25) +function mae_last_element_rel( + a::AbstractArray, + b::AbstractArray, + lastElementRatio::Real = 0.25, +) return last_element_rel(mae, a, b, lastElementRatio) end @@ -61,47 +69,61 @@ function max_dev(a::AbstractArray, b::AbstractArray, dev::AbstractArray) return Δ end -function mae_last_element_rel_dev(a::AbstractArray, b::AbstractArray, dev::AbstractArray, lastElementRatio::Real) +function mae_last_element_rel_dev( + a::AbstractArray, + b::AbstractArray, + dev::AbstractArray, + lastElementRatio::Real, +) num = length(a) Δ = deviation(a, b, dev) - Δ[1:end-1] .*= (1.0-lastElementRatio) - Δ[ end ] *= lastElementRatio + Δ[1:end-1] .*= (1.0 - lastElementRatio) + Δ[end] *= lastElementRatio Δ = sum(Δ) / num return Δ end -function mse_last_element_rel_dev(a::AbstractArray, b::AbstractArray, dev::AbstractArray, lastElementRatio::Real) +function mse_last_element_rel_dev( + a::AbstractArray, + b::AbstractArray, + dev::AbstractArray, + lastElementRatio::Real, +) num = length(a) Δ = deviation(a, b, dev) Δ = Δ .^ 2 - Δ[1:end-1] .*= (1.0-lastElementRatio) - Δ[ end ] *= lastElementRatio + Δ[1:end-1] .*= (1.0 - lastElementRatio) + Δ[end] *= lastElementRatio Δ = sum(Δ) / num return Δ end -function stiffness_corridor(solution::FMUSolution, corridor::AbstractArray{<:AbstractArray{<:Tuple{Real, Real}}}; lossFct=Flux.Losses.mse) +function stiffness_corridor( + solution::FMUSolution, + corridor::AbstractArray{<:AbstractArray{<:Tuple{Real,Real}}}; + lossFct = Flux.Losses.mse, +) @assert !isnothing(solution.eigenvalues) "stiffness_corridor: Need eigenvalue information, that is not present in the given `FMUSolution`. Use keyword `recordEigenvalues=true` for FMU or NeuralFMU simulation." - eigs_over_time = solution.eigenvalues.saveval + eigs_over_time = solution.eigenvalues.saveval num_eigs_over_time = length(eigs_over_time) - + @assert num_eigs_over_time == length(corridor) "stiffness_corridor: length of time points with eigenvalues $(num_eigs_over_time) doesn't match time points in corridor $(length(corridor))." l = 0.0 - for i in 2:num_eigs_over_time + for i = 2:num_eigs_over_time eigs = eigs_over_time[i] - num_eigs = Int(length(eigs)/2) + num_eigs = Int(length(eigs) / 2) - for j in 1:num_eigs + for j = 1:num_eigs re = eigs[(j-1)*2+1] im = eigs[j*2] c_min, c_max = corridor[i][j] - if re > c_max + if re > c_max l += lossFct(re, c_max) / num_eigs / num_eigs_over_time end - if re < c_min + if re < c_min l += lossFct(c_min, re) / num_eigs / num_eigs_over_time end end @@ -110,28 +132,32 @@ function stiffness_corridor(solution::FMUSolution, corridor::AbstractArray{<:Abs return l end -function stiffness_corridor(solution::FMUSolution, corridor::AbstractArray{<:Tuple{Real, Real}}; lossFct=Flux.Losses.mse) +function stiffness_corridor( + solution::FMUSolution, + corridor::AbstractArray{<:Tuple{Real,Real}}; + lossFct = Flux.Losses.mse, +) @assert !isnothing(solution.eigenvalues) "stiffness_corridor: Need eigenvalue information, that is not present in the given `FMUSolution`. Use keyword `recordEigenvalues=true` for FMU or NeuralFMU simulation." - eigs_over_time = solution.eigenvalues.saveval + eigs_over_time = solution.eigenvalues.saveval num_eigs_over_time = length(eigs_over_time) - + @assert num_eigs_over_time == length(corridor) "stiffness_corridor: length of time points with eigenvalues $(num_eigs_over_time) doesn't match time points in corridor $(length(corridor))." l = 0.0 - for i in 2:num_eigs_over_time + for i = 2:num_eigs_over_time eigs = eigs_over_time[i] - num_eigs = Int(length(eigs)/2) + num_eigs = Int(length(eigs) / 2) c_min, c_max = corridor[i] - for j in 1:num_eigs + for j = 1:num_eigs re = eigs[(j-1)*2+1] im = eigs[j*2] - if re > c_max + if re > c_max l += lossFct(re, c_max) / num_eigs / num_eigs_over_time end - if re < c_min + if re < c_min l += lossFct(c_min, re) / num_eigs / num_eigs_over_time end end @@ -140,27 +166,31 @@ function stiffness_corridor(solution::FMUSolution, corridor::AbstractArray{<:Tup return l end -function stiffness_corridor(solution::FMUSolution, corridor::Tuple{Real, Real}; lossFct=Flux.Losses.mse) +function stiffness_corridor( + solution::FMUSolution, + corridor::Tuple{Real,Real}; + lossFct = Flux.Losses.mse, +) @assert !isnothing(solution.eigenvalues) "stiffness_corridor: Need eigenvalue information, that is not present in the given `FMUSolution`. Use keyword `recordEigenvalues=true` for FMU or NeuralFMU simulation." - eigs_over_time = solution.eigenvalues.saveval + eigs_over_time = solution.eigenvalues.saveval num_eigs_over_time = length(eigs_over_time) - + c_min, c_max = corridor l = 0.0 - for i in 2:num_eigs_over_time + for i = 2:num_eigs_over_time eigs = eigs_over_time[i] - num_eigs = Int(length(eigs)/2) + num_eigs = Int(length(eigs) / 2) - for j in 1:num_eigs + for j = 1:num_eigs re = eigs[(j-1)*2+1] im = eigs[j*2] - if re > c_max + if re > c_max l += lossFct(re, c_max) / num_eigs / num_eigs_over_time end - if re < c_min + if re < c_min l += lossFct(re, c_min) / num_eigs / num_eigs_over_time end end @@ -169,77 +199,110 @@ function stiffness_corridor(solution::FMUSolution, corridor::Tuple{Real, Real}; return l end -function loss(model, batchElement::FMU2BatchElement; - logLoss::Bool=true, - lossFct=Flux.Losses.mse, p=nothing) +function loss( + model, + batchElement::FMU2BatchElement; + logLoss::Bool = true, + lossFct = Flux.Losses.mse, + p = nothing, +) model = nfmu.neuralODE.model[layers] # evaluate model - result = run!(model, batchElement, p=p) + result = run!(model, batchElement, p = p) - return loss!(batchElement, lossFct; logLoss=logLoss) + return loss!(batchElement, lossFct; logLoss = logLoss) end -function loss(nfmu::NeuralFMU, batch::AbstractArray{<:FMU2BatchElement}; - batchIndex::Integer=rand(1:length(batch)), - lossFct=Flux.Losses.mse, - logLoss::Bool=true, - solvekwargs...) +function loss( + nfmu::NeuralFMU, + batch::AbstractArray{<:FMU2BatchElement}; + batchIndex::Integer = rand(1:length(batch)), + lossFct = Flux.Losses.mse, + logLoss::Bool = true, + solvekwargs..., +) # cut out data batch from data targets_data = batch[batchIndex].targets - nextBatchElement = nothing + nextBatchElement = nothing if batchIndex < length(batch) && batch[batchIndex].tStop == batch[batchIndex+1].tStart nextBatchElement = batch[batchIndex+1] end - solution = run!(nfmu, batch[batchIndex]; nextBatchElement=nextBatchElement, progressDescr="Sim. Batch $(batchIndex)/$(length(batch)) |", solvekwargs...) - + solution = run!( + nfmu, + batch[batchIndex]; + nextBatchElement = nextBatchElement, + progressDescr = "Sim. Batch $(batchIndex)/$(length(batch)) |", + solvekwargs..., + ) + if !solution.success - logWarning(nfmu.fmu, "Solving the NeuralFMU as part of the loss function failed with return code `$(solution.states.retcode)`.\nThis is often because the ODE cannot be solved. Did you initialize the NeuralFMU model?\nOften additional solver errors/warnings are printed before this warning.\nHowever, it is tried to compute a loss on the partial retrieved solution from $(unsense(solution.states.t[1]))s to $(unsense(solution.states.t[end]))s.") - return Inf + logWarning( + nfmu.fmu, + "Solving the NeuralFMU as part of the loss function failed with return code `$(solution.states.retcode)`.\nThis is often because the ODE cannot be solved. Did you initialize the NeuralFMU model?\nOften additional solver errors/warnings are printed before this warning.\nHowever, it is tried to compute a loss on the partial retrieved solution from $(unsense(solution.states.t[1]))s to $(unsense(solution.states.t[end]))s.", + ) + return Inf else - return loss!(batch[batchIndex], lossFct; logLoss=logLoss) - end + return loss!(batch[batchIndex], lossFct; logLoss = logLoss) + end end -function loss(model, batch::AbstractArray{<:FMU2BatchElement}; - batchIndex::Integer=rand(1:length(batch)), - lossFct=Flux.Losses.mse, - logLoss::Bool=true, p=nothing) +function loss( + model, + batch::AbstractArray{<:FMU2BatchElement}; + batchIndex::Integer = rand(1:length(batch)), + lossFct = Flux.Losses.mse, + logLoss::Bool = true, + p = nothing, +) run!(model, batch[batchIndex], p) - return loss!(batch[batchIndex], lossFct; logLoss=logLoss) + return loss!(batch[batchIndex], lossFct; logLoss = logLoss) end -function batch_loss(neuralFMU::ME_NeuralFMU, batch::AbstractArray{<:FMU2BatchElement}; update::Bool=false, logLoss::Bool=false, lossFct=nothing, kwargs...) +function batch_loss( + neuralFMU::ME_NeuralFMU, + batch::AbstractArray{<:FMU2BatchElement}; + update::Bool = false, + logLoss::Bool = false, + lossFct = nothing, + kwargs..., +) accu = nothing - if update + if update @assert lossFct != nothing "update=true, but no keyword lossFct provided. Please provide one." numBatch = length(batch) - for i in 1:numBatch + for i = 1:numBatch b = batch[i] - b_next = nothing + b_next = nothing if i < numBatch && batch[i].tStop == batch[i+1].tStart b_next = batch[i+1] end if !isnothing(b.xStart) - run!(neuralFMU, b; nextBatchElement=b_next, progressDescr="Sim. Batch $(i)/$(numBatch) |", kwargs...) + run!( + neuralFMU, + b; + nextBatchElement = b_next, + progressDescr = "Sim. Batch $(i)/$(numBatch) |", + kwargs..., + ) end - + if isnothing(accu) - accu = loss!(b, lossFct; logLoss=logLoss) + accu = loss!(b, lossFct; logLoss = logLoss) else - accu += loss!(b, lossFct; logLoss=logLoss) + accu += loss!(b, lossFct; logLoss = logLoss) end - + end else for b in batch @@ -247,9 +310,9 @@ function batch_loss(neuralFMU::ME_NeuralFMU, batch::AbstractArray{<:FMU2BatchEle @assert length(b.losses) > 0 "batch_loss(): `update=false` but no existing losses for batch element $(b)" if isnothing(accu) - accu = b.losses[end].loss + accu = b.losses[end].loss else - accu += b.losses[end].loss + accu += b.losses[end].loss end end end @@ -257,22 +320,29 @@ function batch_loss(neuralFMU::ME_NeuralFMU, batch::AbstractArray{<:FMU2BatchEle return accu end -function batch_loss(model, batch::AbstractArray{<:FMU2BatchElement}; update::Bool=false, logLoss::Bool=false, lossFct=nothing, p=nothing) +function batch_loss( + model, + batch::AbstractArray{<:FMU2BatchElement}; + update::Bool = false, + logLoss::Bool = false, + lossFct = nothing, + p = nothing, +) accu = nothing - if update + if update @assert lossFct != nothing "update=true, but no keyword lossFct provided. Please provide one." numBatch = length(batch) - for i in 1:numBatch + for i = 1:numBatch b = batch[i] - + run!(model, b, p) - + if isnothing(accu) - accu = loss!(b, lossFct; logLoss=logLoss) + accu = loss!(b, lossFct; logLoss = logLoss) else - accu += loss!(b, lossFct; logLoss=logLoss) + accu += loss!(b, lossFct; logLoss = logLoss) end end @@ -292,8 +362,8 @@ function batch_loss(model, batch::AbstractArray{<:FMU2BatchElement}; update::Boo end mutable struct ToggleLoss - index::Int - losses + index::Int + losses::Any function ToggleLoss(losses...) @assert length(losses) >= 2 "ToggleLoss needs at least 2 losses, $(length(losses)) given." @@ -305,9 +375,9 @@ function (t::ToggleLoss)(args...; kwargs...) ret = t.losses[t.index](args...; kwargs...) t.index += 1 if t.index > length(t.losses) - t.index = 1 + t.index = 1 end - return ret + return ret end end # module diff --git a/src/misc.jl b/src/misc.jl index 869fa275..b69056ee 100644 --- a/src/misc.jl +++ b/src/misc.jl @@ -40,14 +40,14 @@ function lin_interp(t, x, t_sample) dt = t_right - t_left h = t_sample - t_left - x_left + dx/dt*h + x_left + dx / dt * h end """ Writes/Copies flatted (Flux.destructure) training parameters `p_net` to non-flat model `net` with data offset `c`. """ -function transferFlatParams!(net, p_net, c=1; netRange=nothing) - +function transferFlatParams!(net, p_net, c = 1; netRange = nothing) + if netRange == nothing netRange = 1:length(net.layers) end @@ -55,22 +55,22 @@ function transferFlatParams!(net, p_net, c=1; netRange=nothing) if !isa(net.layers[l], Flux.Dense) continue end - ni = size(net.layers[l].weight,2) - no = size(net.layers[l].weight,1) + ni = size(net.layers[l].weight, 2) + no = size(net.layers[l].weight, 1) w = zeros(no, ni) b = zeros(no) - for i in 1:ni - for o in 1:no - w[o,i] = p_net[1][c + (i-1)*no + (o-1)] + for i = 1:ni + for o = 1:no + w[o, i] = p_net[1][c+(i-1)*no+(o-1)] end end - c += ni*no + c += ni * no - for o in 1:no - b[o] = p_net[1][c + (o-1)] + for o = 1:no + b[o] = p_net[1][c+(o-1)] end c += no @@ -80,8 +80,8 @@ function transferFlatParams!(net, p_net, c=1; netRange=nothing) end end -function transferParams!(net, p_net, c=1; netRange=nothing) - +function transferParams!(net, p_net, c = 1; netRange = nothing) + if netRange == nothing netRange = 1:length(net.layers) end @@ -90,11 +90,11 @@ function transferParams!(net, p_net, c=1; netRange=nothing) continue end - for w in 1:length(net.layers[l].weight) + for w = 1:length(net.layers[l].weight) net.layers[l].weight[w] = p_net[1+(l-1)*2][w] end - - for b in 1:length(net.layers[l].bias) + + for b = 1:length(net.layers[l].bias) net.layers[l].bias[b] = p_net[l*2][b] end end @@ -117,7 +117,7 @@ function timeToIndex(ts::AbstractArray{<:Real}, target::Real) tLen = length(ts) @assert target >= tStart "timeToIndex(...): Time ($(target)) < tStart ($(tStart))" - + # because of the event handling condition, `target` can be outside of the simulation interval! # OLD: @assert target <= tStop "timeToIndex(...): Time ($(target)) > tStop ($(tStop))" # NEW: @@ -139,13 +139,13 @@ function timeToIndex(ts::AbstractArray{<:Real}, target::Real) # estimate start value steps = 0 - i = min(max(round(Integer, (target-tStart)/(tStop-tStart)*tLen), 1), tLen) + i = min(max(round(Integer, (target - tStart) / (tStop - tStart) * tLen), 1), tLen) lastStep = Inf while !(ts[i] <= target && ts[i+1] > target) - dt = target - ts[i] - step = round(Integer, dt/(tStop-tStart)*tLen) - if abs(step) >= lastStep - step = Int(sign(dt))*(lastStep-1) + dt = target - ts[i] + step = round(Integer, dt / (tStop - tStart) * tLen) + if abs(step) >= lastStep + step = Int(sign(dt)) * (lastStep - 1) end if step == 0 step = Int(sign(dt)) @@ -169,10 +169,10 @@ function timeToIndex(ts::AbstractArray{<:Real}, target::Real) t = ts[i] next_t = ts[i+1] @assert t <= target && next_t >= target "No fitting time found, numerical issue." - if (target-t) < (next_t-target) - return i - else - return i+1 + if (target - t) < (next_t - target) + return i + else + return i + 1 end - -end \ No newline at end of file + +end diff --git a/src/neural.jl b/src/neural.jl index 25d467f6..85cba25f 100644 --- a/src/neural.jl +++ b/src/neural.jl @@ -3,26 +3,57 @@ # Licensed under the MIT license. See LICENSE file in the project root for details. # -import FMIImport.FMIBase: assert_integrator_valid, isdual, istracked, issense, undual, unsense, unsense_copy, untrack, FMUSnapshot -import FMIImport: finishSolveFMU, handleEvents, prepareSolveFMU, snapshot_if_needed!, getSnapshot +import FMIImport.FMIBase: + assert_integrator_valid, + isdual, + istracked, + issense, + undual, + unsense, + unsense_copy, + untrack, + FMUSnapshot +import FMIImport: + finishSolveFMU, handleEvents, prepareSolveFMU, snapshot_if_needed!, getSnapshot import Optim import FMIImport.FMIBase.ProgressMeter -import FMISensitivity.SciMLSensitivity.SciMLBase: CallbackSet, ContinuousCallback, ODESolution, ReturnCode, RightRootFind, - VectorContinuousCallback, set_u!, terminate!, u_modified!, build_solution +import FMISensitivity.SciMLSensitivity.SciMLBase: + CallbackSet, + ContinuousCallback, + ODESolution, + ReturnCode, + RightRootFind, + VectorContinuousCallback, + set_u!, + terminate!, + u_modified!, + build_solution import OrdinaryDiffEq: isimplicit, alg_autodiff using FMISensitivity.ReverseDiff: TrackedArray -import FMISensitivity.SciMLSensitivity: InterpolatingAdjoint, ReverseDiffVJP, AutoForwardDiff +import FMISensitivity.SciMLSensitivity: + InterpolatingAdjoint, ReverseDiffVJP, AutoForwardDiff import ThreadPools import FMIImport.FMIBase using FMIImport.FMIBase.DiffEqCallbacks using FMIImport.FMIBase.SciMLBase: ODEFunction, ODEProblem, solve -using FMIImport.FMIBase: fmi2ComponentState, - fmi2ComponentStateContinuousTimeMode, fmi2ComponentStateError, - fmi2ComponentStateEventMode, fmi2ComponentStateFatal, - fmi2ComponentStateInitializationMode, fmi2ComponentStateInstantiated, - fmi2ComponentStateTerminated, fmi2StatusOK, fmi2Type, fmi2TypeCoSimulation, - fmi2TypeModelExchange, logError, logInfo, logWarning , fast_copy! +using FMIImport.FMIBase: + fmi2ComponentState, + fmi2ComponentStateContinuousTimeMode, + fmi2ComponentStateError, + fmi2ComponentStateEventMode, + fmi2ComponentStateFatal, + fmi2ComponentStateInitializationMode, + fmi2ComponentStateInstantiated, + fmi2ComponentStateTerminated, + fmi2StatusOK, + fmi2Type, + fmi2TypeCoSimulation, + fmi2TypeModelExchange, + logError, + logInfo, + logWarning, + fast_copy! using FMISensitivity.SciMLSensitivity: ForwardDiffSensitivity, InterpolatingAdjoint, ReverseDiffVJP, ZygoteVJP import DifferentiableEigen @@ -43,48 +74,48 @@ abstract type NeuralFMU end """ Structure definition for a NeuralFMU, that runs in mode `Model Exchange` (ME). """ -mutable struct ME_NeuralFMU{M, R} <: NeuralFMU +mutable struct ME_NeuralFMU{M,R} <: NeuralFMU model::M p::AbstractArray{<:Real} re::R - solvekwargs + solvekwargs::Any - re_model - re_p + re_model::Any + re_p::Any fmu::FMU - tspan - saveat - saved_values - recordValues - solver + tspan::Any + saveat::Any + saved_values::Any + recordValues::Any + solver::Any - valueStack + valueStack::Any customCallbacksBefore::Array customCallbacksAfter::Array - x0::Union{Array{Float64}, Nothing} + x0::Union{Array{Float64},Nothing} firstRun::Bool - - tolerance::Union{Real, Nothing} - parameters::Union{Dict{<:Any, <:Any}, Nothing} - + + tolerance::Union{Real,Nothing} + parameters::Union{Dict{<:Any,<:Any},Nothing} + modifiedState::Bool execution_start::Real - condition_buffer::Union{AbstractArray{<:Real}, Nothing} + condition_buffer::Union{AbstractArray{<:Real},Nothing} snapshots::Bool - function ME_NeuralFMU{M, R}(model::M, p::AbstractArray{<:Real}, re::R) where {M, R} + function ME_NeuralFMU{M,R}(model::M, p::AbstractArray{<:Real}, re::R) where {M,R} inst = new() - inst.model = model - inst.p = p - inst.re = re + inst.model = model + inst.p = p + inst.re = re inst.x0 = nothing inst.saveat = nothing @@ -106,27 +137,27 @@ mutable struct ME_NeuralFMU{M, R} <: NeuralFMU inst.condition_buffer = nothing inst.snapshots = false - - return inst + + return inst end end """ Structure definition for a NeuralFMU, that runs in mode `Co-Simulation` (CS). """ -mutable struct CS_NeuralFMU{F, C} <: NeuralFMU - model +mutable struct CS_NeuralFMU{F,C} <: NeuralFMU + model::Any fmu::F - - tspan - - p::Union{AbstractArray{<:Real}, Nothing} - re # restrucure function + + tspan::Any + + p::Union{AbstractArray{<:Real},Nothing} + re::Any # restrucure function snapshots::Bool - function CS_NeuralFMU{F, C}() where {F, C} - inst = new{F, C}() + function CS_NeuralFMU{F,C}() where {F,C} + inst = new{F,C}() inst.re = nothing inst.p = nothing @@ -137,7 +168,7 @@ mutable struct CS_NeuralFMU{F, C} <: NeuralFMU end end -function evaluateModel(nfmu::ME_NeuralFMU, c::FMU2Component, x; p=nfmu.p, t=c.default_t) +function evaluateModel(nfmu::ME_NeuralFMU, c::FMU2Component, x; p = nfmu.p, t = c.default_t) @assert getCurrentInstance(nfmu.fmu) == c "Thread `$(Threads.threadid())` wants to evaluate wrong component!" # [ToDo]: Skip array check, e.g. by using a flag @@ -154,7 +185,14 @@ function evaluateModel(nfmu::ME_NeuralFMU, c::FMU2Component, x; p=nfmu.p, t=c.de return nfmu.re(p)(x) end -function evaluateModel(nfmu::ME_NeuralFMU, c::FMU2Component, dx, x; p=nfmu.p, t=c.default_t) +function evaluateModel( + nfmu::ME_NeuralFMU, + c::FMU2Component, + dx, + x; + p = nfmu.p, + t = c.default_t, +) @assert getCurrentInstance(nfmu.fmu) == c "Thread `$(Threads.threadid())` wants to evaluate wrong component!" # [ToDo]: Skip array check, e.g. by using a flag @@ -175,7 +213,14 @@ end ##### EVENT HANDLING START -function startCallback(integrator, nfmu::ME_NeuralFMU, c::Union{FMU2Component, Nothing}, t, writeSnapshot, readSnapshot) +function startCallback( + integrator, + nfmu::ME_NeuralFMU, + c::Union{FMU2Component,Nothing}, + t, + writeSnapshot, + readSnapshot, +) ignore_derivatives() do @@ -184,9 +229,20 @@ function startCallback(integrator, nfmu::ME_NeuralFMU, c::Union{FMU2Component, N t = unsense(t) @assert t == nfmu.tspan[1] "startCallback(...): Called for non-start-point t=$(t)" - - c, x0 = prepareSolveFMU(nfmu.fmu, c, fmi2TypeModelExchange; parameters=nfmu.parameters, t_start=nfmu.tspan[1], t_stop=nfmu.tspan[end], tolerance=nfmu.tolerance, x0=nfmu.x0, handleEvents=FMIFlux.handleEvents, cleanup=true) - + + c, x0 = prepareSolveFMU( + nfmu.fmu, + c, + fmi2TypeModelExchange; + parameters = nfmu.parameters, + t_start = nfmu.tspan[1], + t_stop = nfmu.tspan[end], + tolerance = nfmu.tolerance, + x0 = nfmu.x0, + handleEvents = FMIFlux.handleEvents, + cleanup = true, + ) + if c.eventInfo.nextEventTime == t && c.eventInfo.nextEventTimeDefined == fmi2True @debug "Initial time event detected!" else @@ -206,11 +262,14 @@ function startCallback(integrator, nfmu::ME_NeuralFMU, c::Union{FMU2Component, N # c = readSnapshot.instance if t != readSnapshot.t - logWarning(c.fmu, "Snapshot time mismatch, snapshot time = $(readSnapshot.t), but start time is $(t)") + logWarning( + c.fmu, + "Snapshot time mismatch, snapshot time = $(readSnapshot.t), but start time is $(t)", + ) end @debug "ME_NeuralFMU: Applying snapshot..." - FMIBase.apply!(c, readSnapshot; t=t) + FMIBase.apply!(c, readSnapshot; t = t) @debug "ME_NeuralFMU: Snapshot applied." end @@ -224,7 +283,7 @@ function stopCallback(nfmu::ME_NeuralFMU, c::FMU2Component, t) @assert getCurrentInstance(nfmu.fmu) == c "Thread `$(Threads.threadid())` wants to evaluate wrong component!" ignore_derivatives() do - + t = unsense(t) @assert t == nfmu.tspan[end] "stopCallback(...): Called for non-start-point t=$(t)" @@ -248,14 +307,14 @@ function time_choice(nfmu::ME_NeuralFMU, c::FMU2Component, integrator, tStart, t c.solution.evals_timechoice += 1 if c.eventInfo.nextEventTimeDefined == fmi2True - + if c.eventInfo.nextEventTime >= tStart && c.eventInfo.nextEventTime <= tStop @debug "time_choice(...): At $(integrator.t) next time event announced @$(c.eventInfo.nextEventTime)s" return c.eventInfo.nextEventTime else # the time event is outside the simulation range! @debug "Next time event @$(c.eventInfo.nextEventTime)s is outside simulation time range ($(tStart), $(tStop)), skipping." - return nothing + return nothing end else return nothing @@ -265,40 +324,71 @@ end # [ToDo] for now, ReverseDiff (together with the rrule) seems to have a problem with the SubArray here (when `collect` it accesses array elements that are #undef), # so I added an additional (single allocating) dispatch... # Type is ReverseDiff.TrackedReal{Float64, Float64, ReverseDiff.TrackedArray{Float64, Float64, 1, Vector{Float64}, Vector{Float64}}}[#undef, #undef, #undef, ...] -function condition!(nfmu::ME_NeuralFMU, c::FMU2Component, out::AbstractArray{<:ReverseDiff.TrackedReal}, x, t, integrator, handleEventIndicators) +function condition!( + nfmu::ME_NeuralFMU, + c::FMU2Component, + out::AbstractArray{<:ReverseDiff.TrackedReal}, + x, + t, + integrator, + handleEventIndicators, +) if !isassigned(out, 1) if isnothing(nfmu.condition_buffer) - logInfo(nfmu.fmu, "There is currently an issue with the condition buffer pre-allocation, the buffer can't be overwritten by the generated rrule.\nBuffer is generated automatically.") + logInfo( + nfmu.fmu, + "There is currently an issue with the condition buffer pre-allocation, the buffer can't be overwritten by the generated rrule.\nBuffer is generated automatically.", + ) @assert length(out) == length(handleEventIndicators) "Number of event indicators to handle ($(handleEventIndicators)) doesn't fit buffer size $(length(out))." nfmu.condition_buffer = zeros(eltype(out), length(out)) - elseif eltype(out) != eltype(nfmu.condition_buffer) || length(out) != length(nfmu.condition_buffer) + elseif eltype(out) != eltype(nfmu.condition_buffer) || + length(out) != length(nfmu.condition_buffer) nfmu.condition_buffer = zeros(eltype(out), length(out)) end - out[:] = nfmu.condition_buffer + out[:] = nfmu.condition_buffer end - invoke(condition!, Tuple{ME_NeuralFMU, FMU2Component, Any, Any, Any, Any, Any}, nfmu, c, out, x, t, integrator, handleEventIndicators) - + invoke( + condition!, + Tuple{ME_NeuralFMU,FMU2Component,Any,Any,Any,Any,Any}, + nfmu, + c, + out, + x, + t, + integrator, + handleEventIndicators, + ) + return nothing end -function condition!(nfmu::ME_NeuralFMU, c::FMU2Component, out, x, t, integrator, handleEventIndicators) +function condition!( + nfmu::ME_NeuralFMU, + c::FMU2Component, + out, + x, + t, + integrator, + handleEventIndicators, +) @assert getCurrentInstance(nfmu.fmu) == c "Thread `$(Threads.threadid())` wants to evaluate wrong component!" - @assert c.state == fmi2ComponentStateContinuousTimeMode "condition!(...):\n" * FMIBase.ERR_MSG_CONT_TIME_MODE + @assert c.state == fmi2ComponentStateContinuousTimeMode "condition!(...):\n" * + FMIBase.ERR_MSG_CONT_TIME_MODE # [ToDo] Evaluate on light-weight model (sub-model) without fmi2GetXXX or similar and the bottom ANN. # Basically only the layers from very top to FMU need to be evaluated here. - prev_t = c.default_t - prev_ec = c.default_ec + prev_t = c.default_t + prev_ec = c.default_ec prev_ec_idcs = c.default_ec_idcs c.default_t = t c.default_ec = out c.default_ec_idcs = handleEventIndicators - + evaluateModel(nfmu, c, x) # write back to condition buffer if (!isdual(out) && isdual(c.output.ec)) || (!istracked(out) && istracked(c.output.ec)) @@ -311,7 +401,7 @@ function condition!(nfmu::ME_NeuralFMU, c::FMU2Component, out, x, t, integrator, c.default_t = prev_t c.default_ec = prev_ec c.default_ec_idcs = prev_ec_idcs - + c.solution.evals_condition += 1 @debug "condition!(...) -> [typeof=$(typeof(out))]\n$(unsense(out))" @@ -320,11 +410,12 @@ function condition!(nfmu::ME_NeuralFMU, c::FMU2Component, out, x, t, integrator, end global lastIndicator = nothing -global lastIndicatorX = nothing +global lastIndicatorX = nothing global lastIndicatorT = nothing -function conditionSingle(nfmu::ME_NeuralFMU, c::FMU2Component, index, x, t, integrator) +function conditionSingle(nfmu::ME_NeuralFMU, c::FMU2Component, index, x, t, integrator) - @assert c.state == fmi2ComponentStateContinuousTimeMode "condition(...):\n" * FMIBase.ERR_MSG_CONT_TIME_MODE + @assert c.state == fmi2ComponentStateContinuousTimeMode "condition(...):\n" * + FMIBase.ERR_MSG_CONT_TIME_MODE @assert getCurrentInstance(nfmu.fmu) == c "Thread `$(Threads.threadid())` wants to evaluate wrong component!" if c.fmu.handleEventIndicators != nothing && index ∉ c.fmu.handleEventIndicators @@ -333,7 +424,8 @@ function conditionSingle(nfmu::ME_NeuralFMU, c::FMU2Component, index, x, t, inte global lastIndicator - if lastIndicator == nothing || length(lastIndicator) != c.fmu.modelDescription.numberOfEventIndicators + if lastIndicator == nothing || + length(lastIndicator) != c.fmu.modelDescription.numberOfEventIndicators lastIndicator = zeros(c.fmu.modelDescription.numberOfEventIndicators) end @@ -343,13 +435,13 @@ function conditionSingle(nfmu::ME_NeuralFMU, c::FMU2Component, index, x, t, inte evaluateModel(nfmu, c, x) c.default_t = -1.0 c.default_ec = EMPTY_fmi2Real - + c.solution.evals_condition += 1 - + return lastIndicator[index] end -function smoothmax(vec::AbstractVector; alpha=0.5) +function smoothmax(vec::AbstractVector; alpha = 0.5) dividend = 0.0 divisor = 0.0 e = Float64(ℯ) @@ -357,7 +449,7 @@ function smoothmax(vec::AbstractVector; alpha=0.5) dividend += x * e^(alpha * x) divisor += e^(alpha * x) end - return dividend/divisor + return dividend / divisor end function smoothmax(a, b; kwargs...) @@ -365,21 +457,32 @@ function smoothmax(a, b; kwargs...) end # [ToDo] Check, that the new determined state is the right root of the event instant! -function f_optim(x, nfmu::ME_NeuralFMU, c::FMU2Component, right_x_fmu, idx, sign::Real, out, indicatorValue, handleEventIndicators; _unsense::Bool=false) - - prev_ec = c.default_ec +function f_optim( + x, + nfmu::ME_NeuralFMU, + c::FMU2Component, + right_x_fmu, + idx, + sign::Real, + out, + indicatorValue, + handleEventIndicators; + _unsense::Bool = false, +) + + prev_ec = c.default_ec prev_ec_idcs = c.default_ec_idcs prev_y_refs = c.default_y_refs prev_y = c.default_y #@info "\ndx: $(c.default_dx)\n x: $(x)" - + c.default_ec = out c.default_ec_idcs = handleEventIndicators c.default_y_refs = c.fmu.modelDescription.stateValueReferences c.default_y = zeros(typeof(x[1]), length(c.fmu.modelDescription.stateValueReferences)) - - evaluateModel(nfmu, c, x; p=unsense(nfmu.p)) + + evaluateModel(nfmu, c, x; p = unsense(nfmu.p)) # write back to condition buffer # if (!isdual(out) && isdual(c.output.ec)) || (!istracked(out) && istracked(c.output.ec)) # @assert false "Type missmatch! Can't propagate sensitivities!" @@ -393,12 +496,12 @@ function f_optim(x, nfmu::ME_NeuralFMU, c::FMU2Component, right_x_fmu, idx, sign c.default_ec_idcs = prev_ec_idcs c.default_y_refs = prev_y_refs c.default_y = prev_y - + # propagete the new state-guess `x` through the NeuralFMU - + #condition!(nfmu, c, buffer, x, c.t, nothing, handleEventIndicators) ec = c.output.ec[idx] @@ -406,7 +509,8 @@ function f_optim(x, nfmu::ME_NeuralFMU, c::FMU2Component, right_x_fmu, idx, sign #@info "\nec: $(ec)\n-> $(unsense(ec))\ny: $(y)\n-> $(unsense(y))" - errorIndicator = Flux.Losses.mae(indicatorValue, ec) + smoothmax(-sign*ec*1000.0, 0.0) + errorIndicator = + Flux.Losses.mae(indicatorValue, ec) + smoothmax(-sign * ec * 1000.0, 0.0) # if errorIndicator > 0.0 # errorIndicator = max(errorIndicator, 1.0) # end @@ -414,13 +518,13 @@ function f_optim(x, nfmu::ME_NeuralFMU, c::FMU2Component, right_x_fmu, idx, sign #@info "ErrorState: $(errorState) | ErrorIndicator: $(errorIndicator)" - ret = errorState + errorIndicator + ret = errorState + errorIndicator # if _unsense # ret = unsense(ret) # end - return ret + return ret end function sampleStateChangeJacobian(nfmu, c, left_x, right_x, t, idx::Integer; step = 1e-8) @@ -428,10 +532,10 @@ function sampleStateChangeJacobian(nfmu, c, left_x, right_x, t, idx::Integer; st @debug "sampleStateChangeJacobian(x = $(left_x))" c.solution.evals_∂xr_∂xl += 1 - + numStates = length(left_x) jac = zeros(numStates, numStates) - + # first, jump to before the event instance # if length(c.solution.snapshots) > 0 # c.t != t # sn = getSnapshot(c.solution, t) @@ -444,12 +548,12 @@ function sampleStateChangeJacobian(nfmu, c, left_x, right_x, t, idx::Integer; st new_left_x = copy(left_x) if length(c.solution.snapshots) > 0 # c.t != t sn = getSnapshot(c.solution, t) - FMIBase.apply!(c, sn; x_c=new_left_x, t=t) + FMIBase.apply!(c, sn; x_c = new_left_x, t = t) #@info "[?] Set snapshot @ t=$(t) (sn.t=$(sn.t))" end - new_right_x = stateChange!(nfmu, c, new_left_x, t, idx; snapshots=false) + new_right_x = stateChange!(nfmu, c, new_left_x, t, idx; snapshots = false) statesChanged = (c.eventInfo.valuesOfContinuousStatesChanged == fmi2True) - + # [ToDo: these tests should be included, but will drastically fail on FMUs with no support for get/setState] # @assert statesChanged "Can't reproduce event (statesChanged)!" # @assert left_x == new_left_x "Can't reproduce event (left_x)!" @@ -457,8 +561,8 @@ function sampleStateChangeJacobian(nfmu, c, left_x, right_x, t, idx::Integer; st at_least_one_state_change = false - for i in 1:numStates - + for i = 1:numStates + #new_left_x[:] .= left_x new_left_x = copy(left_x) new_left_x[i] += step @@ -466,36 +570,36 @@ function sampleStateChangeJacobian(nfmu, c, left_x, right_x, t, idx::Integer; st # first, jump to before the event instance if length(c.solution.snapshots) > 0 # c.t != t sn = getSnapshot(c.solution, t) - FMIBase.apply!(c, sn; x_c=new_left_x, t=t) + FMIBase.apply!(c, sn; x_c = new_left_x, t = t) #@info "[e] Set snapshot @ t=$(t) (sn.t=$(sn.t))" end # [ToDo] Don't check if event was handled via event-indicator, because there is no guarantee that it is reset (like for the bouncing ball) # to match the sign from before the event! Better check if FMU detects a new event! # fmi2EnterEventMode(c) # handleEvents(c) - new_right_x = stateChange!(nfmu, c, new_left_x, t, idx; snapshots=false) + new_right_x = stateChange!(nfmu, c, new_left_x, t, idx; snapshots = false) statesChanged = (c.eventInfo.valuesOfContinuousStatesChanged == fmi2True) at_least_one_state_change = statesChanged || at_least_one_state_change #new_indicator_sign = idx > 0 ? sign(fmi2GetEventIndicators(c)[idx]) : 1.0 #@info "Sample P: t:$(t) $(new_left_x) -> $(new_right_x)" grad = (new_right_x .- right_x) ./ step # (left_x .- new_left_x) - + # choose other direction - if !statesChanged + if !statesChanged #@info "New_indicator sign is $(new_indicator_sign) (should be $(indicator_sign)), retry..." #new_left_x[:] .= left_x new_left_x = copy(left_x) new_left_x[i] -= step - + if length(c.solution.snapshots) > 0 # c.t != t sn = getSnapshot(c.solution, t) - FMIBase.apply!(c, sn; x_c=new_left_x, t=t) + FMIBase.apply!(c, sn; x_c = new_left_x, t = t) #@info "[e] Set snapshot @ t=$(t) (sn.t=$(sn.t))" end #fmi2EnterEventMode(c) #handleEvents(c) - new_right_x = stateChange!(nfmu, c, new_left_x, t, idx; snapshots=false) + new_right_x = stateChange!(nfmu, c, new_left_x, t, idx; snapshots = false) statesChanged = (c.eventInfo.valuesOfContinuousStatesChanged == fmi2True) at_least_one_state_change = statesChanged || at_least_one_state_change #new_indicator_sign = idx > 0 ? sign(fmi2GetEventIndicators(c)[idx]) : 1.0 @@ -507,7 +611,7 @@ function sampleStateChangeJacobian(nfmu, c, left_x, right_x, t, idx::Integer; st else grad = (right_x .- right_x) # ... so zero, this state is not sensitive at all! end - + end # if length(c.solution.snapshots) > 0 # c.t != t @@ -522,14 +626,15 @@ function sampleStateChangeJacobian(nfmu, c, left_x, right_x, t, idx::Integer; st #@info "t=$(t) idx=$(idx)\n left_x: $(left_x) -> right_x: $(right_x) [$(indicator_sign)]\nnew_left_x: $(new_left_x) -> new_right_x: $(new_right_x) [$(new_indicator_sign)]" - jac[i,:] = grad + jac[i, :] = grad end #@assert at_least_one_state_change "Sampling state change jacobian failed, can't find another state that triggers the event!" if !at_least_one_state_change - @warn "Sampling state change jacobian failed, can't find another state that triggers the event!\ncommon reasons for that are:\n(a) The FMU is not able to revisit events (which should be possible with fmiXGet/SetState).\n(b) The state change is not dependent on the previous state (hard reset).\nThis is printed only 3 times." maxlog=3 + @info "Sampling state change jacobian failed, can't find another state that triggers the event!\ncommon reasons for that are:\n(a) The FMU is not able to revisit events (which should be possible with fmiXGet/SetState).\n(b) The state change is not dependent on the previous state (hard reset).\nThis is printed only 3 times." maxlog = + 3 end - + # finally, jump back to the correct FMU state # if length(c.solution.snapshots) > 0 # c.t != t # @info "Reset snapshot @ t = $(t)" @@ -537,14 +642,14 @@ function sampleStateChangeJacobian(nfmu, c, left_x, right_x, t, idx::Integer; st # FMIBase.apply!(c, sn; x_c=left_x, t=t) # end # stateChange!(nfmu, c, left_x, t, idx) - if length(c.solution.snapshots) > 0 + if length(c.solution.snapshots) > 0 #@info "Reset exact snapshot @t=$(t)" - sn = getSnapshot(c.solution, t; exact=true) + sn = getSnapshot(c.solution, t; exact = true) if !isnothing(sn) - FMIBase.apply!(c, sn; x_c=left_x, t=t) + FMIBase.apply!(c, sn; x_c = left_x, t = t) end end - + #@info "Jac:\n$(jac)" #@assert isapprox(jac, [0.0 0.0; 0.0 -0.7]; atol=1e-4) "Jac missmatch, is $(jac)" @@ -552,10 +657,20 @@ function sampleStateChangeJacobian(nfmu, c, left_x, right_x, t, idx::Integer; st end function is_integrator_sensitive(integrator) - return istracked(integrator.u) || istracked(integrator.t) || isdual(integrator.u) || isdual(integrator.t) + return istracked(integrator.u) || + istracked(integrator.t) || + isdual(integrator.u) || + isdual(integrator.t) end -function stateChange!(nfmu, c, left_x::AbstractArray{<:Float64}, t::Float64, idx; snapshots=nfmu.snapshots) +function stateChange!( + nfmu, + c, + left_x::AbstractArray{<:Float64}, + t::Float64, + idx; + snapshots = nfmu.snapshots, +) # unpack references # if typeof(cRef) != UInt64 @@ -591,11 +706,11 @@ function stateChange!(nfmu, c, left_x::AbstractArray{<:Float64}, t::Float64, idx # end # end - right_x = left_x + right_x = left_x if c.eventInfo.valuesOfContinuousStatesChanged == fmi2True - ignore_derivatives() do + ignore_derivatives() do if idx == 0 @debug "stateChange!($(idx)): NeuralFMU time event with state change.\nt = $(t)\nleft_x = $(left_x)" else @@ -606,19 +721,45 @@ function stateChange!(nfmu, c, left_x::AbstractArray{<:Float64}, t::Float64, idx right_x_fmu = fmi2GetContinuousStates(c) # the new FMU state after handled events # if there is an ANN above the FMU, propaget FMU state through top ANN by optimization - if nfmu.modifiedState + if nfmu.modifiedState before = fmi2GetEventIndicators(c) buffer = copy(before) - handleEventIndicators = Vector{UInt32}(collect(i for i in 1:length(nfmu.fmu.modelDescription.numberOfEventIndicators))) - - _f(_x) = f_optim(_x, nfmu, c, right_x_fmu, idx, sign(before[idx]), buffer, before[idx], handleEventIndicators; _unsense=true) - _f_g(_x) = f_optim(_x, nfmu, c, right_x_fmu, idx, sign(before[idx]), buffer, before[idx], handleEventIndicators; _unsense=false) + handleEventIndicators = Vector{UInt32}( + collect( + i for i = 1:length(nfmu.fmu.modelDescription.numberOfEventIndicators) + ), + ) + + _f(_x) = f_optim( + _x, + nfmu, + c, + right_x_fmu, + idx, + sign(before[idx]), + buffer, + before[idx], + handleEventIndicators; + _unsense = true, + ) + _f_g(_x) = f_optim( + _x, + nfmu, + c, + right_x_fmu, + idx, + sign(before[idx]), + buffer, + before[idx], + handleEventIndicators; + _unsense = false, + ) function _g!(G, x) #if istracked(integrator.u) # ReverseDiff.gradient!(G, _f_g, x) #else # if isdual(integrator.u) - ForwardDiff.gradient!(G, _f_g, x) + ForwardDiff.gradient!(G, _f_g, x) # else # @assert false "Unknown AD framework! -> $(typeof(integrator.u[1]))" #end @@ -631,16 +772,19 @@ function stateChange!(nfmu, c, left_x::AbstractArray{<:Float64}, t::Float64, idx after = fmi2GetEventIndicators(c) if sign(before[idx]) != sign(after[idx]) - logError(nfmu.fmu, "Eventhandling failed,\nRight state: $(right_x)\nRight FMU state: $(right_x_fmu)\nIndicator (bef.): $(before[idx])\nIndicator (aft.): $(after[idx])") + logError( + nfmu.fmu, + "Eventhandling failed,\nRight state: $(right_x)\nRight FMU state: $(right_x_fmu)\nIndicator (bef.): $(before[idx])\nIndicator (aft.): $(after[idx])", + ) end - + else # if there is no ANN above, then: right_x = right_x_fmu end else - ignore_derivatives() do + ignore_derivatives() do if idx == 0 @debug "stateChange!($(idx)): NeuralFMU time event without state change.\nt = $(t)\nx = $(left_x)" else @@ -652,7 +796,7 @@ function stateChange!(nfmu, c, left_x::AbstractArray{<:Float64}, t::Float64, idx # u_modified!(integrator, false) end - if snapshots + if snapshots s = snapshot_if_needed!(c.solution, t) # if !isnothing(s) # @info "Add snapshot @t=$(s.t)" @@ -689,14 +833,15 @@ function affectFMU!(nfmu::ME_NeuralFMU, c::FMU2Component, integrator, idx) @assert getCurrentInstance(nfmu.fmu) == c "Thread `$(Threads.threadid())` wants to evaluate wrong component!" # assert_integrator_valid(integrator) - @assert c.state == fmi2ComponentStateContinuousTimeMode "affectFMU!(...):\n" * FMIBase.ERR_MSG_CONT_TIME_MODE + @assert c.state == fmi2ComponentStateContinuousTimeMode "affectFMU!(...):\n" * + FMIBase.ERR_MSG_CONT_TIME_MODE # [NOTE] Here unsensing is OK, because we just want to reset the FMU to the correct state! # The values come directly from the integrator and are NOT function arguments! t = unsense(integrator.t) left_x = unsense_copy(integrator.u) right_x = nothing - + ignore_derivatives() do # if snapshots && length(c.solution.snapshots) > 0 @@ -704,33 +849,33 @@ function affectFMU!(nfmu::ME_NeuralFMU, c::FMU2Component, integrator, idx) # FMIBase.apply!(c, sn) # end - #if c.x != left_x + #if c.x != left_x # capture status of `force` mode = c.force c.force = true # there are fx-evaluations before the event is handled, reset the FMU state to the current integrator step - evaluateModel(nfmu, c, left_x; t=t) # evaluate NeuralFMU (set new states) + evaluateModel(nfmu, c, left_x; t = t) # evaluate NeuralFMU (set new states) # [NOTE] No need to reset time here, because we did pass a event instance! # c.default_t = -1.0 c.force = mode - #end + #end end integ_sens = nfmu.snapshots right_x = stateChange!(nfmu, c, left_x, t, idx) - + # sensitivities needed if integ_sens - jac = I + jac = I if c.eventInfo.valuesOfContinuousStatesChanged == fmi2True jac = sampleStateChangeJacobian(nfmu, c, left_x, right_x, t, idx) end - VJP = jac * integrator.u + VJP = jac * integrator.u #tgrad = tvec .* integrator.t staticOff = right_x .- unsense(VJP) # .- unsense(tgrad) @@ -741,7 +886,7 @@ function affectFMU!(nfmu::ME_NeuralFMU, c::FMU2Component, integrator, idx) end #@info "affect right_x = $(right_x)" - + # [Note] enabling this causes serious issues with time events! (wrong sensitivities!) # u_modified!(integrator, true) @@ -761,28 +906,34 @@ function affectFMU!(nfmu::ME_NeuralFMU, c::FMU2Component, integrator, idx) end # calculates state events per second - pt = t-nfmu.tspan[1] - ne = 0 + pt = t - nfmu.tspan[1] + ne = 0 for event in c.solution.events #if t - event.t < pt - if event.indicator > 0 # count only state events - ne += 1 - end + if event.indicator > 0 # count only state events + ne += 1 + end #end end ratio = ne / pt - + if ne >= 100 && ratio > c.fmu.executionConfig.maxStateEventsPerSecond - logError(c.fmu, "Event chattering detected $(round(Integer, ratio)) state events/s (allowed are $(c.fmu.executionConfig.maxStateEventsPerSecond)), aborting at t=$(t) (rel. t=$(pt)) at state event $(ne):") - for i in 1:c.fmu.modelDescription.numberOfEventIndicators + logError( + c.fmu, + "Event chattering detected $(round(Integer, ratio)) state events/s (allowed are $(c.fmu.executionConfig.maxStateEventsPerSecond)), aborting at t=$(t) (rel. t=$(pt)) at state event $(ne):", + ) + for i = 1:c.fmu.modelDescription.numberOfEventIndicators num = 0 for e in c.solution.events if e.indicator == i - num += 1 - end + num += 1 + end end if num > 0 - logError(c.fmu, "\tEvent indicator #$(i) triggered $(num) ($(round(num/ne*100.0; digits=1))%)") + logError( + c.fmu, + "\tEvent indicator #$(i) triggered $(num) ($(round(num/ne*100.0; digits=1))%)", + ) end end @@ -796,7 +947,15 @@ function affectFMU!(nfmu::ME_NeuralFMU, c::FMU2Component, integrator, idx) end # Does one step in the simulation. -function stepCompleted(nfmu::ME_NeuralFMU, c::FMU2Component, x, t, integrator, tStart, tStop) +function stepCompleted( + nfmu::ME_NeuralFMU, + c::FMU2Component, + x, + t, + integrator, + tStart, + tStop, +) # assert_integrator_valid(integrator) @@ -814,17 +973,21 @@ function stepCompleted(nfmu::ME_NeuralFMU, c::FMU2Component, x, t, integrator, t dt = unsense(integrator.t) - unsense(integrator.tprev) events = length(c.solution.events) steps = c.solution.evals_stepcompleted - simLen = tStop-tStart + simLen = tStop - tStart c.progressMeter.desc = "t=$(roundToLength(t, 10))s | Δt=$(roundToLength(dt, 10))s | STPs=$(steps) | EVTs=$(events) |" #@info "$(tStart) $(tStop) $(t)" if simLen > 0.0 - ProgressMeter.update!(c.progressMeter, floor(Integer, 1000.0*(t-tStart)/simLen) ) + ProgressMeter.update!( + c.progressMeter, + floor(Integer, 1000.0 * (t - tStart) / simLen), + ) end end if c != nothing - (status, enterEventMode, terminateSimulation) = fmi2CompletedIntegratorStep(c, fmi2True) + (status, enterEventMode, terminateSimulation) = + fmi2CompletedIntegratorStep(c, fmi2True) if terminateSimulation == fmi2True logError(c.fmu, "stepCompleted(...): FMU requested termination!") @@ -846,14 +1009,14 @@ end # save FMU values function saveValues(nfmu::ME_NeuralFMU, c::FMU2Component, recordValues, _x, _t, integrator) - t = unsense(_t) + t = unsense(_t) x = unsense(_x) c.solution.evals_savevalues += 1 # ToDo: Evaluate on light-weight model (sub-model) without fmi2GetXXX or similar and the bottom ANN - evaluateModel(nfmu, c, x; t=t) # evaluate NeuralFMU (set new states) - + evaluateModel(nfmu, c, x; t = t) # evaluate NeuralFMU (set new states) + values = fmi2GetReal(c, recordValues) @debug "Save values @t=$(t)\nintegrator.t=$(unsense(integrator.t))\n$(values)" @@ -862,33 +1025,43 @@ function saveValues(nfmu::ME_NeuralFMU, c::FMU2Component, recordValues, _x, _t, return (values...,) end -function saveEigenvalues(nfmu::ME_NeuralFMU, c::FMU2Component, _x, _t, integrator, sensitivity::Symbol) +function saveEigenvalues( + nfmu::ME_NeuralFMU, + c::FMU2Component, + _x, + _t, + integrator, + sensitivity::Symbol, +) - @assert c.state == fmi2ComponentStateContinuousTimeMode "saveEigenvalues(...):\n" * FMIBase.ERR_MSG_CONT_TIME_MODE + @assert c.state == fmi2ComponentStateContinuousTimeMode "saveEigenvalues(...):\n" * + FMIBase.ERR_MSG_CONT_TIME_MODE c.solution.evals_saveeigenvalues += 1 A = nothing if sensitivity == :ForwardDiff - A = ForwardDiff.jacobian(x -> evaluateModel(nfmu, c, x; t=_t), _x) # TODO: chunk_size! - elseif sensitivity == :ReverseDiff - A = ReverseDiff.jacobian(x -> evaluateModel(nfmu, c, x; t=_t), _x) - elseif sensitivity == :Zygote - A = Zygote.jacobian(x -> evaluateModel(nfmu, c, x; t=_t), _x)[1] + A = ForwardDiff.jacobian(x -> evaluateModel(nfmu, c, x; t = _t), _x) # TODO: chunk_size! + elseif sensitivity == :ReverseDiff + A = ReverseDiff.jacobian(x -> evaluateModel(nfmu, c, x; t = _t), _x) + elseif sensitivity == :Zygote + A = Zygote.jacobian(x -> evaluateModel(nfmu, c, x; t = _t), _x)[1] elseif sensitivity == :none - A = ForwardDiff.jacobian(x -> evaluateModel(nfmu, c, x; t=_t), unsense(_x)) + A = ForwardDiff.jacobian(x -> evaluateModel(nfmu, c, x; t = _t), unsense(_x)) end eigs, _ = DifferentiableEigen.eigen(A) return (eigs...,) end -function fx(nfmu::ME_NeuralFMU, +function fx( + nfmu::ME_NeuralFMU, c::FMU2Component, dx,#::Array{<:Real}, x,#::Array{<:Real}, p,#::Array, - t)#::Real) + t, +)#::Real) if isnothing(c) # this should never happen! @@ -898,21 +1071,23 @@ function fx(nfmu::ME_NeuralFMU, ############ - evaluateModel(nfmu, c, dx, x; p=p, t=t) + evaluateModel(nfmu, c, dx, x; p = p, t = t) ignore_derivatives() do c.solution.evals_fx_inplace += 1 - end + end return dx end -function fx(nfmu::ME_NeuralFMU, +function fx( + nfmu::ME_NeuralFMU, c::FMU2Component, x,#::Array{<:Real}, p,#::Array, - t)#::Real) - + t, +)#::Real) + if c === nothing # this should never happen! return zeros(length(x)) @@ -921,8 +1096,8 @@ function fx(nfmu::ME_NeuralFMU, ignore_derivatives() do c.solution.evals_fx_outofplace += 1 end - - return evaluateModel(nfmu, c, x; p=p, t=t) + + return evaluateModel(nfmu, c, x; p = p, t = t) end ##### EVENT HANDLING END @@ -939,21 +1114,26 @@ Constructs a ME-NeuralFMU where the FMU is at an arbitrary location inside of th # Keyword arguments - `recordValues` additionally records internal FMU variables """ -function ME_NeuralFMU(fmu::FMU2, - model, - tspan, - solver=nothing; - recordValues = nothing, - saveat=nothing, - solvekwargs...) +function ME_NeuralFMU( + fmu::FMU2, + model, + tspan, + solver = nothing; + recordValues = nothing, + saveat = nothing, + solvekwargs..., +) if !is64(model) model = convert64(model) - logInfo(fmu, "Model is not Float64, but this is necessary for (Neural)FMUs.\nModel parameters are automatically converted to Float64.") + logInfo( + fmu, + "Model is not Float64, but this is necessary for (Neural)FMUs.\nModel parameters are automatically converted to Float64.", + ) end - + p, re = Flux.destructure(model) - nfmu = ME_NeuralFMU{typeof(model), typeof(re)}(model, p, re) + nfmu = ME_NeuralFMU{typeof(model),typeof(re)}(model, p, re) ###### @@ -970,7 +1150,7 @@ function ME_NeuralFMU(fmu::FMU2, nfmu.parameters = nothing ###### - + nfmu end @@ -985,55 +1165,58 @@ Constructs a CS-NeuralFMU where the FMU is at an arbitrary location inside of th # Keyword arguments - `recordValues` additionally records FMU variables """ -function CS_NeuralFMU(fmu::FMU2, - model, - tspan; - recordValues=[]) +function CS_NeuralFMU(fmu::FMU2, model, tspan; recordValues = []) if !is64(model) model = convert64(model) - logInfo(fmu, "Model is not Float64, but this is necessary for (Neural)FMUs.\nModel parameters are automatically converted to Float64.") + logInfo( + fmu, + "Model is not Float64, but this is necessary for (Neural)FMUs.\nModel parameters are automatically converted to Float64.", + ) end - nfmu = CS_NeuralFMU{FMU2, FMU2Component}() + nfmu = CS_NeuralFMU{FMU2,FMU2Component}() nfmu.fmu = fmu - nfmu.model = model + nfmu.model = model nfmu.tspan = tspan nfmu.p, nfmu.re = Flux.destructure(nfmu.model) - + return nfmu end -function CS_NeuralFMU(fmus::Vector{<:FMU2}, - model, - tspan; - recordValues=[]) +function CS_NeuralFMU(fmus::Vector{<:FMU2}, model, tspan; recordValues = []) if !is64(model) model = convert64(model) for fmu in fmus - logInfo(fmu, "Model is not Float64, but this is necessary for (Neural)FMUs.\nModel parameters are automatically converted to Float64.") + logInfo( + fmu, + "Model is not Float64, but this is necessary for (Neural)FMUs.\nModel parameters are automatically converted to Float64.", + ) end end - nfmu = CS_NeuralFMU{Vector{FMU2}, Vector{FMU2Component} }() - + nfmu = CS_NeuralFMU{Vector{FMU2},Vector{FMU2Component}}() + nfmu.fmu = fmus - nfmu.model = model + nfmu.model = model nfmu.tspan = tspan nfmu.p, nfmu.re = Flux.destructure(nfmu.model) - + return nfmu end function checkExecTime(integrator, nfmu::ME_NeuralFMU, c, max_execution_duration::Real) dist = max(nfmu.execution_start + max_execution_duration - time(), 0.0) - + if dist <= 0.0 - logInfo(nfmu.fmu, "Reached max execution duration ($(max_execution_duration)), terminating integration ...") + logInfo( + nfmu.fmu, + "Reached max execution duration ($(max_execution_duration)), terminating integration ...", + ) terminate!(integrator) end @@ -1055,29 +1238,35 @@ Evaluates the ME_NeuralFMU `nfmu` in the timespan given during construction or i [ToDo] """ -function (nfmu::ME_NeuralFMU)(x_start::Union{Array{<:Real}, Nothing} = nfmu.x0, - tspan::Tuple{Float64, Float64} = nfmu.tspan; +function (nfmu::ME_NeuralFMU)( + x_start::Union{Array{<:Real},Nothing} = nfmu.x0, + tspan::Tuple{Float64,Float64} = nfmu.tspan; showProgress::Bool = false, - progressDescr::String=DEFAULT_PROGRESS_DESCR, - tolerance::Union{Real, Nothing} = nothing, - parameters::Union{Dict{<:Any, <:Any}, Nothing} = nothing, - p=nfmu.p, - solver=nfmu.solver, - saveEventPositions::Bool=false, - max_execution_duration::Real=-1.0, - recordValues::fmi2ValueReferenceFormat=nfmu.recordValues, - recordEigenvaluesSensitivity::Symbol=:none, - recordEigenvalues::Bool=(recordEigenvaluesSensitivity != :none), - saveat=nfmu.saveat, # ToDo: Data type - sensealg=nfmu.fmu.executionConfig.sensealg, # ToDo: AbstractSensitivityAlgorithm - writeSnapshot::Union{FMUSnapshot, Nothing}=nothing, - readSnapshot::Union{FMUSnapshot, Nothing}=nothing, - cleanSnapshots::Bool=true, - solvekwargs...) + progressDescr::String = DEFAULT_PROGRESS_DESCR, + tolerance::Union{Real,Nothing} = nothing, + parameters::Union{Dict{<:Any,<:Any},Nothing} = nothing, + p = nfmu.p, + solver = nfmu.solver, + saveEventPositions::Bool = false, + max_execution_duration::Real = -1.0, + recordValues::fmi2ValueReferenceFormat = nfmu.recordValues, + recordEigenvaluesSensitivity::Symbol = :none, + recordEigenvalues::Bool = (recordEigenvaluesSensitivity != :none), + saveat = nfmu.saveat, # ToDo: Data type + sensealg = nfmu.fmu.executionConfig.sensealg, # ToDo: AbstractSensitivityAlgorithm + writeSnapshot::Union{FMUSnapshot,Nothing} = nothing, + readSnapshot::Union{FMUSnapshot,Nothing} = nothing, + cleanSnapshots::Bool = true, + solvekwargs..., +) if !isnothing(saveat) - if saveat[1] != tspan[1] || saveat[end] != tspan[end] - logWarning(nfmu.fmu, "NeuralFMU changed time interval, start time is $(tspan[1]) and stop time is $(tspan[end]), but saveat from constructor gives $(saveat[1]) and $(saveat[end]).\nPlease provide correct `saveat` via keyword with matching start/stop time.", 1) + if saveat[1] != tspan[1] || saveat[end] != tspan[end] + logWarning( + nfmu.fmu, + "NeuralFMU changed time interval, start time is $(tspan[1]) and stop time is $(tspan[end]), but saveat from constructor gives $(saveat[1]) and $(saveat[end]).\nPlease provide correct `saveat` via keyword with matching start/stop time.", + 1, + ) saveat = collect(saveat) while saveat[1] < tspan[1] popfirst!(saveat) @@ -1087,17 +1276,17 @@ function (nfmu::ME_NeuralFMU)(x_start::Union{Array{<:Real}, Nothing} = nfmu.x0, end end end - + recordValues = prepareValueReference(nfmu.fmu, recordValues) saving = (length(recordValues) > 0) - + t_start = tspan[1] t_stop = tspan[end] nfmu.tspan = tspan nfmu.x0 = x_start - nfmu.p = p + nfmu.p = p ignore_derivatives() do @debug "ME_NeuralFMU(showProgress=$(showProgress), tspan=$(tspan), x0=$(nfmu.x0))" @@ -1108,12 +1297,13 @@ function (nfmu::ME_NeuralFMU)(x_start::Union{Array{<:Real}, Nothing} = nfmu.x0, if isnothing(parameters) if !isnothing(nfmu.fmu.default_p_refs) - nfmu.parameters = Dict(nfmu.fmu.default_p_refs .=> unsense(nfmu.fmu.default_p)) + nfmu.parameters = + Dict(nfmu.fmu.default_p_refs .=> unsense(nfmu.fmu.default_p)) end else nfmu.parameters = parameters end - + end callbacks = [] @@ -1122,7 +1312,7 @@ function (nfmu::ME_NeuralFMU)(x_start::Union{Array{<:Real}, Nothing} = nfmu.x0, @debug "ME_NeuralFMU: Starting callback..." c = startCallback(nothing, nfmu, c, t_start, writeSnapshot, readSnapshot) - + ignore_derivatives() do c.solution = FMUSolution(c) @@ -1140,11 +1330,13 @@ function (nfmu::ME_NeuralFMU)(x_start::Union{Array{<:Real}, Nothing} = nfmu.x0, # time event handling if nfmu.fmu.executionConfig.handleTimeEvents && nfmu.fmu.hasTimeEvents - timeEventCb = IterativeCallback((integrator) -> time_choice(nfmu, c, integrator, t_start, t_stop), - (integrator) -> affectFMU!(nfmu, c, integrator, 0), - Float64; - initial_affect=(c.eventInfo.nextEventTime == t_start), # already checked in the outer closure: c.eventInfo.nextEventTimeDefined == fmi2True - save_positions=(saveEventPositions, saveEventPositions)) + timeEventCb = IterativeCallback( + (integrator) -> time_choice(nfmu, c, integrator, t_start, t_stop), + (integrator) -> affectFMU!(nfmu, c, integrator, 0), + Float64; + initial_affect = (c.eventInfo.nextEventTime == t_start), # already checked in the outer closure: c.eventInfo.nextEventTimeDefined == fmi2True + save_positions = (saveEventPositions, saveEventPositions), + ) push!(callbacks, timeEventCb) end @@ -1159,37 +1351,48 @@ function (nfmu::ME_NeuralFMU)(x_start::Union{Array{<:Real}, Nothing} = nfmu.x0, if !isnothing(c.fmu.handleEventIndicators) handleIndicators = c.fmu.handleEventIndicators else # handle all - handleIndicators = collect(UInt32(i) for i in 1:c.fmu.modelDescription.numberOfEventIndicators) + handleIndicators = collect( + UInt32(i) for i = 1:c.fmu.modelDescription.numberOfEventIndicators + ) end numEventInds = length(handleIndicators) if c.fmu.executionConfig.useVectorCallbacks - eventCb = VectorContinuousCallback((out, x, t, integrator) -> condition!(nfmu, c, out, x, t, integrator, handleIndicators), - (integrator, idx) -> affectFMU!(nfmu, c, integrator, idx), - numEventInds; - rootfind=RightRootFind, - save_positions=(saveEventPositions, saveEventPositions), - interp_points=c.fmu.executionConfig.rootSearchInterpolationPoints) + eventCb = VectorContinuousCallback( + (out, x, t, integrator) -> + condition!(nfmu, c, out, x, t, integrator, handleIndicators), + (integrator, idx) -> affectFMU!(nfmu, c, integrator, idx), + numEventInds; + rootfind = RightRootFind, + save_positions = (saveEventPositions, saveEventPositions), + interp_points = c.fmu.executionConfig.rootSearchInterpolationPoints, + ) push!(callbacks, eventCb) else - for idx in 1:c.fmu.modelDescription.numberOfEventIndicators - eventCb = ContinuousCallback((x, t, integrator) -> conditionSingle(nfmu, c, idx, x, t, integrator), - (integrator) -> affectFMU!(nfmu, c, integrator, idx); - rootfind=RightRootFind, - save_positions=(saveEventPositions, saveEventPositions), - interp_points=c.fmu.executionConfig.rootSearchInterpolationPoints) + for idx = 1:c.fmu.modelDescription.numberOfEventIndicators + eventCb = ContinuousCallback( + (x, t, integrator) -> + conditionSingle(nfmu, c, idx, x, t, integrator), + (integrator) -> affectFMU!(nfmu, c, integrator, idx); + rootfind = RightRootFind, + save_positions = (saveEventPositions, saveEventPositions), + interp_points = c.fmu.executionConfig.rootSearchInterpolationPoints, + ) push!(callbacks, eventCb) end end end if max_execution_duration > 0.0 - terminateCb = ContinuousCallback((x, t, integrator) -> checkExecTime(integrator, nfmu, c, max_execution_duration), - (integrator) -> terminate!(integrator); - save_positions=(false, false)) + terminateCb = ContinuousCallback( + (x, t, integrator) -> + checkExecTime(integrator, nfmu, c, max_execution_duration), + (integrator) -> terminate!(integrator); + save_positions = (false, false), + ) push!(callbacks, terminateCb) logInfo(nfmu.fmu, "Setting max execeution time to $(max_execution_duration)") end @@ -1200,58 +1403,93 @@ function (nfmu::ME_NeuralFMU)(x_start::Union{Array{<:Real}, Nothing} = nfmu.x0, end if showProgress - c.progressMeter = ProgressMeter.Progress(1000; desc=progressDescr, color=:blue, dt=1.0) + c.progressMeter = + ProgressMeter.Progress(1000; desc = progressDescr, color = :blue, dt = 1.0) ProgressMeter.update!(c.progressMeter, 0) # show it! else c.progressMeter = nothing end # integrator step callback - stepCb = FunctionCallingCallback((x, t, integrator) -> stepCompleted(nfmu, c, x, t, integrator, t_start, t_stop); - func_everystep=true, - func_start=true) + stepCb = FunctionCallingCallback( + (x, t, integrator) -> + stepCompleted(nfmu, c, x, t, integrator, t_start, t_stop); + func_everystep = true, + func_start = true, + ) push!(callbacks, stepCb) # [ToDo] Allow for AD-primitives for sensitivity analysis of recorded values if saving - c.solution.values = SavedValues(Float64, Tuple{collect(Float64 for i in 1:length(recordValues))...}) + c.solution.values = SavedValues( + Float64, + Tuple{collect(Float64 for i = 1:length(recordValues))...}, + ) c.solution.valueReferences = recordValues if isnothing(saveat) - savingCB = SavingCallback((x, t, integrator) -> saveValues(nfmu, c, recordValues, x, t, integrator), - c.solution.values) + savingCB = SavingCallback( + (x, t, integrator) -> + saveValues(nfmu, c, recordValues, x, t, integrator), + c.solution.values, + ) else - savingCB = SavingCallback((x, t, integrator) -> saveValues(nfmu, c, recordValues, x, t, integrator), - c.solution.values, - saveat=saveat) + savingCB = SavingCallback( + (x, t, integrator) -> + saveValues(nfmu, c, recordValues, x, t, integrator), + c.solution.values, + saveat = saveat, + ) end push!(callbacks, savingCB) end if recordEigenvalues - @assert recordEigenvaluesSensitivity ∈ (:none, :ForwardDiff, :ReverseDiff, :Zygote) "Keyword `recordEigenvaluesSensitivity` must be one of (:none, :ForwardDiff, :ReverseDiff, :Zygote)" - + @assert recordEigenvaluesSensitivity ∈ + (:none, :ForwardDiff, :ReverseDiff, :Zygote) "Keyword `recordEigenvaluesSensitivity` must be one of (:none, :ForwardDiff, :ReverseDiff, :Zygote)" + recordEigenvaluesType = nothing - if recordEigenvaluesSensitivity == :ForwardDiff - recordEigenvaluesType = FMISensitivity.ForwardDiff.Dual - elseif recordEigenvaluesSensitivity == :ReverseDiff - recordEigenvaluesType = FMISensitivity.ReverseDiff.TrackedReal + if recordEigenvaluesSensitivity == :ForwardDiff + recordEigenvaluesType = FMISensitivity.ForwardDiff.Dual + elseif recordEigenvaluesSensitivity == :ReverseDiff + recordEigenvaluesType = FMISensitivity.ReverseDiff.TrackedReal elseif recordEigenvaluesSensitivity ∈ (:none, :Zygote) recordEigenvaluesType = fmi2Real end - dtypes = collect(recordEigenvaluesType for _ in 1:2*length(c.fmu.modelDescription.stateValueReferences)) + dtypes = collect( + recordEigenvaluesType for + _ = 1:2*length(c.fmu.modelDescription.stateValueReferences) + ) c.solution.eigenvalues = SavedValues(recordEigenvaluesType, Tuple{dtypes...}) - + savingCB = nothing if isnothing(saveat) - savingCB = SavingCallback((u,t,integrator) -> saveEigenvalues(nfmu, c, u, t, integrator, recordEigenvaluesSensitivity), - c.solution.eigenvalues) + savingCB = SavingCallback( + (u, t, integrator) -> saveEigenvalues( + nfmu, + c, + u, + t, + integrator, + recordEigenvaluesSensitivity, + ), + c.solution.eigenvalues, + ) else - savingCB = SavingCallback((u,t,integrator) -> saveEigenvalues(nfmu, c, u, t, integrator, recordEigenvaluesSensitivity), - c.solution.eigenvalues, - saveat=saveat) + savingCB = SavingCallback( + (u, t, integrator) -> saveEigenvalues( + nfmu, + c, + u, + t, + integrator, + recordEigenvaluesSensitivity, + ), + c.solution.eigenvalues, + saveat = saveat, + ) end push!(callbacks, savingCB) end @@ -1268,7 +1506,7 @@ function (nfmu::ME_NeuralFMU)(x_start::Union{Array{<:Real}, Nothing} = nfmu.x0, # function fx_op(x, p, t) # return fx(nfmu, c, x, p, t) # end - + # function fx_jac(J, x, p, t) # J[:] = ReverseDiff.jacobian(_x -> fx_op(_x, p, t), x) # return nothing @@ -1305,12 +1543,12 @@ function (nfmu::ME_NeuralFMU)(x_start::Union{Array{<:Real}, Nothing} = nfmu.x0, # logWarning(nfmu.fmu, "Implicit solver detected for NeuralFMU.\nOnly relevant if you use AD: Continuous adjoint method is applied, which requires solving backward in time.\nThis might be not supported by every FMU.", 1) # sensealg = InterpolatingAdjoint(; autojacvec=ReverseDiffVJP(true), checkpointing=true) # else - sensealg = ReverseDiffAdjoint() + sensealg = ReverseDiffAdjoint() #end end args = Vector{Any}() - kwargs = Dict{Symbol, Any}(nfmu.solvekwargs..., solvekwargs...) + kwargs = Dict{Symbol,Any}(nfmu.solvekwargs..., solvekwargs...) if !isnothing(saveat) kwargs[:saveat] = saveat @@ -1319,29 +1557,36 @@ function (nfmu::ME_NeuralFMU)(x_start::Union{Array{<:Real}, Nothing} = nfmu.x0, ignore_derivatives() do if !isnothing(solver) push!(args, solver) - end + end end #kwargs[:callback]=CallbackSet(callbacks...) #kwargs[:sensealg]=sensealg #kwargs[:u0] = nfmu.x0 # this is because of `IntervalNonlinearProblem has no field u0` - + @debug "ME_NeuralFMU: Start solving ..." - - c.solution.states = solve(prob, args...; callback=CallbackSet(callbacks...), sensealg=sensealg, u0=nfmu.x0, kwargs...) + + c.solution.states = solve( + prob, + args...; + callback = CallbackSet(callbacks...), + sensealg = sensealg, + u0 = nfmu.x0, + kwargs..., + ) @debug "ME_NeuralFMU: ... finished solving!" ignore_derivatives() do @assert !isnothing(c.solution.states) "Solving NeuralODE returned `nothing`!" - + # ReverseDiff returns an array instead of an ODESolution, this needs to be corrected # [TODO] doesn`t Array cover the TrackedArray case? - if isa(c.solution.states, TrackedArray) || isa(c.solution.states, Array) + if isa(c.solution.states, TrackedArray) || isa(c.solution.states, Array) @assert !isnothing(saveat) "Keyword `saveat` is nothing, please provide the keyword when using ReverseDiff." - + t = collect(saveat) while t[1] < tspan[1] popfirst!(t) @@ -1350,15 +1595,16 @@ function (nfmu::ME_NeuralFMU)(x_start::Union{Array{<:Real}, Nothing} = nfmu.x0, pop!(t) end u = c.solution.states - c.solution.success = (size(u) == (length(nfmu.x0), length(t))) + c.solution.success = (size(u) == (length(nfmu.x0), length(t))) if size(u)[2] > 0 # at least some recorded points - c.solution.states = build_solution(prob, solver, t, collect(u[:,i] for i in 1:size(u)[2])) + c.solution.states = + build_solution(prob, solver, t, collect(u[:, i] for i = 1:size(u)[2])) end else c.solution.success = (c.solution.states.retcode == ReturnCode.Success) end - + end # ignore_derivatives @debug "ME_NeuralFMU: Stopping callback..." @@ -1368,7 +1614,7 @@ function (nfmu::ME_NeuralFMU)(x_start::Union{Array{<:Real}, Nothing} = nfmu.x0, # cleanup snapshots to release memory if cleanSnapshots - for snapshot in c.solution.snapshots + for snapshot in c.solution.snapshots FMIBase.freeSnapshot!(snapshot) end c.solution.snapshots = Vector{FMUSnapshot}(undef, 0) @@ -1376,9 +1622,7 @@ function (nfmu::ME_NeuralFMU)(x_start::Union{Array{<:Real}, Nothing} = nfmu.x0, return c.solution end -function (nfmu::ME_NeuralFMU)(x0::Union{Array{<:Real}, Nothing}, - t::Real; - p=nothing) +function (nfmu::ME_NeuralFMU)(x0::Union{Array{<:Real},Nothing}, t::Real; p = nothing) c = nothing @@ -1393,25 +1637,36 @@ Evaluates the CS_NeuralFMU in the timespan given during construction or in a cus Via optional argument `reset`, the FMU is reset every time evaluation is started (default=`true`). """ -function (nfmu::CS_NeuralFMU{F, C})(inputFct, - t_step::Real, - tspan::Tuple{Float64, Float64} = nfmu.tspan; - p=nfmu.p, - tolerance::Union{Real, Nothing} = nothing, - parameters::Union{Dict{<:Any, <:Any}, Nothing} = nothing) where {F, C} +function (nfmu::CS_NeuralFMU{F,C})( + inputFct, + t_step::Real, + tspan::Tuple{Float64,Float64} = nfmu.tspan; + p = nfmu.p, + tolerance::Union{Real,Nothing} = nothing, + parameters::Union{Dict{<:Any,<:Any},Nothing} = nothing, +) where {F,C} t_start, t_stop = tspan c = (hasCurrentInstance(nfmu.fmu) ? getCurrentInstance(nfmu.fmu) : nothing) - c, _ = prepareSolveFMU(nfmu.fmu, c, fmi2TypeCoSimulation; parameters=parameters, t_start=t_start, t_stop=t_stop, tolerance=tolerance, cleanup=true) - + c, _ = prepareSolveFMU( + nfmu.fmu, + c, + fmi2TypeCoSimulation; + parameters = parameters, + t_start = t_start, + t_stop = t_stop, + tolerance = tolerance, + cleanup = true, + ) + ts = collect(t_start:t_step:t_stop) - + model_input = inputFct.(ts) firstStep = true function simStep(input) - y = nothing + y = nothing if !firstStep ignore_derivatives() do @@ -1427,17 +1682,17 @@ function (nfmu::CS_NeuralFMU{F, C})(inputFct, @assert !isnothing(nfmu.re) "Using explicite parameters without destructing the model." y = nfmu.re(p)(input) end - + return y end valueStack = simStep.(model_input) - + ignore_derivatives() do c.solution.success = true end - c.solution.values = SavedValues{typeof(ts[1]), typeof(valueStack[1])}(ts, valueStack) + c.solution.values = SavedValues{typeof(ts[1]),typeof(valueStack[1])}(ts, valueStack) # [ToDo] check if this is still the case for current releases of related libraries # this is not possible in CS (pullbacks are sometimes called after the finished simulation), clean-up happens at the next call @@ -1446,27 +1701,40 @@ function (nfmu::CS_NeuralFMU{F, C})(inputFct, return c.solution end -function (nfmu::CS_NeuralFMU{Vector{F}, Vector{C}})(inputFct, - t_step::Real, - tspan::Tuple{Float64, Float64} = nfmu.tspan; - p=nothing, - tolerance::Union{Real, Nothing} = nothing, - parameters::Union{Vector{Union{Dict{<:Any, <:Any}, Nothing}}, Nothing} = nothing) where {F, C} +function (nfmu::CS_NeuralFMU{Vector{F},Vector{C}})( + inputFct, + t_step::Real, + tspan::Tuple{Float64,Float64} = nfmu.tspan; + p = nothing, + tolerance::Union{Real,Nothing} = nothing, + parameters::Union{Vector{Union{Dict{<:Any,<:Any},Nothing}},Nothing} = nothing, +) where {F,C} t_start, t_stop = tspan numFMU = length(nfmu.fmu) cs = nothing ignore_derivatives() do - cs = Vector{Union{FMU2Component, Nothing}}(undef, numFMU) - for i in 1:numFMU - cs[i] = (hasCurrentInstance(nfmu.fmu[i]) ? getCurrentInstance(nfmu.fmu[i]) : nothing) + cs = Vector{Union{FMU2Component,Nothing}}(undef, numFMU) + for i = 1:numFMU + cs[i] = ( + hasCurrentInstance(nfmu.fmu[i]) ? getCurrentInstance(nfmu.fmu[i]) : nothing + ) end end - for i in 1:numFMU - cs[i], _ = prepareSolveFMU(nfmu.fmu[i], cs[i], fmi2TypeCoSimulation; parameters=parameters, t_start=t_start, t_stop=t_stop, tolerance=tolerance, cleanup=true) + for i = 1:numFMU + cs[i], _ = prepareSolveFMU( + nfmu.fmu[i], + cs[i], + fmi2TypeCoSimulation; + parameters = parameters, + t_start = t_start, + t_stop = t_stop, + tolerance = tolerance, + cleanup = true, + ) end - + solution = FMUSolution(nothing) ts = collect(t_start:t_step:t_stop) @@ -1490,7 +1758,7 @@ function (nfmu::CS_NeuralFMU{Vector{F}, Vector{C}})(inputFct, y = nfmu.model(input) else # flattened, explicite parameters @assert nfmu.re != nothing "Using explicite parameters without destructing the model." - + if length(p) == 1 y = nfmu.re(p[1])(input) else @@ -1505,10 +1773,10 @@ function (nfmu::CS_NeuralFMU{Vector{F}, Vector{C}})(inputFct, ignore_derivatives() do solution.success = true # ToDo: Check successful simulation! - end - - solution.values = SavedValues{typeof(ts[1]), typeof(valueStack[1])}(ts, valueStack) - + end + + solution.values = SavedValues{typeof(ts[1]),typeof(valueStack[1])}(ts, valueStack) + # [ToDo] check if this is still the case for current releases of related libraries # this is not possible in CS (pullbacks are sometimes called after the finished simulation), clean-up happens at the next call # cs = finishSolveFMU(nfmu.fmu, cs, freeInstance, terminate) @@ -1517,8 +1785,8 @@ function (nfmu::CS_NeuralFMU{Vector{F}, Vector{C}})(inputFct, end # adapting the Flux functions -function Flux.params(nfmu::ME_NeuralFMU; destructure::Bool=false) - if destructure +function Flux.params(nfmu::ME_NeuralFMU; destructure::Bool = false) + if destructure nfmu.p, nfmu.re = Flux.destructure(nfmu.model) end @@ -1531,12 +1799,12 @@ function Flux.params(nfmu::ME_NeuralFMU; destructure::Bool=false) return ps end -function Flux.params(nfmu::CS_NeuralFMU; destructure::Bool=false) # true) - if destructure +function Flux.params(nfmu::CS_NeuralFMU; destructure::Bool = false) # true) + if destructure nfmu.p, nfmu.re = Flux.destructure(nfmu.model) - - # else - # return Flux.params(nfmu.model) + + # else + # return Flux.params(nfmu.model) end ps = Flux.params(nfmu.p) @@ -1548,12 +1816,19 @@ function Flux.params(nfmu::CS_NeuralFMU; destructure::Bool=false) # true) return ps end -function computeGradient!(jac, loss, params, gradient::Symbol, chunk_size::Union{Symbol, Int}, multiObjective::Bool) +function computeGradient!( + jac, + loss, + params, + gradient::Symbol, + chunk_size::Union{Symbol,Int}, + multiObjective::Bool, +) if gradient == :ForwardDiff if chunk_size == :auto_forwarddiff - + if multiObjective conf = ForwardDiff.JacobianConfig(loss, params) ForwardDiff.jacobian!(jac, loss, params, conf) @@ -1565,26 +1840,42 @@ function computeGradient!(jac, loss, params, gradient::Symbol, chunk_size::Union elseif chunk_size == :auto_fmiflux chunk_size = DEFAULT_CHUNK_SIZE - + if multiObjective - conf = ForwardDiff.JacobianConfig(loss, params, ForwardDiff.Chunk{min(chunk_size, length(params))}()); + conf = ForwardDiff.JacobianConfig( + loss, + params, + ForwardDiff.Chunk{min(chunk_size, length(params))}(), + ) ForwardDiff.jacobian!(jac, loss, params, conf) else - conf = ForwardDiff.GradientConfig(loss, params, ForwardDiff.Chunk{min(chunk_size, length(params))}()); + conf = ForwardDiff.GradientConfig( + loss, + params, + ForwardDiff.Chunk{min(chunk_size, length(params))}(), + ) ForwardDiff.gradient!(jac, loss, params, conf) end else if multiObjective - conf = ForwardDiff.JacobianConfig(loss, params, ForwardDiff.Chunk{min(chunk_size, length(params))}()); + conf = ForwardDiff.JacobianConfig( + loss, + params, + ForwardDiff.Chunk{min(chunk_size, length(params))}(), + ) ForwardDiff.jacobian!(jac, loss, params, conf) else - conf = ForwardDiff.GradientConfig(loss, params, ForwardDiff.Chunk{min(chunk_size, length(params))}()); + conf = ForwardDiff.GradientConfig( + loss, + params, + ForwardDiff.Chunk{min(chunk_size, length(params))}(), + ) ForwardDiff.gradient!(jac, loss, params, conf) end end - elseif gradient == :Zygote + elseif gradient == :Zygote if multiObjective jac[:] = Zygote.jacobian(loss, params)[1] @@ -1592,14 +1883,14 @@ function computeGradient!(jac, loss, params, gradient::Symbol, chunk_size::Union jac[:] = Zygote.gradient(loss, params)[1] end - elseif gradient == :ReverseDiff + elseif gradient == :ReverseDiff if multiObjective ReverseDiff.jacobian!(jac, loss, params) else ReverseDiff.gradient!(jac, loss, params) end - elseif gradient == :FiniteDiff + elseif gradient == :FiniteDiff if multiObjective FiniteDiff.finite_difference_jacobian!(jac, loss, params) @@ -1615,14 +1906,15 @@ function computeGradient!(jac, loss, params, gradient::Symbol, chunk_size::Union # [Todo] Better! grads = nothing if multiObjective - grads = collect(jac[i,:] for i in 1:size(jac)[1]) + grads = collect(jac[i, :] for i = 1:size(jac)[1]) else grads = [jac] end all_zero = any(collect(all(iszero.(grad)) for grad in grads)) has_nan = any(collect(any(isnan.(grad)) for grad in grads)) - has_nothing = any(collect(any(isnothing.(grad)) for grad in grads)) || any(isnothing.(grads)) + has_nothing = + any(collect(any(isnothing.(grad)) for grad in grads)) || any(isnothing.(grads)) @assert !all_zero "Determined gradient containes only zeros.\nThis might be because the loss function is:\n(a) not sensitive regarding the model parameters or\n(b) sensitivities regarding the model parameters are not traceable via AD." @@ -1632,15 +1924,16 @@ function computeGradient!(jac, loss, params, gradient::Symbol, chunk_size::Union computeGradient!(jac, loss, params, gradient, chunk_size, multiObjective) if multiObjective - grads = collect(jac[i,:] for i in 1:size(jac)[1]) + grads = collect(jac[i, :] for i = 1:size(jac)[1]) else grads = [jac] end end has_nan = any(collect(any(isnan.(grad)) for grad in grads)) - has_nothing = any(collect(any(isnothing.(grad)) for grad in grads)) || any(isnothing.(grads)) - + has_nothing = + any(collect(any(isnothing.(grad)) for grad in grads)) || any(isnothing.(grads)) + @assert !has_nan "Gradient determination with $(gradient) failed, because gradient contains `NaNs`.\nNo back-up options available." @assert !has_nothing "Gradient determination with $(gradient) failed, because gradient contains `nothing`.\nNo back-up options available." @@ -1648,27 +1941,37 @@ function computeGradient!(jac, loss, params, gradient::Symbol, chunk_size::Union end lk_TrainApply = ReentrantLock() -function trainStep(loss, params, gradient, chunk_size, optim::FMIFlux.AbstractOptimiser, printStep, proceed_on_assert, cb, multiObjective) +function trainStep( + loss, + params, + gradient, + chunk_size, + optim::FMIFlux.AbstractOptimiser, + printStep, + proceed_on_assert, + cb, + multiObjective, +) global lk_TrainApply - + #try - - for j in 1:length(params) - step = FMIFlux.apply!(optim, params[j]) + for j = 1:length(params) - lock(lk_TrainApply) do - - params[j] .-= step + step = FMIFlux.apply!(optim, params[j]) - if printStep - @info "Step: min(abs()) = $(min(abs.(step)...)) max(abs()) = $(max(abs.(step)...))" - end - + lock(lk_TrainApply) do + + params[j] .-= step + + if printStep + @info "Step: min(abs()) = $(min(abs.(step)...)) max(abs()) = $(max(abs.(step)...))" end - end + end + + end # catch e @@ -1681,9 +1984,9 @@ function trainStep(loss, params, gradient, chunk_size, optim::FMIFlux.AbstractOp # end # end - if cb != nothing + if cb != nothing if isa(cb, AbstractArray) - for _cb in cb + for _cb in cb _cb() end else @@ -1714,16 +2017,23 @@ A function analogous to Flux.train! but with additional features and explicit pa - `multiThreading`: a boolean that determins if multiple gradients are generated in parallel (default `false`) - `multiObjective`: set this if the loss function returns multiple values (multi objective optimization), currently gradients are fired to the optimizer one after another (default `false`) """ -function train!(loss, neuralFMU::Union{ME_NeuralFMU, CS_NeuralFMU}, data, optim; gradient::Symbol=:ReverseDiff, kwargs...) - params = Flux.params(neuralFMU) +function train!( + loss, + neuralFMU::Union{ME_NeuralFMU,CS_NeuralFMU}, + data, + optim; + gradient::Symbol = :ReverseDiff, + kwargs..., +) + params = Flux.params(neuralFMU) snapshots = neuralFMU.snapshots # [Note] :ReverseDiff, :Zygote need it for state change sampling and the rrule # :ForwardDiff needs it for state change sampling neuralFMU.snapshots = true - - _train!(loss, params, data, optim; gradient=gradient, kwargs...) + + _train!(loss, params, data, optim; gradient = gradient, kwargs...) neuralFMU.snapshots = snapshots neuralFMU.p = unsense(neuralFMU.p) @@ -1732,27 +2042,41 @@ function train!(loss, neuralFMU::Union{ME_NeuralFMU, CS_NeuralFMU}, data, optim; end # Dispatch for FMIFlux.jl [FMIFlux.AbstractOptimiser] -function _train!(loss, - params::Union{Flux.Params, Zygote.Params, AbstractVector{<:AbstractVector{<:Real}}}, - data, - optim::FMIFlux.AbstractOptimiser; - gradient::Symbol=:ReverseDiff, - cb=nothing, chunk_size::Union{Integer, Symbol}=:auto_fmiflux, - printStep::Bool=false, - proceed_on_assert::Bool=false, - multiThreading::Bool=false, - multiObjective::Bool=false) - - if length(params) <= 0 || length(params[1]) <= 0 +function _train!( + loss, + params::Union{Flux.Params,Zygote.Params,AbstractVector{<:AbstractVector{<:Real}}}, + data, + optim::FMIFlux.AbstractOptimiser; + gradient::Symbol = :ReverseDiff, + cb = nothing, + chunk_size::Union{Integer,Symbol} = :auto_fmiflux, + printStep::Bool = false, + proceed_on_assert::Bool = false, + multiThreading::Bool = false, + multiObjective::Bool = false, +) + + if length(params) <= 0 || length(params[1]) <= 0 @warn "train!(...): Empty parameter array, training on an empty parameter array doesn't make sense." - return + return end - if multiThreading && Threads.nthreads() == 1 + if multiThreading && Threads.nthreads() == 1 @warn "train!(...): Multi-threading is set via flag `multiThreading=true`, but this Julia process does not have multiple threads. This will not result in a speed-up. Please spawn Julia in multi-thread mode to speed-up training." end - _trainStep = (i,) -> trainStep(loss, params, gradient, chunk_size, optim, printStep, proceed_on_assert, cb, multiObjective) + _trainStep = + (i,) -> trainStep( + loss, + params, + gradient, + chunk_size, + optim, + printStep, + proceed_on_assert, + cb, + multiObjective, + ) if multiThreading ThreadPools.qforeach(_trainStep, 1:length(data)) @@ -1763,8 +2087,17 @@ function _train!(loss, end # Dispatch for Flux.jl [Flux.Optimise.AbstractOptimiser] -function _train!(loss, params::Union{Flux.Params, Zygote.Params, AbstractVector{<:AbstractVector{<:Real}}}, data, optim::Flux.Optimise.AbstractOptimiser; gradient::Symbol=:ReverseDiff, chunk_size::Union{Integer, Symbol}=:auto_fmiflux, multiObjective::Bool=false, kwargs...) - +function _train!( + loss, + params::Union{Flux.Params,Zygote.Params,AbstractVector{<:AbstractVector{<:Real}}}, + data, + optim::Flux.Optimise.AbstractOptimiser; + gradient::Symbol = :ReverseDiff, + chunk_size::Union{Integer,Symbol} = :auto_fmiflux, + multiObjective::Bool = false, + kwargs..., +) + grad_buffer = nothing if multiObjective @@ -1772,22 +2105,49 @@ function _train!(loss, params::Union{Flux.Params, Zygote.Params, AbstractVector{ grad_buffer = zeros(Float64, length(params[1]), length(dim)) else - grad_buffer = zeros(Float64, length(params[1])) + grad_buffer = zeros(Float64, length(params[1])) end grad_fun! = (G, p) -> computeGradient!(G, loss, p, gradient, chunk_size, multiObjective) _optim = FluxOptimiserWrapper(optim, grad_fun!, grad_buffer) - _train!(loss, params, data, _optim; gradient=gradient, chunk_size=chunk_size, multiObjective=multiObjective, kwargs...) + _train!( + loss, + params, + data, + _optim; + gradient = gradient, + chunk_size = chunk_size, + multiObjective = multiObjective, + kwargs..., + ) end # Dispatch for Optim.jl [Optim.AbstractOptimizer] -function _train!(loss, params::Union{Flux.Params, Zygote.Params, AbstractVector{<:AbstractVector{<:Real}}}, data, optim::Optim.AbstractOptimizer; gradient::Symbol=:ReverseDiff, chunk_size::Union{Integer, Symbol}=:auto_fmiflux, multiObjective::Bool=false, kwargs...) - if length(params) <= 0 || length(params[1]) <= 0 +function _train!( + loss, + params::Union{Flux.Params,Zygote.Params,AbstractVector{<:AbstractVector{<:Real}}}, + data, + optim::Optim.AbstractOptimizer; + gradient::Symbol = :ReverseDiff, + chunk_size::Union{Integer,Symbol} = :auto_fmiflux, + multiObjective::Bool = false, + kwargs..., +) + if length(params) <= 0 || length(params[1]) <= 0 @warn "train!(...): Empty parameter array, training on an empty parameter array doesn't make sense." - return + return end - + grad_fun! = (G, p) -> computeGradient!(G, loss, p, gradient, chunk_size, multiObjective) _optim = OptimOptimiserWrapper(optim, grad_fun!, loss, params[1]) - _train!(loss, params, data, _optim; gradient=gradient, chunk_size=chunk_size, multiObjective=multiObjective, kwargs...) + _train!( + loss, + params, + data, + _optim; + gradient = gradient, + chunk_size = chunk_size, + multiObjective = multiObjective, + kwargs..., + ) end diff --git a/src/optimiser.jl b/src/optimiser.jl index 7cecff81..ae628bcb 100644 --- a/src/optimiser.jl +++ b/src/optimiser.jl @@ -14,12 +14,22 @@ struct OptimOptimiserWrapper{G} <: AbstractOptimiser optim::Optim.AbstractOptimizer grad_fun!::G - state::Union{Optim.AbstractOptimizerState, Nothing} - d::Union{Optim.OnceDifferentiable, Nothing} - options - - function OptimOptimiserWrapper(optim::Optim.AbstractOptimizer, grad_fun!::G, loss, params) where {G} - options = Optim.Options(outer_iterations=1, iterations=1, g_calls_limit=1, f_calls_limit=5) + state::Union{Optim.AbstractOptimizerState,Nothing} + d::Union{Optim.OnceDifferentiable,Nothing} + options::Any + + function OptimOptimiserWrapper( + optim::Optim.AbstractOptimizer, + grad_fun!::G, + loss, + params, + ) where {G} + options = Optim.Options( + outer_iterations = 1, + iterations = 1, + g_calls_limit = 1, + f_calls_limit = 5, + ) # should be ignored anyway, because function `g!` is given autodiff = :forward # = ::finite @@ -33,7 +43,7 @@ struct OptimOptimiserWrapper{G} <: AbstractOptimiser end export OptimOptimiserWrapper - + function apply!(optim::OptimOptimiserWrapper, params) res = Optim.optimize(optim.d, params, optim.optim, optim.options, optim.state) @@ -47,26 +57,37 @@ end struct FluxOptimiserWrapper{G} <: AbstractOptimiser optim::Flux.Optimise.AbstractOptimiser grad_fun!::G - grad_buffer::Union{AbstractVector{Float64}, AbstractMatrix{Float64}} + grad_buffer::Union{AbstractVector{Float64},AbstractMatrix{Float64}} multiGrad::Bool - function FluxOptimiserWrapper(optim::Flux.Optimise.AbstractOptimiser, grad_fun!::G, grad_buffer::AbstractVector{Float64}) where {G} + function FluxOptimiserWrapper( + optim::Flux.Optimise.AbstractOptimiser, + grad_fun!::G, + grad_buffer::AbstractVector{Float64}, + ) where {G} return new{G}(optim, grad_fun!, grad_buffer, false) end - function FluxOptimiserWrapper(optim::Flux.Optimise.AbstractOptimiser, grad_fun!::G, grad_buffer::AbstractMatrix{Float64}) where {G} + function FluxOptimiserWrapper( + optim::Flux.Optimise.AbstractOptimiser, + grad_fun!::G, + grad_buffer::AbstractMatrix{Float64}, + ) where {G} return new{G}(optim, grad_fun!, grad_buffer, true) end end export FluxOptimiserWrapper - + function apply!(optim::FluxOptimiserWrapper, params) optim.grad_fun!(optim.grad_buffer, params) if optim.multiGrad - return collect(Flux.Optimise.apply!(optim.optim, params, optim.grad_buffer[:,i]) for i in 1:size(optim.grad_buffer)[2]) + return collect( + Flux.Optimise.apply!(optim.optim, params, optim.grad_buffer[:, i]) for + i = 1:size(optim.grad_buffer)[2] + ) else return Flux.Optimise.apply!(optim.optim, params, optim.grad_buffer) end @@ -75,4 +96,3 @@ end ### generic FMIFlux.AbstractOptimiser ### - \ No newline at end of file diff --git a/src/scheduler.jl b/src/scheduler.jl index 4ae8c67e..ed7618eb 100644 --- a/src/scheduler.jl +++ b/src/scheduler.jl @@ -19,19 +19,27 @@ mutable struct WorstElementScheduler <: BatchScheduler elementIndex::Integer applyStep::Integer plotStep::Integer - batch + batch::Any neuralFMU::NeuralFMU losses::Vector{Float64} logLoss::Bool ### type specific ### - lossFct - runkwargs + lossFct::Any + runkwargs::Any printMsg::String updateStep::Integer - excludeIndices - - function WorstElementScheduler(neuralFMU::NeuralFMU, batch, lossFct=Flux.Losses.mse; applyStep::Integer=1, plotStep::Integer=1, updateStep::Integer=1, excludeIndices=nothing) + excludeIndices::Any + + function WorstElementScheduler( + neuralFMU::NeuralFMU, + batch, + lossFct = Flux.Losses.mse; + applyStep::Integer = 1, + plotStep::Integer = 1, + updateStep::Integer = 1, + excludeIndices = nothing, + ) inst = new() inst.neuralFMU = neuralFMU inst.step = 0 @@ -46,7 +54,7 @@ mutable struct WorstElementScheduler <: BatchScheduler inst.printMsg = "" inst.updateStep = updateStep inst.excludeIndices = excludeIndices - + return inst end end @@ -63,19 +71,26 @@ mutable struct LossAccumulationScheduler <: BatchScheduler elementIndex::Integer applyStep::Integer plotStep::Integer - batch + batch::Any neuralFMU::NeuralFMU losses::Vector{Float64} logLoss::Bool ### type specific ### - lossFct - runkwargs + lossFct::Any + runkwargs::Any printMsg::String lossAccu::Array{<:Real} updateStep::Integer - - function LossAccumulationScheduler(neuralFMU::NeuralFMU, batch, lossFct=Flux.Losses.mse; applyStep::Integer=1, plotStep::Integer=1, updateStep::Integer=1) + + function LossAccumulationScheduler( + neuralFMU::NeuralFMU, + batch, + lossFct = Flux.Losses.mse; + applyStep::Integer = 1, + plotStep::Integer = 1, + updateStep::Integer = 1, + ) inst = new() inst.neuralFMU = neuralFMU inst.step = 0 @@ -87,7 +102,7 @@ mutable struct LossAccumulationScheduler <: BatchScheduler inst.updateStep = updateStep inst.losses = [] inst.logLoss = false - + inst.printMsg = "" inst.lossAccu = zeros(length(batch)) @@ -106,17 +121,23 @@ mutable struct WorstGrowScheduler <: BatchScheduler elementIndex::Integer applyStep::Integer plotStep::Integer - batch + batch::Any neuralFMU::NeuralFMU losses::Vector{Float64} logLoss::Bool ### type specific ### - lossFct - runkwargs + lossFct::Any + runkwargs::Any printMsg::String - function WorstGrowScheduler(neuralFMU::NeuralFMU, batch, lossFct=Flux.Losses.mse; applyStep::Integer=1, plotStep::Integer=1) + function WorstGrowScheduler( + neuralFMU::NeuralFMU, + batch, + lossFct = Flux.Losses.mse; + applyStep::Integer = 1, + plotStep::Integer = 1, + ) inst = new() inst.neuralFMU = neuralFMU inst.step = 0 @@ -145,15 +166,20 @@ mutable struct RandomScheduler <: BatchScheduler elementIndex::Integer applyStep::Integer plotStep::Integer - batch + batch::Any neuralFMU::NeuralFMU losses::Vector{Float64} logLoss::Bool - + ### type specific ### printMsg::String - function RandomScheduler(neuralFMU::NeuralFMU, batch; applyStep::Integer=1, plotStep::Integer=1) + function RandomScheduler( + neuralFMU::NeuralFMU, + batch; + applyStep::Integer = 1, + plotStep::Integer = 1, + ) inst = new() inst.neuralFMU = neuralFMU inst.step = 0 @@ -181,15 +207,20 @@ mutable struct SequentialScheduler <: BatchScheduler elementIndex::Integer applyStep::Integer plotStep::Integer - batch + batch::Any neuralFMU::NeuralFMU losses::Vector{Float64} logLoss::Bool - + ### type specific ### printMsg::String - function SequentialScheduler(neuralFMU::NeuralFMU, batch; applyStep::Integer=1, plotStep::Integer=1) + function SequentialScheduler( + neuralFMU::NeuralFMU, + batch; + applyStep::Integer = 1, + plotStep::Integer = 1, + ) inst = new() inst.neuralFMU = neuralFMU inst.step = 0 @@ -206,7 +237,7 @@ mutable struct SequentialScheduler <: BatchScheduler end end -function initialize!(scheduler::BatchScheduler; print::Bool=true, runkwargs...) +function initialize!(scheduler::BatchScheduler; print::Bool = true, runkwargs...) lastIndex = 0 scheduler.step = 0 @@ -216,21 +247,21 @@ function initialize!(scheduler::BatchScheduler; print::Bool=true, runkwargs...) scheduler.runkwargs = runkwargs end - scheduler.elementIndex = apply!(scheduler; print=print) - + scheduler.elementIndex = apply!(scheduler; print = print) + if scheduler.plotStep > 0 plot(scheduler, lastIndex) end end -function update!(scheduler::BatchScheduler; print::Bool=true) +function update!(scheduler::BatchScheduler; print::Bool = true) lastIndex = scheduler.elementIndex scheduler.step += 1 - + if scheduler.applyStep > 0 && scheduler.step % scheduler.applyStep == 0 - scheduler.elementIndex = apply!(scheduler; print=print) + scheduler.elementIndex = apply!(scheduler; print = print) end # max/avg error @@ -238,19 +269,19 @@ function update!(scheduler::BatchScheduler; print::Bool=true) losssum = 0.0 avgsum = 0.0 maxe = 0.0 - for i in 1:num + for i = 1:num l = nominalLoss(scheduler.batch[i]) l = l == Inf ? 0.0 : l - + losssum += l avgsum += l / num - if l > maxe - maxe = l + if l > maxe + maxe = l end end push!(scheduler.losses, losssum) - + if print scheduler.printMsg = "AVG: $(roundToLength(avgsum, 8)) | MAX: $(roundToLength(maxe, 8)) | SUM: $(roundToLength(losssum, 8))" @info scheduler.printMsg @@ -264,47 +295,102 @@ end function plot(scheduler::BatchScheduler, lastIndex::Integer) num = length(scheduler.batch) - xs = 1:num + xs = 1:num ys = collect((nominalLoss(b) != Inf ? nominalLoss(b) : 0.0) for b in scheduler.batch) - ys_shadow = collect((length(b.losses) > 1 ? nominalLoss(b.losses[end-1]) : 1e-16) for b in scheduler.batch) - - title = "[$(scheduler.step)]" + ys_shadow = collect( + (length(b.losses) > 1 ? nominalLoss(b.losses[end-1]) : 1e-16) for + b in scheduler.batch + ) + + title = "[$(scheduler.step)]" if hasfield(typeof(scheduler), :printMsg) title = title * " " * scheduler.printMsg end - fig = Plots.plot(; layout=Plots.grid(2,1), size=(480,960), xlabel="Batch ID", ylabel="Loss", background_color_legend=colorant"rgba(255,255,255,0.5)", title=title) + fig = Plots.plot(; + layout = Plots.grid(2, 1), + size = (480, 960), + xlabel = "Batch ID", + ylabel = "Loss", + background_color_legend = colorant"rgba(255,255,255,0.5)", + title = title, + ) if hasfield(typeof(scheduler), :lossAccu) normScale = max(ys..., ys_shadow...) / max(scheduler.lossAccu...) - Plots.bar!(fig[1], xs, scheduler.lossAccu .* normScale, label="Accum. loss (norm.)", color=:blue, bar_width=1.0, alpha=0.2); + Plots.bar!( + fig[1], + xs, + scheduler.lossAccu .* normScale, + label = "Accum. loss (norm.)", + color = :blue, + bar_width = 1.0, + alpha = 0.2, + ) end good = [] bad = [] - for i in 1:num + for i = 1:num if ys[i] > ys_shadow[i] push!(bad, i) else push!(good, i) end end - - Plots.bar!(fig[1], xs[good], ys[good], label="Loss (better)", color=:green, bar_width=1.0); - Plots.bar!(fig[1], xs[bad], ys[bad], label="Loss (worse)", color=:orange, bar_width=1.0); - for i in 1:length(ys_shadow) - Plots.plot!(fig[1], [xs[i]-0.5, xs[i]+0.5], [ys_shadow[i], ys_shadow[i]], label=(i == 1 ? "Last loss" : :none), linewidth=2, color=:black); + Plots.bar!( + fig[1], + xs[good], + ys[good], + label = "Loss (better)", + color = :green, + bar_width = 1.0, + ) + Plots.bar!( + fig[1], + xs[bad], + ys[bad], + label = "Loss (worse)", + color = :orange, + bar_width = 1.0, + ) + + for i = 1:length(ys_shadow) + Plots.plot!( + fig[1], + [xs[i] - 0.5, xs[i] + 0.5], + [ys_shadow[i], ys_shadow[i]], + label = (i == 1 ? "Last loss" : :none), + linewidth = 2, + color = :black, + ) end - + if lastIndex > 0 - Plots.plot!(fig[1], [lastIndex], [0.0], color=:pink, marker=:circle, label="Current ID [$(lastIndex)]", markersize = 5.0) # current batch element + Plots.plot!( + fig[1], + [lastIndex], + [0.0], + color = :pink, + marker = :circle, + label = "Current ID [$(lastIndex)]", + markersize = 5.0, + ) # current batch element end - Plots.plot!(fig[1], [scheduler.elementIndex], [0.0], color=:pink, marker=:circle, label="Next ID [$(scheduler.elementIndex)]", markersize = 3.0) # next batch element - - Plots.plot!(fig[2], 1:length(scheduler.losses), scheduler.losses; yaxis=:log) - + Plots.plot!( + fig[1], + [scheduler.elementIndex], + [0.0], + color = :pink, + marker = :circle, + label = "Next ID [$(scheduler.elementIndex)]", + markersize = 3.0, + ) # next batch element + + Plots.plot!(fig[2], 1:length(scheduler.losses), scheduler.losses; yaxis = :log) + display(fig) end @@ -329,9 +415,9 @@ function roundToLength(number::Real, len::Integer) expLen = 0 - if abs(number) <= 1.0 - expLen = Integer(floor(log10(1.0/number))) + 1 - else + if abs(number) <= 1.0 + expLen = Integer(floor(log10(1.0 / number))) + 1 + else expLen = Integer(floor(log10(number))) + 1 end @@ -342,15 +428,15 @@ function roundToLength(number::Real, len::Integer) len -= 2 # 2 spaces needed for regular exponent end - if isneg - number = -number + if isneg + number = -number end - + return Printf.format(Printf.Format("%.$(len)e"), number) end -function apply!(scheduler::WorstElementScheduler; print::Bool=true) - +function apply!(scheduler::WorstElementScheduler; print::Bool = true) + avgsum = 0.0 losssum = 0.0 @@ -360,33 +446,37 @@ function apply!(scheduler::WorstElementScheduler; print::Bool=true) updateAll = (scheduler.step % scheduler.updateStep == 0) num = length(scheduler.batch) - for i in 1:num + for i = 1:num l = (nominalLoss(scheduler.batch[i]) != Inf ? nominalLoss(scheduler.batch[i]) : 0.0) - + if updateAll FMIFlux.run!(scheduler.neuralFMU, scheduler.batch[i]; scheduler.runkwargs...) - FMIFlux.loss!(scheduler.batch[i], scheduler.lossFct; logLoss=scheduler.logLoss) + FMIFlux.loss!( + scheduler.batch[i], + scheduler.lossFct; + logLoss = scheduler.logLoss, + ) l = nominalLoss(scheduler.batch[i]) end - + losssum += l avgsum += l / num if isnothing(scheduler.excludeIndices) || i ∉ scheduler.excludeIndices if l > maxe - maxe = l + maxe = l maxind = i end end - + end return maxind end -function apply!(scheduler::LossAccumulationScheduler; print::Bool=true) - +function apply!(scheduler::LossAccumulationScheduler; print::Bool = true) + avgsum = 0.0 losssum = 0.0 @@ -401,13 +491,17 @@ function apply!(scheduler::LossAccumulationScheduler; print::Bool=true) updateAll = (scheduler.step % scheduler.updateStep == 0) num = length(scheduler.batch) - for i in 1:num + for i = 1:num l = (nominalLoss(scheduler.batch[i]) != Inf ? nominalLoss(scheduler.batch[i]) : 0.0) - + if updateAll FMIFlux.run!(scheduler.neuralFMU, scheduler.batch[i]; scheduler.runkwargs...) - FMIFlux.loss!(scheduler.batch[i], scheduler.lossFct; logLoss=scheduler.logLoss) + FMIFlux.loss!( + scheduler.batch[i], + scheduler.lossFct; + logLoss = scheduler.logLoss, + ) l = nominalLoss(scheduler.batch[i]) end @@ -417,12 +511,12 @@ function apply!(scheduler::LossAccumulationScheduler; print::Bool=true) avgsum += l / num if l > maxe - maxe = l + maxe = l end end # find largest accumulated loss - for i in 1:num + for i = 1:num if scheduler.lossAccu[i] > scheduler.lossAccu[nextind] nextind = i end @@ -431,8 +525,8 @@ function apply!(scheduler::LossAccumulationScheduler; print::Bool=true) return nextind end -function apply!(scheduler::WorstGrowScheduler; print::Bool=true) - +function apply!(scheduler::WorstGrowScheduler; print::Bool = true) + avgsum = 0.0 losssum = 0.0 @@ -441,34 +535,38 @@ function apply!(scheduler::WorstGrowScheduler; print::Bool=true) maxind = 0 num = length(scheduler.batch) - for i in 1:num - + for i = 1:num + FMIFlux.run!(scheduler.neuralFMU, scheduler.batch[i]; scheduler.runkwargs...) - l = FMIFlux.loss!(scheduler.batch[i], scheduler.lossFct; logLoss=scheduler.logLoss) + l = FMIFlux.loss!( + scheduler.batch[i], + scheduler.lossFct; + logLoss = scheduler.logLoss, + ) l_der = l # fallback for first run (greatest error) if length(scheduler.batch[i].losses) >= 2 l_der = (l - nominalLoss(scheduler.batch[i].losses[end-1])) end - + losssum += l avgsum += l / num - if l > maxe - maxe = l + if l > maxe + maxe = l end if l_der > maxe_der maxe_der = l_der maxind = i end - + end return maxind end -function apply!(scheduler::RandomScheduler; print::Bool=true) +function apply!(scheduler::RandomScheduler; print::Bool = true) next = rand(1:length(scheduler.batch)) @@ -479,9 +577,9 @@ function apply!(scheduler::RandomScheduler; print::Bool=true) return next end -function apply!(scheduler::SequentialScheduler; print::Bool=true) +function apply!(scheduler::SequentialScheduler; print::Bool = true) - next = scheduler.elementIndex+1 + next = scheduler.elementIndex + 1 if next > length(scheduler.batch) next = 1 end diff --git a/test/batching.jl b/test/batching.jl index 9ed8aa59..8a262c08 100644 --- a/test/batching.jl +++ b/test/batching.jl @@ -5,7 +5,7 @@ using FMIFlux.Flux -import Random +import Random Random.seed!(1234); t_start = 0.0 @@ -17,36 +17,36 @@ tData = t_start:t_step:t_stop posData, velData, accData = syntTrainingData(tData) # load FMU for NeuralFMU -fmu = loadFMU("SpringPendulum1D", EXPORTINGTOOL, EXPORTINGVERSION; type=:ME) +fmu = loadFMU("SpringPendulum1D", EXPORTINGTOOL, EXPORTINGVERSION; type = :ME) using FMIFlux.FMIImport using FMIFlux.FMIImport.FMICore # loss function for training -losssum_single = function(p) +losssum_single = function (p) global problem, X0, posData - solution = problem(X0; p=p, showProgress=false, saveat=tData) + solution = problem(X0; p = p, showProgress = false, saveat = tData) if !solution.success - return Inf + return Inf end - posNet = getState(solution, 1; isIndex=true) - + posNet = getState(solution, 1; isIndex = true) + return Flux.Losses.mse(posNet, posData) end -losssum_multi = function(p) +losssum_multi = function (p) global problem, X0, posData - solution = problem(X0; p=p, showProgress=false, saveat=tData) + solution = problem(X0; p = p, showProgress = false, saveat = tData) if !solution.success return [Inf, Inf] end - posNet = getState(solution, 1; isIndex=true) - velNet = getState(solution, 2; isIndex=true) - + posNet = getState(solution, 1; isIndex = true) + velNet = getState(solution, 2; isIndex = true) + return [Flux.Losses.mse(posNet, posData), Flux.Losses.mse(velNet, velData)] end @@ -56,14 +56,16 @@ c1 = CacheLayer() c2 = CacheRetrieveLayer(c1) # the "Chain" for training -net = Chain(x -> fmu(;x=x, dx_refs=:all), - dx -> c1(dx), - Dense(numStates, 12, tanh), - Dense(12, 1, identity), - dx -> c2(1, dx[1])) +net = Chain( + x -> fmu(; x = x, dx_refs = :all), + dx -> c1(dx), + Dense(numStates, 12, tanh), + Dense(12, 1, identity), + dx -> c2(1, dx[1]), +) solver = Tsit5() -problem = ME_NeuralFMU(fmu, net, (t_start, t_stop), solver; saveat=tData) +problem = ME_NeuralFMU(fmu, net, (t_start, t_stop), solver; saveat = tData) @test problem != nothing # before @@ -72,7 +74,13 @@ lossBefore = losssum_single(p_net[1]) # single objective optim = OPTIMISER(ETA) -FMIFlux.train!(losssum_single, problem, Iterators.repeated((), NUMSTEPS), optim; gradient=GRADIENT) +FMIFlux.train!( + losssum_single, + problem, + Iterators.repeated((), NUMSTEPS), + optim; + gradient = GRADIENT, +) # multi objective # lastLoss = sum(losssum_multi(p_net[1])) diff --git a/test/eval.jl b/test/eval.jl index 3ed0fd67..db96c756 100644 --- a/test/eval.jl +++ b/test/eval.jl @@ -2,9 +2,9 @@ using PkgEval using FMIFlux using Test -config = Configuration(; julia="1.10", time_limit=120*60); +config = Configuration(; julia = "1.10", time_limit = 120 * 60); -package = Package(; name="FMIFlux"); +package = Package(; name = "FMIFlux"); @info "PkgEval" result = evaluate([config], [package]) diff --git a/test/fmu_params.jl b/test/fmu_params.jl index a81e7f2b..049f4db3 100644 --- a/test/fmu_params.jl +++ b/test/fmu_params.jl @@ -6,7 +6,7 @@ using Flux using DifferentialEquations: Tsit5 -import Random +import Random Random.seed!(1234); t_start = 0.0 @@ -19,7 +19,7 @@ posData, velData, accData = syntTrainingData(tData) # load FMU for NeuralFMU # [TODO] Replace by a suitable discontinuous FMU -fmu = loadFMU("SpringPendulum1D", EXPORTINGTOOL, EXPORTINGVERSION; type=:ME) +fmu = loadFMU("SpringPendulum1D", EXPORTINGTOOL, EXPORTINGVERSION; type = :ME) using FMIFlux.FMIImport using FMIFlux.FMIImport.FMICore @@ -33,25 +33,24 @@ p_refs = fmu.modelDescription.parameterValueReferences p = fmi2GetReal(c, p_refs) # loss function for training -losssum = function(p) +losssum = function (p) #@info "$p" global problem, X0, posData, solution - solution = problem(X0; p=p, showProgress=true, saveat=tData) + solution = problem(X0; p = p, showProgress = true, saveat = tData) if !solution.success - return Inf + return Inf end - posNet = getState(solution, 1; isIndex=true) - + posNet = getState(solution, 1; isIndex = true) + return Flux.Losses.mse(posNet, posData) end numStates = length(fmu.modelDescription.stateValueReferences) # the "Chain" for training -net = Chain(FMUParameterRegistrator(fmu, p_refs, p), - x -> fmu(x=x, dx_refs=:all)) # , fmuLayer(p)) +net = Chain(FMUParameterRegistrator(fmu, p_refs, p), x -> fmu(x = x, dx_refs = :all)) # , fmuLayer(p)) optim = OPTIMISER(ETA) solver = Tsit5() @@ -60,7 +59,7 @@ problem = ME_NeuralFMU(fmu, net, (t_start, t_stop), solver) problem.modifiedState = false @test problem != nothing -solutionBefore = problem(X0; saveat=tData) +solutionBefore = problem(X0; saveat = tData) @test solutionBefore.success @test length(solutionBefore.states.t) == length(tData) @test solutionBefore.states.t[1] == t_start @@ -79,10 +78,16 @@ lossBefore = losssum(p_net[1]) # j_fwd = ForwardDiff.gradient(losssum, p_net[1]) # j_rwd = ReverseDiff.gradient(losssum, p_net[1]) -FMIFlux.train!(losssum, problem, Iterators.repeated((), NUMSTEPS), optim; gradient=GRADIENT) +FMIFlux.train!( + losssum, + problem, + Iterators.repeated((), NUMSTEPS), + optim; + gradient = GRADIENT, +) # check results -solutionAfter = problem(X0; saveat=tData) +solutionAfter = problem(X0; saveat = tData) @test solutionAfter.success @test length(solutionAfter.states.t) == length(tData) @test solutionAfter.states.t[1] == t_start diff --git a/test/hybrid_CS.jl b/test/hybrid_CS.jl index 29e74206..6760768a 100644 --- a/test/hybrid_CS.jl +++ b/test/hybrid_CS.jl @@ -5,7 +5,7 @@ using Flux -import Random +import Random Random.seed!(1234); t_start = 0.0 @@ -16,22 +16,22 @@ tData = t_start:t_step:t_stop # generate training data posData, velData, accData = syntTrainingData(tData) -fmu = loadFMU("SpringPendulumExtForce1D", EXPORTINGTOOL, EXPORTINGVERSION; type=:CS) +fmu = loadFMU("SpringPendulumExtForce1D", EXPORTINGTOOL, EXPORTINGVERSION; type = :CS) # sine(t) as external force -extForce = function(t) +extForce = function (t) return [sin(t)] end # loss function for training -losssum = function(p) - solution = problem(extForce, t_step; p=p) +losssum = function (p) + solution = problem(extForce, t_step; p = p) if !solution.success - return Inf + return Inf end - accNet = getValue(solution, 1; isIndex=true) + accNet = getValue(solution, 1; isIndex = true) Flux.Losses.mse(accNet, accData) end @@ -40,10 +40,16 @@ end numInputs = length(fmu.modelDescription.inputValueReferences) numOutputs = length(fmu.modelDescription.outputValueReferences) -net = Chain(u -> fmu(;u_refs=fmu.modelDescription.inputValueReferences, u=u, y_refs=fmu.modelDescription.outputValueReferences), - Dense(numOutputs, 16, tanh; init=Flux.identity_init), - Dense(16, 16, tanh; init=Flux.identity_init), - Dense(16, numOutputs; init=Flux.identity_init)) +net = Chain( + u -> fmu(; + u_refs = fmu.modelDescription.inputValueReferences, + u = u, + y_refs = fmu.modelDescription.outputValueReferences, + ), + Dense(numOutputs, 16, tanh; init = Flux.identity_init), + Dense(16, 16, tanh; init = Flux.identity_init), + Dense(16, numOutputs; init = Flux.identity_init), +) problem = CS_NeuralFMU(fmu, net, (t_start, t_stop)) @test problem != nothing @@ -54,7 +60,13 @@ p_net = Flux.params(problem) lossBefore = losssum(p_net[1]) optim = OPTIMISER(ETA) -FMIFlux.train!(losssum, problem, Iterators.repeated((), NUMSTEPS), optim; gradient=GRADIENT) +FMIFlux.train!( + losssum, + problem, + Iterators.repeated((), NUMSTEPS), + optim; + gradient = GRADIENT, +) lossAfter = losssum(p_net[1]) @test lossAfter < lossBefore diff --git a/test/hybrid_ME.jl b/test/hybrid_ME.jl index 162142ed..6e46717b 100644 --- a/test/hybrid_ME.jl +++ b/test/hybrid_ME.jl @@ -6,7 +6,7 @@ using Flux using DifferentialEquations -import Random +import Random Random.seed!(1234); t_start = 0.0 @@ -18,27 +18,27 @@ tData = t_start:t_step:t_stop posData, velData, accData = syntTrainingData(tData) # load FMU for NeuralFMU -fmu = loadFMU("SpringPendulum1D", EXPORTINGTOOL, EXPORTINGVERSION; type=:ME) +fmu = loadFMU("SpringPendulum1D", EXPORTINGTOOL, EXPORTINGVERSION; type = :ME) # loss function for training -losssum = function(p) +losssum = function (p) global problem, X0, posData - solution = problem(X0; p=p, showProgress=false, saveat=tData) + solution = problem(X0; p = p, saveat = tData) if !solution.success - return Inf + return Inf end - posNet = getState(solution, 1; isIndex=true) - velNet = getState(solution, 2; isIndex=true) - + posNet = getState(solution, 1; isIndex = true) + velNet = getState(solution, 2; isIndex = true) + return Flux.Losses.mse(posNet, posData) + Flux.Losses.mse(velNet, velData) end numStates = length(fmu.modelDescription.stateValueReferences) # some NeuralFMU setups -nets = [] +nets = [] c1 = CacheLayer() c2 = CacheRetrieveLayer(c1) @@ -51,106 +51,125 @@ numGetVRs = length(getVRs) y = zeros(fmi2Real, numGetVRs) setVRs = [stringToValueReference(fmu, "mass.m")] numSetVRs = length(setVRs) +setVal = [1.1] # 1. default ME-NeuralFMU (learn dynamics and states, almost-neutral setup, parameter count << 100) -net = Chain(x -> c1(x), - Dense(numStates, 1, tanh; init=init), - x -> c2(x[1], 1), - x -> fmu(;x=x, dx_refs=:all), - x -> c3(x), - Dense(numStates, 1, tanh; init=init), - x -> c4(1, x[1])) +net = Chain( + x -> c1(x), + Dense(numStates, 1, tanh; init = init), + x -> c2(x[1], 1), + x -> fmu(; x = x, dx_refs = :all), + x -> c3(x), + Dense(numStates, 1, tanh; init = init), + x -> c4(1, x[1]), +) push!(nets, net) # 2. default ME-NeuralFMU (learn dynamics) -net = Chain(x -> fmu(;x=x, dx_refs=:all), - x -> c1(x), - Dense(numStates, 16, tanh; init=init), - Dense(16, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c2(1, x[1])) +net = Chain( + x -> fmu(; x = x, dx_refs = :all), + x -> c3(x), + Dense(numStates, 8, tanh; init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c4(1, x[1]), +) push!(nets, net) # 3. default ME-NeuralFMU (learn states) -net = Chain(x -> c1(x), - Dense(numStates, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c2(x[1], 1), - x -> fmu(;x=x, dx_refs=:all)) +net = Chain( + x -> c1(x), + Dense(numStates, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c2(x[1], 1), + x -> fmu(; x = x, dx_refs = :all), +) push!(nets, net) # 4. default ME-NeuralFMU (learn dynamics and states) -net = Chain(x -> c1(x), - Dense(numStates, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c2(x[1], 1), - x -> fmu(;x=x, dx_refs=:all), - x -> c3(x), - Dense(numStates, 16, tanh, init=init), - Dense(16, 1, tanh, init=init), - x -> c4(1, x[1])) +net = Chain( + x -> c1(x), + Dense(numStates, 8, tanh; init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c2(x[1], 1), + x -> fmu(; x = x, dx_refs = :all), + x -> c3(x), + Dense(numStates, 8, tanh, init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh, init = init), + x -> c4(1, x[1]), +) push!(nets, net) # 5. NeuralFMU with hard setting time to 0.0 -net = Chain(states -> fmu(;x=states, t=0.0, dx_refs=:all), - x -> c1(x), - Dense(numStates, 8, tanh; init=init), - Dense(8, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c2(1, x[1])) +net = Chain( + states -> fmu(; x = states, t = 0.0, dx_refs = :all), + x -> c3(x), + Dense(numStates, 8, tanh; init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c4(1, x[1]), +) push!(nets, net) # 6. NeuralFMU with additional getter -net = Chain(x -> fmu(;x=x, y_refs=getVRs, dx_refs=:all), - x -> c1(x), - Dense(numStates+numGetVRs, 8, tanh; init=init), - Dense(8, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c2(1, x[1])) +net = Chain( + x -> fmu(; x = x, y_refs = getVRs, dx_refs = :all), + x -> c3(x), + Dense(numStates + numGetVRs, 8, tanh; init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c4(1, x[1]), +) push!(nets, net) # 7. NeuralFMU with additional setter -net = Chain(x -> fmu(;x=x, u_refs=setVRs, u=[1.1], dx_refs=:all), - x -> c1(x), - Dense(numStates, 8, tanh; init=init), - Dense(8, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c2(1, x[1])) +net = Chain( + x -> fmu(; x = x, u_refs = setVRs, u = setVal, dx_refs = :all), + x -> c3(x), + Dense(numStates, 8, tanh; init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c4(1, x[1]), +) push!(nets, net) # 8. NeuralFMU with additional setter and getter -net = Chain(x -> fmu(;x=x, u_refs=setVRs, u=[1.1], y_refs=getVRs, dx_refs=:all), - x -> c1(x), - Dense(numStates+numGetVRs, 8, tanh; init=init), - Dense(8, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c2(1, x[1])) +net = Chain( + x -> fmu(; x = x, u_refs = setVRs, u = setVal, y_refs = getVRs, dx_refs = :all), + x -> c3(x), + Dense(numStates + numGetVRs, 8, tanh; init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c4(1, x[1]), +) push!(nets, net) # 9. an empty NeuralFMU (this does only make sense for debugging) -net = Chain(x -> fmu(x=x, dx_refs=:all)) +net = Chain(x -> fmu(x = x, dx_refs = :all)) push!(nets, net) solvers = [Tsit5()]#, Rosenbrock23(autodiff=false)] for solver in solvers @testset "Solver: $(solver)" begin - for i in 1:length(nets) + for i = 1:length(nets) @testset "Net setup $(i)/$(length(nets)) (Continuous NeuralFMU)" begin global nets, problem, iterCB global LAST_LOSS, FAILED_GRADIENTS + # if i ∈ (1, 3, 4) + # @warn "Currently skipping nets $(i) ∈ (1, 3, 4)" + # continue + # end + optim = OPTIMISER(ETA) net = nets[i] problem = ME_NeuralFMU(fmu, net, (t_start, t_stop), solver) @test problem != nothing - # if i ∈ (3, 4, 6) - # @warn "Currently skipping nets ∈ (3, 4, 6)" - # continue - # end - # [Note] this is not needed from a mathematical perspective, because the system is continuous differentiable if i ∈ (1, 3, 4) problem.modifiedState = true @@ -160,7 +179,7 @@ for solver in solvers p_net = Flux.params(problem) @test length(p_net) == 1 - solutionBefore = problem(X0; p=p_net[1], saveat=tData) + solutionBefore = problem(X0; p = p_net[1], saveat = tData) if solutionBefore.success @test length(solutionBefore.states.t) == length(tData) @test solutionBefore.states.t[1] == t_start @@ -173,14 +192,21 @@ for solver in solvers if length(p_net[1]) == 0 @info "The following warning is not an issue, because training on zero parameters must throw a warning:" end - + FAILED_GRADIENTS = 0 - FMIFlux.train!(losssum, problem, Iterators.repeated((), NUMSTEPS), optim; gradient=GRADIENT, cb=()->callback(p_net)) + FMIFlux.train!( + losssum, + problem, + Iterators.repeated((), NUMSTEPS), + optim; + gradient = GRADIENT, + cb = () -> callback(p_net), + ) @info "Failed Gradients: $(FAILED_GRADIENTS) / $(NUMSTEPS)" @test FAILED_GRADIENTS <= FAILED_GRADIENTS_QUOTA * NUMSTEPS # check results - solutionAfter = problem(X0; p=p_net[1], saveat=tData) + solutionAfter = problem(X0; p = p_net[1], saveat = tData) if solutionAfter.success @test length(solutionAfter.states.t) == length(tData) @test solutionAfter.states.t[1] == t_start @@ -189,7 +215,6 @@ for solver in solvers end end - end end diff --git a/test/hybrid_ME_dis.jl b/test/hybrid_ME_dis.jl index 00e0a16a..746a7800 100644 --- a/test/hybrid_ME_dis.jl +++ b/test/hybrid_ME_dis.jl @@ -6,8 +6,8 @@ using Flux using DifferentialEquations -import Random -Random.seed!(5678); +import Random +Random.seed!(1234); t_start = 0.0 t_step = 0.01 @@ -17,21 +17,21 @@ tData = t_start:t_step:t_stop # generate training data posData = collect(abs(cos(u .* 1.0)) for u in tData) * 2.0 -fmu = loadFMU("BouncingBall1D", "Dymola", "2023x"; type=:ME) +fmu = loadFMU("BouncingBall1D", "Dymola", "2023x"; type = :ME) # loss function for training -losssum = function(p) +losssum = function (p) global problem, X0, posData - solution = problem(X0; p=p, saveat=tData)#, sensealg=...) + solution = problem(X0; p = p, saveat = tData) if !solution.success - return Inf + return Inf end - posNet = getState(solution, 1; isIndex=true) + posNet = getState(solution, 1; isIndex = true) #velNet = getState(solution, 2; isIndex=true) - - return Flux.Losses.mse(posNet, posData) #+ FMIFlux.Losses.mse(velNet, velData) + + return Flux.Losses.mse(posNet, posData) #+ Flux.Losses.mse(velNet, velData) end numStates = length(fmu.modelDescription.stateValueReferences) @@ -46,106 +46,124 @@ c4 = CacheRetrieveLayer(c3) init = Flux.glorot_uniform getVRs = [stringToValueReference(fmu, "mass_s")] -y = zeros(fmi2Real, length(getVRs)) numGetVRs = length(getVRs) +y = zeros(fmi2Real, numGetVRs) setVRs = [stringToValueReference(fmu, "damping")] numSetVRs = length(setVRs) setVal = [0.8] # 1. default ME-NeuralFMU (learn dynamics and states, almost-neutral setup, parameter count << 100) -net1 = function() - net = Chain(x -> c1(x), - Dense(numStates, 1, tanh; init=init), - x -> c2(x[1], 1), - x -> fmu(;x=x, dx_refs=:all), - x -> c3(x), - Dense(numStates, 1, tanh; init=init), - x -> c4(1, x[1])) +net1 = function () + net = Chain( + x -> c1(x), + Dense(numStates, 1, tanh; init = init), + x -> c2(x[1], 1), + x -> fmu(; x = x, dx_refs = :all), + x -> c3(x), + Dense(numStates, 1, tanh; init = init), + x -> c4(1, x[1]), + ) end push!(nets, net1) # 2. default ME-NeuralFMU (learn dynamics) -net2 = function() - net = Chain(x -> fmu(;x=x, dx_refs=:all), - x -> c3(x), - Dense(numStates, 16, tanh; init=init), - Dense(16, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c4(1, x[1])) +net2 = function () + net = Chain( + x -> fmu(; x = x, dx_refs = :all), + x -> c3(x), + Dense(numStates, 8, tanh; init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c4(1, x[1]), + ) end push!(nets, net2) # 3. default ME-NeuralFMU (learn states) -net3 = function() - net = Chain(x -> c1(x), - Dense(numStates, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c2(x[1], 1), - x -> fmu(;x=x, dx_refs=:all)) +net3 = function () + net = Chain( + x -> c1(x), + Dense(numStates, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c2(x[1], 1), + x -> fmu(; x = x, dx_refs = :all), + ) end push!(nets, net3) # 4. default ME-NeuralFMU (learn dynamics and states) -net4 = function() - net = Chain(x -> c1(x), - Dense(numStates, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c2(x[1], 1), - x -> fmu(;x=x, dx_refs=:all), - x -> c3(x), - Dense(numStates, 16, tanh, init=init), - Dense(16, 1, tanh, init=init), - x -> c4(1, x[1])) +net4 = function () + net = Chain( + x -> c1(x), + Dense(numStates, 8, tanh; init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c2(x[1], 1), + x -> fmu(; x = x, dx_refs = :all), + x -> c3(x), + Dense(numStates, 8, tanh, init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh, init = init), + x -> c4(1, x[1]), + ) end push!(nets, net4) # 5. NeuralFMU with hard setting time to 0.0 -net5 = function() - net = Chain(states -> fmu(;x=states, t=0.0, dx_refs=:all), - x -> c3(x), - Dense(numStates, 8, tanh; init=init), - Dense(8, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c4(1, x[1])) +net5 = function () + net = Chain( + states -> fmu(; x = states, t = 0.0, dx_refs = :all), + x -> c3(x), + Dense(numStates, 8, tanh; init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c4(1, x[1]), + ) end push!(nets, net5) # 6. NeuralFMU with additional getter -net6 = function() - net = Chain(x -> fmu(;x=x, y_refs=getVRs, dx_refs=:all), - x -> c3(x), - Dense(numStates+numGetVRs, 8, tanh; init=init), - Dense(8, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c4(1, x[1])) +net6 = function () + net = Chain( + x -> fmu(; x = x, y_refs = getVRs, dx_refs = :all), + x -> c3(x), + Dense(numStates + numGetVRs, 8, tanh; init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c4(1, x[1]), + ) end push!(nets, net6) # 7. NeuralFMU with additional setter -net7 = function() - net = Chain(x -> fmu(;x=x, u_refs=setVRs, u=setVal, dx_refs=:all), - x -> c3(x), - Dense(numStates, 8, tanh; init=init), - Dense(8, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c4(1, x[1])) +net7 = function () + net = Chain( + x -> fmu(; x = x, u_refs = setVRs, u = setVal, dx_refs = :all), + x -> c3(x), + Dense(numStates, 8, tanh; init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c4(1, x[1]), + ) end push!(nets, net7) # 8. NeuralFMU with additional setter and getter -net8 = function() - net = Chain(x -> fmu(;x=x, u_refs=setVRs, u=setVal, y_refs=getVRs, dx_refs=:all), - x -> c3(x), - Dense(numStates+numGetVRs, 8, tanh; init=init), - Dense(8, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c4(1, x[1])) +net8 = function () + net = Chain( + x -> fmu(; x = x, u_refs = setVRs, u = setVal, y_refs = getVRs, dx_refs = :all), + x -> c3(x), + Dense(numStates + numGetVRs, 8, tanh; init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c4(1, x[1]), + ) end push!(nets, net8) # 9. an empty NeuralFMU (this does only make sense for debugging) -net9 = function() - net = Chain(x -> fmu(x=x, dx_refs=:all)) +net9 = function () + net = Chain(x -> fmu(x = x, dx_refs = :all)) end push!(nets, net9) @@ -153,9 +171,9 @@ solvers = [Tsit5()]#, Rosenbrock23(autodiff=false)] for solver in solvers @testset "Solver: $(solver)" begin - for i in 1:length(nets) + for i = 1:length(nets) @testset "Net setup $(i)/$(length(nets)) (Discontinuous NeuralFMU)" begin - global nets, problem, lastLoss, iterCB + global nets, problem, iterCB global LAST_LOSS, FAILED_GRADIENTS if i ∈ (1, 3, 4) @@ -173,34 +191,34 @@ for solver in solvers maxtries = 1000 while true net = net_constructor() - problem = ME_NeuralFMU(fmu, net, (t_start, t_stop), solver) - + problem = ME_NeuralFMU(fmu, net, (t_start, t_stop), solver) + if i ∈ (1, 3, 4) problem.modifiedState = true end p_net = Flux.params(problem) - solutionBefore = problem(X0; p=p_net[1], saveat=tData) + solutionBefore = problem(X0; p = p_net[1], saveat = tData) ne = length(solutionBefore.events) if ne > 0 && ne <= 10 - break + break else if tries >= maxtries @warn "Solution before did not trigger an acceptable event count (=$(ne) ∉ [1,10]) for net $(i)! Can't find a valid start configuration ($(maxtries) tries)!" - break + break end tries += 1 end end + @test !isnothing(problem) + # train it ... p_net = Flux.params(problem) @test length(p_net) == 1 - - @test problem !== nothing - solutionBefore = problem(X0; p=p_net[1], saveat=tData) + solutionBefore = problem(X0; p = p_net[1], saveat = tData) if solutionBefore.success @test length(solutionBefore.states.t) == length(tData) @test solutionBefore.states.t[1] == t_start @@ -213,20 +231,27 @@ for solver in solvers if length(p_net[1]) == 0 @info "The following warning is not an issue, because training on zero parameters must throw a warning:" end - + FAILED_GRADIENTS = 0 - FMIFlux.train!(losssum, problem, Iterators.repeated((), NUMSTEPS), optim; gradient=GRADIENT, cb=()->callback(p_net)) + FMIFlux.train!( + losssum, + problem, + Iterators.repeated((), NUMSTEPS), + optim; + gradient = GRADIENT, + cb = () -> callback(p_net), + ) @info "Failed Gradients: $(FAILED_GRADIENTS) / $(NUMSTEPS)" @test FAILED_GRADIENTS <= FAILED_GRADIENTS_QUOTA * NUMSTEPS # check results - solutionAfter = problem(X0; p=p_net[1], saveat=tData) + solutionAfter = problem(X0; p = p_net[1], saveat = tData) if solutionAfter.success @test length(solutionAfter.states.t) == length(tData) @test solutionAfter.states.t[1] == t_start @test solutionAfter.states.t[end] == t_stop end - + # fig = plot(solutionAfter; title="Net $(i) - $(FAILED_GRADIENTS) / $(FAILED_GRADIENTS_QUOTA * NUMSTEPS)") # plot!(fig, tData, posData) # display(fig) diff --git a/test/layers.jl b/test/layers.jl index 2ecc4f51..5b5c11dc 100644 --- a/test/layers.jl +++ b/test/layers.jl @@ -18,21 +18,21 @@ s = ShiftScale(shift, scale) s = ShiftScale(inputArray) @test s(input) == [2.5, -0.8, -1.0] -s = ShiftScale(inputArray; range=-1:1) -for i in 1:length(inputArray) - res = s(collect(inputArray[j][i] for j in 1:length(inputArray[i]))) +s = ShiftScale(inputArray; range = -1:1) +for i = 1:length(inputArray) + res = s(collect(inputArray[j][i] for j = 1:length(inputArray[i]))) @test max(res...) <= 1 @test min(res...) >= -1 end -s = ShiftScale(inputArray; range=-2:2) -for i in 1:length(inputArray) - res = s(collect(inputArray[j][i] for j in 1:length(inputArray[i]))) +s = ShiftScale(inputArray; range = -2:2) +for i = 1:length(inputArray) + res = s(collect(inputArray[j][i] for j = 1:length(inputArray[i]))) @test max(res...) <= 2 @test min(res...) >= -2 end -s = ShiftScale(inputArray; range=:NormalDistribution) +s = ShiftScale(inputArray; range = :NormalDistribution) # ToDo: Test for :NormalDistribution # ScaleShift @@ -44,10 +44,10 @@ s = ScaleShift(inputArray) p = ShiftScale(inputArray) s = ScaleShift(p) -for i in 1:length(inputArray) - in = collect(inputArray[j][i] for j in 1:length(inputArray[i])) +for i = 1:length(inputArray) + in = collect(inputArray[j][i] for j = 1:length(inputArray[i])) @test p(in) != in @test s(p(in)) == in end -# ToDo: Add remaining layers \ No newline at end of file +# ToDo: Add remaining layers diff --git a/test/multi.jl b/test/multi.jl index 5ad6bcfb..f1022813 100644 --- a/test/multi.jl +++ b/test/multi.jl @@ -6,7 +6,7 @@ using Flux using DifferentialEquations: Tsit5 -import Random +import Random Random.seed!(1234); t_start = 0.0 @@ -18,45 +18,49 @@ tData = t_start:t_step:t_stop posData, velData, accData = syntTrainingData(tData) # setup traing data -extForce = function(t) +extForce = function (t) return [sin(t), cos(t)] end # loss function for training -losssum = function(p) - solution = problem(extForce, t_step; p=p) +losssum = function (p) + solution = problem(extForce, t_step; p = p) if !solution.success - return Inf + return Inf end - accNet = getValue(solution, 1; isIndex=true) + accNet = getValue(solution, 1; isIndex = true) FMIFlux.Losses.mse(accNet, accData) end # Load FMUs fmus = Vector{FMU2}() -for i in 1:2 # how many FMUs do you want? - _fmu = loadFMU("SpringPendulumExtForce1D", EXPORTINGTOOL, EXPORTINGVERSION; type=:CS) +for i = 1:2 # how many FMUs do you want? + _fmu = loadFMU("SpringPendulumExtForce1D", EXPORTINGTOOL, EXPORTINGVERSION; type = :CS) push!(fmus, _fmu) end # NeuralFMU setup -total_fmu_outdim = sum(map(x->length(x.modelDescription.outputValueReferences), fmus)) - -evalFMU = function(i, u) - fmus[i](;u_refs=fmus[i].modelDescription.inputValueReferences, u=u, y_refs=fmus[i].modelDescription.outputValueReferences) +total_fmu_outdim = sum(map(x -> length(x.modelDescription.outputValueReferences), fmus)) + +evalFMU = function (i, u) + fmus[i](; + u_refs = fmus[i].modelDescription.inputValueReferences, + u = u, + y_refs = fmus[i].modelDescription.outputValueReferences, + ) end net = Chain( - Parallel( - vcat, - inputs -> evalFMU(1, inputs[1:1]), - inputs -> evalFMU(2, inputs[2:2]) + Parallel(vcat, inputs -> evalFMU(1, inputs[1:1]), inputs -> evalFMU(2, inputs[2:2])), + Dense(total_fmu_outdim, 16, tanh; init = Flux.identity_init), + Dense(16, 16, tanh; init = Flux.identity_init), + Dense( + 16, + length(fmus[1].modelDescription.outputValueReferences); + init = Flux.identity_init, ), - Dense(total_fmu_outdim, 16, tanh; init=Flux.identity_init), - Dense(16, 16, tanh; init=Flux.identity_init), - Dense(16, length(fmus[1].modelDescription.outputValueReferences); init=Flux.identity_init), ) problem = CS_NeuralFMU(fmus, net, (t_start, t_stop)) @@ -70,13 +74,19 @@ p_net = Flux.params(problem) optim = OPTIMISER(ETA) lossBefore = losssum(p_net[1]) -FMIFlux.train!(losssum, problem, Iterators.repeated((), NUMSTEPS), optim; gradient=GRADIENT) +FMIFlux.train!( + losssum, + problem, + Iterators.repeated((), NUMSTEPS), + optim; + gradient = GRADIENT, +) lossAfter = losssum(p_net[1]) @test lossAfter < lossBefore # check results solutionAfter = problem(extForce, t_step) -for i in 1:length(fmus) +for i = 1:length(fmus) unloadFMU(fmus[i]) end diff --git a/test/multi_threading.jl b/test/multi_threading.jl index a3265242..a3c6f89b 100644 --- a/test/multi_threading.jl +++ b/test/multi_threading.jl @@ -6,7 +6,7 @@ using Flux using DifferentialEquations: Tsit5, Rosenbrock23 -import Random +import Random Random.seed!(5678); t_start = 0.0 @@ -18,27 +18,27 @@ tData = t_start:t_step:t_stop posData, velData, accData = syntTrainingData(tData) # load FMU for training -fmu = loadFMU("SpringFrictionPendulum1D", EXPORTINGTOOL, EXPORTINGVERSION; type=:ME) +fmu = loadFMU("SpringFrictionPendulum1D", EXPORTINGTOOL, EXPORTINGVERSION; type = :ME) # loss function for training -losssum = function(p) +losssum = function (p) global problem, X0, posData - solution = problem(X0; p=p, showProgress=true, saveat=tData) + solution = problem(X0; p = p, showProgress = true, saveat = tData) if !solution.success - return Inf + return Inf end # posNet = getState(solution, 1; isIndex=true) - velNet = getState(solution, 2; isIndex=true) - + velNet = getState(solution, 2; isIndex = true) + return FMIFlux.Losses.mse(velNet, velData) # Flux.Losses.mse(posNet, posData) end # callback function for training global iterCB = 0 global lastLoss = 0.0 -callb = function(p) +callb = function (p) global iterCB += 1 global lastLoss @@ -63,26 +63,28 @@ c3 = CacheLayer() c4 = CacheRetrieveLayer(c3) # 1. Discontinuous ME-NeuralFMU (learn dynamics and states) -net = Chain(x -> c1(x), - Dense(numStates, 16, tanh), - Dense(16, 1, identity), - x -> c2(x[1], 1), - x -> fmu(;x=x, dx_refs=:all), - x -> c3(x), - Dense(numStates, 16, tanh), - Dense(16, 16, tanh), - Dense(16, 1, identity), - x -> c4(1, x[1])) +net = Chain( + x -> c1(x), + Dense(numStates, 16, tanh), + Dense(16, 1, identity), + x -> c2(x[1], 1), + x -> fmu(; x = x, dx_refs = :all), + x -> c3(x), + Dense(numStates, 16, tanh), + Dense(16, 16, tanh), + Dense(16, 1, identity), + x -> c4(1, x[1]), +) push!(nets, net) -for i in 1:length(nets) +for i = 1:length(nets) @testset "Net setup $(i)/$(length(nets))" begin global nets, problem, lastLoss, iterCB net = nets[i] solver = Tsit5() - problem = ME_NeuralFMU(fmu, net, (t_start, t_stop), solver; saveat=tData) - + problem = ME_NeuralFMU(fmu, net, (t_start, t_stop), solver; saveat = tData) + @test problem !== nothing solutionBefore = problem(X0) @@ -105,20 +107,36 @@ for i in 1:length(nets) lastLoss = startLoss st = time() optim = OPTIMISER(ETA) - FMIFlux.train!(losssum, problem, Iterators.repeated((), NUMSTEPS), optim; cb=()->callb(p_net), multiThreading=false, gradient=GRADIENT) - dt = round(time()-st; digits=2) + FMIFlux.train!( + losssum, + problem, + Iterators.repeated((), NUMSTEPS), + optim; + cb = () -> callb(p_net), + multiThreading = false, + gradient = GRADIENT, + ) + dt = round(time() - st; digits = 2) @info "Training time single threaded (not pre-compiled): $(dt)s" p_net[1][:] = p_start[:] lastLoss = startLoss st = time() optim = OPTIMISER(ETA) - FMIFlux.train!(losssum, problem, Iterators.repeated((), NUMSTEPS), optim; cb=()->callb(p_net), multiThreading=false, gradient=GRADIENT) - dt = round(time()-st; digits=2) + FMIFlux.train!( + losssum, + problem, + Iterators.repeated((), NUMSTEPS), + optim; + cb = () -> callb(p_net), + multiThreading = false, + gradient = GRADIENT, + ) + dt = round(time() - st; digits = 2) @info "Training time single threaded (pre-compiled): $(dt)s" # [ToDo] currently not implemented - + # p_net[1][:] = p_start[:] # lastLoss = startLoss # st = time() diff --git a/test/optim.jl b/test/optim.jl index 5c1e2a7f..1285c9ca 100644 --- a/test/optim.jl +++ b/test/optim.jl @@ -7,7 +7,7 @@ using Flux using DifferentialEquations using FMIFlux.Optim -import Random +import Random Random.seed!(1234); t_start = 0.0 @@ -19,27 +19,27 @@ tData = t_start:t_step:t_stop posData, velData, accData = syntTrainingData(tData) # load FMU for NeuralFMU -fmu = loadFMU("SpringPendulum1D", EXPORTINGTOOL, EXPORTINGVERSION; type=:ME) +fmu = loadFMU("SpringPendulum1D", EXPORTINGTOOL, EXPORTINGVERSION; type = :ME) # loss function for training -losssum = function(p) +losssum = function (p) global problem, X0, posData - solution = problem(X0; p=p, showProgress=false, saveat=tData) + solution = problem(X0; p = p, saveat = tData) if !solution.success - return Inf + return Inf end - posNet = getState(solution, 1; isIndex=true) - velNet = getState(solution, 2; isIndex=true) - + posNet = getState(solution, 1; isIndex = true) + velNet = getState(solution, 2; isIndex = true) + return Flux.Losses.mse(posNet, posData) + Flux.Losses.mse(velNet, velData) end numStates = length(fmu.modelDescription.stateValueReferences) # some NeuralFMU setups -nets = [] +nets = [] c1 = CacheLayer() c2 = CacheRetrieveLayer(c1) @@ -52,106 +52,128 @@ numGetVRs = length(getVRs) y = zeros(fmi2Real, numGetVRs) setVRs = [stringToValueReference(fmu, "mass.m")] numSetVRs = length(setVRs) +setVal = [1.1] # 1. default ME-NeuralFMU (learn dynamics and states, almost-neutral setup, parameter count << 100) -net = Chain(x -> c1(x), - Dense(numStates, 1, tanh; init=init), - x -> c2(x[1], 1), - x -> fmu(;x=x, dx_refs=:all), - x -> c3(x), - Dense(numStates, 1, tanh; init=init), - x -> c4(1, x[1])) +net = Chain( + x -> c1(x), + Dense(numStates, 1, tanh; init = init), + x -> c2(x[1], 1), + x -> fmu(; x = x, dx_refs = :all), + x -> c3(x), + Dense(numStates, 1, tanh; init = init), + x -> c4(1, x[1]), +) push!(nets, net) # 2. default ME-NeuralFMU (learn dynamics) -net = Chain(x -> fmu(;x=x, dx_refs=:all), - x -> c1(x), - Dense(numStates, 16, tanh; init=init), - Dense(16, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c2(1, x[1])) +net = Chain( + x -> fmu(; x = x, dx_refs = :all), + x -> c3(x), + Dense(numStates, 8, tanh; init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c4(1, x[1]), +) push!(nets, net) # 3. default ME-NeuralFMU (learn states) -net = Chain(x -> c1(x), - Dense(numStates, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c2(x[1], 1), - x -> fmu(;x=x, dx_refs=:all)) +net = Chain( + x -> c1(x), + Dense(numStates, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c2(x[1], 1), + x -> fmu(; x = x, dx_refs = :all), +) push!(nets, net) # 4. default ME-NeuralFMU (learn dynamics and states) -net = Chain(x -> c1(x), - Dense(numStates, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c2(x[1], 1), - x -> fmu(;x=x, dx_refs=:all), - x -> c3(x), - Dense(numStates, 16, tanh, init=init), - Dense(16, 1, tanh, init=init), - x -> c4(1, x[1])) +net = Chain( + x -> c1(x), + Dense(numStates, 8, tanh; init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c2(x[1], 1), + x -> fmu(; x = x, dx_refs = :all), + x -> c3(x), + Dense(numStates, 8, tanh, init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh, init = init), + x -> c4(1, x[1]), +) push!(nets, net) # 5. NeuralFMU with hard setting time to 0.0 -net = Chain(states -> fmu(;x=states, t=0.0, dx_refs=:all), - x -> c1(x), - Dense(numStates, 8, tanh; init=init), - Dense(8, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c2(1, x[1])) +net = Chain( + states -> fmu(; x = states, t = 0.0, dx_refs = :all), + x -> c3(x), + Dense(numStates, 8, tanh; init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c4(1, x[1]), +) push!(nets, net) # 6. NeuralFMU with additional getter -net = Chain(x -> fmu(;x=x, y_refs=getVRs, dx_refs=:all), - x -> c1(x), - Dense(numStates+numGetVRs, 8, tanh; init=init), - Dense(8, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c2(1, x[1])) +net = Chain( + x -> fmu(; x = x, y_refs = getVRs, dx_refs = :all), + x -> c3(x), + Dense(numStates + numGetVRs, 8, tanh; init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c4(1, x[1]), +) push!(nets, net) # 7. NeuralFMU with additional setter -net = Chain(x -> fmu(;x=x, u_refs=setVRs, u=[1.1], dx_refs=:all), - x -> c1(x), - Dense(numStates, 8, tanh; init=init), - Dense(8, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c2(1, x[1])) +net = Chain( + x -> fmu(; x = x, u_refs = setVRs, u = setVal, dx_refs = :all), + x -> c3(x), + Dense(numStates, 8, tanh; init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c4(1, x[1]), +) push!(nets, net) # 8. NeuralFMU with additional setter and getter -net = Chain(x -> fmu(;x=x, u_refs=setVRs, u=[1.1], y_refs=getVRs, dx_refs=:all), - x -> c1(x), - Dense(numStates+numGetVRs, 8, tanh; init=init), - Dense(8, 16, tanh; init=init), - Dense(16, 1, tanh; init=init), - x -> c2(1, x[1])) +net = Chain( + x -> fmu(; x = x, u_refs = setVRs, u = setVal, y_refs = getVRs, dx_refs = :all), + x -> c3(x), + Dense(numStates + numGetVRs, 8, tanh; init = init), + Dense(8, 16, tanh; init = init), + Dense(16, 1, tanh; init = init), + x -> c4(1, x[1]), +) push!(nets, net) # 9. an empty NeuralFMU (this does only make sense for debugging) -net = Chain(x -> fmu(x=x, dx_refs=:all)) +net = Chain(x -> fmu(x = x, dx_refs = :all)) push!(nets, net) solvers = [Tsit5()]#, Rosenbrock23(autodiff=false)] for solver in solvers @testset "Solver: $(solver)" begin - for i in 1:length(nets) - @testset "Net setup $(i)/$(length(nets))" begin + for i = 1:length(nets) + @testset "Net setup $(i)/$(length(nets)) (Continuous NeuralFMU)" begin global nets, problem, iterCB global LAST_LOSS, FAILED_GRADIENTS - optim = GradientDescent(; alphaguess=ETA, linesearch=Optim.LineSearches.Static()) # BFGS() + # if i ∈ (1, 3, 4) + # @warn "Currently skipping nets $(i) ∈ (1, 3, 4)" + # continue + # end + + optim = GradientDescent(; + alphaguess = ETA, + linesearch = Optim.LineSearches.Static(), + ) # BFGS() net = nets[i] problem = ME_NeuralFMU(fmu, net, (t_start, t_stop), solver) @test problem != nothing - # if i ∈ (3, 4, 6) - # @warn "Currently skipping nets ∈ (3, 4, 6)" - # continue - # end - # [Note] this is not needed from a mathematical perspective, because the system is continuous differentiable if i ∈ (1, 3, 4) problem.modifiedState = true @@ -161,7 +183,7 @@ for solver in solvers p_net = Flux.params(problem) @test length(p_net) == 1 - solutionBefore = problem(X0; p=p_net[1], saveat=tData) + solutionBefore = problem(X0; p = p_net[1], saveat = tData) if solutionBefore.success @test length(solutionBefore.states.t) == length(tData) @test solutionBefore.states.t[1] == t_start @@ -174,14 +196,21 @@ for solver in solvers if length(p_net[1]) == 0 @info "The following warning is not an issue, because training on zero parameters must throw a warning:" end - + FAILED_GRADIENTS = 0 - FMIFlux.train!(losssum, problem, Iterators.repeated((), NUMSTEPS), optim; gradient=GRADIENT, cb=()->callback(p_net)) + FMIFlux.train!( + losssum, + problem, + Iterators.repeated((), NUMSTEPS), + optim; + gradient = GRADIENT, + cb = () -> callback(p_net), + ) @info "Failed Gradients: $(FAILED_GRADIENTS) / $(NUMSTEPS)" @test FAILED_GRADIENTS <= FAILED_GRADIENTS_QUOTA * NUMSTEPS # check results - solutionAfter = problem(X0; p=p_net[1], saveat=tData) + solutionAfter = problem(X0; p = p_net[1], saveat = tData) if solutionAfter.success @test length(solutionAfter.states.t) == length(tData) @test solutionAfter.states.t[1] == t_start @@ -190,7 +219,6 @@ for solver in solvers end end - end end diff --git a/test/runtests.jl b/test/runtests.jl index fbd30753..36c31268 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -12,27 +12,28 @@ using FMIFlux.Flux import FMIFlux.FMISensitivity: FiniteDiff, ForwardDiff, ReverseDiff -using FMIFlux.FMIImport: stringToValueReference, fmi2ValueReference, prepareSolveFMU, fmi2Real +using FMIFlux.FMIImport: + stringToValueReference, fmi2ValueReference, prepareSolveFMU, fmi2Real using FMIFlux.FMIImport: FMU_EXECUTION_CONFIGURATIONS using FMIFlux.FMIImport: getState, getValue, getTime -exportingToolsWindows = [("Dymola", "2022x")] # [("ModelicaReferenceFMUs", "0.0.25")] +exportingToolsWindows = [("Dymola", "2022x")] # [("ModelicaReferenceFMUs", "0.0.25")] exportingToolsLinux = [("Dymola", "2022x")] # number of training steps to perform -global NUMSTEPS = 20 +global NUMSTEPS = 30 global ETA = 1e-5 -global GRADIENT = nothing -global EXPORTINGTOOL = nothing +global GRADIENT = nothing +global EXPORTINGTOOL = nothing global EXPORTINGVERSION = nothing global X0 = [2.0, 0.0] global OPTIMISER = Descent -global FAILED_GRADIENTS_QUOTA = 7/20 +global FAILED_GRADIENTS_QUOTA = 1/3 # callback for bad optimization steps counter global FAILED_GRADIENTS = 0 global LAST_LOSS -callback = function(p) +callback = function (p) global LAST_LOSS, FAILED_GRADIENTS loss = losssum(p[1]) if loss >= LAST_LOSS @@ -44,9 +45,9 @@ end # training data for pendulum experiment function syntTrainingData(tData) - posData = cos.(tData*3.0)* 2.0 - velData = sin.(tData*3.0)*-6.0 - accData = cos.(tData*3.0)*-18.0 + posData = cos.(tData * 3.0) * 2.0 + velData = sin.(tData * 3.0) * -6.0 + accData = cos.(tData * 3.0) * -18.0 return posData, velData, accData end @@ -60,46 +61,46 @@ function runtests(exportingTool) global EXPORTINGTOOL = exportingTool[1] global EXPORTINGVERSION = exportingTool[2] - @info "Testing FMUs exported from $(EXPORTINGTOOL) ($(EXPORTINGVERSION))" + @info "Testing FMUs exported from $(EXPORTINGTOOL) ($(EXPORTINGVERSION))" @testset "Testing FMUs exported from $(EXPORTINGTOOL) ($(EXPORTINGVERSION))" begin - @info "Solution Gradients (solution_gradients.jl)" + @info "Solution Gradients (solution_gradients.jl)" @testset "Solution Gradients" begin include("solution_gradients.jl") end - @info "Time Event Solution Gradients (time_solution_gradients.jl)" + @info "Time Event Solution Gradients (time_solution_gradients.jl)" @testset "Time Event Solution Gradients" begin include("time_solution_gradients.jl") end for _GRADIENT ∈ (:ReverseDiff, :ForwardDiff) # , :FiniteDiff) - + global GRADIENT = _GRADIENT - @info "Gradient: $(GRADIENT)" + @info "Gradient: $(GRADIENT)" @testset "Gradient: $(GRADIENT)" begin - - @info "Layers (layers.jl)" + + @info "Layers (layers.jl)" @testset "Layers" begin include("layers.jl") end - @info "ME-NeuralFMU (Continuous) (hybrid_ME.jl)" + @info "ME-NeuralFMU (Continuous) (hybrid_ME.jl)" @testset "ME-NeuralFMU (Continuous)" begin include("hybrid_ME.jl") end - @info "ME-NeuralFMU (Discontinuous) (hybrid_ME_dis.jl)" + @info "ME-NeuralFMU (Discontinuous) (hybrid_ME_dis.jl)" @testset "ME-NeuralFMU (Discontinuous)" begin include("hybrid_ME_dis.jl") end - @info "NeuralFMU with FMU parameter optimization (fmu_params.jl)" + @info "NeuralFMU with FMU parameter optimization (fmu_params.jl)" @testset "NeuralFMU with FMU parameter optimization" begin include("fmu_params.jl") end - @info "Training modes (train_modes.jl)" + @info "Training modes (train_modes.jl)" @testset "Training modes" begin include("train_modes.jl") end @@ -110,26 +111,26 @@ function runtests(exportingTool) # include("multi_threading.jl") # end - @info "CS-NeuralFMU (hybrid_CS.jl)" + @info "CS-NeuralFMU (hybrid_CS.jl)" @testset "CS-NeuralFMU" begin include("hybrid_CS.jl") end - @info "Multiple FMUs (multi.jl)" + @info "Multiple FMUs (multi.jl)" @testset "Multiple FMUs" begin include("multi.jl") end - @info "Batching (batching.jl)" + @info "Batching (batching.jl)" @testset "Batching" begin include("batching.jl") end - @info "Optimizers from Optim.jl (optim.jl)" + @info "Optimizers from Optim.jl (optim.jl)" @testset "Optim" begin include("optim.jl") end - + end end @@ -138,7 +139,7 @@ function runtests(exportingTool) # @testset "Benchmark: Supported sensitivities " begin # include("supported_sensitivities.jl") # end - + end end diff --git a/test/solution_gradients.jl b/test/solution_gradients.jl index 4c6ddab5..f455d56e 100644 --- a/test/solution_gradients.jl +++ b/test/solution_gradients.jl @@ -9,18 +9,21 @@ using DifferentialEquations using FMIFlux, FMIZoo, Test import FMIFlux.FMISensitivity.SciMLSensitivity.SciMLBase: RightRootFind, LeftRootFind import FMIFlux.FMIImport.FMIBase: unsense -using FMIFlux.FMISensitivity.SciMLSensitivity.ForwardDiff, FMIFlux.FMISensitivity.SciMLSensitivity.ReverseDiff, FMIFlux.FMISensitivity.SciMLSensitivity.FiniteDiff, FMIFlux.FMISensitivity.SciMLSensitivity.Zygote +using FMIFlux.FMISensitivity.SciMLSensitivity.ForwardDiff, + FMIFlux.FMISensitivity.SciMLSensitivity.ReverseDiff, + FMIFlux.FMISensitivity.SciMLSensitivity.FiniteDiff, + FMIFlux.FMISensitivity.SciMLSensitivity.Zygote using FMIFlux.FMIImport, FMIFlux.FMIImport.FMICore, FMIZoo -import LinearAlgebra:I +import LinearAlgebra: I import FMIFlux: isimplicit -import Random +import Random Random.seed!(5678); global solution = nothing global events = 0 -ENERGY_LOSS = 0.7 +ENERGY_LOSS = 0.7 RADIUS = 0.0 GRAVITY = 9.81 DBL_MIN = 1e-10 # 2.2250738585072013830902327173324040642192159804623318306e-308 @@ -34,59 +37,61 @@ tData = t_start:t_step:t_stop posData = ones(Float64, length(tData)) x0_bb = [1.0, 0.0] -solvekwargs = Dict{Symbol, Any}(:saveat => tData, :abstol => 1e-6, :reltol => 1e-6, :dtmax => 1e-2) +solvekwargs = + Dict{Symbol,Any}(:saveat => tData, :abstol => 1e-6, :reltol => 1e-6, :dtmax => 1e-2) numStates = 2 -solvers = [Tsit5(), Rosenbrock23(autodiff=false)]#, FBDF(autodiff=false)] +solvers = [Tsit5(), Rosenbrock23(autodiff = false)]#, FBDF(autodiff=false)] -Wr = rand(2,2)*1e-4 # zeros(2,2) # -br = rand(2)*1e-4 # zeros(2) # +Wr = rand(2, 2) * 1e-4 # zeros(2,2) # +br = rand(2) * 1e-4 # zeros(2) # -W1 = [1.0 0.0; 0.0 1.0] - Wr -b1 = [0.0, 0.0] - br -W2 = [1.0 0.0; 0.0 1.0] - Wr -b2 = [0.0, 0.0] - br +W1 = [1.0 0.0; 0.0 1.0] - Wr +b1 = [0.0, 0.0] - br +W2 = [1.0 0.0; 0.0 1.0] - Wr +b2 = [0.0, 0.0] - br ∂xn_∂xp = [0.0 0.0; 0.0 -ENERGY_LOSS] # setup BouncingBallODE -fx = function(x) - return [x[2], -GRAVITY] +fx = function (x) + return [x[2], -GRAVITY] end -fx_bb = function(dx, x, p, t) +fx_bb = function (dx, x, p, t) dx[:] = re_bb(p)(x) return nothing end net_bb = Chain(#Dense(W1, b1, identity), - fx, - Dense(W2, b2, identity)) + fx, + Dense(W2, b2, identity), +) p_net_bb, re_bb = Flux.destructure(net_bb) -ff = ODEFunction{true}(fx_bb) +ff = ODEFunction{true}(fx_bb) prob_bb = ODEProblem{true}(ff, x0_bb, (t_start, t_stop), p_net_bb) -condition = function(out, x, t, integrator) +condition = function (out, x, t, integrator) #x = re_bb(p_net_bb)[1](x) - out[1] = x[1]-RADIUS + out[1] = x[1] - RADIUS end -time_choice = function(integrator) +time_choice = function (integrator) ts = [0.451523640985728, 1.083656738365748, 1.5261499065317576, 1.8358951242479626] - i = 1 - while ts[i] <= integrator.t + i = 1 + while ts[i] <= integrator.t i += 1 if i > length(ts) - return nothing + return nothing end end return ts[i] end -affect_right! = function(integrator, idx) +affect_right! = function (integrator, idx) #@info "affect_right! triggered by #$(idx)" @@ -102,21 +107,21 @@ affect_right! = function(integrator, idx) condition(out, unsense(x), unsense(t), integrator) if sign(out[idx]) > 0.0 @info "Event for bouncing ball (white-box) triggered, but not valid!" - return nothing + return nothing end end - + s_new = RADIUS + DBL_MIN - v_new = -integrator.u[2]*ENERGY_LOSS + v_new = -integrator.u[2] * ENERGY_LOSS u_new = [s_new, v_new] - global events + global events events += 1 #@info "[$(events)] New state at $(integrator.t) is $(u_new) triggered by #$(idx)" integrator.u .= u_new end -affect_left! = function(integrator, idx) +affect_left! = function (integrator, idx) #@info "affect_left! triggered by #$(idx)" @@ -131,96 +136,106 @@ affect_left! = function(integrator, idx) condition(out, unsense(x), unsense(t), integrator) if sign(out[idx]) < 0.0 @warn "Event for bouncing ball triggered, but not valid!" - return nothing + return nothing end s_new = integrator.u[1] - v_new = -integrator.u[2]*ENERGY_LOSS + v_new = -integrator.u[2] * ENERGY_LOSS u_new = [s_new, v_new] - global events + global events events += 1 #@info "[$(events)] New state at $(integrator.t) is $(u_new)" integrator.u .= u_new end -stepCompleted = function(x, t, integrator) - +stepCompleted = function (x, t, integrator) + end NUMEVENTINDICATORS = 1 # 2 -rightCb = VectorContinuousCallback(condition, #_double, - affect_right!, - NUMEVENTINDICATORS; - rootfind=RightRootFind, save_positions=(false, false)) -leftCb = VectorContinuousCallback(condition, #_double, - affect_left!, - NUMEVENTINDICATORS; - rootfind=LeftRootFind, save_positions=(false, false)) - -timeCb = IterativeCallback(time_choice, - (indicator) -> affect_right!(indicator, 0), - Float64; - initial_affect=false, - save_positions=(false, false)) - -stepCb = FunctionCallingCallback(stepCompleted; - func_everystep=true, - func_start=true) +rightCb = VectorContinuousCallback( + condition, #_double, + affect_right!, + NUMEVENTINDICATORS; + rootfind = RightRootFind, + save_positions = (false, false), +) +leftCb = VectorContinuousCallback( + condition, #_double, + affect_left!, + NUMEVENTINDICATORS; + rootfind = LeftRootFind, + save_positions = (false, false), +) + +timeCb = IterativeCallback( + time_choice, + (indicator) -> affect_right!(indicator, 0), + Float64; + initial_affect = false, + save_positions = (false, false), +) + +stepCb = FunctionCallingCallback(stepCompleted; func_everystep = true, func_start = true) # load FMU for NeuralFMU #fmu = loadFMU("BouncingBall", "ModelicaReferenceFMUs", "0.0.25"; type=:ME) #fmu_params = nothing -fmu = loadFMU("BouncingBall1D", "Dymola", "2023x"; type=:ME) +fmu = loadFMU("BouncingBall1D", "Dymola", "2023x"; type = :ME) fmu_params = Dict("damping" => 0.7, "mass_radius" => 0.0, "mass_s_min" => DBL_MIN) fmu.executionConfig.isolatedStateDependency = true net = Chain(#Dense(W1, b1, identity), - x -> fmu(;x=x, dx_refs=:all), - Dense(W2, b2, identity)) + x -> fmu(; x = x, dx_refs = :all), + Dense(W2, b2, identity), +) -prob = ME_NeuralFMU(fmu, net, (t_start, t_stop)) +prob = ME_NeuralFMU(fmu, net, (t_start, t_stop)) prob.snapshots = true # needed for correct sensitivities # ANNs -losssum = function(p; sensealg=nothing, solver=nothing) +losssum = function (p; sensealg = nothing, solver = nothing) global posData - posNet = mysolve(p; sensealg=sensealg, solver=solver) + posNet = mysolve(p; sensealg = sensealg, solver = solver) return Flux.Losses.mae(posNet, posData) end -losssum_bb = function(p; sensealg=nothing, root=:Right, solver=nothing) +losssum_bb = function (p; sensealg = nothing, root = :Right, solver = nothing) global posData - posNet = mysolve_bb(p; sensealg=sensealg, root=root, solver=solver) - + posNet = mysolve_bb(p; sensealg = sensealg, root = root, solver = solver) + return Flux.Losses.mae(posNet, posData) end -mysolve = function(p; sensealg=nothing, solver=nothing) +mysolve = function (p; sensealg = nothing, solver = nothing) global solution, events # write global prob, x0_bb, posData # read-only events = 0 - solution = prob(x0_bb; - p=p, - solver=solver, - parameters=fmu_params, - sensealg=sensealg, - cleanSnapshots=false, solvekwargs...) + solution = prob( + x0_bb; + p = p, + solver = solver, + parameters = fmu_params, + sensealg = sensealg, + cleanSnapshots = false, + solvekwargs..., + ) return collect(u[1] for u in solution.states.u) end -mysolve_bb = function(p; sensealg=nothing, root=:Right, solver=nothing) +mysolve_bb = function (p; sensealg = nothing, root = :Right, solver = nothing) global solution # write global prob_bb, events # read events = 0 callback = nothing - if root == :Right + if root == :Right callback = CallbackSet(rightCb, stepCb) elseif root == :Left callback = CallbackSet(leftCb, stepCb) @@ -229,18 +244,24 @@ mysolve_bb = function(p; sensealg=nothing, root=:Right, solver=nothing) else @assert false "unknwon root `$(root)`" end - solution = solve(prob_bb; p=p, alg=solver, callback=callback, - sensealg=sensealg, solvekwargs...) + solution = solve( + prob_bb; + p = p, + alg = solver, + callback = callback, + sensealg = sensealg, + solvekwargs..., + ) if !isa(solution, AbstractArray) if solution.retcode != FMIFlux.ReturnCode.Success @error "Solution failed!" - return Inf + return Inf end return collect(u[1] for u in solution.u) else - return solution[1,:] # collect(solution[:,i] for i in 1:size(solution)[2]) + return solution[1, :] # collect(solution[:,i] for i in 1:size(solution)[2]) end end @@ -250,19 +271,37 @@ using FMIFlux.FMISensitivity.SciMLSensitivity sensealg = ReverseDiffAdjoint() # InterpolatingAdjoint(autojacvec=ReverseDiffVJP(false)) # c = nothing -c, _ = FMIFlux.prepareSolveFMU(prob.fmu, c, fmi2TypeModelExchange; parameters=prob.parameters, t_start=prob.tspan[1], t_stop=prob.tspan[end], x0=prob.x0, handleEvents=FMIFlux.handleEvents, cleanup=true) +c, _ = FMIFlux.prepareSolveFMU( + prob.fmu, + c, + fmi2TypeModelExchange; + parameters = prob.parameters, + t_start = prob.tspan[1], + t_stop = prob.tspan[end], + x0 = prob.x0, + handleEvents = FMIFlux.handleEvents, + cleanup = true, +) ### START CHECK CONDITIONS -condition_bb_check = function(x) +condition_bb_check = function (x) buffer = similar(x, 1) condition(buffer, x, t_start, nothing) - return buffer + return buffer end -condition_nfmu_check = function(x) +condition_nfmu_check = function (x) buffer = similar(x, 1) - FMIFlux.condition!(prob, FMIFlux.getInstance(prob), buffer, x, t_start, nothing, [UInt32(1)]) - return buffer + FMIFlux.condition!( + prob, + FMIFlux.getInstance(prob), + buffer, + x, + t_start, + nothing, + [UInt32(1)], + ) + return buffer end jac_fwd1 = ForwardDiff.jacobian(condition_bb_check, x0_bb) jac_fwd2 = ForwardDiff.jacobian(condition_nfmu_check, x0_bb) @@ -274,14 +313,14 @@ jac_fin1 = FiniteDiff.finite_difference_jacobian(condition_bb_check, x0_bb) jac_fin2 = FiniteDiff.finite_difference_jacobian(condition_nfmu_check, x0_bb) atol = 1e-6 -@test isapprox(jac_fin1, jac_fwd1; atol=atol) -@test isapprox(jac_fin1, jac_rwd1; atol=atol) -@test isapprox(jac_fin2, jac_fwd2; atol=atol) -@test isapprox(jac_fin2, jac_rwd2; atol=atol) +@test isapprox(jac_fin1, jac_fwd1; atol = atol) +@test isapprox(jac_fin1, jac_rwd1; atol = atol) +@test isapprox(jac_fin2, jac_fwd2; atol = atol) +@test isapprox(jac_fin2, jac_rwd2; atol = atol) ### START CHECK AFFECT -affect_bb_check = function(x) +affect_bb_check = function (x) # convert TrackedArrays to Array{<:TrackedReal,1} if !isa(x, AbstractVector{<:Float64}) @@ -290,11 +329,11 @@ affect_bb_check = function(x) x = copy(x) end - integrator = (t=t_start, u=x) + integrator = (t = t_start, u = x) affect_right!(integrator, 1) return integrator.u end -affect_nfmu_check = function(x) +affect_nfmu_check = function (x) global prob # convert TrackedArrays to Array{<:TrackedReal,1} @@ -303,12 +342,22 @@ affect_nfmu_check = function(x) else x = copy(x) end - - c, _ = FMIFlux.prepareSolveFMU(prob.fmu, nothing, fmi2TypeModelExchange; parameters = fmu_params, t_start = prob.tspan[1], t_stop = prob.tspan[end], x0=unsense(x), handleEvents=FMIFlux.handleEvents, cleanup=true) - integrator = (t=t_start, u=x, opts=(internalnorm=(a,b)->1.0,) ) + c, _ = FMIFlux.prepareSolveFMU( + prob.fmu, + nothing, + fmi2TypeModelExchange; + parameters = fmu_params, + t_start = prob.tspan[1], + t_stop = prob.tspan[end], + x0 = unsense(x), + handleEvents = FMIFlux.handleEvents, + cleanup = true, + ) + + integrator = (t = t_start, u = x, opts = (internalnorm = (a, b) -> 1.0,)) FMIFlux.affectFMU!(prob, c, integrator, 1) - + return integrator.u end #t_event_time = 0.451523640985728 @@ -316,134 +365,192 @@ x_event_left = [-1.0, -1.0] # [-3.808199081191736e-15, -4.429446918069994] x_event_right = [0.0, 0.7] # [2.2250738585072014e-308, 3.1006128426489954] x_no_event = [0.1, -1.0] -@test isapprox(affect_bb_check(x_event_left), x_event_right; atol=1e-4) -@test isapprox(affect_nfmu_check(x_event_left), x_event_right; atol=1e-4) +@test isapprox(affect_bb_check(x_event_left), x_event_right; atol = 1e-4) +@test isapprox(affect_nfmu_check(x_event_left), x_event_right; atol = 1e-4) jac_con1 = ForwardDiff.jacobian(affect_bb_check, x_event_left) jac_con2 = ForwardDiff.jacobian(affect_nfmu_check, x_event_left) -@test isapprox(jac_con1, ∂xn_∂xp; atol=1e-4) -@test isapprox(jac_con2, ∂xn_∂xp; atol=1e-4) +@test isapprox(jac_con1, ∂xn_∂xp; atol = 1e-4) +@test isapprox(jac_con2, ∂xn_∂xp; atol = 1e-4) jac_con1 = ReverseDiff.jacobian(affect_bb_check, x_event_left) jac_con2 = ReverseDiff.jacobian(affect_nfmu_check, x_event_left) -@test isapprox(jac_con1, ∂xn_∂xp; atol=1e-4) -@test isapprox(jac_con2, ∂xn_∂xp; atol=1e-4) +@test isapprox(jac_con1, ∂xn_∂xp; atol = 1e-4) +@test isapprox(jac_con2, ∂xn_∂xp; atol = 1e-4) # [Note] checking via FiniteDiff is not possible here, because finite differences offsets might not trigger the events at all # no-event -@test isapprox(affect_bb_check(x_no_event), x_no_event; atol=1e-4) -@test isapprox(affect_nfmu_check(x_no_event), x_no_event; atol=1e-4) +@test isapprox(affect_bb_check(x_no_event), x_no_event; atol = 1e-4) +@test isapprox(affect_nfmu_check(x_no_event), x_no_event; atol = 1e-4) jac_con1 = ForwardDiff.jacobian(affect_bb_check, x_no_event) jac_con2 = ForwardDiff.jacobian(affect_nfmu_check, x_no_event) -@test isapprox(jac_con1, I; atol=1e-4) -@test isapprox(jac_con2, I; atol=1e-4) +@test isapprox(jac_con1, I; atol = 1e-4) +@test isapprox(jac_con2, I; atol = 1e-4) jac_con1 = ReverseDiff.jacobian(affect_bb_check, x_no_event) jac_con2 = ReverseDiff.jacobian(affect_nfmu_check, x_no_event) -@test isapprox(jac_con1, I; atol=1e-4) -@test isapprox(jac_con2, I; atol=1e-4) +@test isapprox(jac_con1, I; atol = 1e-4) +@test isapprox(jac_con2, I; atol = 1e-4) ### -for solver in solvers +for solver in solvers @info "Solver: $(solver)" # Solution (plain) - losssum(p_net; sensealg=sensealg, solver=solver) + losssum(p_net; sensealg = sensealg, solver = solver) @test length(solution.events) == NUMEVENTS - losssum_bb(p_net_bb; sensealg=sensealg, solver=solver) + losssum_bb(p_net_bb; sensealg = sensealg, solver = solver) @test events == NUMEVENTS # Solution FWD (FMU) - grad_fwd_f = ForwardDiff.gradient(p -> losssum(p; sensealg=sensealg, solver=solver), p_net) - @test length(solution.events) == NUMEVENTS + grad_fwd_f = + ForwardDiff.gradient(p -> losssum(p; sensealg = sensealg, solver = solver), p_net) + @test length(solution.events) == NUMEVENTS # Solution FWD (right) root = :Right - grad_fwd_r = ForwardDiff.gradient(p -> losssum_bb(p; sensealg=sensealg, root=root, solver=solver), p_net_bb) + grad_fwd_r = ForwardDiff.gradient( + p -> losssum_bb(p; sensealg = sensealg, root = root, solver = solver), + p_net_bb, + ) @test events == NUMEVENTS # Solution FWD (left) root = :Left - grad_fwd_l = ForwardDiff.gradient(p -> losssum_bb(p; sensealg=sensealg, root=root, solver=solver), p_net_bb) + grad_fwd_l = ForwardDiff.gradient( + p -> losssum_bb(p; sensealg = sensealg, root = root, solver = solver), + p_net_bb, + ) @test events == NUMEVENTS # Solution FWD (time) root = :Time - grad_fwd_t = ForwardDiff.gradient(p -> losssum_bb(p; sensealg=sensealg, root=root, solver=solver), p_net_bb) + grad_fwd_t = ForwardDiff.gradient( + p -> losssum_bb(p; sensealg = sensealg, root = root, solver = solver), + p_net_bb, + ) @test events == NUMEVENTS # Solution RWD (FMU) - grad_rwd_f = ReverseDiff.gradient(p -> losssum(p; sensealg=sensealg, solver=solver), p_net) - @test length(solution.events) == NUMEVENTS + grad_rwd_f = + ReverseDiff.gradient(p -> losssum(p; sensealg = sensealg, solver = solver), p_net) + @test length(solution.events) == NUMEVENTS # Solution RWD (right) root = :Right - grad_rwd_r = ReverseDiff.gradient(p -> losssum_bb(p; sensealg=sensealg, root=root, solver=solver), p_net_bb) + grad_rwd_r = ReverseDiff.gradient( + p -> losssum_bb(p; sensealg = sensealg, root = root, solver = solver), + p_net_bb, + ) @test events == NUMEVENTS # Solution RWD (left) root = :Left - grad_rwd_l = ReverseDiff.gradient(p -> losssum_bb(p; sensealg=sensealg, root=root, solver=solver), p_net_bb) + grad_rwd_l = ReverseDiff.gradient( + p -> losssum_bb(p; sensealg = sensealg, root = root, solver = solver), + p_net_bb, + ) @test events == NUMEVENTS # Solution RWD (time) root = :Time - grad_rwd_t = ReverseDiff.gradient(p -> losssum_bb(p; sensealg=sensealg, root=root, solver=solver), p_net_bb) + grad_rwd_t = ReverseDiff.gradient( + p -> losssum_bb(p; sensealg = sensealg, root = root, solver = solver), + p_net_bb, + ) @test events == NUMEVENTS # Ground Truth - absstep=1e-6 - grad_fin_r = FiniteDiff.finite_difference_gradient(p -> losssum_bb(p; sensealg=sensealg, root=:Right, solver=solver), p_net_bb, Val{:central}; absstep=absstep) - grad_fin_l = FiniteDiff.finite_difference_gradient(p -> losssum_bb(p; sensealg=sensealg, root=:Left, solver=solver), p_net_bb, Val{:central}; absstep=absstep) - grad_fin_t = FiniteDiff.finite_difference_gradient(p -> losssum_bb(p; sensealg=sensealg, root=:Time, solver=solver), p_net_bb, Val{:central}; absstep=absstep) - grad_fin_f = FiniteDiff.finite_difference_gradient(p -> losssum(p; sensealg=sensealg, solver=solver), p_net, Val{:central}; absstep=absstep) + absstep = 1e-6 + grad_fin_r = FiniteDiff.finite_difference_gradient( + p -> losssum_bb(p; sensealg = sensealg, root = :Right, solver = solver), + p_net_bb, + Val{:central}; + absstep = absstep, + ) + grad_fin_l = FiniteDiff.finite_difference_gradient( + p -> losssum_bb(p; sensealg = sensealg, root = :Left, solver = solver), + p_net_bb, + Val{:central}; + absstep = absstep, + ) + grad_fin_t = FiniteDiff.finite_difference_gradient( + p -> losssum_bb(p; sensealg = sensealg, root = :Time, solver = solver), + p_net_bb, + Val{:central}; + absstep = absstep, + ) + grad_fin_f = FiniteDiff.finite_difference_gradient( + p -> losssum(p; sensealg = sensealg, solver = solver), + p_net, + Val{:central}; + absstep = absstep, + ) local atol = 1e-3 - + # check if finite differences match together - @test isapprox(grad_fin_f, grad_fin_r; atol=atol) - @test isapprox(grad_fin_f, grad_fin_l; atol=atol) + @test isapprox(grad_fin_f, grad_fin_r; atol = atol) + @test isapprox(grad_fin_f, grad_fin_l; atol = atol) - @test isapprox(grad_fin_f, grad_fwd_f; atol=0.2) # [ToDo: this is too much!] - @test isapprox(grad_fin_f, grad_rwd_f; atol=atol) + @test isapprox(grad_fin_f, grad_fwd_f; atol = 0.2) # [ToDo: this is too much!] + @test isapprox(grad_fin_f, grad_rwd_f; atol = atol) # Jacobian Test - jac_fwd_r = ForwardDiff.jacobian(p -> mysolve_bb(p; sensealg=sensealg, solver=solver), p_net) - jac_fwd_f = ForwardDiff.jacobian(p -> mysolve(p; sensealg=sensealg, solver=solver), p_net) + jac_fwd_r = ForwardDiff.jacobian( + p -> mysolve_bb(p; sensealg = sensealg, solver = solver), + p_net, + ) + jac_fwd_f = + ForwardDiff.jacobian(p -> mysolve(p; sensealg = sensealg, solver = solver), p_net) - jac_rwd_r = ReverseDiff.jacobian(p -> mysolve_bb(p; sensealg=sensealg, solver=solver), p_net) - jac_rwd_f = ReverseDiff.jacobian(p -> mysolve(p; sensealg=sensealg, solver=solver), p_net) + jac_rwd_r = ReverseDiff.jacobian( + p -> mysolve_bb(p; sensealg = sensealg, solver = solver), + p_net, + ) + jac_rwd_f = + ReverseDiff.jacobian(p -> mysolve(p; sensealg = sensealg, solver = solver), p_net) # [TODO] why this?! - jac_rwd_r[2:end,:] = jac_rwd_r[2:end,:] .- jac_rwd_r[1:end-1,:] - jac_rwd_f[2:end,:] = jac_rwd_f[2:end,:] .- jac_rwd_f[1:end-1,:] - - jac_fin_r = FiniteDiff.finite_difference_jacobian(p -> mysolve_bb(p; sensealg=sensealg, solver=solver), p_net, Val{:central}; absstep=absstep) - jac_fin_f = FiniteDiff.finite_difference_jacobian(p -> mysolve(p; sensealg=sensealg, solver=solver), p_net, Val{:central}; absstep=absstep) + jac_rwd_r[2:end, :] = jac_rwd_r[2:end, :] .- jac_rwd_r[1:end-1, :] + jac_rwd_f[2:end, :] = jac_rwd_f[2:end, :] .- jac_rwd_f[1:end-1, :] + + jac_fin_r = FiniteDiff.finite_difference_jacobian( + p -> mysolve_bb(p; sensealg = sensealg, solver = solver), + p_net, + Val{:central}; + absstep = absstep, + ) + jac_fin_f = FiniteDiff.finite_difference_jacobian( + p -> mysolve(p; sensealg = sensealg, solver = solver), + p_net, + Val{:central}; + absstep = absstep, + ) ### local atol = 1e-3 - @test isapprox(jac_fin_f, jac_fin_r; atol=atol) + @test isapprox(jac_fin_f, jac_fin_r; atol = atol) - @test isapprox(jac_fin_f, jac_fwd_f; atol=1e1) # [ToDo] this is too much! + @test isapprox(jac_fin_f, jac_fwd_f; atol = 1e1) # [ToDo] this is too much! @test mean(abs.(jac_fin_f .- jac_fwd_f)) < 0.15 # added another test for this case... - @test isapprox(jac_fin_f, jac_rwd_f; atol=atol) + @test isapprox(jac_fin_f, jac_rwd_f; atol = atol) - @test isapprox(jac_fin_r, jac_fwd_r; atol=atol) - @test isapprox(jac_fin_r, jac_rwd_r; atol=atol) + @test isapprox(jac_fin_r, jac_fwd_r; atol = atol) + @test isapprox(jac_fin_r, jac_rwd_r; atol = atol) ### end diff --git a/test/supported_sensitivities.jl b/test/supported_sensitivities.jl index 258ebaac..9e6abaaf 100644 --- a/test/supported_sensitivities.jl +++ b/test/supported_sensitivities.jl @@ -6,54 +6,75 @@ using Flux using DifferentialEquations -import Random +import Random Random.seed!(5678); # boundaries t_start = 0.0 t_step = 0.1 -t_stop = 5.0 +t_stop = 5.0 tData = t_start:t_step:t_stop tspan = (t_start, t_stop) posData = ones(Float64, length(tData)) +nfmu = nothing # load FMU for NeuralFMU -#fmu = loadFMU("BouncingBall", "ModelicaReferenceFMUs", "0.0.25"; type=:ME) -fmu = loadFMU("BouncingBall1D", "Dymola", "2023x"; type=:ME) -#fmu.handleEventIndicators = [UInt32(1)] +fmus = [] + +# this is a non-simultaneous event system (one event per time instant) +f = loadFMU("BouncingBall", "ModelicaReferenceFMUs", "0.0.25"; type = :ME) +@assert f.modelDescription.numberOfEventIndicators == 1 "Wrong number of event indicators: $(f.modelDescription.numberOfEventIndicators) != 1" +push!(fmus, f) + +# this is a simultaneous event system (two events per time instant) +f = loadFMU("BouncingBall1D", "Dymola", "2023x"; type = :ME) +@assert f.modelDescription.numberOfEventIndicators == 2 "Wrong number of event indicators: $(f.modelDescription.numberOfEventIndicators) != 2" +push!(fmus, f) x0_bb = [1.0, 0.0] numStates = length(x0_bb) -net = Chain(x -> fmu(;x=x, dx_refs=:all), - Dense(2, 16, tanh), - Dense(16, 2, identity)) +function net_const(fmu) + net = + Chain(x -> fmu(; x = x, dx_refs = :all), Dense(2, 16, tanh), Dense(16, 2, identity)) + return net +end # loss function for training -losssum = function(p) +losssum = function (p) global nfmu, x0_bb, posData - solution = nfmu(x0_bb; p=p, saveat=tData) + solution = nfmu(x0_bb; p = p, saveat = tData) if !solution.success - return Inf + return Inf end - posNet = getState(solution, 1; isIndex=true) - + posNet = getState(solution, 1; isIndex = true) + return FMIFlux.Losses.mse(posNet, posData) end -solvers = [Tsit5(), FBDF(autodiff=false)] # Tsit5(), , FBDF(autodiff=true)] -for solver in solvers - - global nfmu - @info "Solver: $(solver)" - nfmu = ME_NeuralFMU(fmu, net, (t_start, t_stop), solver; saveat=tData) - nfmu.modifiedState = false - nfmu.snapshots = true - - best_timing, best_gradient, best_sensealg = FMIFlux.checkSensalgs!(losssum, nfmu) - @test best_timing != Inf +for fmu in fmus + @info "##### CHECKING FMU WITH $(fmu.modelDescription.numberOfEventIndicators) SIMULTANEOUS EVENT INDICATOR(S) #####" + solvers = [ + Tsit5(), + Rosenbrock23(autodiff = false), + Rodas5(autodiff = false), + FBDF(autodiff = false), + ] + for solver in solvers + + global nfmu + @info "##### SOLVER: $(solver) #####" + + net = net_const(fmu) + nfmu = ME_NeuralFMU(fmu, net, (t_start, t_stop), solver; saveat = tData) + nfmu.modifiedState = false + nfmu.snapshots = true + + best_timing, best_gradient, best_sensealg = FMIFlux.checkSensalgs!(losssum, nfmu) + #@test best_timing != Inf + end end -unloadFMU(fmu) \ No newline at end of file +unloadFMU.(fmus) diff --git a/test/time_solution_gradients.jl b/test/time_solution_gradients.jl index ebd90d74..afd65393 100644 --- a/test/time_solution_gradients.jl +++ b/test/time_solution_gradients.jl @@ -8,17 +8,20 @@ using DifferentialEquations using FMIFlux, FMIZoo, Test import FMIFlux.FMISensitivity.SciMLSensitivity.SciMLBase: RightRootFind, LeftRootFind using FMIFlux.FMIImport.FMIBase: unsense -using FMIFlux.FMISensitivity.SciMLSensitivity.ForwardDiff, FMIFlux.FMISensitivity.SciMLSensitivity.ReverseDiff, FMIFlux.FMISensitivity.SciMLSensitivity.FiniteDiff, FMIFlux.FMISensitivity.SciMLSensitivity.Zygote +using FMIFlux.FMISensitivity.SciMLSensitivity.ForwardDiff, + FMIFlux.FMISensitivity.SciMLSensitivity.ReverseDiff, + FMIFlux.FMISensitivity.SciMLSensitivity.FiniteDiff, + FMIFlux.FMISensitivity.SciMLSensitivity.Zygote using FMIFlux.FMIImport, FMIFlux.FMIImport.FMICore, FMIZoo -import LinearAlgebra:I +import LinearAlgebra: I -import Random +import Random Random.seed!(5678); global solution = nothing global events = 0 -ENERGY_LOSS = 0.7 +ENERGY_LOSS = 0.7 RADIUS = 0.0 GRAVITY = 9.81 GRAVITY_SIGN = -1 @@ -36,28 +39,29 @@ tData = t_start:t_step:t_stop posData = ones(Float64, length(tData)) x0_bb = [0.5, 0.0] -solvekwargs = Dict{Symbol, Any}(:saveat => tData, :abstol => 1e-6, :reltol => 1e-6, :dtmax => 1e-2) +solvekwargs = + Dict{Symbol,Any}(:saveat => tData, :abstol => 1e-6, :reltol => 1e-6, :dtmax => 1e-2) numStates = 2 solvers = [Tsit5()]#, Rosenbrock23(autodiff=false)] -Wr = rand(2,2)*1e-4 -br = rand(2)*1e-4 +Wr = rand(2, 2) * 1e-4 +br = rand(2) * 1e-4 -W1 = [1.0 0.0; 0.0 1.0] - Wr -b1 = [0.0, 0.0] - br -W2 = [1.0 0.0; 0.0 1.0] - Wr -b2 = [0.0, 0.0] - br +W1 = [1.0 0.0; 0.0 1.0] - Wr +b1 = [0.0, 0.0] - br +W2 = [1.0 0.0; 0.0 1.0] - Wr +b2 = [0.0, 0.0] - br ∂xn_∂xp = [0.0 0.0; 0.0 -ENERGY_LOSS] # setup BouncingBallODE global fx_dx_cache = zeros(Real, 2) -fx = function(x, t; kwargs...) +fx = function (x, t; kwargs...) return _fx(x, t; kwargs...) end -_fx = function(x, t) +_fx = function (x, t) global fx_dx_cache fx_dx_cache[1] = x[2] @@ -65,28 +69,29 @@ _fx = function(x, t) return fx_dx_cache end -fx_bb = function(dx, x, p, t) +fx_bb = function (dx, x, p, t) dx[:] = re_bb(p)(x) return nothing end net_bb = Chain(#Dense(W1, b1, identity), - x -> fx(x, 0.0), - Dense(W2, b2, identity)) + x -> fx(x, 0.0), + Dense(W2, b2, identity), +) p_net_bb, re_bb = Flux.destructure(net_bb) -ff = ODEFunction{true}(fx_bb) +ff = ODEFunction{true}(fx_bb) prob_bb = ODEProblem{true}(ff, x0_bb, (t_start, t_stop), p_net_bb) -condition = function(out, x, t, integrator) +condition = function (out, x, t, integrator) #x = re_bb(p_net_bb)[1](x) - out[1] = x[1]-RADIUS - out[2] = x[1]-RADIUS + out[1] = x[1] - RADIUS + out[2] = x[1] - RADIUS end import FMIFlux: unsense -time_choice = function(integrator) - next = (floor(integrator.t/TIME_FREQ)+1) * TIME_FREQ +time_choice = function (integrator) + next = (floor(integrator.t / TIME_FREQ) + 1) * TIME_FREQ if next <= t_stop #@info "next: $(next)" @@ -96,17 +101,17 @@ time_choice = function(integrator) end end -time_affect! = function(integrator) +time_affect! = function (integrator) global GRAVITY_SIGN GRAVITY_SIGN = -GRAVITY_SIGN - global events + global events events += 1 #u_modified!(integrator, false) end -affect_right! = function(integrator, idx) +affect_right! = function (integrator, idx) #@info "affect_right! triggered by #$(idx)" @@ -122,23 +127,23 @@ affect_right! = function(integrator, idx) condition(out, unsense(x), unsense(t), integrator) if sign(out[idx]) > 0.0 @info "Event for bouncing ball (white-box) triggered, but not valid!" - return nothing + return nothing end end - + s_new = RADIUS + DBL_MIN v_new = -1.0 * unsense(integrator.u[2]) * ENERGY_LOSS left_x = unsense(integrator.u) right_x = [s_new, v_new] - global events + global events events += 1 #@info "[$(events)] New state at $(integrator.t) is $(u_new) triggered by #$(idx)" #integrator.u[:] .= u_new - for i in 1:length(left_x) + for i = 1:length(left_x) if left_x[i] != 0.0 # abs(left_x[i]) > 1e-128 scale = right_x[i] / left_x[i] integrator.u[i] *= scale @@ -146,13 +151,16 @@ affect_right! = function(integrator, idx) shift = right_x[i] - left_x[i] integrator.u[i] += shift #integrator.u[i] = right_x[i] - logWarning(c.fmu, "Probably wrong sensitivities @t=$(unsense(t)) for ∂x^+ / ∂x^-\nCan't scale zero state #$(i) from $(left_x[i]) to $(right_x[i])\nNew state after transform is: $(integrator.u[i])") + logWarning( + c.fmu, + "Probably wrong sensitivities @t=$(unsense(t)) for ∂x^+ / ∂x^-\nCan't scale zero state #$(i) from $(left_x[i]) to $(right_x[i])\nNew state after transform is: $(integrator.u[i])", + ) end end - return nothing + return nothing end -affect_left! = function(integrator, idx) +affect_left! = function (integrator, idx) #@info "affect_left! triggered by #$(idx)" @@ -167,95 +175,112 @@ affect_left! = function(integrator, idx) condition(out, unsense(x), unsense(t), integrator) if sign(out[idx]) < 0.0 @warn "Event for bouncing ball triggered, but not valid!" - return nothing + return nothing end s_new = integrator.u[1] - v_new = -integrator.u[2]*ENERGY_LOSS + v_new = -integrator.u[2] * ENERGY_LOSS u_new = [s_new, v_new] - global events + global events events += 1 #@info "[$(events)] New state at $(integrator.t) is $(u_new)" integrator.u .= u_new end -stepCompleted = function(x, t, integrator) - +stepCompleted = function (x, t, integrator) + end NUMEVENTINDICATORS = 2 -rightCb = VectorContinuousCallback(condition, #_double, - affect_right!, - NUMEVENTINDICATORS; - rootfind=RightRootFind, save_positions=(false, false), - interp_points=INTERP_POINTS) -leftCb = VectorContinuousCallback(condition, #_double, - affect_left!, - NUMEVENTINDICATORS; - rootfind=LeftRootFind, save_positions=(false, false), - interp_points=INTERP_POINTS) - -gravityCb = IterativeCallback(time_choice, - time_affect!, - Float64; - initial_affect=false, - save_positions=(false, false)) - -stepCb = FunctionCallingCallback(stepCompleted; - func_everystep=true, - func_start=true) +rightCb = VectorContinuousCallback( + condition, #_double, + affect_right!, + NUMEVENTINDICATORS; + rootfind = RightRootFind, + save_positions = (false, false), + interp_points = INTERP_POINTS, +) +leftCb = VectorContinuousCallback( + condition, #_double, + affect_left!, + NUMEVENTINDICATORS; + rootfind = LeftRootFind, + save_positions = (false, false), + interp_points = INTERP_POINTS, +) + +gravityCb = IterativeCallback( + time_choice, + time_affect!, + Float64; + initial_affect = false, + save_positions = (false, false), +) + +stepCb = FunctionCallingCallback(stepCompleted; func_everystep = true, func_start = true) # load FMU for NeuralFMU -fmu = loadFMU("BouncingBallGravitySwitch1D", "Dymola", "2023x"; type=:ME) -fmu_params = Dict("damping" => ENERGY_LOSS, "mass_radius" => RADIUS, "gravity" => GRAVITY, "period" => TIME_FREQ, "mass_m" => MASS, "mass_s_min" => DBL_MIN) +fmu = loadFMU("BouncingBallGravitySwitch1D", "Dymola", "2023x"; type = :ME) +fmu_params = Dict( + "damping" => ENERGY_LOSS, + "mass_radius" => RADIUS, + "gravity" => GRAVITY, + "period" => TIME_FREQ, + "mass_m" => MASS, + "mass_s_min" => DBL_MIN, +) fmu.executionConfig.isolatedStateDependency = true net = Chain(#Dense(W1, b1, identity), - x -> fmu(;x=x, dx_refs=:all), - Dense(W2, b2, identity)) + x -> fmu(; x = x, dx_refs = :all), + Dense(W2, b2, identity), +) -prob = ME_NeuralFMU(fmu, net, (t_start, t_stop)) +prob = ME_NeuralFMU(fmu, net, (t_start, t_stop)) prob.snapshots = true # needed for correct sensitivities # ANNs -losssum = function(p; sensealg=nothing, solver=nothing) +losssum = function (p; sensealg = nothing, solver = nothing) global posData - posNet = mysolve(p; sensealg=sensealg, solver=solver) + posNet = mysolve(p; sensealg = sensealg, solver = solver) return Flux.Losses.mae(posNet, posData) end -losssum_bb = function(p; sensealg=nothing, root=:Right, solver=nothing) +losssum_bb = function (p; sensealg = nothing, root = :Right, solver = nothing) global posData - posNet = mysolve_bb(p; sensealg=sensealg, root=root, solver=solver) - + posNet = mysolve_bb(p; sensealg = sensealg, root = root, solver = solver) + return Flux.Losses.mae(posNet, posData) end -mysolve = function(p; sensealg=nothing, solver=nothing) +mysolve = function (p; sensealg = nothing, solver = nothing) global solution, events # write global prob, x0_bb, posData # read-only events = 0 - solution = prob(x0_bb; - p=p, - solver=solver, - parameters=fmu_params, - sensealg=sensealg, solvekwargs...) + solution = prob( + x0_bb; + p = p, + solver = solver, + parameters = fmu_params, + sensealg = sensealg, + solvekwargs..., + ) return collect(u[1] for u in solution.states.u) end -mysolve_bb = function(p; sensealg=nothing, root=:Right, solver=nothing) +mysolve_bb = function (p; sensealg = nothing, root = :Right, solver = nothing) global solution, GRAVITY_SIGN global prob_bb, events # read events = 0 callback = nothing - if root == :Right + if root == :Right callback = CallbackSet(gravityCb, rightCb, stepCb) elseif root == :Left callback = CallbackSet(gravityCb, leftCb, stepCb) @@ -264,17 +289,25 @@ mysolve_bb = function(p; sensealg=nothing, root=:Right, solver=nothing) end GRAVITY_SIGN = -1 - solution = solve(prob_bb, solver; u0=x0_bb, p=p, callback=callback, sensealg=sensealg, solvekwargs...) + solution = solve( + prob_bb, + solver; + u0 = x0_bb, + p = p, + callback = callback, + sensealg = sensealg, + solvekwargs..., + ) if !isa(solution, AbstractArray) if solution.retcode != FMIFlux.ReturnCode.Success @error "Solution failed!" - return Inf + return Inf end return collect(u[1] for u in solution.u) else - return solution[1,:] # collect(solution[:,i] for i in 1:size(solution)[2]) + return solution[1, :] # collect(solution[:,i] for i in 1:size(solution)[2]) end end @@ -284,20 +317,30 @@ using FMIFlux.FMISensitivity.SciMLSensitivity sensealg = ReverseDiffAdjoint() # InterpolatingAdjoint(autojacvec=ReverseDiffVJP(false)) # c = nothing -c, _ = FMIFlux.prepareSolveFMU(prob.fmu, c, fmi2TypeModelExchange; parameters=prob.parameters, t_start=prob.tspan[1], t_stop=prob.tspan[end], x0=prob.x0, handleEvents=FMIFlux.handleEvents, cleanup=true) +c, _ = FMIFlux.prepareSolveFMU( + prob.fmu, + c, + fmi2TypeModelExchange; + parameters = prob.parameters, + t_start = prob.tspan[1], + t_stop = prob.tspan[end], + x0 = prob.x0, + handleEvents = FMIFlux.handleEvents, + cleanup = true, +) ### START CHECK CONDITIONS -condition_bb_check = function(x) +condition_bb_check = function (x) buffer = similar(x, NUMEVENTINDICATORS) condition(buffer, x, t_start, nothing) - return buffer + return buffer end -condition_nfmu_check = function(x) +condition_nfmu_check = function (x) buffer = similar(x, fmu.modelDescription.numberOfEventIndicators) - inds = collect(UInt32(i) for i in 1:fmu.modelDescription.numberOfEventIndicators) + inds = collect(UInt32(i) for i = 1:fmu.modelDescription.numberOfEventIndicators) FMIFlux.condition!(prob, FMIFlux.getInstance(prob), buffer, x, t_start, nothing, inds) - return buffer + return buffer end jac_fwd1 = ForwardDiff.jacobian(condition_bb_check, x0_bb) jac_fwd2 = ForwardDiff.jacobian(condition_nfmu_check, x0_bb) @@ -309,14 +352,14 @@ jac_fin1 = FiniteDiff.finite_difference_jacobian(condition_bb_check, x0_bb) jac_fin2 = FiniteDiff.finite_difference_jacobian(condition_nfmu_check, x0_bb) atol = 1e-6 -@test isapprox(jac_fin1, jac_fwd1; atol=atol) -@test isapprox(jac_fin1, jac_rwd1; atol=atol) -@test isapprox(jac_fin2, jac_fwd2; atol=atol) -@test isapprox(jac_fin2, jac_rwd2; atol=atol) +@test isapprox(jac_fin1, jac_fwd1; atol = atol) +@test isapprox(jac_fin1, jac_rwd1; atol = atol) +@test isapprox(jac_fin2, jac_fwd2; atol = atol) +@test isapprox(jac_fin2, jac_rwd2; atol = atol) ### START CHECK AFFECT -affect_bb_check = function(x, t, idx=1) +affect_bb_check = function (x, t, idx = 1) # convert TrackedArrays to Array{<:TrackedReal,1} if !isa(x, AbstractVector{<:Float64}) @@ -325,7 +368,7 @@ affect_bb_check = function(x, t, idx=1) x = copy(x) end - integrator = (t=t, u=x) + integrator = (t = t, u = x) if idx == 0 time_affect!(integrator) else @@ -334,7 +377,7 @@ affect_bb_check = function(x, t, idx=1) return integrator.u end -affect_nfmu_check = function(x, t, idx=1) +affect_nfmu_check = function (x, t, idx = 1) global prob # convert TrackedArrays to Array{<:TrackedReal,1} @@ -343,12 +386,22 @@ affect_nfmu_check = function(x, t, idx=1) else x = copy(x) end - - c, _ = FMIFlux.prepareSolveFMU(prob.fmu, nothing, fmi2TypeModelExchange; parameters=fmu_params, t_start=unsense(t), t_stop=prob.tspan[end], x0=unsense(x), handleEvents=FMIFlux.handleEvents, cleanup=true) - integrator = (t=t, u=x, opts=(internalnorm=(a,b)->1.0,) ) + c, _ = FMIFlux.prepareSolveFMU( + prob.fmu, + nothing, + fmi2TypeModelExchange; + parameters = fmu_params, + t_start = unsense(t), + t_stop = prob.tspan[end], + x0 = unsense(x), + handleEvents = FMIFlux.handleEvents, + cleanup = true, + ) + + integrator = (t = t, u = x, opts = (internalnorm = (a, b) -> 1.0,)) FMIFlux.affectFMU!(prob, c, integrator, idx) - + return integrator.u end #t_event_time = 0.451523640985728 @@ -378,126 +431,158 @@ t_no_event = t_start # no-event -@test isapprox(affect_bb_check(x_no_event, t_no_event), x_no_event; atol=1e-4) -@test isapprox(affect_nfmu_check(x_no_event, t_no_event), x_no_event; atol=1e-4) +@test isapprox(affect_bb_check(x_no_event, t_no_event), x_no_event; atol = 1e-4) +@test isapprox(affect_nfmu_check(x_no_event, t_no_event), x_no_event; atol = 1e-4) jac_con1 = ForwardDiff.jacobian(x -> affect_bb_check(x, t_no_event), x_no_event) jac_con2 = ForwardDiff.jacobian(x -> affect_nfmu_check(x, t_no_event), x_no_event) -@test isapprox(jac_con1, I; atol=1e-4) -@test isapprox(jac_con2, I; atol=1e-4) +@test isapprox(jac_con1, I; atol = 1e-4) +@test isapprox(jac_con2, I; atol = 1e-4) jac_con1 = ReverseDiff.jacobian(x -> affect_bb_check(x, t_no_event), x_no_event) jac_con2 = ReverseDiff.jacobian(x -> affect_nfmu_check(x, t_no_event), x_no_event) -@test isapprox(jac_con1, I; atol=1e-4) -@test isapprox(jac_con2, I; atol=1e-4) +@test isapprox(jac_con1, I; atol = 1e-4) +@test isapprox(jac_con2, I; atol = 1e-4) ### TIME-EVENTS t_event = t_start + 1.1 -@test isapprox(affect_bb_check(x_no_event, t_event, 0), x_no_event; atol=1e-4) -@test isapprox(affect_nfmu_check(x_no_event, t_event, 0), x_no_event; atol=1e-4) +@test isapprox(affect_bb_check(x_no_event, t_event, 0), x_no_event; atol = 1e-4) +@test isapprox(affect_nfmu_check(x_no_event, t_event, 0), x_no_event; atol = 1e-4) jac_con1 = ForwardDiff.jacobian(x -> affect_bb_check(x, t_event, 0), x_no_event) jac_con2 = ForwardDiff.jacobian(x -> affect_nfmu_check(x, t_event, 0), x_no_event) -@test isapprox(jac_con1, I; atol=1e-4) -@test isapprox(jac_con2, I; atol=1e-4) +@test isapprox(jac_con1, I; atol = 1e-4) +@test isapprox(jac_con2, I; atol = 1e-4) jac_con1 = ReverseDiff.jacobian(x -> affect_bb_check(x, t_event, 0), x_no_event) jac_con2 = ReverseDiff.jacobian(x -> affect_nfmu_check(x, t_event, 0), x_no_event) -@test isapprox(jac_con1, I; atol=1e-4) -@test isapprox(jac_con2, I; atol=1e-4) +@test isapprox(jac_con1, I; atol = 1e-4) +@test isapprox(jac_con2, I; atol = 1e-4) jac_con1 = ReverseDiff.jacobian(t -> affect_bb_check(x_event_left, t[1], 0), [t_event]) jac_con2 = ReverseDiff.jacobian(t -> affect_nfmu_check(x_event_left, t[1], 0), [t_event]) ### -NUMEVENTS=4 +NUMEVENTS = 4 -for solver in solvers +for solver in solvers @info "Solver: $(solver)" global GRAVITY_SIGN # Solution (plain) GRAVITY_SIGN = -1 - losssum(p_net; sensealg=sensealg, solver=solver) + losssum(p_net; sensealg = sensealg, solver = solver) @test length(solution.events) == NUMEVENTS GRAVITY_SIGN = -1 - losssum_bb(p_net_bb; sensealg=sensealg, solver=solver) + losssum_bb(p_net_bb; sensealg = sensealg, solver = solver) @test events == NUMEVENTS # Solution FWD (FMU) GRAVITY_SIGN = -1 - grad_fwd_f = ForwardDiff.gradient(p -> losssum(p; sensealg=sensealg, solver=solver), p_net) + grad_fwd_f = + ForwardDiff.gradient(p -> losssum(p; sensealg = sensealg, solver = solver), p_net) @test length(solution.events) == NUMEVENTS # Solution FWD (right) GRAVITY_SIGN = -1 root = :Right - grad_fwd_r = ForwardDiff.gradient(p -> losssum_bb(p; sensealg=sensealg, root=root, solver=solver), p_net_bb) + grad_fwd_r = ForwardDiff.gradient( + p -> losssum_bb(p; sensealg = sensealg, root = root, solver = solver), + p_net_bb, + ) @test events == NUMEVENTS # Solution RWD (FMU) GRAVITY_SIGN = -1 - grad_rwd_f = ReverseDiff.gradient(p -> losssum(p; sensealg=sensealg, solver=solver), p_net) + grad_rwd_f = + ReverseDiff.gradient(p -> losssum(p; sensealg = sensealg, solver = solver), p_net) @test length(solution.events) == NUMEVENTS # Solution RWD (right) GRAVITY_SIGN = -1 root = :Right - grad_rwd_r = ReverseDiff.gradient(p -> losssum_bb(p; sensealg=sensealg, root=root, solver=solver), p_net_bb) + grad_rwd_r = ReverseDiff.gradient( + p -> losssum_bb(p; sensealg = sensealg, root = root, solver = solver), + p_net_bb, + ) @test events == NUMEVENTS # Ground Truth - grad_fin_r = FiniteDiff.finite_difference_gradient(p -> losssum_bb(p; sensealg=sensealg, root=:Right, solver=solver), p_net_bb, Val{:central}; absstep=1e-6) - grad_fin_f = FiniteDiff.finite_difference_gradient(p -> losssum(p; sensealg=sensealg, solver=solver), p_net, Val{:central}; absstep=1e-6) + grad_fin_r = FiniteDiff.finite_difference_gradient( + p -> losssum_bb(p; sensealg = sensealg, root = :Right, solver = solver), + p_net_bb, + Val{:central}; + absstep = 1e-6, + ) + grad_fin_f = FiniteDiff.finite_difference_gradient( + p -> losssum(p; sensealg = sensealg, solver = solver), + p_net, + Val{:central}; + absstep = 1e-6, + ) local atol = 1e-3 - + # check if finite differences match together - @test isapprox(grad_fin_f, grad_fin_r; atol=atol) - @test isapprox(grad_fin_f, grad_fwd_f; atol=atol) - @test isapprox(grad_fin_f, grad_rwd_f; atol=atol) - @test isapprox(grad_fwd_r, grad_rwd_r; atol=atol) + @test isapprox(grad_fin_f, grad_fin_r; atol = atol) + @test isapprox(grad_fin_f, grad_fwd_f; atol = atol) + @test isapprox(grad_fin_f, grad_rwd_f; atol = atol) + @test isapprox(grad_fwd_r, grad_rwd_r; atol = atol) # Jacobian Test - jac_fwd_r = ForwardDiff.jacobian(p -> mysolve_bb(p; sensealg=sensealg, solver=solver), p_net) + jac_fwd_r = ForwardDiff.jacobian( + p -> mysolve_bb(p; sensealg = sensealg, solver = solver), + p_net, + ) @test !any(isnan.(jac_fwd_r)) - jac_fwd_f = ForwardDiff.jacobian(p -> mysolve(p; sensealg=sensealg, solver=solver), p_net) + jac_fwd_f = + ForwardDiff.jacobian(p -> mysolve(p; sensealg = sensealg, solver = solver), p_net) @test !any(isnan.(jac_fwd_f)) - jac_rwd_r = ReverseDiff.jacobian(p -> mysolve_bb(p; sensealg=sensealg, solver=solver), p_net) + jac_rwd_r = ReverseDiff.jacobian( + p -> mysolve_bb(p; sensealg = sensealg, solver = solver), + p_net, + ) @test !any(isnan.(jac_rwd_r)) - jac_rwd_f = ReverseDiff.jacobian(p -> mysolve(p; sensealg=sensealg, solver=solver), p_net) + jac_rwd_f = + ReverseDiff.jacobian(p -> mysolve(p; sensealg = sensealg, solver = solver), p_net) @test !any(isnan.(jac_rwd_f)) # [TODO] why this?! - jac_rwd_r[2:end,:] = jac_rwd_r[2:end,:] .- jac_rwd_r[1:end-1,:] - jac_rwd_f[2:end,:] = jac_rwd_f[2:end,:] .- jac_rwd_f[1:end-1,:] - - jac_fin_r = FiniteDiff.finite_difference_jacobian(p -> mysolve_bb(p; sensealg=sensealg, solver=solver), p_net) - jac_fin_f = FiniteDiff.finite_difference_jacobian(p -> mysolve(p; sensealg=sensealg, solver=solver), p_net) + jac_rwd_r[2:end, :] = jac_rwd_r[2:end, :] .- jac_rwd_r[1:end-1, :] + jac_rwd_f[2:end, :] = jac_rwd_f[2:end, :] .- jac_rwd_f[1:end-1, :] + + jac_fin_r = FiniteDiff.finite_difference_jacobian( + p -> mysolve_bb(p; sensealg = sensealg, solver = solver), + p_net, + ) + jac_fin_f = FiniteDiff.finite_difference_jacobian( + p -> mysolve(p; sensealg = sensealg, solver = solver), + p_net, + ) ### local atol = 1e-3 - @test isapprox(jac_fin_f, jac_fin_r; atol=atol) - @test isapprox(jac_fin_f, jac_fwd_f; atol=atol) + @test isapprox(jac_fin_f, jac_fin_r; atol = atol) + @test isapprox(jac_fin_f, jac_fwd_f; atol = atol) # [ToDo] whyever... but this is not required to work (but: too much atol here!) - @test isapprox(jac_fin_f, jac_rwd_f; atol=0.5) + @test isapprox(jac_fin_f, jac_rwd_f; atol = 0.5) - @test isapprox(jac_fin_r, jac_fwd_r; atol=atol) - @test isapprox(jac_fin_r, jac_rwd_r; atol=atol) + @test isapprox(jac_fin_r, jac_fwd_r; atol = atol) + @test isapprox(jac_fin_r, jac_rwd_r; atol = atol) ### end diff --git a/test/train_modes.jl b/test/train_modes.jl index 3655b7b3..fe78507e 100644 --- a/test/train_modes.jl +++ b/test/train_modes.jl @@ -7,7 +7,7 @@ using Flux using DifferentialEquations: Tsit5, Rosenbrock23 import FMIFlux.FMIImport: fmi2FreeInstance! -import Random +import Random Random.seed!(5678); t_start = 0.0 @@ -19,20 +19,20 @@ tData = t_start:t_step:t_stop posData, velData, accData = syntTrainingData(tData) # load FMU for NeuralFMU -fmu = loadFMU("SpringFrictionPendulum1D", EXPORTINGTOOL, EXPORTINGVERSION; type=:ME) +fmu = loadFMU("SpringFrictionPendulum1D", EXPORTINGTOOL, EXPORTINGVERSION; type = :ME) # loss function for training -losssum = function(p) +losssum = function (p) global problem, X0, posData - solution = problem(X0; p=p, saveat=tData) + solution = problem(X0; p = p, saveat = tData) if !solution.success - return Inf + return Inf end #posNet = getState(solution, 1; isIndex=true) - velNet = getState(solution, 2; isIndex=true) - + velNet = getState(solution, 2; isIndex = true) + return Flux.Losses.mse(velNet, velData) # Flux.Losses.mse(posNet, posData) end @@ -41,7 +41,7 @@ vr = stringToValueReference(fmu, "mass.m") numStates = length(fmu.modelDescription.stateValueReferences) # some NeuralFMU setups -nets = [] +nets = [] global comp comp = nothing @@ -56,7 +56,7 @@ for handleEvents in [true, false] configstr = "$(config)" @testset "config: $(configstr[1:64])..." begin - + global problem, lastLoss, iterCB, comp fmu.executionConfig = config @@ -78,19 +78,21 @@ for handleEvents in [true, false] c1 = CacheLayer() c2 = CacheRetrieveLayer(c1) - net = Chain(states -> fmu(; x=states, dx_refs=:all), - dx -> c1(dx), - Dense(numStates, 16, tanh), - Dense(16, 1, identity), - dx -> c2(1, dx[1]) ) - + net = Chain( + states -> fmu(; x = states, dx_refs = :all), + dx -> c1(dx), + Dense(numStates, 16, tanh), + Dense(16, 1, identity), + dx -> c2(1, dx[1]), + ) + optim = OPTIMISER(ETA) solver = Tsit5() problem = ME_NeuralFMU(fmu, net, (t_start, t_stop), solver) @test problem != nothing - - solutionBefore = problem(X0; saveat=tData) + + solutionBefore = problem(X0; saveat = tData) if solutionBefore.success @test length(solutionBefore.states.t) == length(tData) @test solutionBefore.states.t[1] == t_start @@ -106,13 +108,19 @@ for handleEvents in [true, false] @info "Start-Loss for net: $lastLoss" lossBefore = losssum(p_net[1]) - FMIFlux.train!(losssum, problem, Iterators.repeated((), NUMSTEPS), optim; gradient=GRADIENT) + FMIFlux.train!( + losssum, + problem, + Iterators.repeated((), NUMSTEPS), + optim; + gradient = GRADIENT, + ) lossAfter = losssum(p_net[1]) @test lossAfter < lossBefore # check results - solutionAfter = problem(X0; saveat=tData) + solutionAfter = problem(X0; saveat = tData) if solutionAfter.success @test length(solutionAfter.states.t) == length(tData) @test solutionAfter.states.t[1] == t_start @@ -129,7 +137,7 @@ for handleEvents in [true, false] # end end - + end end end