Proper orthogonal decomposition (POD)
In this example, we will demonstrate the use of proper orthogonal decomposition (POD) for decomposing a flow field into basis modes.
using ILMPostProcessing
using Plots
Get the flow field data
First, we need some flow field data to analyze. For this purpose, we will use ViscousFlow.jl to get snapshots of the flow for a flat plate at 30 degrees angle of attack at Reynolds number 100.
using ViscousFlow
my_params = Dict()
my_params["Re"] = 100
my_params["freestream speed"] = 1.0 # in x-dir
my_params["freestream angle"] = 0.0 # relative to horizontal
xlim = (-1.0, 5.0)
ylim = (-2.0, 2.0)
my_params["grid Re"] = 4.0
g = setup_grid(xlim, ylim, my_params)
Δs = surface_point_spacing(g, my_params)
body = Plate(1.0, Δs)
cent = [0.5,0.0]
α = -30π/180
X = MotionTransform(cent, α)
joint = Joint(X)
m = RigidBodyMotion(joint, body)
x = init_motion_state(body, m)
update_body!(body, x, m)
sys = viscousflow_system(g, body, phys_params = my_params, motions = m);
u0 = init_sol(sys)
tspan = (0.0, 20.0)
integrator = init(u0, tspan, sys)
# Solve to 10 convective time units
step!(integrator, 10)
Assemble snapshots of the velocity field from the solution data
Here, we make use of the capability of the velocity
function to generate an array of velocity fields at a range of times. We will save every 5th time step in this array.
sol = integrator.sol
tpod = sol.t[2:5:end]
X = velocity(sol, sys, tpod);
Perform the POD
The POD is simply performed with the PODModes
function. This provides a structure containing the modes (phi
), the expansion coefficients (a
), and the modal energies (lambda
). By default, PODModes
retains 99% of the energy. This can be changed with the optional argument tolerance
.
modes = pod(X);
The a
array is of size $N_t \times r$, where $N_t$ is the number of time values, and $r$ is the number of modes. The modes are ordered from highest energy to lowest energy.
modes.a
101×10 Matrix{Float64}:
0.139488 0.00736536 -0.0768184 … -0.0256712 -0.0123668
0.138101 0.00750601 -0.0758466 -0.0178839 -0.00779976
0.136146 0.00779498 -0.075173 -0.00937233 -0.00320754
0.133823 0.00853107 -0.0748546 -0.00108265 0.000901443
0.131309 0.00992053 -0.0747563 0.00607686 0.00419214
0.128777 0.0120599 -0.0746398 … 0.0115034 0.0065311
0.12639 0.0149328 -0.0742144 0.014987 0.00791158
0.124287 0.0184193 -0.0731801 0.0166409 0.00834968
0.122573 0.0223155 -0.0712662 0.0167553 0.00783233
0.121308 0.0263596 -0.0682605 0.0156501 0.00634241
⋮ ⋱
-0.109734 -0.0348703 -0.0499863 -0.0118352 0.00881401
-0.111545 -0.0231225 -0.0565372 -0.00877868 0.0102615
-0.112448 -0.0104652 -0.0612092 -0.00464563 0.0102618
-0.112411 0.0028515 -0.0638602 … 0.000203529 0.00879315
-0.111426 0.0165666 -0.064389 0.00535478 0.00599874
-0.1095 0.0304125 -0.0627401 0.010392 0.00215723
-0.106666 0.0441202 -0.0589079 0.0149453 -0.00236466
-0.102978 0.0574227 -0.0529397 0.0187283 -0.00716857
-0.0985084 0.070059 -0.0449373 … 0.0215588 -0.0118793
In this case, 7 modes were retained, at 51 times.
If we wanted to re-assemble the modes and coefficients to recover the flow at some time instant, we could use the mapreduce
function, e.g.,
vel_assemble = mapreduce((aj, phi_j) -> aj .* phi_j, +, modes.a[end,:], modes.phi) + modes.Xmean
CartesianGrids.Edges{CartesianGrids.Primal, 160, 104, Float64, Vector{Float64}} data
u (in grid orientation)
103×160 Matrix{Float64}:
1.02786 1.02968 1.03141 1.03312 … 0.987704 0.986192 0.985118
1.02696 1.02874 1.03049 1.03222 0.986571 0.985142 0.983924
1.02595 1.0277 1.02945 1.03119 0.985336 0.983939 0.98266
1.02485 1.02658 1.02832 1.03007 0.98399 0.982594 0.981281
1.02366 1.02539 1.02712 1.02886 0.982521 0.981118 0.979782
1.0224 1.02411 1.02584 1.02758 … 0.980927 0.979515 0.978164
1.02106 1.02276 1.02448 1.02621 0.979204 0.977787 0.976428
1.01964 1.02133 1.02303 1.02476 0.97735 0.975934 0.974573
1.01813 1.0198 1.0215 1.02321 0.975366 0.973954 0.972599
1.01653 1.01819 1.01987 1.02157 0.97325 0.971849 0.970506
⋮ ⋱
0.951463 0.95149 0.951528 0.951578 1.0403 1.03811 1.03584
0.95317 0.953264 0.953368 0.953482 … 1.03951 1.03745 1.03531
0.954834 0.954996 0.955165 0.955342 1.0387 1.03676 1.03474
0.956457 0.956686 0.95692 0.95716 1.03788 1.03606 1.03414
0.958035 0.958334 0.958635 0.958936 1.03705 1.03533 1.03352
0.959567 0.959941 0.96031 0.960669 1.03621 1.0346 1.03289
0.961047 0.961503 0.961939 0.96235 … 1.03537 1.03387 1.03224
0.962455 0.963005 0.963497 0.963945 1.03452 1.03315 1.03159
0.963738 0.964395 0.96491 0.965368 1.03364 1.03244 1.03091
v (in grid orientation)
104×159 Matrix{Float64}:
0.0752184 0.0755549 0.075833 … -0.0818985 -0.0806515 -0.0790153
0.0770362 0.0772896 0.0775429 -0.0832243 -0.0821639 -0.0800897
0.0788121 0.0790406 0.0792777 -0.0845858 -0.0835934 -0.0813074
0.0805632 0.0807891 0.0810229 -0.0859737 -0.0849907 -0.0825866
0.0822974 0.0825304 0.0827697 -0.0873806 -0.0863863 -0.0839
0.0840187 0.0842636 0.0845142 … -0.0888015 -0.0877898 -0.0852362
0.0857289 0.0859886 0.0862544 -0.0902324 -0.0892017 -0.0865875
0.0874281 0.0877046 0.0879885 -0.0916688 -0.0906184 -0.0879467
0.0891155 0.0894103 0.0897146 -0.0931061 -0.0920353 -0.0893069
0.090789 0.0911036 0.0914303 -0.0945392 -0.093447 -0.0906617
⋮ ⋱
-0.0316417 -0.0320594 -0.0325492 … 0.0359338 0.0372454 0.0395536
-0.031547 -0.0319554 -0.0324353 0.0339406 0.03519 0.0374097
-0.0313856 -0.031786 -0.032258 0.032066 0.0332539 0.0353837
-0.0311566 -0.0315511 -0.0320186 0.0303021 0.03143 0.0334677
-0.0308577 -0.0312498 -0.0317179 0.0286428 0.0297128 0.0316549
-0.0304842 -0.030881 -0.0313584 … 0.0270831 0.0281006 0.0299387
-0.0300284 -0.0304453 -0.0309466 0.0256185 0.0265982 0.0283116
-0.0294786 -0.0299525 -0.030499 0.0242424 0.0252262 0.0267573
-0.0288208 -0.0294384 -0.0300405 0.0229449 0.0240227 0.0252288
In this last line, modes.a[end,:]
obtains the expansion coefficients at the last time available.
Let's print the first mode, and the corresponding history of the modal coefficient in the decomposition
plot(layout=[2;1],plot(modes.phi[1].u,sys,title="u"),
plot(modes.phi[1].v,sys,title="v"),
plot(tpod,modes.a[:,1],xlim=(0,Inf),xlabel="\$t\$",ylabel="\$a_1(t)\$"))
The energy associated with this mode is
modes.lambda[1]
0.719789956020543
Now let's print the $r$th mode, and the history of the coefficient in the decomposition
plot(layout=[2;1],plot(modes.phi[end].u,sys,title="u"),
plot(modes.phi[end].v,sys,title="v"),
plot(tpod,modes.a[:,end],xlim=(0,Inf),xlabel="\$t\$",ylabel="\$a_r(t)\$"))
The energy associated with this mode is
modes.lambda[end]
0.006852880686168134
POD functions
ILMPostProcessing.pod
— Functionpod(X::Vector{T}[; tolerance=0.99])
Calculate the POD modes and associated time-varying coefficients from an array of snapshot data X
. This X
plays the role of a snapshot matrix, whose columns are snapshots of the data. However, it is actually to be stored as a type Vector{T}
where T<:GridData
. It can be generated with a function like velocity(sol,sys,trange)
, where sol
is a ODESolution
, sys
is an ILMSystem
, and trange
is an array of times, e.g., trange=range(1,10,length=100)
. The number of POD modes retained in the decomposition is set by tolerance
: this specifies the fraction of the total energy to keep, and defaults to 99 percent.
The output of PODModes
is a structure with the following fields
Xmean
: temporal mean of the data. typeT
Xnorm
: originalX
vector with mean removed. Each element is of typeT
phi
: vector of POD modes. Each element is of typeT
a
: matrix of POD coefficients. Number of columns is same as number of entries inphi
. Columnk
constitutes the time-varying coefficient for modek
inphi
.lambda
: vector of modal energies, arranged in decreasing order, corresponding to the modes inphi
psi
: matrix of
pod(X::Vector{T},r::Int)
Perform POD on snapshot data X
and truncate to r
modes
This page was generated using Literate.jl.