Single Block with Splitting

Let's make this into two main GridGeneration functions: GridGeneration.SplitBlock(block, splitLocations, bndInfo, interInfo) and GridGeneration.SolveAllBlocks(metric, blocks, bndInfo, interInfo). And thus all we have to do is

  • Input single Tortuga block and desired split locations with the block
  • Split block into multi-block grid and update bndInfo and interInfo
  • Solve for the optimal distribution in each block

Split Block Algorithm

We want something along the lines:

splitLocations = [
    [ horizontal split indices... ],   # split along the x axis
    [ vertical split indices... ]      # split along the y axis
]

blocks, bndInfo, interInfo = GridGeneration.SplitBlock(block, splitLocations, bndInfo, interInfo)

where the SplitBlock function

function SplitBlock(block, splitLocations, bndInfo, interInfo)
    blocks = []
    horzSplits = [1, splitLocations[1]..., size(block, 2)]
    vertSplits = [1, splitLocations[2]..., size(block, 3)]

    parentni = size(block, 2)
    parentnj = size(block, 3)
    parentnk = 1

    blockId = 1
    blockBoundaries = []
    interfaces = []
    for j in 1:length(vertSplits)-1
        for i in 1:length(horzSplits)-1
            ni = horzSplits[i+1] - horzSplits[i] + 1
            nj = vertSplits[j+1] - vertSplits[j] + 1
            nk = 1
            
            subblock = block[:, horzSplits[i]:horzSplits[i+1], vertSplits[j]:vertSplits[j+1]]
            push!(blocks, subblock) 

            # get boundary info 
            blockInfo = Dict(
                "block" => blockId,
                "start" => (horzSplits[i], vertSplits[j]),
                "end" => (horzSplits[i+1], vertSplits[j+1]),
                "parentDims" => (parentni, parentnj, parentnk)
            )

            boundaries = GetTouchingBoundaries(blockInfo, bndInfo)
            append!(blockBoundaries, boundaries)
            
            # get interface info
            # if not at the end, look forward to the next block
            if i < length(horzSplits) - 1
                blockBId = blockId + 1
                # println("blockID: ", blockId, " next blockID: ", blockBId)
                push!(interInfo, Dict(
                    "blockA" => blockId, "start_blkA" => [ni,1,1], "end_blkA" => [ni,nj,nk],
                    "blockB" => blockBId, "start_blkB" => [1,1,1], "end_blkB" => [1,nj,nk],
                    "offset" => [0.0, 0.0, 0.0], "angle" => 0.0))
            end

            # if not at top, look up
            if j < length(vertSplits) - 1
                blockBId = blockId + length(horzSplits) - 1
                # println("blockID: ", blockId, " next blockID: ", blockBId)
                push!(interInfo, Dict(
                    "blockA" => blockId, "start_blkA" => [1,nj,1], "end_blkA" => [ni,nj,nk],
                    "blockB" => blockBId, "start_blkB" => [1,1,1], "end_blkB" => [ni,1,nk],
                    "offset" => [0.0, 0.0, 0.0], "angle" => 0.0))
            end

            blockId += 1
        end
    end
    updatedBndInfo = GroupBoundariesByName(blockBoundaries)
    updatedInterInfo = interfaces

    return blocks, updatedBndInfo, updatedInterInfo
end

with two helper functions GetTouchingBoundaries and GroupBoundariesByName. The former function will take in the block id and the starting and ending indices of the new subblock.

Here we handle the interface info by looking to the right and above.

Get Touching Boundaries - Idea 1

The function will check each of the edges of the child (newly created subblock) to see if they are on the boundary of their parent block (the original block being split). If the edge is on a boundary, inherit the boundary type from parent to child. We can check if the child is on the boundary of the parent by seeing if child start indices are equal to 1 or if the end indices are equal to size of the parent.

function GetTouchingBoundaries(blockInfo::Dict, bndInfo)
    # Parent extents
    N1, N2, N3 = blockInfo["parentDims"]

    # Child window in parent coordinates (inclusive)
    i0, j0 = blockInfo["start"]
    i1, j1 = blockInfo["end"]

    # Child-local sizes
    ni = i1 - i0 + 1
    nj = j1 - j0 + 1

    child_id = blockInfo["block"]
    out = Vector{Dict{String,Any}}()

    # LEFT side of child touches parent's LEFT boundary if i0 == 1
    if i0 == 1
        push!(out, Dict(
            "block" => child_id,
            "name"  => getBoundaryNameBySide(bndInfo; side=:left),
            "start" => [1, 1, 1],
            "end"   => [1, nj, 1],
        ))
    end

    # RIGHT side of child touches parent's RIGHT boundary if i1 == N1
    if i1 == N1
        push!(out, Dict(
            "block" => child_id,
            "name"  => getBoundaryNameBySide(bndInfo; side=:right),
            "start" => [ni, 1, 1],
            "end"   => [ni, nj, 1],
        ))
    end

    # BOTTOM side of child touches parent's BOTTOM boundary if j0 == 1
    if j0 == 1
        push!(out, Dict(
            "block" => child_id,
            "name"  => getBoundaryNameBySide(bndInfo; side=:bottom),
            "start" => [1, 1, 1],
            "end"   => [ni, 1, 1],
        ))
    end

    # TOP side of child touches parent's TOP boundary if j1 == N2
    if j1 == N2
        push!(out, Dict(
            "block" => child_id,
            "name"  => getBoundaryNameBySide(bndInfo; side=:top),
            "start" => [1, nj, 1],
            "end"   => [ni, nj, 1],
        ))
    end

    return out
end

with the helper function GetBoundaryNameBySide(bndInfo; side=:side) where we take advantage of the fact that bndInfo will only have information about the single inputted block. This function we can write as

function GetBoundaryNameBySide(bndInfo; side=:none)
    




end

Get Touching Boundaries - Idea 2 (Current)

Another option is to use this old existing function of mine:

function GetTouchingBoundaries(block::Dict, bndInfo)
    # Child window in parent (global) coordinates, inclusive
    block_i1, block_j1 = block["start"]
    block_i2, block_j2 = block["end"]
    blockId = block["block"]

    # Global -> child-local index maps
    to_local_i(i) = i - block_i1 + 1
    to_local_j(j) = j - block_j1 + 1

    touchingFaces = Vector{Dict{String,Any}}()

    for bnd in bndInfo
        name = bnd["name"]
        for face in bnd["faces"]
            faceStart = face["start"];  faceEnd = face["end"]
            i1, j1 = faceStart[1], faceStart[2]
            i2, j2 = faceEnd[1],   faceEnd[2]

            # Vertical face on parent's left/right boundary?
            if i1 == i2 && (i1 == block_i1 || i1 == block_i2)
                jlo = max(min(j1, j2), block_j1)
                jhi = min(max(j1, j2), block_j2)
                if jhi > jlo
                    push!(touchingFaces, Dict(
                        "name"  => name,
                        "block" => blockId,
                        "start" => [to_local_i(i1), to_local_j(jlo), 1],
                        "end"   => [to_local_i(i2), to_local_j(jhi), 1],
                    ))
                end

            # Horizontal face on parent's bottom/top boundary?
            elseif j1 == j2 && (j1 == block_j1 || j1 == block_j2)
                ilo = max(min(i1, i2), block_i1)
                ihi = min(max(i1, i2), block_i2)
                if ihi > ilo
                    push!(touchingFaces, Dict(
                        "name"  => name,
                        "block" => blockId,
                        "start" => [to_local_i(ilo), to_local_j(j1), 1],
                        "end"   => [to_local_i(ihi), to_local_j(j2), 1],
                    ))
                end
            end
        end
    end

    return touchingFaces
end

Group Boundaries by Name

Algorithm reformulates the list of boundaries into the Tortuga format, a list of dicts of dicts, where the first dict is the type of boundary and the sub dict are the blocks and edges.

function GroupBoundariesByName(faceList)
    boundaryGroups = Dict()

    for face in faceList
        name = face["name"]
        
        # Copy face and remove redundant name
        faceCopy = copy(face)
        delete!(faceCopy, "name")
        
        if !haskey(boundaryGroups, name)
            boundaryGroups[name] = []
        end
        push!(boundaryGroups[name], faceCopy)
    end

    groupedInfo = []
    for (name, faces) in boundaryGroups
        if !isempty(faces)
            push!(groupedInfo, Dict("name" => name, "faces" => faces))
        end
    end

    return groupedInfo
end

Split Examples

Inputting the following splits

splitLocations = [
    [ 300, 500 ],   # split along the x axis
    [ 40 ]      # split along the y axis
]

yields:

test

Solve All Blocks Algorithm

With the single block splitting working, let's move to creating an algorithm to solve the ODE across all the edges of the blocks. The challenging part here comes from the required consistency across the edges of the blocks. Consider blocks $B_1$ with left/right edges $e$ and $f$ and neighboring block $B_2$ with left/right edges $f$ and $g$. Now if we solve the ODE along $e,f,g$ and find the optimal number of points (let's denote this via $N_e, N_f,$ and $N_g$) are such that $N_e > N_f, N_g > N_f$ with $N_e \neq N_g$, then we will have an issue solving along $e$ with the optimal number of points. To fix this issue, we can take the global optimal number $N_\text{opt}$ of the pairs, that is $N_\text{opt} = \max(N_e, N_f, N_g)$ and solve the $e,f,$ and $g$ edges with $N_\text{opt}$.

Let's have an array blockInstructions of size of blocks that will contain the max number of points in each direction. Now we loop through the blocks and do the following:

  • for each dir
    • collect the neighboring block IDs using the interface info and save in array
    • Solve for the number of points for in the dir on both edges
    • Send the number of points to each neighbor with the following logic:
      • if incoming is greater than saved, overwrite with incoming, else keep the saved value. We can do this by looking at the correct spot in the blockInstructions using the neighboring block Id, and in the correct direction using the current dir.

Once we loop through all blocks, each block should now contain the max number of points to use in each direction. We can finally loop through all the blocks and solve the ODE with the correct number of points.

Algorithm

function SolveAllBlocks(metric, blocks, bndInfo, interInfo)
    blockDirOptN = similar(blocks)

    for i in 1:length(blockDirOptN)
        blockDirOptN[i] = [-1,-1]
    end

    # compute max number of points for each block
    for (blockId, block) in enumerate(blocks)
        for dir in 1:2  # 1 for horizontal, 2 for vertical
            blockNeighbors = GetNeighbors(blockId, interInfo, dir; include_start=true)
            # println("blockId: ", blockId, " dir: ", dir, " neighbors: ", blockNeighbors)

            #############
            # method 1
            #############
            if dir == 1  # horizontal
                left   = block[:, 1, :]
                right  = block[:, end, :]
                optN = GridGeneration.GetOptNEdgePair(left, right, metric)
            else  # vertical
                bottom   = block[:, :, 1]
                top  = block[:, :, end]
                optN = GridGeneration.GetOptNEdgePair(bottom, top, metric)
            end

            # Update the blockDirOptN with the max number of points
            for computeBlocks in blockNeighbors
                if blockDirOptN[computeBlocks][dir] < optN
                    blockDirOptN[computeBlocks][dir] = optN
                end
            end
        end
    end

    # solve all blocks using the optimal number
    computedBlocks = similar(blocks)
    p1 = plot()
    for (blockId, block) in enumerate(blocks)
        optNs = blockDirOptN[blockId]

        computedBlock, bndInfo, interInfo = GridGeneration.SolveBlockFixedN(block, bndInfo, interInfo, metric, optNs)

        computedBlocks[blockId] = computedBlock
    end

    # update the boundary information and interface information 
    GridGeneration.UpdateBndInfo!(bndInfo, computedBlocks; verbose=false)
    updatedInterInfo = GridGeneration.UpdateInterInfo(interInfo, computedBlocks; verbose=false)

    return computedBlocks, bndInfo, updatedInterInfo
end

We can find the neighboring cells in a direction using this recursive function:

function GetNeighbors(blockId::Int, interInfo, dir::Int; include_start::Bool=false)
    seen = Set{Int}()

    function visit(bid::Int)
        # already visited this block
        bid ∈ seen && return
        push!(seen, bid)

        for info in interInfo
            if info["blockA"] == bid || info["blockB"] == bid
                # Orientation is defined by the A-side span (shared for both sides)
                startA = info["start_blkA"]
                endA   = info["end_blkA"]
                is_vertical   = (startA[1] != endA[1])
                is_horizontal = (startA[2] != endA[2])

                if (dir == 2 && is_vertical) || (dir == 1 && is_horizontal)
                    other = (info["blockA"] == bid) ? info["blockB"] : info["blockA"]
                    visit(other)
                end
            end
        end
    end

    visit(blockId)

    return include_start ? collect(seen) : [b for b in seen if b != blockId]
end

Examples

Putting this all together and using our custom metric creator, we can use the code as follows:

Example 1

include("GridGeneration.jl")

#################
# Load in initial single block grid
#################
# load in the initial grid - no trailing edge 
initialGrid = GetAirfoilGrid(airfoilPath="examples/single_block_ns/airfoil/data/A-airfoil.txt", radius = 2)
# throw away trailing edge stuff
airfoilGrid = initialGrid[:, 101:end-100, :]
airfoil = airfoilGrid[:,:,1]

# define the boundary information
bndInfo = getBoundaryConditions(airfoilGrid)

# define interInfo
interInfo = Any[]

#################
# Make a custom metric
#################

metricFunc1 = make_getMetric(airfoil;
    A_airfoil = 50.0,  ℓ_airfoil = 0.5, p_airfoil = 2,   
    A_origin  = 500.0,  ℓ_origin  = 0.1, p_origin  = 10,   
    floor     = 1e-4,  origin_center=(1, -0.6),
profile   = :rational)  # or :gauss

metricFunc2 = make_getMetric(airfoil;
    A_airfoil = 0.0,  ℓ_airfoil = 0.5, p_airfoil = 2,   
    A_origin  = 700.0,  ℓ_origin  = 0.1, p_origin  = 10,   
    floor     = 1e-4,  origin_center=(0.5, 0.1),
profile   = :rational)  # or :gauss

metricFunc = (x,y) -> metricFunc1(x,y) .+ metricFunc2(x,y)



#################
# Add split locations 
#################


splitLocations = [
    [ 300, 500 ],   # split along the x axis
    [ 40, 80 ]      # split along the y axis
]

#################
## Split the blocks
#################

blocks, bndInfo, interInfo = GridGeneration.SplitBlock(airfoilGrid, splitLocations, bndInfo, interInfo)

#################
##### Run the solver on them
#################

blocks, bndInfo, interInfo = GridGeneration.SolveAllBlocks(metricFunc, blocks, bndInfo, interInfo)

which yields

example1