September 3, 2025

About

GridapTopOpt is a library that is able to handle level set method based topology optimization. This library is built with Gridap family, which enables us to do with finite element analysis, can be run as multiple processing ( or threading) with MPI. The code is implemented with a little bit old version of Gridap, and I would like to run it with PETSc multi-threading, I revised their tutorial code.

Code

In their code,

using Gridap, Gridap.Geometry, Gridap.Adaptivity, Gridap.MultiField, Gridap.TensorValues
using GridapEmbedded, GridapEmbedded.LevelSetCutters
using GridapSolvers, GridapSolvers.BlockSolvers, GridapSolvers.NonlinearSolvers
using GridapGmsh
using PartitionedArrays
using GridapPETSc
using GridapTopOpt
using Gridap.FESpaces: get_fe_space, get_cell_dof_ids
using Gridap.FESpaces: FEFunction
using GridapTopOpt: StateParamMap
"""
  This example appears in our manuscript:
    "Level-set topology optimisation with unfitted finite elements and automatic shape differentiation"
    by Z.J. Wegert, J. Manyer, C. Mallon, S. Badia, V.J. Challis. (10.1016/j.cma.2025.118203)
  (MPI) Three-dimensional minimum elastic compliance of a wheel using a CutFEM formulation based
    on Burman et al. (2018) [10.1016/j.cma.2017.09.005] & automatic shape differentiation.
  Optimisation problem:
      Min J(Ω) = ∫ ε(d) ⊙ σ(ε(d)) dΩ
       Ω
    s.t., Vol(Ω) = 0.3,
          ⎡d∈V=H¹(Ω;u(Γ_D)=0),
          ⎣∫ ε(s) ⊙ σ ∘ ε(d) dΩ + j(d,s) + i(d,s) = ∫ s⋅g dΓ_N, ∀v∈V.
  In the above, j(d,s) is the ghost penalty term over the ghost skeleton Γg
  with outward normal n_Γg, and i(d,s) enforces zero temperature within the
  isolated volumes marked by χ. These are given by
      j(d,s) = ∫ γh³[[∇(d)⋅n_Γg]]⋅[[(∇(s)⋅n_Γg]] dΓg, &
      i(d,s) = ∫ χd ⋅ s dΩ.
"""
function main()
  # Params
  vf =  0.5
  γ_evo = 0.1
  max_steps = 10
  # vf =  0.3
  # α_coeff = 1.0
  α_coeff = γ_evo*max_steps
  iter_mod = 50
  D = 3
  mesh_name = "Wheel_3d_a.msh"
  mesh_file = (@__DIR__)*"/Meshes/$mesh_name"
  # Output path
  path = "./results/Wheel3D_CutFEM_org_b/"
  files_path = path*"data/"
  model_path = path*"model/"
  mkpath(files_path); mkpath(model_path);
  # Load mesh
  model = GmshDiscreteModel(mesh_file)
  model = UnstructuredDiscreteModel(model)
  f_diri(x) =
    ((cos(30π/180)<=x[1]<=cos(15π/180)) && abs(x[2] - sqrt(1-x[1]^2))<1e-4) ||
    ((cos(97.5π/180)<=x[1]<=cos(82.5π/180)) && abs(x[2] - sqrt(1-x[1]^2))<1e-4) ||
    ((cos(165π/180)<=x[1]<=cos(150π/180)) && abs(x[2] - sqrt(1-x[1]^2))<1e-4) ||
    ((cos(142.5π/180)<=x[1]<=cos(127.5π/180)) && abs(x[2] - -sqrt(1-x[1]^2))<1e-4) ||
    ((cos(52.5π/180)<=x[1]<=cos(37.5π/180)) && abs(x[2] - -sqrt(1-x[1]^2))<1e-4)
  update_labels!(1,model,f_diri,"Gamma_D_new")
  writevtk(model,model_path*"model")
  # Get triangulation and element size
  Ω_bg = Triangulation(model)
  hₕ = get_element_diameter_field(model)
  hmin = minimum(get_element_diameters(model))
  # Cut the background model
  reffe_scalar = ReferenceFE(lagrangian,Float64,1)
  V_φ = TestFESpace(model,reffe_scalar)
  V_reg = TestFESpace(model,reffe_scalar;dirichlet_tags=["Gamma_N",])
  U_reg = TrialFESpace(V_reg)
  _f1((x,y,z),q,r) = - cos(q*π*x)*cos(q*π*y)*cos(q*π*z)/q - r/q
  _f2((x,y,z)) = -sqrt(x^2+y^2)+0.9
  φh = interpolate(x->min(_f1(x,4,0.1),_f2(x)),V_φ)
  # Check LS
  GridapTopOpt.correct_ls!(φh)
  # Setup integration meshes and measures
  order = 1
  degree = 2*(order+1)
  Γ_N = BoundaryTriangulation(model,tags=["Gamma_N",])
  dΓ_N = Measure(Γ_N,degree)
  dΩ_bg = Measure(Ω_bg,degree)
  Ω_data = EmbeddedCollection(model,φh) do cutgeo,cutgeo_facets,_φh
    Ω = DifferentiableTriangulation(Triangulation(cutgeo,PHYSICAL),V_φ)
    Γ  = DifferentiableTriangulation(EmbeddedBoundary(cutgeo),V_φ)
    Γg = GhostSkeleton(cutgeo)
    Ω_act = Triangulation(cutgeo,ACTIVE)
    # Isolated volumes
    # φ_cell_values = map(get_cell_dof_values,local_views(_φh))
    # FEFunction φh の所属する FESpace を取得
    space = get_fe_space(_φh)
    # セルごとの DOF インデックスを取得
    cell_ids = get_cell_dof_ids(space)
    # φh の DOF 値のベクトルを取得
    u_vec = get_free_dof_values(_φh)
    # 各セルごとの DOF 値を抽出
    φ_cell_values = [u_vec[ids] for ids in cell_ids]
    ψ,_ = get_isolated_volumes_mask_polytopal(model,φ_cell_values,["Gamma_D_new",])
    (;
      :Ω_act => Ω_act,
      :Ω     => Ω,
      :dΩ    => Measure(Ω,degree),
      :Γg    => Γg,
      :dΓg   => Measure(Γg,degree),
      :n_Γg  => get_normal_vector(Γg),
      :Γ     => Γ,
      :dΓ    => Measure(Γ,degree),
      :n_Γ   => get_normal_vector(Γ),
      :ψ     => ψ
    )
  end
  # Setup spaces
  reffe_d = ReferenceFE(lagrangian,VectorValue{D,Float64},order)
  function build_spaces(Ω_act)
    V = TestFESpace(Ω_act,reffe_d,conformity=:H1,dirichlet_tags=["Gamma_D_new",])
    U = TrialFESpace(V)
    return U,V
  end
  ### Weak form
  # Material parameters
  function lame_parameters(E,ν)
    λ = (E*ν)/((1+ν)*(1-2*ν))
    μ = E/(2*(1+ν))
    (λ, μ)
  end
  λs, μs = lame_parameters(1.0,0.3)
  α_Gd = 1e-7
  k_d = 1.0
  # α_Gd = 3e-5
  # k_d = 10.0
  γ_Gd(h) = α_Gd*(λs + μs)*h^3
  # Terms
  σ(ε) = λs*tr(ε)*one(ε) + 2*μs*ε
  a_s_Ω(d,s) = ε(s) ⊙ (σ ∘ ε(d)) # Elasticity
  j_s_k(d,s) = mean(γ_Gd ∘ hₕ)*(jump(Ω_data.n_Γg ⋅ ∇(s)) ⋅ jump(Ω_data.n_Γg ⋅ ∇(d)))
  v_s_ψ(d,s) = (k_d*Ω_data.ψ)*(d⋅s) # Isolated volume term
  g((x,y,z)) = 100VectorValue(-y,x,0.0)
  a(d,s,φ) = ∫(a_s_Ω(d,s) + v_s_ψ(d,s))Ω_data.dΩ + ∫(j_s_k(d,s))Ω_data.dΓg
  l(s,φ) = ∫(s⋅g)dΓ_N
  ## Optimisation functionals
  vol_D = sum(∫(1)dΩ_bg)
  J_comp(d,φ) = ∫(ε(d) ⊙ (σ ∘ ε(d)))Ω_data.dΩ
  Vol(d,φ) = ∫(1/vol_D)Ω_data.dΩ - ∫(vf/vol_D)dΩ_bg
  dVol(q,d,φ) = ∫(-1/vol_D*q/(abs(Ω_data.n_Γ ⋅ ∇(φ))))Ω_data.dΓ
  ## Setup solver and FE operators
  elast_ls = PETScLinearSolver(lu_pardiso_ksp_setup())
  state_collection = EmbeddedCollection_in_φh(model,φh) do _φh
    update_collection!(Ω_data,_φh)
    U,V = build_spaces(Ω_data.Ω_act)
    state_map = AffineFEStateMap(a,l,U,V,V_φ;ls=elast_ls,adjoint_ls=elast_ls)
    (;
      :state_map => state_map,
      :J => StateParamMap(J_comp,state_map),
      :C => map(Ci -> StateParamMap(Ci,state_map),[Vol,])
    )
  end
  pcf = EmbeddedPDEConstrainedFunctionals(state_collection;analytic_dC=(dVol,))
  ## Evolution Method
  # evolve_ls = PETScLinearSolver()
  evolve_ls = PETScLinearSolver(lu_pardiso_ksp_setup())
  evolve_nls = NewtonSolver(evolve_ls;maxiter=1,verbose=true)
  # evolve_nls = NewtonSolver(evolve_ls;maxiter=5,verbose=true)
  # reinit_nls = NewtonSolver(PETScLinearSolver();maxiter=20,rtol=1.e-14,verbose=true)
  reinit_nls = NewtonSolver(PETScLinearSolver(lu_pardiso_ksp_setup());maxiter=20,rtol=1.e-14,verbose=true)
  evo = CutFEMEvolver(V_φ,Ω_data,dΩ_bg,hₕ;max_steps,γg=0.01,ode_ls=evolve_ls,ode_nl=evolve_nls)
  reinit = StabilisedReinitialiser(V_φ,Ω_data,dΩ_bg,hₕ;stabilisation_method=ArtificialViscosity(0.5),nls=reinit_nls)
  # reinit = StabilisedReinitialiser(V_φ,Ω_data,dΩ_bg,hₕ;stabilisation_method=ArtificialViscosity(0.1),nls=reinit_nls)
  ls_evo = LevelSetEvolution(evo,reinit)
  ## Hilbertian extension-regularisation problems
  hilb_ls = CGAMGSolver()
  α_floor = 1e-6
  _α(hₕ) = (α_coeff*hₕ)^2 + α_floor
  # _α(hₕ) = (α_coeff*hₕ)^2
  a_hilb(p,q) =∫((_α ∘ hₕ)*∇(p)⋅∇(q) + p*q)dΩ_bg;
  vel_ext = VelocityExtension(a_hilb,U_reg,V_reg;ls=hilb_ls)
  ## Optimiser
  converged(m) = GridapTopOpt.default_al_converged(
    m;
    L_tol = 0.01hmin,
    C_tol = 0.01
  )
  optimiser = AugmentedLagrangian(pcf,ls_evo,vel_ext,φh;
    γ=γ_evo,verbose=true,constraint_names=[:Vol],converged)
  
  println("optimiser")
    for (it,uh,φh) in optimiser
    # @info "it/iter_mod = $(it) / $(iter_mod)"
    @info "iter = $(it)"
    # if iszero(it % iter_mod)
    writevtk(Ω_bg,files_path*"Omega_act_$it",
      cellfields=["φ"=>φh,"|∇(φ)|"=>(norm ∘ ∇(φh)),"uh"=>uh,"ψ"=>Ω_data.ψ])
    writevtk(Ω_data.Ω,files_path*"Omega_in_$it",cellfields=["uh"=>uh])
    # end
    psave(files_path*"LSF_$it",get_free_dof_values(φh))
    write_history(path*"/history.txt",optimiser.history;ranks)
  end
  println("it = get_history")
  it = get_history(optimiser).niter; uh = get_state(pcf)
  writevtk(Ω_bg,path*"Omega_act_$it",cellfields=["φ"=>φh,"|∇(φ)|"=>(norm ∘ ∇(φh)),"uh"=>uh,"ψ"=>Ω_data.ψ])
  writevtk(Ω_data.Ω,path*"Omega_in_$it",cellfields=["uh"=>uh])
  psave(path*"LSF_$it",get_free_dof_values(φh))
  nothing
end
## CG-AMG solver
CGAMGSolver(;kwargs...) = PETScLinearSolver(gamg_ksp_setup(;kwargs...))
function gamg_ksp_setup(;rtol=10^-8,maxits=100)
  function ksp_setup(ksp)
    pc = Ref{GridapPETSc.PETSC.PC}()
    rtol = PetscScalar(rtol)
    atol = GridapPETSc.PETSC.PETSC_DEFAULT
    dtol = GridapPETSc.PETSC.PETSC_DEFAULT
    maxits = PetscInt(maxits)
    @check_error_code GridapPETSc.PETSC.KSPSetType(ksp[],GridapPETSc.PETSC.KSPCG)
    @check_error_code GridapPETSc.PETSC.KSPSetTolerances(ksp[], rtol, atol, dtol, maxits)
    @check_error_code GridapPETSc.PETSC.KSPGetPC(ksp[],pc)
    @check_error_code GridapPETSc.PETSC.PCSetType(pc[],GridapPETSc.PETSC.PCGAMG)
    @check_error_code GridapPETSc.PETSC.KSPView(ksp[],C_NULL)
  end
  return ksp_setup
end
function lu_pardiso_ksp_setup(; rtol=1e-8, maxits=100)
  function ksp_setup(ksp)
    pc = Ref{GridapPETSc.PETSC.PC}()
    # 収束条件
    rtol   = PetscScalar(rtol)
    atol   = GridapPETSc.PETSC.PETSC_DEFAULT
    dtol   = GridapPETSc.PETSC.PETSC_DEFAULT
    maxits = PetscInt(maxits)
    # KSP を preonly に
    @check_error_code GridapPETSc.PETSC.KSPSetType(ksp[], GridapPETSc.PETSC.KSPPREONLY)
    @check_error_code GridapPETSc.PETSC.KSPSetTolerances(ksp[], rtol, atol, dtol, maxits)
    # PC を LU に
    @check_error_code GridapPETSc.PETSC.KSPGetPC(ksp[], pc)
    @check_error_code GridapPETSc.PETSC.PCSetType(pc[], GridapPETSc.PETSC.PCLU)
    # factorization solver type を MKL Pardiso に
    # (エラーが出る場合はここを MUMPS/SuperLU_DIST に変える)
    @check_error_code GridapPETSc.PETSC.PCFactorSetMatSolverType(pc[], "mkl_pardiso")
    # デバッグ用に KSPView
    @check_error_code GridapPETSc.PETSC.KSPView(ksp[], C_NULL)
  end
  return ksp_setup
end
## Run
GridapPETSc.with() do
  main()
end

Result

The number of cells and Points are 36552 and 146208.

Epoch: 1

Epoch: 15

Epoch: 40

Epoch: 60

Intersection and Displacement