20 const std::vector<Eigen::Vector2d>&
vertices,
double expectedArea,
bool reverse=
false)
22 const double length=base.mesh.xMax()[0]-base.mesh.xMin()[0];
23 const auto& grain=base.poly.grains.begin()->second;
24 const auto normal=grain->reciprocalLatticeDirection(Eigen::Vector3d::UnitZ());
25 auto plane=base.dislocationDynamics().periodicGlidePlaneFactory.get(
model::GlidePlaneKey<3>(Eigen::Vector3d::Zero(),normal));
26 std::vector<OriginalNode> nodes(
vertices.size());
27 std::vector<std::pair<Eigen::Vector3d,const OriginalNode* const>> polygon;
33 const auto records=plane->polygonPatchIntersection(polygon,
true);
34 require(records.size()>=3,
"Walk returned no contour");
36 for(
size_t i=0;
i<records.size(); ++
i)
38 const Eigen::Vector3d
p=plane->referencePlane->globalPosition(std::get<0>(records[
i]));
39 const Eigen::Vector3d
q=plane->referencePlane->globalPosition(std::get<0>(records[(
i+1)%records.size()]));
41 const auto& provenance=std::get<6>(records[
i]);
46 require(segment.sourceIndex<polygon.size() && segment.sinkIndex==(segment.sourceIndex+1)%polygon.size(),
47 "Invalid original directed segment indices");
48 require(segment.source==polygon[segment.sourceIndex].second && segment.sink==polygon[segment.sinkIndex].second,
49 "Segment provenance lost its original node pointers");
51 if(provenance.polygonLocation)
53 const auto& location=*provenance.polygonLocation;
54 verifySegment(location.segment);
55 require(location.parameter>=0 && location.parameter<=1,
"Invalid original segment parameter");
56 const Eigen::Vector3d expected=(1-location.parameter)*polygon[location.segment.sourceIndex].first+
57 location.parameter*polygon[location.segment.sinkIndex].first;
58 require((expected-
p).norm()<1.e-6,
"Original segment provenance does not reconstruct the intersection");
60 const auto metadataPatch=plane->getPatch(std::get<1>(records[
i]));
62 size_t incidentEdges=0;
63 for(
const auto& edge : metadataPatch->edges())
65 const Eigen::Vector2d d=*edge->sink-*edge->source;
66 const Eigen::Vector2d delta=std::get<0>(records[
i])-*edge->source;
67 const double t=delta.dot(d)/d.squaredNorm();
68 if(
t>=-1.e-8 &&
t<=1+1.e-8 && (delta-
t*d).norm()<1.e-6)
71 for(
const auto* face : edge->meshIntersection->faces)
73 physical=physical || !face->periodicFacePair.second ||
74 face->regionIDs!=face->periodicFacePair.second->regionIDs;
78 require(provenance.onPhysicalBoundary==physical && provenance.atPatchCorner==(incidentEdges>1),
79 "Boundary metadata disagrees with actual mesh faces");
81 for(
size_t neighbor : {(
i+records.size()-1)%records.size(),(
i+1)%records.size()})
83 seam=seam || ((std::get<0>(records[
i])-std::get<0>(records[
neighbor])).norm()<1.e-6 &&
84 (std::get<1>(records[
i])-std::get<1>(records[
neighbor])).norm()>1.e-6);
86 require(provenance.crossesPeriodicSeam==seam,
"Seam classification disagrees with contour patch transitions");
87 if(provenance.originalVertex)
89 require(provenance.kind==Kind::OriginalVertex &&
90 std::get<5>(records[
i])==polygon[*provenance.originalVertex].second,
91 "Original vertex identity changed");
93 else if(provenance.onPhysicalBoundary)
95 require(provenance.kind==(provenance.polygonLocation ? Kind::PhysicalBoundaryIntersection : Kind::PhysicalBoundaryCorner),
96 "Physical boundary node misclassified");
100 require(provenance.kind==Kind::PeriodicCrossing && provenance.crossesPeriodicSeam,
101 "Periodic crossing misclassified");
103 if(provenance.incomingArc==Arc::OriginalSegment)
105 require(provenance.incomingOriginalSegment.has_value(),
"Polygon arc has no original segment");
106 verifySegment(*provenance.incomingOriginalSegment);
107 const auto& origin=*provenance.incomingOriginalSegment;
108 const Eigen::Vector3d
a=polygon[origin.sourceIndex].first;
109 const Eigen::Vector3d d=polygon[origin.sinkIndex].first-
a;
110 const Eigen::Vector3d previous=plane->referencePlane->globalPosition(std::get<0>(records[(
i+records.size()-1)%records.size()]));
111 require((
p-
a).cross(d).norm()<1.e-6 && (previous-
a).cross(d).norm()<1.e-6,
112 "Incoming polygon arc attributed to wrong segment");
116 require(!provenance.incomingOriginalSegment,
"Boundary or seam arc falsely attributed to original segment");
117 if(provenance.incomingArc==Arc::PeriodicSeam)
119 require(provenance.crossesPeriodicSeam &&
120 (std::get<0>(records[
i])-std::get<0>(records[(
i+records.size()-1)%records.size()])).norm()<1.e-6,
121 "Seam arc is not a zero-length periodic transition");
125 const Eigen::Vector3d mapped=
p+std::get<1>(records[
i]);
126 require((mapped.array()>=base.mesh.xMin().array()-1.e-6).all() &&
127 (mapped.array()<=base.mesh.xMax().array()+1.e-6).all(),
"Mapped node outside RVE");
128 if(!std::get<5>(records[
i]))
130 require(std::get<2>(records[
i]).
first>=0,
"Inserted boundary node lacks edge ID");
131 const auto patch=plane->getPatch(std::get<1>(records[
i]));
132 for(
short id : {std::get<2>(records[
i]).first,std::get<2>(records[
i]).second})
136 const auto edge=patch->edges().at(
id);
137 const Eigen::Vector2d direction=*edge->sink-*edge->source;
138 const Eigen::Vector2d delta=std::get<0>(records[
i])-*edge->source;
139 const double t=delta.dot(direction)/direction.squaredNorm();
140 require(
t>=-1.e-8 &&
t<=1+1.e-8 && (delta-
t*direction).norm()<1.e-6,
141 "Intersection record references the wrong patch edge");
146 require(std::abs(
area/(length*length)-(reverse ? -expectedArea : expectedArea))<1.e-8,
147 "Wrong retained area or orientation");
148 const bool retainsOriginal=std::any_of(records.begin(),records.end(),[](
const auto& record)
150 return std::get<5>(record)!=nullptr;
152 require(!retainsOriginal || std::get<5>(records.back())!=
nullptr,
"Contour must finish at an original node");
155 const auto runtime=plane->polygonPatchIntersection(polygon,
false);
156 require(runtime.size()==records.size(),
"Runtime walk differs from generator walk");
157 std::vector<std::pair<Eigen::Vector2d,const OriginalNode* const>> localPolygon;
158 for(
const auto&
vertex : polygon)
160 localPolygon.emplace_back(plane->referencePlane->localPosition(
vertex.first),
vertex.second);
162 const auto localRecords=plane->polygonPatchIntersection(localPolygon,
true);
163 for(
size_t i=0;
i<records.size(); ++
i)
165 const auto&
a=std::get<6>(records[
i]);
166 const auto&
b=std::get<6>(localRecords.at(
i));
167 require(
a.kind==
b.kind &&
a.originalVertex==
b.originalVertex &&
a.incomingArc==
b.incomingArc,
168 "2D and 3D provenance disagree");
322 const std::filesystem::path root(
argv[1]);
323 const auto folder=std::filesystem::temp_directory_path()/
324 (
"periodic-walk-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
325 const auto input=folder/
"inputFiles";
326 std::filesystem::create_directories(input);
327 std::filesystem::create_directories(folder/
"F");
328 std::filesystem::copy_file(root/
"Library/Materials/W.txt",input/
"W.txt");
329 std::filesystem::copy_file(root/
"Library/Meshes/unitCube24.msh",input/
"mesh.msh");
335 for(
const auto& face:region.second->faces())
337 if(face.second->periodicFacePair.first.x()>0.9)
345 for(
int rank : {0,1,3})
349 std::ofstream
f(input/
"DefectiveCrystal.txt");
350 f<<
"physics=DislocationDynamics;\nuseFEM=0;\nNsteps=0;\nmaxResolveSteps=0;\ndtMax=1;\noutputFrequency=1;\noutputBinary=0;\nstartAtTimeStep=0;\nEwaldLengthFactor=0;\n";
351 f<<
"periodicImageSize="<<(rank==3 ?
"0 0 0" :
"0")<<
";\n";
352 std::ofstream
p(input/
"polycrystal.txt");
353 const double length=200/std::sqrt(3.0);
354 p<<std::setprecision(17)<<
"materialFile=W.txt;\nmeshFile=mesh.msh;\nabsoluteTemperature=300;\nC2G1=1 0 0 0 1 0 0 0 1;\nF="
355 <<length<<
" 0 0 0 "<<length<<
" 0 0 0 "<<length<<
";\nX0=0 0 0;\nperiodicFaceIDs=";
358 p<<(rank==3 ? -1 : seed);
363 for(
bool reverse:{
false,
true})
365 exercise(base,{{-.2,-.2},{.2,-.2},{.2,.2},{-.2,.2}},.16,reverse);
366 std::vector<Eigen::Vector2d> balanced{{-.2,-.2},{.2,.2},{-.2,.2},{.2,-.2}};
367 for(
size_t start=0;start<balanced.size();++start)
369 exercise(base,balanced,0.0,reverse);
370 auto clippedBalanced=balanced;
371 for(
auto&
vertex : clippedBalanced)
375 exercise(base,clippedBalanced,0.0,reverse);
376 std::rotate(balanced.begin(),balanced.begin()+1,balanced.end());
379 exercise(base,{{.1,.3},{.5,.5},{.1,.4}},.02,reverse);
382 exercise(base,{{-.2,.2},{1.2,.2},{1.2,.8},{-.2,.8}},.84,reverse);
383 exercise(base,{{.1,.1},{.9,.9},{-.1,.3}},.16,reverse);
389 for(
double epsilon : {1.e-3,1.e-4,1.e-5})
391 std::vector<Eigen::Vector2d> overlap={{0,.5},{.1,.4},{0,.4},
395 for(
size_t start=0; start<overlap.size(); ++start)
397 exercise(base,overlap,expected,reverse);
398 std::rotate(overlap.begin(),overlap.begin()+1,overlap.end());
403 std::vector<Eigen::Vector2d> surfaceReturn={{0,.4},{.1,.3},{.1,.5001},
404 {.08,.5},{.05,.4},{.3,.3},{.3,.5001},{.28,.5},{0,.5}};
405 for(
size_t start=0; start<surfaceReturn.size(); ++start)
407 exercise(base,surfaceReturn,.051,reverse);
408 std::rotate(surfaceReturn.begin(),surfaceReturn.begin()+1,surfaceReturn.end());
411 exercise(base,{{-.2,.8},{-.2,.2},{.2,.2},{.2,.8}},.12,reverse);
412 exercise(base,{{-.2,.2},{.2,.2},{.2,.5},{-.2,.5}},.12,reverse);
421 for(
double offset : {0.0,0.4,0.5,0.6,1.5})
423 std::vector<Eigen::Vector2d> folded={{-.4,0},{.2,.6},{-.2,.6},{.4,0}};
424 for(
auto& point : folded)
428 for(
size_t start=0; start<folded.size(); ++start)
431 std::rotate(folded.begin(),folded.begin()+1,folded.end());
435 exercise(base,{{-.2,-.2},{1.2,-.2},{1.2,.2},{-.2,.2}},.56,reverse);
437 exercise(base,{{-.2,.2},{1.2,.2},{1.2,.8},{-.2,.8}},.42,reverse);
439 exercise(base,{{-.4,.1},{-.2,.1},{-.2,.7},{.2,.7},{.2,.1},{.4,.1},{.4,.9},{-.4,.9}},.08,reverse);
446 std::vector<Eigen::Vector2d> folded={{-.4,0},{.2,.6},{-.2,.6},{.4,0}};
447 for(
size_t start=0;start<folded.size();++start)
450 std::rotate(folded.begin(),folded.begin()+1,folded.end());
454 exercise(base,{{0,.2},{.2,.8},{1,1},{1,-1},{-1,-1},{-1,1},{-.2,.8}},-.97,reverse);
456 exercise(base,{{0,0},{.8,0},{.8,.8},{0,.8}},.25,reverse);
464 std::ofstream
p(input/
"polycrystal.txt");
465 const double length=200/std::sqrt(3.0);
466 p<<std::setprecision(17)<<
"materialFile=W.txt;\nmeshFile=mesh.msh;\nabsoluteTemperature=300;\nC2G1=1 0 0 0 1 0 0 0 1;\nF="
467 <<length<<
" 0 0 0 "<<length<<
" 0 0 0 "<<length<<
";\nX0=0 0 0;\nperiodicFaceIDs=;\n";
473 for(
const auto& region : base.mesh.regions())
475 for(
const auto& face : region.second->faces())
477 if(face.second->outNormal().x() < -.99)
479 left=face.second.get();
481 if(face.second->outNormal().x() > .99)
483 right=face.second.get();
488 const Eigen::Vector3d shift((base.mesh.xMax()[0]-base.mesh.xMin()[0]),0,0);
491 for(
bool reverse : {
false,
true})
493 exercise(base,{{-.2,.1},{1.2,.1},{1.2,.7},{-.2,.7}},.415,reverse);
496 exercise(base,{{1.95,.4},{2.05,.4},{2.05,.45},{1.95,.45}},.005,reverse);
500 std::filesystem::copy_file(root/
"Library/Meshes/bicrystal_48.msh",input/
"mesh.msh",
501 std::filesystem::copy_options::overwrite_existing);
503 std::ofstream
p(input/
"polycrystal.txt");
504 const double length=200/std::sqrt(3.0);
505 p<<std::setprecision(17)<<
"materialFile=W.txt;\nmeshFile=mesh.msh;\nabsoluteTemperature=300;\nC2G1=1 0 0 0 1 0 0 0 1;\nC2G2=1 0 0 0 1 0 0 0 1;\nF="
506 <<length<<
" 0 0 0 "<<length<<
" 0 0 0 "<<length<<
";\nX0=0 0 0;\nperiodicFaceIDs=-1;\n";
509 std::ofstream
f(input/
"DefectiveCrystal.txt");
510 f<<
"physics=DislocationDynamics;\nuseFEM=0;\nNsteps=0;\nmaxResolveSteps=0;\ndtMax=1;\noutputFrequency=1;\noutputBinary=0;\nstartAtTimeStep=0;\nperiodicImageSize=0 0 0;\nEwaldLengthFactor=0;\n";
514 for(
bool reverse : {
false,
true})
516 exercise(base,{{-.4,-.1},{-.2,-.1},{-.2,.1},{-.4,.1}},.04,reverse);
517 exercise(base,{{-.4,-.1},{-.6,-.1},{-.6,.1},{-.4,.1}},-.02,reverse);
520 std::filesystem::remove_all(folder);
521 std::cout<<
"PASS: periodic polygon boundary walking\n";
524 catch(
const std::exception& error)
526 std::cerr<<error.what()<<
'\n';