Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Adds a pertubation advection open boundary matching scheme #3977

Draft
wants to merge 32 commits into
base: main
Choose a base branch
from

Conversation

jagoosw
Copy link
Collaborator

@jagoosw jagoosw commented Dec 6, 2024

This PR adds a PertubationAdvection open boundary condition which assumes that there is some mean flow (i.e. prescribed in a channel or from a coarser resolution model) which is homogeneous on the boundary but can vary in time.

On the boundary, we approximate (for an x-boundary) to:

$$\frac{\partial u}{\partial t} \approx -U\frac{\partial u\prime}{\partial x},$$

where $u=u\prime+U$
I have chosen to take an explicit/backwards Euler step:

$$\frac{u\prime(t^{n+1}) - u\prime(t^n)}{\Delta t} + \frac{U\prime(t^{n+1}) - U\prime(t^n)}{\Delta t} \approx -U\frac{u\prime(x_b, t^{n+1}) - u\prime(x_{b-1}, t^{n+1})}{\Delta x},$$

because then we don't have to store any other information (i.e. if we did a forward step we would need $u\prime(x_{b-1}, t^n)$ ). This results in:

$$u(t^{n+1}) = \frac{u^n+\tilde{U}u(x_{b-1}, t^{n+1})}{1+\tilde{U}},$$

where $\tilde{U}=\min\left(1, U\Delta t / \Delta x\right)$. I have also added restoring to $U$ (i.e. damping $u\prime\to0$) in some timescale $\tau$ which can be different for inflow and outflow to allow the scheme to work when there is inflow, which results in:

$$u(t^{n+1}) = \frac{u^n+\tilde{U}u(x_{b-1}, t^{n+1}) + U\tilde{\tau}}{1+\tilde{U}+\tilde{\tau}},$$

where $\tilde{\tau}=\Delta t / \tau$.

This has the limitation that if we want to extend to have wall tangent mean velocity ($\partial_t u \approx -(\vec{U}\cdot\nabla) u$) then we either have to solve the whole boundary at once since every point is going to depend on its boundary neighbours, or we need to take a forward Euler step in which case we need the first interior point at the previous time step.

I think we probably will need to address this for real nesting cases at some point.

I will link a full write-up of how I derived this scheme when it's done. I have also only implemented in the x direction for testing and will do the others once I'm happy with it.

For now, it seems to be working okay (left plot vorticity, right plot x-velocity):

open.mp4

and the drag coefficient is ~1.4 at Re = 100 and I'm running a case at Re=1000 now.

I have also checked the Strouhal number which matches exactly to the expected values (~0.17).

A problem I think this scheme might have is that as the flow changes direction the assumption that the perturbation is small falls apart, which is maybe why it's been a bit unstable so far in an oscillating case.

@jagoosw jagoosw added feature 🌟 Something new and shiny science 🌊 Sometimes addictive, sometimes allergenic numerics 🧮 So things don't blow up and boil the lobsters alive labels Dec 6, 2024
@jagoosw
Copy link
Collaborator Author

jagoosw commented Dec 6, 2024

@tomchor, do you think something like this:

"""
drag(modell; bounding_box = (-1, 3, -2, 2), ν = 1e-3)
Returns the drag within the `bounding_box` computed by:
```math
\\frac{\\partial \\vec{u}}{\\partial t} + (\\vec{u}\\cdot\\nabla)\\vec{u}=-\\nabla P + \\nabla\\cdot\\vec{\\tau} + \\vec{F},\\newline
\\vec{F}_T=\\int_\\Omega\\vec{F}dV = \\int_\\Omega\\left(\\frac{\\partial \\vec{u}}{\\partial t} + (\\vec{u}\\cdot\\nabla)\\vec{u}+\\nabla P - \\nabla\\cdot\\vec{\\tau}\\right)dV,\\newline
\\vec{F}_T=\\int_\\Omega\\left(\\frac{\\partial \\vec{u}}{\\partial t}\\right)dV + \\oint_{\\partial\\Omega}\\left(\\vec{u}(\\vec{u}\\cdot\\hat{n}) + P\\hat{n} - \\vec{\\tau}\\cdot\\hat{n}\\right)dS,\\newline
\\F_u=\\int_\\Omega\\left(\\frac{\\partial u}{\\partial t}\\right)dV + \\oint_{\\partial\\Omega}\\left(u(\\vec{u}\\cdot\\hat{n}) - \\tau_{xx}\\right)dS + \\int_{\\partial\\Omega}P\\hat{x}\\cdot d\\vec{S},\\newline
F_u=\\int_\\Omega\\left(\\frac{\\partial u}{\\partial t}\\right)dV - \\int_{\\partial\\Omega_1} \\left(u^2 - 2\\nu\\frac{\\partial u}{\\partial x} + P\\right)dS + \\int_{\\partial\\Omega_2}\\left(u^2 - 2\\nu\\frac{\\partial u}{\\partial x}+P\\right)dS - \\int_{\\partial\\Omega_2} uvdS + \\int_{\\partial\\Omega_4} uvdS,
```
where the bounding box is ``\\Omega`` which is formed from the boundaries ``\\partial\\Omega_{1}``, ``\\partial\\Omega_{2}``, ``\\partial\\Omega_{3}``, and ``\\partial\\Omega_{4}``
which have outward directed normals ``-\\hat{x}``, ``\\hat{x}``, ``-\\hat{y}``, and ``\\hat{y}``
"""
function drag(model;
bounding_box = (-1, 3, -2, 2),
ν = 1e-3)
u, v, _ = model.velocities
uᶜ = Field(@at (Center, Center, Center) u)
vᶜ = Field(@at (Center, Center, Center) v)
xc, yc, _ = nodes(uᶜ)
i₁ = findfirst(xc .> bounding_box[1])
i₂ = findlast(xc .< bounding_box[2])
j₁ = findfirst(yc .> bounding_box[3])
j₂ = findlast(yc .< bounding_box[4])
uₗ² = Field(uᶜ^2, indices = (i₁, j₁:j₂, 1))
uᵣ² = Field(uᶜ^2, indices = (i₂, j₁:j₂, 1))
uvₗ = Field(uᶜ*vᶜ, indices = (i₁:i₂, j₁, 1))
uvᵣ = Field(uᶜ*vᶜ, indices = (i₁:i₂, j₂, 1))
∂₁uₗ = Field(∂x(uᶜ), indices = (i₁, j₁:j₂, 1))
∂₁uᵣ = Field(∂x(uᶜ), indices = (i₂, j₁:j₂, 1))
∂ₜuᶜ = Field(@at (Center, Center, Center) model.timestepper.Gⁿ.u)
∂ₜu = Field(∂ₜuᶜ, indices = (i₁:i₂, j₁:j₂, 1))
p = model.pressures.pNHS
∫∂ₓp = Field(∂x(p), indices = (i₁:i₂, j₁:j₂, 1))
a_local = Field(Integral(∂ₜu))
a_flux = Field(Integral(uᵣ²)) - Field(Integral(uₗ²)) + Field(Integral(uvᵣ)) - Field(Integral(uvₗ))
a_viscous_stress = 2ν * (Field(Integral(∂₁uᵣ)) - Field(Integral(∂₁uₗ)))
a_pressure = Field(Integral(∫∂ₓp))
return a_local + a_flux + a_pressure - a_viscous_stress
end

could belong in Oceanostics?

@jagoosw
Copy link
Collaborator Author

jagoosw commented Dec 6, 2024

I have also run the same case with different domain lengths (6m, 12m - shown, 18m and 30m) and they all produce very similar Cd and St

@tomchor
Copy link
Collaborator

tomchor commented Dec 6, 2024

@tomchor, do you think something like this:

"""
drag(modell; bounding_box = (-1, 3, -2, 2), ν = 1e-3)
Returns the drag within the `bounding_box` computed by:
```math
\\frac{\\partial \\vec{u}}{\\partial t} + (\\vec{u}\\cdot\\nabla)\\vec{u}=-\\nabla P + \\nabla\\cdot\\vec{\\tau} + \\vec{F},\\newline
\\vec{F}_T=\\int_\\Omega\\vec{F}dV = \\int_\\Omega\\left(\\frac{\\partial \\vec{u}}{\\partial t} + (\\vec{u}\\cdot\\nabla)\\vec{u}+\\nabla P - \\nabla\\cdot\\vec{\\tau}\\right)dV,\\newline
\\vec{F}_T=\\int_\\Omega\\left(\\frac{\\partial \\vec{u}}{\\partial t}\\right)dV + \\oint_{\\partial\\Omega}\\left(\\vec{u}(\\vec{u}\\cdot\\hat{n}) + P\\hat{n} - \\vec{\\tau}\\cdot\\hat{n}\\right)dS,\\newline
\\F_u=\\int_\\Omega\\left(\\frac{\\partial u}{\\partial t}\\right)dV + \\oint_{\\partial\\Omega}\\left(u(\\vec{u}\\cdot\\hat{n}) - \\tau_{xx}\\right)dS + \\int_{\\partial\\Omega}P\\hat{x}\\cdot d\\vec{S},\\newline
F_u=\\int_\\Omega\\left(\\frac{\\partial u}{\\partial t}\\right)dV - \\int_{\\partial\\Omega_1} \\left(u^2 - 2\\nu\\frac{\\partial u}{\\partial x} + P\\right)dS + \\int_{\\partial\\Omega_2}\\left(u^2 - 2\\nu\\frac{\\partial u}{\\partial x}+P\\right)dS - \\int_{\\partial\\Omega_2} uvdS + \\int_{\\partial\\Omega_4} uvdS,
```
where the bounding box is ``\\Omega`` which is formed from the boundaries ``\\partial\\Omega_{1}``, ``\\partial\\Omega_{2}``, ``\\partial\\Omega_{3}``, and ``\\partial\\Omega_{4}``
which have outward directed normals ``-\\hat{x}``, ``\\hat{x}``, ``-\\hat{y}``, and ``\\hat{y}``
"""
function drag(model;
bounding_box = (-1, 3, -2, 2),
ν = 1e-3)
u, v, _ = model.velocities
uᶜ = Field(@at (Center, Center, Center) u)
vᶜ = Field(@at (Center, Center, Center) v)
xc, yc, _ = nodes(uᶜ)
i₁ = findfirst(xc .> bounding_box[1])
i₂ = findlast(xc .< bounding_box[2])
j₁ = findfirst(yc .> bounding_box[3])
j₂ = findlast(yc .< bounding_box[4])
uₗ² = Field(uᶜ^2, indices = (i₁, j₁:j₂, 1))
uᵣ² = Field(uᶜ^2, indices = (i₂, j₁:j₂, 1))
uvₗ = Field(uᶜ*vᶜ, indices = (i₁:i₂, j₁, 1))
uvᵣ = Field(uᶜ*vᶜ, indices = (i₁:i₂, j₂, 1))
∂₁uₗ = Field(∂x(uᶜ), indices = (i₁, j₁:j₂, 1))
∂₁uᵣ = Field(∂x(uᶜ), indices = (i₂, j₁:j₂, 1))
∂ₜuᶜ = Field(@at (Center, Center, Center) model.timestepper.Gⁿ.u)
∂ₜu = Field(∂ₜuᶜ, indices = (i₁:i₂, j₁:j₂, 1))
p = model.pressures.pNHS
∫∂ₓp = Field(∂x(p), indices = (i₁:i₂, j₁:j₂, 1))
a_local = Field(Integral(∂ₜu))
a_flux = Field(Integral(uᵣ²)) - Field(Integral(uₗ²)) + Field(Integral(uvᵣ)) - Field(Integral(uvₗ))
a_viscous_stress = 2ν * (Field(Integral(∂₁uᵣ)) - Field(Integral(∂₁uₗ)))
a_pressure = Field(Integral(∫∂ₓp))
return a_local + a_flux + a_pressure - a_viscous_stress
end

could belong in Oceanostics?

Sure! Why not? :)

Comment on lines 38 to 64
@inline function _fill_east_halo!(j, k, grid, u, bc::PAOBC, loc::Tuple{Face, Any, Any}, clock, model_fields)
i = grid.Nx + 1

Δt = clock.last_stage_Δt

Δt = ifelse(isinf(Δt), 0, Δt)

Δx = xspacing(i, j, k, grid, loc...)

ūⁿ⁺¹ = getbc(bc, j, k, grid, clock, model_fields)

uᵢⁿ = @inbounds u[i, j, k]
uᵢ₋₁ⁿ⁺¹ = @inbounds u[i - 1, j, k]

U = max(0, min(1, Δt / Δx * ūⁿ⁺¹))

τ = ifelse(ūⁿ⁺¹ >= 0,
bc.classification.matching_scheme.outflow_timescale,
bc.classification.matching_scheme.inflow_timescale)


τ̃ = Δt / τ

uᵢⁿ⁺¹ = (uᵢⁿ + U * uᵢ₋₁ⁿ⁺¹ + ūⁿ⁺¹ * τ̃) / (1 + τ̃ + U)

@inbounds u[i, j, k] = uᵢⁿ⁺¹#ifelse(active_cell(i, j, k, grid), uᵢⁿ⁺¹, zero(grid))
end
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could implement everything using getindex() so that we implement it once (or at max twice; one for left and other for right BC) and can use it in three dimensions.

For example this

    uᵢⁿ     = @inbounds u[i, j, k]
    uᵢ₋₁ⁿ⁺¹ = @inbounds u[i - 1, j, k]

would become

    uᵢⁿ     = @inbounds getindex(u, boundary_indices...)
    uᵢ₋₁ⁿ⁺¹ = @inbounds getindex(u, one_off_boundary_indices...)

This would avoid errors when transcribing to other dimensions (I remember catching a couple for the flat extrapolation matching scheme).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I was trying to think of a clean way to do this

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attempted this now (but I've still not written out the other directions in case we want to change things first)

@jagoosw jagoosw force-pushed the jsw/pertubation-advection-obc branch from 0e645c5 to 32cd354 Compare December 6, 2024 20:11
U = 1
D = 1.
resolution = D / 40
```math
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the pressure contribution shouldn't be included here since G^n doesn't include it right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see the line you're referring to

@jagoosw jagoosw force-pushed the jsw/pertubation-advection-obc branch from afecfb3 to e66f3d1 Compare December 6, 2024 20:43
@glwagner
Copy link
Member

glwagner commented Dec 6, 2024

There's a typo in your top equation right? I think it should be $\partial_t (u + U) = U \partial_x u$.

@glwagner
Copy link
Member

glwagner commented Dec 6, 2024

A problem I think this scheme might have is that as the flow changes direction the assumption that the perturbation is small falls apart, which is maybe why it's been a bit unstable so far in an oscillating case.

What do you mean? What is the consequence of this approximation?

@jagoosw
Copy link
Collaborator Author

jagoosw commented Dec 6, 2024

What do you mean? What is the consequence of this approximation?

I was thinking about this more and really to remain valid then in :

$$\partial_t (u\prime+U)\approx u\prime\partial_xu\prime + U\partial_xu\prime + (U - u\prime)/\tau$$

as $U\to0$ then we need $u\prime\partial_xu\prime\ll u\prime/\tau$ to still be able to throw away the first term, so I think its actually okay as long as you have the relaxation.

I found a typo in the west boundary which might be why the oscillating case wasn't working, rerunning now

@jagoosw
Copy link
Collaborator Author

jagoosw commented Dec 6, 2024

There's a typo in your top equation right? I think it should be ∂ t ( u + U ) = U ∂ x u .
Yeah thanks for catching!

@glwagner
Copy link
Member

glwagner commented Dec 6, 2024

What do you mean? What is the consequence of this approximation?

I was thinking about this more and really to remain valid then in :

as U → 0 then we need u ′ ∂ x u ′ ≪ u ′ / τ to still be able to throw away the first term, so I think its actually okay as long as you have the relaxation.

I found a typo in the west boundary which might be why the oscillating case wasn't working, rerunning now

Okay, I agree with you. You can also analyze the update formula / backward Euler step in the limit U -> 0. It doesn't look like it needs to be regularized in any way in that case though, it looks ok.

@tomchor
Copy link
Collaborator

tomchor commented Dec 9, 2024

Is there a reference for this method? If there is, please include it on the methods docstring. If not, it'd be good to include a brief explanation like the one at the top comment in the dosctring. I don't think this radiation method is very well known.

@jagoosw
Copy link
Collaborator Author

jagoosw commented Dec 12, 2024

I started implementing some code to return the wall-normal velocity which can be put in as the U in this scheme, and I'm not exactly sure where to put it. I have it in the boundary conditions module but it would be very helpful to have it after fields and abstract operations.

If its in boundary conditions I have to make a struct and write a kernel etc. but otherwise I think it can just be something like:

= sum(u * A / sum(A, dims = (2, 3)), dims = (2, 3))

etc.

@jagoosw
Copy link
Collaborator Author

jagoosw commented Dec 12, 2024

I've pushed the wall normal velocity thing in src/ and we can discuss where is appropriate for it

Comment on lines +24 to +31
\\frac{\\partial \\vec{u}}{\\partial t} + (\\vec{u}\\cdot\\nabla)\\vec{u}=-\\nabla P + \\nabla\\cdot\\vec{\\tau} + \\vec{F},\\newline
\\vec{F}_T=\\int_\\Omega\\vec{F}dV = \\int_\\Omega\\left(\\frac{\\partial \\vec{u}}{\\partial t} + (\\vec{u}\\cdot\\nabla)\\vec{u}+\\nabla P - \\nabla\\cdot\\vec{\\tau}\\right)dV,\\newline
\\vec{F}_T=\\int_\\Omega\\left(\\frac{\\partial \\vec{u}}{\\partial t}\\right)dV + \\oint_{\\partial\\Omega}\\left(\\vec{u}(\\vec{u}\\cdot\\hat{n}) + P\\hat{n} - \\vec{\\tau}\\cdot\\hat{n}\\right)dS,\\newline
\\F_u=\\int_\\Omega\\left(\\frac{\\partial u}{\\partial t}\\right)dV + \\oint_{\\partial\\Omega}\\left(u(\\vec{u}\\cdot\\hat{n}) - \\tau_{xx}\\right)dS + \\int_{\\partial\\Omega}P\\hat{x}\\cdot d\\vec{S},\\newline
F_u=\\int_\\Omega\\left(\\frac{\\partial u}{\\partial t}\\right)dV - \\int_{\\partial\\Omega_1} \\left(u^2 - 2\\nu\\frac{\\partial u}{\\partial x} + P\\right)dS + \\int_{\\partial\\Omega_2}\\left(u^2 - 2\\nu\\frac{\\partial u}{\\partial x}+P\\right)dS - \\int_{\\partial\\Omega_2} uvdS + \\int_{\\partial\\Omega_4} uvdS,
```
where the bounding box is ``\\Omega`` which is formed from the boundaries ``\\partial\\Omega_{1}``, ``\\partial\\Omega_{2}``, ``\\partial\\Omega_{3}``, and ``\\partial\\Omega_{4}``
which have outward directed normals ``-\\hat{x}``, ``\\hat{x}``, ``-\\hat{y}``, and ``\\hat{y}``
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we re-write this as unicode? I can't understand at all the equations without rendering the latex code

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess so, docs should render latex in docstrings but since this isn't going to ever be rendered by Documenter its probably makes more sense in unicode

@jagoosw jagoosw force-pushed the jsw/pertubation-advection-obc branch 3 times, most recently from f4cba4e to be2f2ad Compare December 12, 2024 21:30
@jagoosw jagoosw force-pushed the jsw/pertubation-advection-obc branch from be2f2ad to 3257eec Compare December 12, 2024 21:31
Copy link
Collaborator

@tomchor tomchor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One comment is that I think the name is a bit ambiguous. There are several open boundary methods that have been developed that (in addition to outflow) include some sort of added perturbation for inflows to allow for faster turbulence development. When I read PerturbationAdvection I think of those methods. But if I understand the code correctly the "perturbation" here refers only to perturbations in the outflow, no?

Maybe we could name it PerturbationOutflow to avoid confusion?

@jagoosw
Copy link
Collaborator Author

jagoosw commented Dec 15, 2024

The results of simple cases seem more correct doing a forward step + BoundaryAdjacentMean, so I'm going to try pushing that and testing it against the headland case. You can currently choose forward or backward but we might want to just go with the forward version

@jagoosw
Copy link
Collaborator Author

jagoosw commented Dec 15, 2024

I'm also pulling the changes from #3937 in but the diff with main will go away when that PR is merged

@jagoosw
Copy link
Collaborator Author

jagoosw commented Dec 15, 2024

One comment is that I think the name is a bit ambiguous. There are several open boundary methods that have been developed that (in addition to outflow) include some sort of added perturbation for inflows to allow for faster turbulence development. When I read PerturbationAdvection I think of those methods. But if I understand the code correctly the "perturbation" here refers only to perturbations in the outflow, no?

Maybe we could name it PerturbationOutflow to avoid confusion?

I'm not exactly sure, if there was a point inflowing but the mean flow is out it still gets advected. Also when you add inflow pertubation its not advecting anything right? You could also achieve that by setting e.g. U(x, y, z, t) = U0(t) + rand()

@tomchor
Copy link
Collaborator

tomchor commented Dec 15, 2024

One comment is that I think the name is a bit ambiguous. There are several open boundary methods that have been developed that (in addition to outflow) include some sort of added perturbation for inflows to allow for faster turbulence development. When I read PerturbationAdvection I think of those methods. But if I understand the code correctly the "perturbation" here refers only to perturbations in the outflow, no?
Maybe we could name it PerturbationOutflow to avoid confusion?

I'm not exactly sure, if there was a point inflowing but the mean flow is out it still gets advected. Also when you add inflow pertubation its not advecting anything right? You could also achieve that by setting e.g. U(x, y, z, t) = U0(t) + rand()

I thought the inflow was also advecting stuff into the domain. But it would advect a mostly-laminar flow. In any case, imo it would be nice to change the name to something that would differentiate from methods that add a perturbation to inflow velocities in order to speed up transition to turbulence. Do you have suggestions?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature 🌟 Something new and shiny numerics 🧮 So things don't blow up and boil the lobsters alive science 🌊 Sometimes addictive, sometimes allergenic
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants