diff --git a/docs/pages.jl b/docs/pages.jl index b624c42..28eda10 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -8,6 +8,8 @@ pages = [ "Types" => [ "VariableMap" => "variablemap.md", "Boundary Conditions" => "boundaries.md", + "Domain Types" => "domains.md", + "Variational Methods" => "variational.md", ], "API Reference" => "api.md", ] diff --git a/docs/src/domains.md b/docs/src/domains.md new file mode 100644 index 0000000..9d83c7e --- /dev/null +++ b/docs/src/domains.md @@ -0,0 +1,187 @@ +# Domain Types for Unstructured Meshes + +PDEBase.jl provides abstract types and interfaces for working with discretized domains (meshes) from various FEM/FVM/DG frameworks. + +## Overview + +While `DomainSets.jl` provides analytical domain descriptions (intervals, products, set operations), many PDE solvers work with numerical meshes that may not have analytical descriptions. PDEBase provides types to bridge this gap. + +## Type Hierarchy + +``` +DomainSets.Domain{T} + AbstractDiscretizedDomain{T} + MeshDomain{T,M} +``` + +## Abstract Types + +### AbstractDiscretizedDomain + +```@docs +PDEBase.AbstractDiscretizedDomain +``` + +## Concrete Types + +### MeshDomain + +```@docs +PDEBase.MeshDomain +``` + +The `MeshDomain` type wraps mesh objects from external libraries and provides: +- Storage for the underlying mesh +- Named boundary region markers +- A common interface for PDEBase operations + +### Example Usage + +```julia +using PDEBase + +# Assume `grid` is a mesh object from an external library (Ferrite, Gridap, etc.) +# Create a MeshDomain wrapper +domain = MeshDomain(grid) + +# Mark boundary regions +mark_boundary!(domain, "inlet", inlet_facets) +mark_boundary!(domain, "outlet", outlet_facets) +mark_boundary!(domain, "wall", wall_facets) + +# Access boundary markers +markers = get_boundary_markers(domain) +# markers["inlet"] => inlet_facets + +# Access the underlying mesh +mesh = get_mesh(domain) +``` + +## Boundary Regions + +PDEBase provides types for referencing named boundary regions on domains. + +### BoundaryRegion + +```@docs +PDEBase.BoundaryRegion +``` + +Use `BoundaryRegion` to reference named boundaries when specifying boundary conditions: + +```julia +using PDEBase + +domain = MeshDomain(grid) +mark_boundary!(domain, "left", left_faces) +mark_boundary!(domain, "right", right_faces) + +# Create boundary region references +left_boundary = BoundaryRegion(domain, "left") +right_boundary = BoundaryRegion(domain, "right") + +# These can be used in boundary condition specifications +# (syntax depends on downstream discretization package) +``` + +### ConditionalBoundary + +```@docs +PDEBase.ConditionalBoundary +``` + +Use `ConditionalBoundary` when boundaries are defined by conditions rather than explicit markers: + +```julia +using PDEBase, Symbolics +@parameters x y + +# For analytical domains, select boundaries by condition +domain = (x in Interval(0, 1)) * (y in Interval(0, 1)) +left_bc = ConditionalBoundary(domain, [x ~ 0]) +right_bc = ConditionalBoundary(domain, [x ~ 1]) +``` + +## Interface Functions + +### Mesh Access + +```@docs +PDEBase.get_mesh +``` + +### Boundary Markers + +```@docs +PDEBase.get_boundary_markers +PDEBase.mark_boundary! +``` + +### Boundary Region Access + +```@docs +PDEBase.get_parent_domain +PDEBase.get_boundary_name +PDEBase.get_condition +``` + +## Integration with External Mesh Libraries + +### Ferrite.jl + +```julia +using Ferrite, PDEBase + +# Create a Ferrite grid +grid = generate_grid(Quadrilateral, (10, 10)) + +# Wrap in MeshDomain +domain = MeshDomain(grid) + +# Use Ferrite's face sets as boundary markers +mark_boundary!(domain, "left", getfacetset(grid, "left")) +mark_boundary!(domain, "right", getfacetset(grid, "right")) +``` + +### Gridap.jl + +```julia +using Gridap, PDEBase + +# Create a Gridap model +model = CartesianDiscreteModel((0,1,0,1), (10,10)) + +# Wrap in MeshDomain +domain = MeshDomain(model) + +# Use boundary tags +mark_boundary!(domain, "boundary", "boundary") +``` + +### GMSH Import + +```julia +using Gmsh, PDEBase + +# Load mesh from GMSH +gmsh.initialize() +gmsh.open("domain.msh") +# ... extract mesh data ... + +# Wrap mesh with boundary markers from GMSH physical groups +domain = MeshDomain(mesh_data) +for (name, tag) in gmsh_physical_groups + mark_boundary!(domain, name, tag) +end +``` + +## Design Notes + +The domain types in PDEBase are intentionally minimal and abstract. The philosophy is: + +1. **Framework-agnostic**: PDEBase doesn't depend on any specific mesh library +2. **Type wrapper**: `MeshDomain` stores mesh objects opaquely; framework-specific packages interpret them +3. **Named boundaries**: Support for named boundary regions enables clean BC specification +4. **Extensible**: Downstream packages can subtype `AbstractDiscretizedDomain` for specialized needs + +For actual mesh operations (element iteration, DOF mapping, assembly), users should use the native APIs of their mesh library, accessed via `get_mesh()`. diff --git a/docs/src/variational.md b/docs/src/variational.md new file mode 100644 index 0000000..1f318fb --- /dev/null +++ b/docs/src/variational.md @@ -0,0 +1,226 @@ +# Variational Methods and FEM Interface + +PDEBase.jl provides abstract types and interfaces for finite element methods (FEM) and other variational discretization approaches. + +## Overview + +Variational methods reformulate PDEs from their strong (pointwise) form into a weak (integral) form. This is the foundation of: + +- Finite Element Methods (FEM) +- Spectral Galerkin Methods +- Discontinuous Galerkin (DG) Methods +- Finite Volume Methods (FVM) with variational interpretation + +PDEBase provides abstract types that discretization packages can subtype to indicate their variational formulation. + +## Type Hierarchy + +``` +AbstractVariationalMethod + AbstractGalerkinMethod + AbstractRitzGalerkin + RitzGalerkin + AbstractPetrovGalerkin +``` + +## Variational Method Types + +### AbstractVariationalMethod + +```@docs +PDEBase.AbstractVariationalMethod +``` + +### AbstractGalerkinMethod + +```@docs +PDEBase.AbstractGalerkinMethod +``` + +### AbstractRitzGalerkin + +```@docs +PDEBase.AbstractRitzGalerkin +``` + +### AbstractPetrovGalerkin + +```@docs +PDEBase.AbstractPetrovGalerkin +``` + +### RitzGalerkin + +```@docs +PDEBase.RitzGalerkin +``` + +## Unstructured Discretization Types + +For discretizations on unstructured meshes (triangular, tetrahedral, general polyhedral): + +### AbstractUnstructuredDiscretization + +```@docs +PDEBase.AbstractUnstructuredDiscretization +``` + +### AbstractUnstructuredDiscreteSpace + +```@docs +PDEBase.AbstractUnstructuredDiscreteSpace +``` + +## Query Functions + +### get_variational_method + +```@docs +PDEBase.get_variational_method +``` + +### is_galerkin + +```@docs +PDEBase.is_galerkin +``` + +### is_symmetric_galerkin + +```@docs +PDEBase.is_symmetric_galerkin +``` + +## FEM Traits + +PDEBase provides traits to query discretization capabilities: + +```@docs +PDEBase.AbstractFEMTrait +PDEBase.SupportsWeakForm +PDEBase.SupportsStrongForm +PDEBase.SupportsBothForms +PDEBase.get_supported_form +``` + +## Example: Implementing a FEM Discretizer + +Here's how a downstream package might use these types: + +```julia +using PDEBase, ModelingToolkit, SciMLBase + +# Define a FEM discretization using Ritz-Galerkin +struct MyFEMDiscretization{M<:AbstractVariationalMethod, Q} <: AbstractUnstructuredDiscretization + method::M # Variational method (e.g., RitzGalerkin()) + polynomial_order::Int + quadrature::Q + # ... other fields +end + +# Default constructor with standard Galerkin +function MyFEMDiscretization(; order=1, quadrature=GaussQuadrature(order+1)) + MyFEMDiscretization(RitzGalerkin(), order, quadrature) +end + +# Implement the method query +PDEBase.get_variational_method(d::MyFEMDiscretization) = d.method + +# Indicate we support weak form +PDEBase.get_supported_form(::MyFEMDiscretization) = SupportsWeakForm() + +# Define discrete space for unstructured meshes +struct MyFEMSpace{M} <: AbstractUnstructuredDiscreteSpace + mesh::M + dof_handler::Any + # ... DOF mapping, element info, etc. +end + +# Implement PDEBase interface functions +function PDEBase.construct_discrete_space(v::VariableMap, disc::MyFEMDiscretization) + # Build finite element space from variable map + # ... + return MyFEMSpace(mesh, dof_handler) +end + +function PDEBase.discretize_equation!( + disc_state, + pde::Equation, + vareqmap, + eqvar, + bcmap, + depvars, + s::MyFEMSpace, + derivweights, + indexmap, + disc::MyFEMDiscretization +) + # Apply weak form transformation + # Assemble element matrices/vectors + # Handle boundary conditions + # ... +end +``` + +## Strong Form to Weak Form Transformation + +For discretizations that need to transform PDEs from strong to weak form, implement `should_transform` and `transform_pde_system!`: + +```julia +using PDEBase + +# Indicate transformation is needed for Galerkin methods +function PDEBase.should_transform(pdesys::PDESystem, disc::MyFEMDiscretization, boundarymap) + return is_galerkin(disc.method) +end + +# Perform the transformation +function PDEBase.transform_pde_system!( + v::VariableMap, + boundarymap, + pdesys::PDESystem, + disc::MyFEMDiscretization +) + # 1. Introduce test functions v for each trial function u + # 2. Multiply equations by test functions + # 3. Integrate over domain + # 4. Apply integration by parts where needed + # 5. Return transformed PDESystem + # ... +end +``` + +## Weak Form Representation + +When working with weak forms, the PDE is expressed as: + +**Find u in V such that:** `a(u, v) = L(v)` for all v in V + +Where: +- `a(u, v)` is the bilinear form (depends on both u and v) +- `L(v)` is the linear form (depends only on v) +- `V` is the function space (trial = test for Ritz-Galerkin) + +Example for the heat equation `-div(grad(u)) = f`: + +**Strong form:** +```julia +eqs = [-div(grad(u(x,y))) ~ f] +``` + +**Weak form (after integration by parts):** +```julia +# grad(u) dot grad(v) integrated over domain = f*v integrated over domain +eqs = [Integral(domain)(grad(u(x,y)) dot grad(v(x,y))) ~ Integral(domain)(f*v(x,y))] +``` + +## Design Philosophy + +The variational types in PDEBase are designed to be: + +1. **Method descriptors**: Types like `RitzGalerkin` describe the mathematical method, not the implementation +2. **Dispatch hooks**: Enable method dispatch for different formulations +3. **Framework-agnostic**: Work with any FEM library (Ferrite, Gridap, etc.) +4. **Composable**: Can be combined with domain types for complete problem specification + +The actual finite element assembly, basis function evaluation, and numerical integration are left to downstream packages that have access to specific mesh and element implementations. diff --git a/src/PDEBase.jl b/src/PDEBase.jl index 56ec877..282df47 100644 --- a/src/PDEBase.jl +++ b/src/PDEBase.jl @@ -35,6 +35,8 @@ include("parse_boundaries.jl") include("periodic_map.jl") include("make_pdesys_compatible.jl") include("symbolic_discretize.jl") +include("domains.jl") +include("variational.jl") include("precompilation.jl") export AbstractDiscreteSpace, AbstractCartesianDiscreteSpace, AbstractVarEqMapping, @@ -55,4 +57,19 @@ export VariableMap export ivs, all_ivs, depvar, depvars, indvars, x2i export PeriodicMap +# Domain types for unstructured meshes +export AbstractDiscretizedDomain, MeshDomain +export BoundaryRegion, ConditionalBoundary +export get_mesh, get_boundary_markers, get_boundary_name, get_parent_domain, get_condition +export mark_boundary! + +# Variational method types for FEM/FVM/DG discretizations +export AbstractVariationalMethod, AbstractGalerkinMethod +export AbstractRitzGalerkin, AbstractPetrovGalerkin +export RitzGalerkin +export AbstractUnstructuredDiscretization, AbstractUnstructuredDiscreteSpace +export get_variational_method, is_galerkin, is_symmetric_galerkin +export AbstractFEMTrait, SupportsWeakForm, SupportsStrongForm, SupportsBothForms +export get_supported_form + end # module PDEBase diff --git a/src/domains.jl b/src/domains.jl new file mode 100644 index 0000000..752ad35 --- /dev/null +++ b/src/domains.jl @@ -0,0 +1,233 @@ +############################################################################################ +# Domain types for unstructured meshes and general domains +############################################################################################ + +""" + AbstractDiscretizedDomain{T} <: DomainSets.Domain{T} + +Abstract supertype for domains that have been discretized into meshes or grids. +This provides an interface layer between analytical domain descriptions (from DomainSets.jl) +and numerical mesh representations from various FEM/FVM/DG frameworks. + +Concrete implementations should wrap mesh objects from packages like Ferrite.jl, Gridap.jl, +Trixi.jl, or other meshing libraries. + +# Type Parameter +- `T`: The element type of the domain coordinates (e.g., `Float64`, `SVector{3,Float64}`) + +# Interface Functions +Implementations should define: +- `DomainSets.dimension(d::AbstractDiscretizedDomain)` - Spatial dimension of the domain +- `Base.in(point, d::AbstractDiscretizedDomain)` - Point membership test +- `get_boundary_markers(d::AbstractDiscretizedDomain)` - Named boundary regions + +# See Also +- [`MeshDomain`](@ref) for a generic mesh wrapper +- [`BoundaryRegion`](@ref) for named boundary regions +""" +abstract type AbstractDiscretizedDomain{T} <: DomainSets.Domain{T} end + +""" + MeshDomain{T, M} <: AbstractDiscretizedDomain{T} + +A generic wrapper for mesh objects from external meshing libraries. + +This type allows PDEBase to work with meshes from any FEM/FVM/DG framework by +wrapping the native mesh object and providing a common interface. + +# Type Parameters +- `T`: The element type of domain coordinates +- `M`: The type of the wrapped mesh object + +# Fields +- `mesh::M`: The underlying mesh object from an external library +- `boundary_markers::Dict{String, Any}`: Named boundary regions and their markers + +# Example +```julia +using PDEBase +# Assuming `grid` is a mesh from some external library +mesh_domain = MeshDomain{Float64}(grid) +mark_boundary!(mesh_domain, "inlet", inlet_facets) +mark_boundary!(mesh_domain, "outlet", outlet_facets) +``` + +# Note +The mesh object is stored opaquely. Framework-specific discretization packages +should access it via `get_mesh(domain)` and handle it appropriately. +""" +struct MeshDomain{T, M} <: AbstractDiscretizedDomain{T} + mesh::M + boundary_markers::Dict{String, Any} + function MeshDomain{T}(mesh::M) where {T, M} + new{T, M}(mesh, Dict{String, Any}()) + end + function MeshDomain{T, M}(mesh::M, markers::Dict{String, Any}) where {T, M} + new{T, M}(mesh, markers) + end +end + +""" + MeshDomain(mesh; eltype=Float64) + +Construct a MeshDomain from a mesh object with default coordinate type. + +# Arguments +- `mesh`: The mesh object from an external library +- `eltype`: The coordinate element type (default: `Float64`) +""" +MeshDomain(mesh; eltype::Type{T} = Float64) where {T} = MeshDomain{T}(mesh) + +""" + get_mesh(domain::MeshDomain) + +Retrieve the underlying mesh object from a MeshDomain. +""" +get_mesh(domain::MeshDomain) = domain.mesh + +""" + get_boundary_markers(domain::AbstractDiscretizedDomain) + +Get the dictionary of named boundary regions for a discretized domain. +Returns a `Dict{String, Any}` mapping boundary names to their markers/identifiers. + +Default implementation returns an empty dictionary. +""" +get_boundary_markers(::AbstractDiscretizedDomain) = Dict{String, Any}() +get_boundary_markers(domain::MeshDomain) = domain.boundary_markers + +""" + mark_boundary!(domain::MeshDomain, name::String, marker) + +Add a named boundary region to a MeshDomain. + +# Arguments +- `domain`: The MeshDomain to modify +- `name`: A string identifier for the boundary (e.g., "inlet", "wall", "left") +- `marker`: The boundary marker/identifier (framework-specific) + +# Returns +The modified domain (for chaining). + +# Example +```julia +domain = MeshDomain(grid) +mark_boundary!(domain, "inlet", inlet_faces) +mark_boundary!(domain, "wall", wall_faces) +``` +""" +function mark_boundary!(domain::MeshDomain, name::String, marker) + domain.boundary_markers[name] = marker + return domain +end + +############################################################################################ +# Boundary region types for named subdomains +############################################################################################ + +""" + BoundaryRegion{D} + +Represents a named boundary region on a domain. + +This is distinct from boundary *conditions* (like `LowerBoundary`, `UpperBoundary`). +A `BoundaryRegion` identifies a portion of the domain boundary by name, which can +then be used when specifying boundary conditions. + +# Type Parameters +- `D`: The type of the parent domain + +# Fields +- `domain::D`: The parent domain containing this boundary +- `name::String`: Identifier for this boundary region + +# Example +```julia +using PDEBase + +@parameters x +Omega = MeshDomain(grid) +mark_boundary!(Omega, "left", left_facets) + +# Create a boundary region reference +partial_Omega_left = BoundaryRegion(Omega, "left") + +# Use in boundary conditions (syntax supported by downstream packages) +# bcs = [u(partial_Omega_left) ~ 0.0] +``` + +# Note +For analytical domains (from DomainSets), you can also create boundary regions +using selector conditions. See [`ConditionalBoundary`](@ref). +""" +struct BoundaryRegion{D} + domain::D + name::String +end + +""" + BoundaryRegion(domain, name::Symbol) + +Convenience constructor accepting a Symbol for the name. +""" +BoundaryRegion(domain::D, name::Symbol) where {D} = BoundaryRegion{D}(domain, String(name)) + +""" + get_parent_domain(br::BoundaryRegion) + +Get the parent domain of a boundary region. +""" +get_parent_domain(br::BoundaryRegion) = br.domain + +""" + get_boundary_name(br::BoundaryRegion) + +Get the name/identifier of a boundary region. +""" +get_boundary_name(br::BoundaryRegion) = br.name + +""" + ConditionalBoundary{D, C} + +Represents a boundary region selected by a condition on coordinates. + +This allows specifying boundaries without explicit markers, useful for +analytical domains or simple geometries. + +# Type Parameters +- `D`: The type of the parent domain +- `C`: The type of the selection condition + +# Fields +- `domain::D`: The parent domain +- `condition::C`: A condition identifying the boundary (e.g., equations like `x ~ 0`) + +# Example +```julia +using PDEBase, Symbolics +@parameters x y + +Omega = (x in Interval(0, 1)) × (y in Interval(0, 1)) + +# Select boundary where x = 0 +left_boundary = ConditionalBoundary(Omega, [x ~ 0]) +``` +""" +struct ConditionalBoundary{D, C} + domain::D + condition::C +end + +""" + get_parent_domain(cb::ConditionalBoundary) + +Get the parent domain of a conditional boundary. +""" +get_parent_domain(cb::ConditionalBoundary) = cb.domain + +""" + get_condition(cb::ConditionalBoundary) + +Get the selection condition for a conditional boundary. +""" +get_condition(cb::ConditionalBoundary) = cb.condition diff --git a/src/variational.jl b/src/variational.jl new file mode 100644 index 0000000..4ad7d4a --- /dev/null +++ b/src/variational.jl @@ -0,0 +1,226 @@ +############################################################################################ +# Abstract types for variational/weak-form PDE discretization methods +############################################################################################ + +""" + AbstractVariationalMethod + +Abstract supertype for variational formulation methods used in PDE discretization. + +Variational methods reformulate PDEs in weak form, seeking solutions that satisfy +integral equations rather than pointwise equations. This includes finite element +methods (FEM), spectral methods, and other Galerkin-type approaches. + +# Subtypes +- [`AbstractGalerkinMethod`](@ref): Methods where solution is projected onto a finite basis + +# See Also +- [`AbstractRitzGalerkin`](@ref): Standard Galerkin with same test and trial spaces +- [`AbstractPetrovGalerkin`](@ref): Galerkin with different test and trial spaces +""" +abstract type AbstractVariationalMethod end + +""" + AbstractGalerkinMethod <: AbstractVariationalMethod + +Abstract type for Galerkin-type methods that approximate solutions by projection +onto finite-dimensional subspaces. + +The general Galerkin approach: +1. Choose finite-dimensional trial space Vₕ for the solution uₕ +2. Choose finite-dimensional test space Wₕ for the test functions vₕ +3. Find uₕ ∈ Vₕ such that B(uₕ, vₕ) = L(vₕ) for all vₕ ∈ Wₕ + +where B(·,·) is a bilinear (or semilinear) form and L(·) is a linear form. + +# Subtypes +- [`AbstractRitzGalerkin`](@ref): Vₕ = Wₕ (standard Galerkin) +- [`AbstractPetrovGalerkin`](@ref): Vₕ ≠ Wₕ (e.g., SUPG, DG) +""" +abstract type AbstractGalerkinMethod <: AbstractVariationalMethod end + +""" + AbstractRitzGalerkin <: AbstractGalerkinMethod + +Abstract type for Ritz-Galerkin methods where the test and trial spaces are identical. + +Standard finite element methods use Ritz-Galerkin formulations. The weak form is: +Find uₕ ∈ Vₕ such that a(uₕ, vₕ) = b(vₕ) for all vₕ ∈ Vₕ + +where: +- a(·,·) is the bilinear form from the PDE +- b(·) is the linear form (source terms, boundary terms) +- Vₕ is the finite element space + +# Example Implementation +```julia +struct ContinuousGalerkin <: AbstractRitzGalerkin + polynomial_order::Int + quadrature_order::Int +end +``` +""" +abstract type AbstractRitzGalerkin <: AbstractGalerkinMethod end + +""" + AbstractPetrovGalerkin <: AbstractGalerkinMethod + +Abstract type for Petrov-Galerkin methods where test and trial spaces differ. + +Petrov-Galerkin methods are useful for: +- Stabilization (SUPG for advection-dominated problems) +- Discontinuous Galerkin (DG) methods +- Streamline-upwind methods + +The weak form is: +Find uₕ ∈ Vₕ such that a(uₕ, wₕ) = b(wₕ) for all wₕ ∈ Wₕ + +where Vₕ (trial space) ≠ Wₕ (test space). +""" +abstract type AbstractPetrovGalerkin <: AbstractGalerkinMethod end + +""" + RitzGalerkin + +A basic marker type for standard Ritz-Galerkin finite element discretization. + +This type can be used as a tag to indicate that a discretization should use +standard Galerkin formulation. Framework-specific discretizers can dispatch +on this type to apply appropriate transformations. + +# Usage +```julia +# In a downstream package +struct MyFEMDiscretization{M} <: AbstractEquationSystemDiscretization + method::M + # ... other fields +end + +# Create a standard Galerkin discretization +disc = MyFEMDiscretization(RitzGalerkin()) +``` +""" +struct RitzGalerkin <: AbstractRitzGalerkin end + +############################################################################################ +# Abstract types for unstructured mesh discretizations +############################################################################################ + +""" + AbstractUnstructuredDiscretization <: AbstractEquationSystemDiscretization + +Abstract type for discretizations on unstructured meshes. + +Unstructured discretizations work with meshes that don't have a regular Cartesian +structure. This includes triangular/tetrahedral meshes, quadrilateral/hexahedral +meshes, and general polyhedral meshes. + +Discretizations subtyping this should implement the standard PDEBase interface +functions, with the discrete space being an [`AbstractUnstructuredDiscreteSpace`](@ref). + +# Example +```julia +# In a downstream package (e.g., for Ferrite.jl or Gridap.jl integration) +struct FerriteDiscretization{M, Q} <: AbstractUnstructuredDiscretization + method::M # Variational method (e.g., RitzGalerkin) + quadrature::Q # Quadrature specification + # ... other fields +end +``` +""" +abstract type AbstractUnstructuredDiscretization <: AbstractEquationSystemDiscretization end + +""" + AbstractUnstructuredDiscreteSpace <: AbstractDiscreteSpace + +Abstract type for discrete spaces on unstructured meshes. + +This represents the discretized spatial domain and associated finite element +spaces for unstructured mesh discretizations. Unlike Cartesian discrete spaces +which have regular grid structure, unstructured spaces may have: +- Variable element shapes (triangles, quads, tetrahedra, hexahedra, etc.) +- Non-uniform element sizes +- Arbitrary connectivity + +# Interface +Implementations should provide: +- Storage for discrete variables (DOF values) +- Mapping between local (element) and global DOF numbering +- Access to the underlying mesh geometry + +# See Also +- `AbstractCartesianDiscreteSpace`: For structured Cartesian grids +""" +abstract type AbstractUnstructuredDiscreteSpace <: AbstractDiscreteSpace end + +############################################################################################ +# Interface function stubs for variational methods +############################################################################################ + +""" + get_variational_method(disc::AbstractUnstructuredDiscretization) + +Get the variational method (e.g., `RitzGalerkin`) used by a discretization. + +Default implementation returns `nothing`. Discretizations using variational +methods should override this. +""" +get_variational_method(::AbstractUnstructuredDiscretization) = nothing + +""" + is_galerkin(method::AbstractVariationalMethod) -> Bool + +Check if a variational method is a Galerkin-type method. +""" +is_galerkin(::AbstractGalerkinMethod) = true +is_galerkin(::AbstractVariationalMethod) = false + +""" + is_symmetric_galerkin(method::AbstractVariationalMethod) -> Bool + +Check if a Galerkin method uses symmetric test/trial spaces (Ritz-Galerkin). +""" +is_symmetric_galerkin(::AbstractRitzGalerkin) = true +is_symmetric_galerkin(::AbstractVariationalMethod) = false + +############################################################################################ +# Traits for FEM discretization capabilities +############################################################################################ + +""" + abstract type AbstractFEMTrait end + +Base type for traits describing FEM discretization capabilities. +""" +abstract type AbstractFEMTrait end + +""" + SupportsWeakForm <: AbstractFEMTrait + +Trait indicating a discretization can handle PDEs in weak form. +""" +struct SupportsWeakForm <: AbstractFEMTrait end + +""" + SupportsStrongForm <: AbstractFEMTrait + +Trait indicating a discretization can handle PDEs in strong form. +""" +struct SupportsStrongForm <: AbstractFEMTrait end + +""" + SupportsBothForms <: AbstractFEMTrait + +Trait indicating a discretization can handle both weak and strong form PDEs. +""" +struct SupportsBothForms <: AbstractFEMTrait end + +""" + get_supported_form(disc::AbstractDiscretization) -> AbstractFEMTrait + +Query what form (weak/strong/both) a discretization supports. + +Default returns `SupportsStrongForm()`. FEM discretizations should override +to return `SupportsWeakForm()` or `SupportsBothForms()`. +""" +get_supported_form(::AbstractDiscretization) = SupportsStrongForm()