Files
mfem/tests/unit/mesh/test_nurbs.cpp
T

517 lines
15 KiB
C++

// Copyright (c) 2010-2025, Lawrence Livermore National Security, LLC. Produced
// at the Lawrence Livermore National Laboratory. All Rights reserved. See files
// LICENSE and NOTICE for details. LLNL-CODE-806117.
//
// This file is part of the MFEM library. For more information and source code
// availability visit https://mfem.org.
//
// MFEM is free software; you can redistribute it and/or modify it under the
// terms of the BSD-3 license. We welcome feedback and contributions, see file
// CONTRIBUTING.md for details.
#include "mfem.hpp"
using namespace mfem;
#include "unit_tests.hpp"
TEST_CASE("NURBS knot insertion and removal", "[NURBS]")
{
auto mesh_fname = "../../data/pipe-nurbs.mesh";
Mesh mesh1(mesh_fname, 1, 1);
Mesh mesh2(mesh_fname, 1, 1);
Vector k0(1);
Vector k1(1);
Vector k2(1);
k0[0] = 0.5;
k1[0] = 0.5;
k2[0] = 0.5;
Array<Vector*> knots(3);
knots[0] = &k0;
knots[1] = &k1;
knots[2] = &k2;
mesh1.KnotInsert(knots);
REQUIRE(mesh1.GetNodes()->Size() > mesh2.GetNodes()->Size());
mesh1.KnotRemove(knots);
// At this point, mesh1 and mesh2 should coincide. Verify this by comparing
// their Nodes GridFunctions.
REQUIRE(mesh1.GetNodes()->Size() == mesh2.GetNodes()->Size());
Vector d(*mesh1.GetNodes());
d -= *mesh2.GetNodes();
const real_t error = d.Norml2();
REQUIRE(error == MFEM_Approx(0.0));
}
TEST_CASE("NURBS refinement and coarsening by spacing formulas", "[NURBS]")
{
auto mesh_fname = GENERATE("../../data/beam-quad-nurbs-sf.mesh",
"../../data/square-nurbs-pw.mesh");
Mesh mesh1(mesh_fname, 1, 1);
Mesh mesh2(mesh_fname, 1, 1);
const bool beam = mesh1.GetNE() > 1;
Array<int> rf(2);
// [24, 12] works for beam mesh
rf[0] = 24;
rf[1] = beam ? 12 : 24;
mesh1.NURBSUniformRefinement(rf);
rf[0] = 12;
rf[1] = beam ? 6 : 12;
mesh2.NURBSUniformRefinement(rf);
REQUIRE(mesh1.GetNodes()->Size() > mesh2.GetNodes()->Size());
mesh1.NURBSCoarsening(2);
// At this point, mesh1 and mesh2 should coincide. Verify this by comparing
// their Nodes GridFunctions.
REQUIRE(mesh1.GetNodes()->Size() == mesh2.GetNodes()->Size());
Vector d(*mesh1.GetNodes());
d -= *mesh2.GetNodes();
const real_t error = d.Norml2();
REQUIRE(error == MFEM_Approx(0.0));
}
TEST_CASE("NURBS mesh reconstruction", "[NURBS]")
{
auto mesh_fname =
GENERATE("../../data/segment-nurbs.mesh",
"../../data/square-nurbs.mesh",
"../../data/beam-quad-nurbs.mesh",
"../../data/pipe-nurbs.mesh",
"../../miniapps/nurbs/meshes/two-squares-nurbs.mesh",
"../../miniapps/nurbs/meshes/two-squares-nurbs-rot.mesh",
"../../miniapps/nurbs/meshes/two-squares-nurbs-autoedge.mesh",
"../../miniapps/nurbs/meshes/plus-nurbs.mesh",
"../../miniapps/nurbs/meshes/plus-nurbs-permuted.mesh",
"../../miniapps/nurbs/meshes/ijk-hex-nurbs.mesh");
Mesh mesh1(mesh_fname, 1, 1);
// Reconstruct mesh using patches + topology
Array<NURBSPatch*> patches;
mesh1.GetNURBSPatches(patches);
const Mesh patchtopo = mesh1.NURBSext->GetPatchTopology();
NURBSExtension ne(&patchtopo, patches);
Mesh mesh2(ne);
// Meshes should be identical
REQUIRE(mesh1.GetNodes()->Size() > 0);
REQUIRE(mesh1.GetNodes()->Size() == mesh2.GetNodes()->Size());
Vector diff(*mesh1.GetNodes());
diff -= *mesh2.GetNodes();
const real_t error = diff.Norml2();
REQUIRE(error == MFEM_Approx(0.0));
// Compare weights (these are stored separately from nodes)
REQUIRE(mesh1.NURBSext->GetWeights().Size() > 0);
REQUIRE(mesh1.NURBSext->GetWeights().Size() ==
mesh2.NURBSext->GetWeights().Size());
Vector wdiff = mesh1.NURBSext->GetWeights();
wdiff -= mesh2.NURBSext->GetWeights();
const real_t werror = wdiff.Norml2();
REQUIRE(werror == MFEM_Approx(0.0));
// Cleanup
for (auto *p : patches) { delete p; }
}
TEST_CASE("Location conversion check", "[NURBS]")
{
KnotVector kv(3, Vector({0.0,
0.2,0.2,0.2,
0.5,0.5,0.5,
0.8,0.8,0.8,
1.0}));
mfem::out<<"knotvector : ";
kv.Print(mfem::out);
constexpr int samples = 31;
for (int i = 0; i < samples; i++)
{
const real_t u = i/real_t(samples-1);
const int ks = kv.GetSpan (u);
REQUIRE( ((kv[ks] <= u) && (u <= kv[ks+1])) );
const real_t xi = kv.GetRefPoint(u, ks);
REQUIRE( ((0.0 <= xi) && (xi <= 1.0)) );
const real_t un = kv.GetKnotLocation(xi,ks);
REQUIRE((un - u) == MFEM_Approx(0.0));
mfem::out<<i<<" : "<<ks<<" ";
mfem::out<<kv[ks] <<" "<<u<<" "<<kv[ks+1]<<" : ";
mfem::out<<u<<" "<<un<<" = "<<un -u<<std::endl;
}
for (int i = 0; i < kv.Size(); i++)
{
const real_t u = kv[i];
const int ks = kv.GetSpan (u);
REQUIRE( ((kv[ks] <= u) && (u <= kv[ks+1])) );
const real_t xi = kv.GetRefPoint(u, ks);
REQUIRE( ((0.0 <= xi) && (xi <= 1.0)) );
const real_t un = kv.GetKnotLocation(xi,ks);
REQUIRE((un - u) == MFEM_Approx(0.0));
mfem::out<<i<<" : "<<ks<<" ";
mfem::out<<kv[ks] <<" "<<u<<" "<<kv[ks+1]<<" : ";
mfem::out<<u<<" "<<un<<" = "<<un -u<<std::endl;
}
KnotVector kv2(1, Vector({0.0, 1.0/3.0, 2.0/3.0, 1.0}));
mfem::out<<"knotvector2 : ";
kv2.Print(mfem::out);
for (int i = 0; i < samples; i++)
{
const real_t u = i/real_t(samples-1);
const int ks = kv2.GetSpan (u);
REQUIRE( ((kv2[ks] <= u) && (u <= kv2[ks+1])) );
const real_t xi = kv2.GetRefPoint(u, ks);
REQUIRE( ((0.0 <= xi) && (xi <= 1.0)) );
const real_t un = kv2.GetKnotLocation(xi,ks);
REQUIRE((un - u) == MFEM_Approx(0.0));
mfem::out<<i<<" : "<<ks<<" ";
mfem::out<<kv2[ks] <<" "<<u<<" "<<kv2[ks+1]<<" : ";
mfem::out<<u<<" "<<un<<" = "<<un -u<<std::endl;
}
for (int i = 0; i < kv2.Size(); i++)
{
const real_t u = kv2[i];
const int ks = kv2.GetSpan (u);
REQUIRE( ((kv2[ks] <= u) && (u <= kv2[ks+1])) );
const real_t xi = kv2.GetRefPoint(u, ks);
REQUIRE( ((0.0 <= xi) && (xi <= 1.0)) );
const real_t un = kv2.GetKnotLocation(xi,ks);
REQUIRE((un - u) == MFEM_Approx(0.0));
mfem::out<<i<<" : "<<ks<<" ";
mfem::out<<kv2[ks] <<" "<<u<<" "<<kv2[ks+1]<<" : ";
mfem::out<<u<<" "<<un<<" = "<<un -u<<std::endl;
}
}
TEST_CASE("Greville, Botella and Demko points", "[NURBS]")
{
Vector xi;
for ( int p = 1; p <= 9; p++)
{
mfem::out<<"Order : "<<p<<std::endl;
KnotVector kvp(p, Vector({0., 1.}));
mfem::out<<"Knotvector : "; kvp.Print(mfem::out);
kvp.GetGreville(xi);
mfem::out<<"Greville points : "; xi.Print(mfem::out,999);
kvp.GetBotella(xi);
mfem::out<<"Botella points : "; xi.Print(mfem::out,999);
kvp.GetDemko(xi);
mfem::out<<"Demko points : "; xi.Print(mfem::out,999);
}
KnotVector kv(3, Vector({0.0, 0.3, 0.3, 0.3, 0.6, 1.0}));
mfem::out<<"Knotvector : "; kv.Print(mfem::out);
// Greville
Vector greville(kv.GetNCP());
for (int i = 0; i < kv.GetNCP(); i++)
{
greville[i] = kv.GetGreville(i);
}
mfem::out<<"Greville points : "; greville.Print(mfem::out, 32);
Vector gref({0.0,0.1,0.2,0.3,0.4,19./30,26./30, 1.0});
for (int i = 0; i < kv.GetNCP(); i++)
{
REQUIRE((greville[i] - gref[i]) == MFEM_Approx(0.0));
}
// Botella
Vector botella(kv.GetNCP());
for (int i = 0; i < kv.GetNCP(); i++)
{
botella[i] = kv.GetBotella(i);
}
mfem::out<<"Botella points : "; botella.Print(mfem::out, 32);
Vector bref({0.0,0.1,0.2,0.3,
0.444007481526490333,
0.626666666666666594,
0.828131261741523739, 1.0});
for (int i = 0; i < kv.GetNCP(); i++)
{
REQUIRE((botella[i] - bref[i]) == MFEM_Approx(0.0));
}
// Demko
Vector demko(kv.GetNCP());
for (int i = 0; i < kv.GetNCP(); i++)
{
demko[i] = kv.GetDemko(i);
}
mfem::out<<"Demko points : "; demko.Print(mfem::out, 32);
Vector dref({0.0,0.075,0.225,0.3,
0.406122105546614987,
0.621569465634039919,
0.87385648854468001,1.0});
for (int i = 0; i < kv.GetNCP(); i++)
{
REQUIRE((demko[i] - dref[i]) == MFEM_Approx(0.0,1e-9,1e-9));
}
// Chebyshev spline
Vector a(kv.GetNCP());
Vector x(kv.GetNCP());
for ( int i = 0; i <x.Size(); i++)
{
x[i] = std::pow(-1.0, i);
}
kv.GetInterpolant(x, demko, a);
mfem::out<<"Chebyshev spline coeff : "; a.Print(mfem::out, 32);
Vector aref({1.0, -5.0, 5.0, -1.0,
3.24079982256718635,
-5.51623733136825933,
3.75648721902370486, -1.0});
for (int i = 0; i < kv.GetNCP(); i++)
{
REQUIRE((a[i] - aref[i]) == MFEM_Approx(0.0));
}
mfem::out<<"Chebyshev spline \n";
kv.PrintFunction(mfem::out, a, 21);
}
TEST_CASE("NURBS knotvector orientation", "[NURBS]")
{
// This will fail to load without CorrectPatchTopoOrientations
auto mesh_fname = "../../miniapps/nurbs/meshes/3patch-nurbs-flipedge.mesh";
Mesh mesh(mesh_fname, 1, 1);
REQUIRE(mesh.NURBSext->CheckPatches());
}
TEST_CASE("NURBS NC-patch mesh loading", "[NURBS]")
{
auto mesh_fname = GENERATE("../../data/nc3-nurbs.mesh",
"../../data/nc-nurbs3d.mesh");
Mesh mesh(mesh_fname, 1, 1);
const int dim = mesh.Dimension();
const int ne = dim == 2 ? 6 : 24;
REQUIRE(mesh.GetNE() == ne);
mesh.NURBSUniformRefinement();
REQUIRE(mesh.GetNE() == ne * std::pow(2, dim));
}
TEST_CASE("NURBS 1D variable-order mesh load", "[NURBS]")
{
auto mesh_fname = GENERATE("../../data/nurbs-segments2d.mesh",
"../../data/nurbs-segments3d.mesh",
"../../data/nurbs-segments2d-patches.mesh",
"../../data/nurbs-segments3d-patches.mesh",
"../../data/nurbs-segments2d-patches-multispan.mesh");
// Set up hard-coded expected values based on the input meshes.
// This should be easy to update as needed.
struct ExpectedSizes
{
int phys_dim, ne, nv, nkv;
Array<int> orders, ncp;
};
const auto expected = [&]() -> ExpectedSizes
{
ExpectedSizes e;
const bool is_2d = (std::string(mesh_fname).find("2d") != std::string::npos);
e.phys_dim = is_2d ? 2 : 3;
if (std::string(mesh_fname).find("multispan") != std::string::npos)
{
// multispan: 4 input segments w/ 9 elements, 13 vertices
e.ne = 9;
e.nv = 13;
e.nkv = 4;
e.orders = Array<int>({1, 2, 3, 4});
e.ncp = Array<int>({4, 4, 6, 5});
}
else
{
// standard: 3 elements, 6 vertices
e.ne = 3;
e.nv = 6;
e.nkv = 3;
e.orders = Array<int>({1, 2, 3});
e.ncp = Array<int>({2, 3, 4});
}
return e;
}();
Mesh mesh(mesh_fname, 1, 0);
// Basic mesh properties
REQUIRE(mesh.Dimension() == 1);
REQUIRE(mesh.SpaceDimension() == expected.phys_dim);
REQUIRE(mesh.GetNE() == expected.ne);
REQUIRE(mesh.GetNV() == expected.nv);
// NURBS extension must be present and 1D
REQUIRE(mesh.NURBSext != nullptr);
REQUIRE(mesh.NURBSext->Dimension() == 1);
// Check that we have the expected number of knotvectors
const int n_kv = mesh.NURBSext->GetNKV();
REQUIRE(n_kv == expected.nkv);
const Array<int> &orders = mesh.NURBSext->GetOrders();
REQUIRE(orders.Size() == n_kv);
// Validate each KnotVector's order and number of control points.
for (int i = 0; i < n_kv; i++)
{
const KnotVector *kv = mesh.NURBSext->GetKnotVector(i);
REQUIRE(kv != nullptr);
const int o = kv->GetOrder();
const int ncp = kv->GetNCP();
bool matched = false;
for (int j = 0; j < expected.orders.Size(); ++j)
{
if (o == expected.orders[j] && ncp == expected.ncp[j])
{
matched = true;
break;
}
}
REQUIRE(matched);
}
// Additionally, exercise degree elevation and ensure basic invariants hold
{
const int max_order = orders.Max();
mesh.DegreeElevate(max_order, max_order);
REQUIRE(mesh.NURBSext != nullptr);
REQUIRE(mesh.Dimension() == 1);
REQUIRE(mesh.SpaceDimension() == expected.phys_dim);
REQUIRE(mesh.NURBSext->Dimension() == 1);
const Array<int> &new_orders = mesh.NURBSext->GetOrders();
REQUIRE(new_orders.Size() == orders.Size());
for (int i = 0; i < new_orders.Size(); ++i)
{
REQUIRE(new_orders[i] == max_order);
}
}
}
TEST_CASE("NURBS 1D shared KnotVector in patches", "[NURBS]")
{
auto RequireSameKnotVector = [](const KnotVector &a, const KnotVector &b)
{
REQUIRE(a.GetOrder() == b.GetOrder());
REQUIRE(a.GetNCP() == b.GetNCP());
REQUIRE(a.Size() == b.Size());
for (int i = 0; i < a.Size(); i++)
{
REQUIRE( (a[i]-b[i]) == MFEM_Approx(0.));
}
};
SECTION("Same orientation")
{
const auto mesh_fname = "./data/nurbs-segments-same-orientation.mesh";
Mesh mesh(mesh_fname, 1, 0);
REQUIRE(mesh.NURBSext != nullptr);
REQUIRE(mesh.Dimension() == 1);
REQUIRE(mesh.SpaceDimension() == 2);
REQUIRE(mesh.NURBSext->GetNP() == 2);
REQUIRE(mesh.NURBSext->GetNKV() == 1);
const KnotVector *unique_kv = mesh.NURBSext->GetKnotVector(0);
REQUIRE(unique_kv != nullptr);
Array<const KnotVector *> pkv0, pkv1;
mesh.NURBSext->GetPatchKnotVectors(0, pkv0);
mesh.NURBSext->GetPatchKnotVectors(1, pkv1);
REQUIRE(pkv0.Size() == 1);
REQUIRE(pkv1.Size() == 1);
RequireSameKnotVector(*unique_kv, *pkv0[0]);
RequireSameKnotVector(*unique_kv, *pkv1[0]);
RequireSameKnotVector(*pkv0[0], *pkv1[0]);
}
SECTION("Opposite orientation")
{
const auto mesh_fname = "./data/nurbs-segments-opposite-orientation.mesh";
Mesh mesh(mesh_fname, 1, 0);
REQUIRE(mesh.NURBSext != nullptr);
REQUIRE(mesh.Dimension() == 1);
REQUIRE(mesh.SpaceDimension() == 2);
REQUIRE(mesh.NURBSext->GetNP() == 2);
REQUIRE(mesh.NURBSext->GetNKV() == 1);
const KnotVector *unique_kv = mesh.NURBSext->GetKnotVector(0);
REQUIRE(unique_kv != nullptr);
Array<const KnotVector *> pkv0, pkv1;
mesh.NURBSext->GetPatchKnotVectors(0, pkv0);
mesh.NURBSext->GetPatchKnotVectors(1, pkv1);
REQUIRE(pkv0.Size() == 1);
REQUIRE(pkv1.Size() == 1);
RequireSameKnotVector(*unique_kv, *pkv0[0]);
KnotVector flipped(*unique_kv);
flipped.Flip();
RequireSameKnotVector(flipped, *pkv1[0]);
}
}
TEST_CASE("NURBS NC-patch large meshes", "[MFEMData][NURBS]")
{
auto mesh_fname = GENERATE("bricks2D.mesh",
"schwarz2D.mesh",
"schwarz3D.mesh");
const std::string & fpath = (mfem_data_dir + "/nurbs/nc_patch/");
Mesh mesh(fpath + mesh_fname, 1, 1);
const int dim = mesh.Dimension();
const int ne = mesh.GetNE();
mesh.NURBSUniformRefinement();
REQUIRE(mesh.GetNE() == ne * std::pow(2, dim));
}