Isogeometric analysis calculations

Experimental

IGA support is experimental and its API may change.

This page solves a scalar heat-conduction problem on a NURBS patch. The assembly flow is the same as in Finite element calculations: quadrature points are stored in the particle array, update! fills the basis values and physical quadrature measures, and @P2G_Matrix assembles the global matrix.

The difference is the geometric and discrete basis. An IGAMesh uses NURBS control points as grid entries, and the unknown grid.u stores control coefficients, not nodal values. A field value inside the patch is evaluated by the same rational basis functions that describe the geometry.

Quarter Annulus

Problem Setting

Solve

\[-\Delta u = 0 \quad \text{in } \Omega, \qquad u = 1 \quad \text{on } r = r_i, \qquad u = 0 \quad \text{on } r = r_o.\]

The radial sides are left as natural boundaries:

\[\nabla u \cdot \bm{n} = 0 \quad \text{on } \theta = 0,\ \theta = \pi/2.\]

This problem has the analytic solution

\[u(r) = \frac{\log(r_o / r)}{\log(r_o / r_i)}.\]

The circular boundaries are represented exactly by rational quadratic NURBS curves.

Building the NURBS Patch

Create the quarter annulus from two circular arcs and two radial line segments. Tesserae.NURBS.coons_patch blends those four boundary curves into one tensor-product surface. The first parametric direction follows the angle, and the second direction follows the radius.

using Tesserae
using LinearAlgebra
NURBS = Tesserae.NURBS

ri = 1.0
ro = 2.0
center = Vec(0.0, 0.0)

inner_curve = NURBS.arc(center, ri, 0.0, π / 2)
outer_curve = NURBS.arc(center, ro, 0.0, π / 2)
radial0_curve = NURBS.line(Vec(ri, 0.0), Vec(ro, 0.0))
radial1_curve = NURBS.line(Vec(0.0, ri), Vec(0.0, ro))

surface = NURBS.coons_patch(inner_curve, outer_curve, radial0_curve, radial1_curve)
surface = NURBS.elevate(surface, (NURBS.quadratic, NURBS.quadratic))
surface = NURBS.refine(surface, (20, 8))

mesh = IGAMesh(surface)

inner_boundary = boundaries(mesh, 1, 2, -1)
outer_boundary = boundaries(mesh, 1, 2, +1)

plot_patch(surface)
Example block output
Plot helpers
import Plots

function plot_line!(plt, points; kwargs...)
    xs = [point[1] for point in points]
    ys = [point[2] for point in points]
    Plots.plot!(plt, xs, ys; kwargs...)
end

function patch_line(surface, direction, value; n=120)
    other_direction = 3 - direction
    lower, upper = Tesserae.NURBS.domain(surface, other_direction)
    if direction == 1
        [Tesserae.NURBS.evaluate(surface, Vec(value, η)) for η in range(lower, upper; length=n)]
    else
        [Tesserae.NURBS.evaluate(surface, Vec(ξ, value)) for ξ in range(lower, upper; length=n)]
    end
end

function patch_curve(surface, direction, side; n=120)
    lower, upper = Tesserae.NURBS.domain(surface, direction)
    fixed = side == -1 ? lower : upper
    patch_line(surface, direction, fixed; n)
end

function knot_values(surface, direction)
    lower, upper = Tesserae.NURBS.domain(surface, direction)
    values = unique(Tesserae.NURBS.knots(surface, direction))
    filter(ξ -> lower ≤ ξ ≤ upper, values)
end

function plot_knot_lines!(plt, surface; n=80)
    for ξ in knot_values(surface, 1)
        plot_line!(plt, patch_line(surface, 1, ξ; n); color=:gray, linestyle=:dash, linewidth=0.6, label=false)
    end
    for η in knot_values(surface, 2)
        plot_line!(plt, patch_line(surface, 2, η; n); color=:gray, linestyle=:dash, linewidth=0.6, label=false)
    end
    plt
end

function plot_patch(surface)
    plt = Plots.plot(;
        aspect_ratio=:equal,
        xlabel="x",
        ylabel="y",
        size=(560, 420),
        framestyle=:box,
        legend=:outertopright,
    )

    plot_knot_lines!(plt, surface)
    plot_line!(plt, patch_curve(surface, 2, -1); color=:crimson, linewidth=2.5, label="inner")
    plot_line!(plt, patch_curve(surface, 2, +1); color=:steelblue, linewidth=2.5, label="outer")
    plot_line!(plt, patch_curve(surface, 1, -1); color=:black, linewidth=1.4, label="radial")
    plot_line!(plt, patch_curve(surface, 1, +1); color=:black, linewidth=1.4, label=false)
    plt
end

Quadrature Data

From the transfer macros, the objects have the same roles as in MPM and FEM: grid stores the control-point fields, gauss_points is the point array, and weights connects each Gauss point to the active control points.

GridProp = @NamedTuple begin
    x :: Vec{2, Float64}
    u :: Float64
    f :: Float64
end

GaussProp = @NamedTuple begin
    x :: Vec{2, Float64}
    u :: Float64
    V :: Float64
end

grid = generate_grid(GridProp, mesh)
rule = generate_quadrature_rule(basis(mesh))
gauss_points = generate_particles(GaussProp, mesh, rule)
weights = generate_basis_weights(mesh, size(gauss_points); name=Val(:N))

update!(weights, gauss_points, mesh; measure=gauss_points.V)
9×189 BasisWeightArray: 
  Basis: IGABasis{2, Tuple{Quadratic, Quadratic}}((Quadratic(), Quadratic()))
  Basis values: N, ∇N

After update!, N[ip] and ∇N[ip] are the rational basis value and physical gradient associated with local support index ip. The value V[p] is the quadrature weight multiplied by the physical Jacobian measure.

Assembly

The weak form only has the stiffness term:

\[K_{ij} = \int_\Omega \nabla N_i \cdot \nabla N_j \, d\Omega \approx \sum_p \nabla N_i(\bm{x}_p) \cdot \nabla N_j(\bm{x}_p) V_p.\]

As in the FEM example, the global matrix is assembled with @P2G_Matrix:

K = create_sparse_matrix(mesh; ndofs=1)

@P2G_Matrix grid=>(i,j) gauss_points=>p weights=>(ip,jp) begin
    K[i,j] = @∑ ∇N[ip] ⋅ ∇N[jp] * V[p]
end

Boundary Conditions

The inner and outer circular boundaries use Dirichlet data. Because IGA uses control coefficients, constant boundary data are imposed by setting all control coefficients on that boundary to the constant value. The radial sides require no extra assembly for the homogeneous Neumann condition.

inner_nodes = supportnodes(inner_boundary)
outer_nodes = supportnodes(outer_boundary)
boundary_nodes = union(inner_nodes, outer_nodes)

grid.u[inner_nodes] .= 1.0
grid.u[outer_nodes] .= 0.0

dofmask = trues(1, size(grid)...)
dofmask[1, boundary_nodes] .= false

free = DofMap(dofmask)
rhs = grid.f - K * grid.u
free(grid.u) .= Symmetric(extract(K, free)) \ Array(free(rhs));

Verification

The analytic solution depends only on the physical radius. Transfer the solved control coefficients to the existing Gauss points and compute the maximum absolute error over those points.

exact_solution(x) = log(ro / norm(x)) / log(ro / ri)

@G2P grid=>i gauss_points=>p weights=>ip begin
    u[p] = @∑ N[ip] * u[i]
end

max_error = maximum(abs, gauss_points.u .- exact_solution.(gauss_points.x))

round(max_error; sigdigits=3)
2.39e-5
plot_solution(surface, grid.u)
Example block output
Solution helpers
function plot_solution(surface, u)
    solution = Tesserae.NURBS.ControlNet(
        surface.axes,
        map(value -> Vec(value), reshape(u, size(surface.points))),
        surface.weights,
    )
    ξs = knot_values(surface, 1)
    ηs = knot_values(surface, 2)
    shapes = Plots.Shape[]
    values = Float64[]

    for j in 1:(length(ηs)-1), i in 1:(length(ξs)-1)
        corners = (
            Vec(ξs[i], ηs[j]),
            Vec(ξs[i+1], ηs[j]),
            Vec(ξs[i+1], ηs[j+1]),
            Vec(ξs[i], ηs[j+1]),
        )
        points = map(ξ -> Tesserae.NURBS.evaluate(surface, ξ), corners)
        push!(shapes, Plots.Shape([point[1] for point in points], [point[2] for point in points]))
        push!(values, sum(only(Tesserae.NURBS.evaluate(solution, ξ)) for ξ in corners) / length(corners))
    end

    plt = Plots.plot(
        shapes;
        fill_z=permutedims(values),
        color=:viridis,
        linecolor=:white,
        linewidth=0.05,
        colorbar_title="u",
        aspect_ratio=:equal,
        xlabel="x",
        ylabel="y",
        label=false,
        size=(560, 420),
        framestyle=:box,
    )
    plot_line!(plt, patch_curve(surface, 2, -1); color=:black, linewidth=1.5, label=false)
    plot_line!(plt, patch_curve(surface, 2, +1); color=:black, linewidth=1.5, label=false)
    plot_line!(plt, patch_curve(surface, 1, -1); color=:black, linewidth=1.5, label=false)
    plot_line!(plt, patch_curve(surface, 1, +1); color=:black, linewidth=1.5, label=false)
    plt
end

API

IGA

Tesserae.IGAPatchType
IGAPatch(degrees, knot_vectors, controlpoint_ids)

Define a tensor-product IGA patch.

source
Tesserae.IGAMeshType
IGAMesh(patches, controlpoints[, weights])
IGAMesh(mesh::CartesianMesh; degree, weights=nothing)

Define an IGA mesh from patches and control points.

source
Tesserae.boundariesFunction
boundaries(mesh, patch_id)
boundaries(mesh, patch_id, direction, side)

Extract boundary IGA meshes from a parent IGA mesh. The boundary patch keeps the parent control-point ids, so boundary assembly targets the same global DOFs.

source
Tesserae.update!Method
update!(weights::BasisWeightArray{<:IGABasis}, points::QuadraturePoints, fieldmesh::IGAMesh; geometry=fieldmesh, measure=nothing, normal=nothing)

Evaluate the basis of fieldmesh using geometry for the physical mapping. The meshes may have different degrees and control-point numbering, but must have corresponding patches with the same nonzero knot-span intervals. measure and normal have the same roles as in the FEM method.

source

NURBS

Types

Tesserae.NURBS.BSplineAxisType
BSplineAxis(degree::Int, knot_vector::Vector) -> BSplineAxis

One parametric direction of a B-spline basis, defined by a polynomial degree and a knot_vector.

source
Tesserae.NURBS.ControlNetType
ControlNet(axes::Tuple{Vararg{BSplineAxis}}, points::Array[, weights::Array])

Tensor-product spline control net used while building NURBS geometry.

source

Queries

Tesserae.NURBS.degreeFunction
degree(axis::BSplineAxis) -> Int

Return the polynomial degree of a B-spline axis.

source
degree(net::ControlNet, direction::Int) -> Int

Return the polynomial degree in one parametric direction.

source
Tesserae.NURBS.knotsFunction
knots(axis::BSplineAxis) -> Vector

Return the knot vector of a B-spline axis.

source
knots(net::ControlNet, direction::Int) -> Vector

Return the knot vector in one parametric direction.

source
Tesserae.NURBS.domainFunction
domain(axis::BSplineAxis) -> Tuple

Return the active parametric domain of a B-spline axis.

source
domain(net::ControlNet, direction::Int) -> Tuple

Return the active parametric domain in one parametric direction.

source
Tesserae.NURBS.evaluateFunction
evaluate(net::ControlNet, ξ::Vec) -> Vec

Evaluate a rational tensor-product B-spline control net at the parametric coordinate ξ.

source
Tesserae.NURBS.boundariesFunction
boundaries(net::ControlNet) -> Tuple

Extract all boundary control nets.

source
boundaries(net::ControlNet, direction::Int, side::Int) -> ControlNet

Extract one boundary control net. direction is the fixed parametric direction; side is -1 for the lower end and +1 for the upper end.

source

Primitives

Tesserae.NURBS.polylineFunction
polyline(points::AbstractVector{<: Vec}) -> ControlNet

Create a piecewise-linear curve through the given points.

source
Tesserae.NURBS.circleFunction
circle(center::Vec{2}, radius::Real) -> ControlNet
circle(center::Vec{3}, radius::Real; normal::Vec{3}=Vec(0,0,1), xaxis::Vec{3}=default_arc_xaxis(normal)) -> ControlNet

Create an exact rational quadratic circle.

source
Tesserae.NURBS.arcFunction
arc(center::Vec{2}, radius::Real, θ₀::Real, θ₁::Real) -> ControlNet
arc(center::Vec{3}, radius::Real, θ₀::Real, θ₁::Real; normal::Vec{3}=Vec(0,0,1), xaxis::Vec{3}=default_arc_xaxis(normal)) -> ControlNet

Create an exact rational quadratic circular arc. The 2D form lies in the global x-y plane. The 3D form uses normal as the plane normal; θ₀ and θ₁ are measured from xaxis in that plane.

source

Construction

Tesserae.NURBS.coons_patchFunction
coons_patch(bottom, top, left, right) -> ControlNet

Build a tensor-product surface from four boundary curves. bottom and top run in the first parametric direction; left and right run in the second.

source
Tesserae.NURBS.loftFunction
loft(sections::AbstractVector{<: ControlNet}) -> ControlNet

Build one higher parametric dimension by stacking matching control nets.

source
Tesserae.NURBS.sweepFunction
sweep(net::ControlNet, direction::Vec; degree::Int=1, nspans::Int=1)

Sweep a control net by translating it along a straight vector.

source
sweep(section::ControlNet, trajectory::ControlNet)

Sweep a section along a trajectory curve by tensor-product translation.

source
Tesserae.NURBS.revolveFunction
revolve(section::ControlNet, axis_point::Vec, axis_direction::Vec, angle::Real=2π)

Revolve a 3D curve or surface around an axis. The added parametric direction is a rational quadratic circular arc.

source

Degree and Knot Operations

Tesserae.NURBS.elevateFunction
elevate(net::ControlNet, degree::Int; direction::Int) -> ControlNet

Raise one parametric direction of net to the target polynomial degree without changing the represented geometry. The target degree must not be lower than the current degree. Rational nets are elevated in homogeneous coordinates, so both control points and weights are transformed.

source
elevate(net::ControlNet, degrees::Tuple{Vararg{Int}}) -> ControlNet

Raise each parametric direction of net to the corresponding target polynomial degree. The d-th entry of degrees is the target degree for parametric direction d.

source
elevate(net::ControlNet; direction::Int, ntimes::Int=1) -> ControlNet

Raise one parametric direction of net by ntimes degree increments. This is equivalent to raising the direction to current_degree + ntimes.

source
elevate(axis::BSplineAxis, degree::Int) -> BSplineAxis

Raise axis to the target polynomial degree without changing its break points or continuity. The target degree must not be lower than the current degree.

source
Tesserae.NURBS.insert_knotFunction
insert_knot(net::ControlNet, ξ; direction, ntimes=1) -> ControlNet

Insert ξ one or more times in a parametric direction without changing the represented geometry.

source
insert_knot(net::ControlNet, knots::AbstractVector; direction) -> ControlNet

Insert each knot in knots in a parametric direction without changing the represented geometry. Repeated entries request repeated insertions.

source
insert_knot(axis::BSplineAxis, ξ) -> BSplineAxis

Return a B-spline axis with ξ inserted once into the knot vector.

source
insert_knot(axis::BSplineAxis, knots::AbstractVector) -> BSplineAxis

Return a B-spline axis with each knot in knots inserted in order.

source
Tesserae.NURBS.refineFunction
refine(net::ControlNet, ninsertions::Tuple{Vararg{Integer}}) -> ControlNet

Uniformly refine each parametric direction. Each entry gives the number of knots inserted into every nonzero span in that direction.

source
refine(net::ControlNet, n::Integer; direction::Int) -> ControlNet

Uniformly refine one parametric direction by inserting n knots into every nonzero span.

source
refine(axis::BSplineAxis, n::Integer) -> BSplineAxis

Uniformly refine each nonzero knot span of axis.

source

Gmsh

Tesserae.NURBS.writestepFunction
writestep(filename, net::ControlNet)

Write a curve, surface, or volume control net to a STEP file. Two-dimensional curves and surfaces are embedded in the z=0 plane. The method is provided by the Gmsh extension.

source
Tesserae.NURBS.viewmeshFunction
viewmesh(net; mesh_size_factor=0.5)

Open a 3D curve, surface, or volume control net in Gmsh with a generated mesh. Volume control nets are shown by their boundary surface mesh.

source