21 const double tol=1.e-7*(
mesh.xMax()-
mesh.xMin()).norm();
24 for(
const auto& entry : region.second->faces())
26 const auto* face=entry.second.get();
27 if(!seen.insert(face).second)
31 for(
const auto*
triangle : face->simplices())
33 const auto found=
mesh.periodicSimplexPairs().find(
triangle);
34 if(!face->periodicFacePair.second)
36 require(found==
mesh.periodicSimplexPairs().end(),
"Inactive or internal simplex was paired");
40 require(found!=
mesh.periodicSimplexPairs().end(),
"Active face triangle missing");
41 const auto& pair=found->second;
42 require(
triangle->isBoundarySimplex() && pair.partner->isBoundarySimplex(),
"Internal simplex paired");
43 require(face->periodicFacePair.second->simplices().count(pair.partner),
"Partner on wrong face");
44 const auto& reverse=
mesh.periodicSimplexPairs().at(pair.partner);
45 require(reverse.partner==
triangle && (reverse.shift+pair.shift).norm()<tol,
"Bad reciprocal simplex pair");
46 require((
triangle->outNormal()+pair.partner->outNormal()).norm()<1.e-7,
"Normals not antiparallel");
47 for(
size_t i=0;
i<3;++
i)
49 const size_t j=pair.vertexPermutation[
i];
50 require(
j<3 && reverse.vertexPermutation[
j]==
i,
"Bad inverse vertex permutation");
51 require((
triangle->vertices()[
i]->P0+pair.shift-pair.partner->vertices()[
j]->P0).norm()<tol,
"Bad vertex translation");
56 require(
mesh.periodicSimplexPairs().size()==expected,
"Unexpected simplex pairs");
62 const auto&
mesh=fe.mesh;
63 const double tol=1.e-7*(
mesh.xMax()-
mesh.xMin()).norm();
64 using Key=std::pair<size_t,size_t>;
65 std::set<Key> expected;
68 for(
const auto& entry :
mesh.periodicSimplexPairs())
71 const auto& pair=entry.second;
72 const auto&
a=fe.elements().at(
triangle->parents().begin()->first);
73 const auto&
b=fe.elements().at(pair.partner->parents().begin()->first);
75 for(
size_t i=0;
i<
a.nodesPerElement;++
i)
77 const auto& source=
a.node(
i);
84 for(
size_t j=0;
j<
b.nodesPerElement;++
j)
86 const auto& sink=
b.node(
j);
87 if((source.P0+pair.shift-sink.P0).norm()<tol)
89 expected.emplace(std::minmax(source.gID,sink.gID));
93 require(matches==1,
"Geometric FEM partner is missing or ambiguous");
95 require(count==
size_t((
order+1)*(
order+2)/2),
"Wrong number of face nodes for polynomial order");
97 require(fe.periodicNodePairs().size()==expected.size(),
"FEM pairing coverage or deduplication failure");
98 std::vector<size_t> parents(fe.nodeSize());
99 std::iota(parents.begin(),parents.end(),0);
100 const auto root=[&](
size_t i)
108 for(
const auto& pair : expected)
110 const size_t a=root(pair.first),
b=root(pair.second);
111 parents[std::max(
a,
b)]=std::min(
a,
b);
113 std::set<size_t> vertexNodes;
114 for(
const auto& entry : fe.mesh2femIDmap())
116 vertexNodes.insert(entry.second->gID);
119 for(
const auto& pair : fe.periodicNodePairs())
121 require(expected.count({pair.source,pair.sink}),
"Spurious periodic FEM pair");
122 require((fe.node(pair.source).P0+pair.shift-fe.node(pair.sink).P0).norm()<tol,
"Wrong FEM pair offset");
124 require((fe.periodicShift(pair.source)+pair.shift-fe.periodicShift(pair.sink)).norm()<tol,
"Edge/corner path inconsistency");
125 extraNodes+=vertexNodes.count(pair.source)==0;
127 std::map<size_t,size_t> orbitSizes;
128 for(
size_t n=0;n<fe.nodeSize();++n)
130 require(fe.periodicRepresentative(n)==root(n),
"Wrong periodic equivalence class");
131 require((fe.node(root(n)).P0+fe.periodicShift(n)-fe.node(n).P0).norm()<tol,
"Wrong representative offset");
132 ++orbitSizes[root(n)];
136 require(expected.empty(),
"Nonperiodic domain has paired FEM nodes");
140 require(!expected.empty(),
"Periodic domain has no FEM pairs");
141 require(
order==1 || extraNodes>0,
"Higher-order nodes were not paired");
148 for(
const auto& orbit : orbitSizes)
150 largest=std::max(largest,orbit.second);
152 require(largest>=4,
"No edge/corner equivalence class exercised");
155 require(largest>=8,
"Bicrystal corner equivalence class is incomplete");
166 if(
mesh.periodicBasis().cols()==3)
168 auto twoDirections=partial;
171 for(
const auto& entry : region.second->faces())
173 const auto& pair=entry.second->periodicFacePair;
174 if(pair.second && pair.first.cross(
mesh.periodicBasis().col(1)).norm()<1.e-7*pair.first.norm()*
mesh.periodicBasis().col(1).norm())
176 twoDirections.insert(
int(entry.first));
180 if(twoDirections.size()==2)
185 require(twoDirections.size()==2,
"No second independent seed face");
186 mesh.identifyParallelFaces(twoDirections);
187 require(
mesh.periodicBasis().cols()==2,
"Two-face selection did not select two vectors");
194 mesh.identifyParallelFaces(partial);
196 require(
mesh.periodicBasis().cols()==1,
"Partial selection did not select one vector");
199 mesh.identifyParallelFaces({});
203 mesh.identifyParallelFaces({-1});
207 require(fe.
nodeSize()==nodeCount,
"Periodicity changed FEM node numbering");
215 const std::filesystem::path meshes=std::filesystem::path(
argv[1])/
"Library/Meshes";
216 for(
const auto* name : {
"bicrystal_48.msh",
"poly30_periodicXYZ.msh",
"polycrystalCube_10_periodicX.msh",
"polycrystalCube_10_periodicXY.msh"})
220 const Eigen::Matrix3d rotation=Eigen::AngleAxisd(.37,Eigen::Vector3d(1,2,3).normalized()).toRotationMatrix();
223 const int expectedRank=std::string(name).find(
"periodicXY.msh")!=std::string::npos ? 2 :
224 (std::string(name).find(
"periodicX.msh")!=std::string::npos ? 1 : 3);
225 require(
mesh.periodicBasis().cols()==expectedRank,
"Unexpected tutorial lattice rank");
226 std::set<int> partial;
229 for(
const auto& entry : region.second->faces())
231 const auto& pair=entry.second->periodicFacePair;
232 if(pair.second && pair.first.cross(
mesh.periodicBasis().col(0)).norm()<1.e-7*pair.first.norm()*
mesh.periodicBasis().col(0).norm())
234 partial={int(entry.first)};
243 require(!partial.empty(),
"No single-vector seed face");
247 std::cout<<
"PASS periodic FEM orders 1,2,3: "<<name<<std::endl;
250 catch(
const std::exception& error)
252 std::cerr<<error.what()<<std::endl;
bool periodicLatticeContains(const Eigen::Matrix< double, dim, Eigen::Dynamic > &basis, const Eigen::Matrix< double, dim, 1 > &shift, double tolerance)