from_gmsh needs help updating to gmsh version 4
KennethEJansen opened this issue · comments
Ok so this is not mainline SCOREC/core stuff and maybe has not been touched in 5 years BUT it would be really cool to keep this capability to load meshes created by gmsh into mds. When I tried to use the tools described on the
gmshToPumi page with a recent build of gmsh it failed and so I looked at the gmsh manual to see that this tool was obviously written for a gmsh version 2 mesh which they won't write anymore. Version 4 writes the coordinates in model entity group "blocks" and does the same for elements. I thought I mapped all of READS correctly and it does survive reading the mesh but this crashes on verify for the example case provided at the link above (notched2d is a 2D mesh). Suspects/doubts are: 1) It looks like gmsh starts numbering tags from 1. 2) I am far from certain that I have ported everything that readElement
is doing in apf
. Here are my code edits.
diff --git a/mds/mdsGmsh.cc b/mds/mdsGmsh.cc
index a2584a23..7a0f4049 100644
--- a/mds/mdsGmsh.cc
+++ b/mds/mdsGmsh.cc
@@ -115,23 +115,32 @@ void checkMarker(Reader* r, char const* marker)
PCU_ALWAYS_ASSERT(startsWith(marker, r->line));
}
-void readNode(Reader* r)
+void readNode(Reader* r, int bm)
{
- long id;
Node n;
apf::Vector3& p = n.point;
- sscanf(r->line, "%ld %lf %lf %lf", &id, &p[0], &p[1], &p[2]);
- r->nodeMap[id] = n;
+ sscanf(r->line, "%lf %lf %lf", &p[0], &p[1], &p[2]);
+ r->nodeMap[bm] = n;
getLine(r);
}
void readNodes(Reader* r)
{
seekMarker(r, "$Nodes");
- long n = getLong(r);
- getLine(r);
- for (long i = 0; i < n; ++i)
- readNode(r);
+ long Num_EntityBlocks,Num_Nodes,Nodes_Block,junk1,junk2,junk3;
+ sscanf(r->line, "%ld %ld %ld %ld", &Num_EntityBlocks, &Num_Nodes, &junk1, &junk2);
+ getLine(r); // because readNode gets the next line we need this outside for Nodes_Block
+ for (long i = 0; i < Num_EntityBlocks; ++i){
+ sscanf(r->line, "%ld %ld %ld %ld", &junk1, &junk2, &junk3, &Nodes_Block);
+ long blockMap[Nodes_Block];
+ for (long j = 0; j < Nodes_Block; ++j){
+ getLine(r);
+ sscanf(r->line, "%ld", &blockMap[j]);
+ }
+ getLine(r);
+ for (long j = 0; j < Nodes_Block; ++j)
+ readNode(r,blockMap[j]);
+ }
checkMarker(r, "$EndNodes");
}
@@ -146,22 +155,15 @@ apf::MeshEntity* lookupVert(Reader* r, long nodeId, apf::ModelEntity* g)
return n.entity;
}
-void readElement(Reader* r)
+void readElement(Reader* r, long gmshType,long gtag)
{
- long id = getLong(r);
- long gmshType = getLong(r);
+ long id = getLong(r); // tag in 2 and 4
if (isQuadratic(gmshType))
r->isQuadratic = true;
int apfType = apfFromGmsh(gmshType);
PCU_ALWAYS_ASSERT(0 <= apfType);
int nverts = apf::Mesh::adjacentCount[apfType][0];
int dim = apf::Mesh::typeDimension[apfType];
- long ntags = getLong(r);
- PCU_ALWAYS_ASSERT(ntags >= 2);
- getLong(r); /* discard physical type */
- long gtag = getLong(r);
- for (long i = 2; i < ntags; ++i)
- getLong(r); /* discard all other element tags */
apf::ModelEntity* g = r->mesh->findModelEntity(dim, gtag);
apf::Downward verts;
for (int i = 0; i < nverts; ++i) {
@@ -181,10 +183,19 @@ void readElement(Reader* r)
void readElements(Reader* r)
{
seekMarker(r, "$Elements");
- long n = getLong(r);
- getLine(r);
- for (long i = 0; i < n; ++i)
- readElement(r);
+ long Num_EntityBlocks,Num_Elements,Elements_Block,Edim,gtag,gmshType,junk1,junk2;
+ sscanf(r->line, "%ld %ld %ld %ld", &Num_EntityBlocks, &Num_Elements, &junk1, &junk2);
+ getLine(r);
+ for (long i = 0; i < Num_EntityBlocks; ++i){
+ sscanf(r->line, "%ld %ld %ld %ld", &Edim, >ag, &gmshType, &Elements_Block);
+ getLine(r);
+ for (long j = 0; j < Elements_Block; ++j) {
+ if(Edim==2) // for now this is hardcoded but need a way to find largest dim of gmsh or command line argument?
mand line argument?
+ readElement(r,gmshType,gtag);
+ else
+ getLine(r); // do not put lower dim elements in mds
+ }
+ }
checkMarker(r, "$EndElements");
}
Here is the failed output. Obviously an edge that should be classified on a model edge
is classified on a model face but I am not seeing where my code change is goofing that up.
mpirun -np 1 from_gmsh notch2D.dmg notch2D.msh notch2D-serial.smb
APF FAILED: apf::Verify: edge with 1 adjacent faces
centroid: (0, 0.236364, 0)
based on the following:
- edge is classified on a model face
we would expect the adjacent face count to be exactly 2
I guess I found my problem. It wants the 1D elements. changed the Edim==2
to Edim>=1
and it verified.
Since Dan is retired and since Ben drafted me into this SSC challenge, I am drafting him in to help.
I am trying to map version 4 gmsh information contained between $Entities and $EndEntities in the .msh file. I did this by hand and got a verified mesh but boundary conditions on model face 0 did not show up for duty in PHASTA which makes me think that one or more places within SCOREC/core there is an assumption that model tags are unique (e.g., you cant have tag=1 on a face AND a vertexz AND a region, AND an edge like gmsh allows. Further, if you are going to use extrusions and the like, gmsh is going to make is own numbering so you can't really control that either.
SO my new function requirements are:
- Can't load a dmg file because we currently don't have a reliable way to create on from a .msh file (other than Dan's limited cpp route which is orphaned). =>
gmshUT.cc
(UniqueTags) is forked off ofgmsh.sh
/projects/tools/SCOREC-core/core (ExtrusionTagsOffDev)$ ls test/gmsh*.cc
test/gmsh.cc test/gmshUT.cc
23c23,24
< apf::Mesh2* m = apf::loadMdsFromGmsh(gmi_load(argv[1]), argv[2]);
---
> // apf::Mesh2* m = apf::loadMdsFromGmsh(gmi_load(argv[1]), argv[2]);
> apf::Mesh2* m = apf::loadMdsDmgFromGmsh(argv[1], argv[2]);
- so obviously that is calling a slightly modified function that is delaying the loading of the dmg file that does not exist at runtime viz (adding line numbers because both of these functions are one after the other in
mds/mdsGmsh.cc
)
357 void gmshFindDmg(const char* fnameDmg, const char* filename, int* emap)
358 {
359 Reader r;
360 initReader(&r, m, filename);
361 readEntities(&r, emap);
362 freeReader(r);
363 }
364
365 }
366
367 namespace apf {
368
369 Mesh2* loadMdsFromGmsh(gmi_model* g, const char* filename)
370 {
371 Mesh2* m = makeEmptyMdsMesh(g, 0, false);
372 readGmsh(m, filename);
373 return m;
374 }
375
376 Mesh2* loadMdsDmgFromGmsh(const char*fnameDmg, const char* filename)
377 {
378 int emap[100]; \\letting 100 be the max entities until a stronger C++ coder fixes this
379 gmshFindDmg(fnameDmg, filename, &emap); // new function that scans $Entities and writes a d mg
380 Mesh2* m = makeEmptyMdsMesh(gmi_load(fnameDmg), 0, false);
381 readGmsh(m, filename,*emap);
382 return m;
- so all the really tough stuff happens in readEntities but I am not at all confident I have passed the array of ints emap down to that routine properly sich that readEntities can fill it with numbers for use there and later to map from gmsh entity ordering to something SCOREC/core can keep straight (e.g, unique tags). Here finally is readEntities. I have not even tried to compile it yet because I want to sleep tonight and I know if I get started that might not happen. Still I wanted to get the code design in front of Ben's eyes so that he can tell me all the stupid stuff I am doing
127 void readEntities(Reader* r,const char* fnameDmg, int* emap, int* ne)
128 {
129 seekMarker(r, "$Entities");
130 long nlde,ilde,iud,junk1,junk2,junk3;
131 double x,y,z
132 FILE* f = fopen(fnameDmg, "w");
133 sscanf(r->line, "%ld %ld %ld %ld", &nme[0], &nme[1], &nme[2], &nme[3]);
134 fprintf(f, "%ld %ld %ld %ld \n", nme[3], nme[2], nme[1], nme[0]); // just reverse order
135 fprintf(f, "%lf %lf %lf \n ", 0, 0, 0); // Probaby model bounding box?
136 fprintf(f, "%lf %lf %lf \n", 0, 0, 0); //
137 getLine(r); // because readNode gets the next line we need this outside for Nodes_Block
138 int entCnt=0;
139 for (long i = 0; i < nme[0]; ++i){
140 sscanf(r->line, "%ld %lf %lf %lf", &tag, &x, &y, &z);
141 emap[entCnt]=tag; // map(dmgTag)=gmshTag is backward how we need but gmshTags dup
142 entCnt++; // vertex entities start from 1
143 fprintf(f, "%ld %lf %lf %lf \n",entCnt,x,y,z);
144 getLine(r);
145 }
146 for (long i = 0; i < nme[1]; ++i){
147 sscanf(r->line, "%ld %lf %lf %lf", &tag, &x, &y, &z);
148 emap[entCnt]=tag;
149 entCnt++;
150 fprintf(f, "%ld", entCnt);
151 sscanf(r->line, "%lf %lf %lf %ld", &x, &y, &z, &iud);
152 for(long j =0; j < iud; j++) sscanf(r->line,ijunk);
153 sscanf(r->line, "%ld", &nlde); // 2 in straight edged models but...
154 for(long j =0; j < nlde; j++) {
155 sscanf(r->line,ilde);
156 for(int k=0; k < nme[0]) { // have to search since map is backwards
157 if(emap[k] == ilde)
158 fprintf(f, "%ld", k+1); // modVerts started from 1
159 }
160 fprintf(f, "\n");
161 getLine(r);
162 }
163 for (long i = 0; i < nme[2]; ++i){
164 sscanf(r->line, "%ld %lf %lf %lf", &tag, &x, &y, &z);
165 emap[entCnt]=tag; // new face tag map
166 entCnt++;
167 fprintf(f, "%ld %ld \n", entCnt, 1);
168 sscanf(r->line, "%lf %lf %lf %ld", &x, &y, &z, &iud);
169 for(long j =0; j < iud; j++) sscanf(r->line,ijunk);
170 sscanf(r->line, "%ld", &nlde);
171 fprintf(f, "%ld \n", nlde);
172 for(long j =0; j < nlde; j++) {
173 sscanf(r->line,ilde);
174 if(ilde > 0 )
175 isign=1;
176 else
177 isign=0;
178 for(int k=nme[0]; k < nme[0]+nme[1]) { // have to search since map is backwards
179 if(emap[k] == ilde)
180 fprintf(f, "%ld %ld \n", k+1,isign);
181 }
182 }
183 getLine(r);184 }
185 for (long i = 0; i < nme[3]; ++i){ //not even sure that this all hangs with nme[3] > 1 but..
186 sscanf(r->line, "%ld %lf %lf %lf", &tag, &x, &y, &z);
187 emap[entCnt]=tag; // new region tag map
188 entCnt++;
189 fprintf(f, "%ld %ld \n", entCnt, 1);
190 sscanf(r->line, "%lf %lf %lf %ld", &x, &y, &z, &iud);
191 for(long j =0; j < iud; j++) sscanf(r->line,ijunk);
192 sscanf(r->line, "%ld", &nlde);
193 fprintf(f, "%ld \n", nlde);
194 for(long j =0; j < nlde; j++) {
195 sscanf(r->line,ilde);
196 if(ilde > 0 )
197 isign=1;
198 else
199 isign=0;
200 for(int k=nme[0]+nme[1]; k < nme[0]+nme[1]+nme[2]) { // have to search since map is back wards
201 if(emap[k] == ilde)
202 fprintf(f, "%ld %ld \n", k+1,isign);
203 }
204 }
205 getLine(r);
206 }
207 checkMarker(r, "$EndEntities");
208 }
Here is the hand made dmg (on the left) with "comments" off to the right of the gmsh .msh file information that the above code meant to read, translate and write out the dmg info (stuff on the left). Of course the new code is also renumbering entities to make them unique and saving the map so that the mds file has a consistent classification (e.g,. run the gmsh data through the map to get mesh classified against unique tags).
.dmg .msh (between $Entities and $EndEntities)
1 6 12 8 reverse 8 12 6 1
0 0 0 dmg has two lines with 3 zeros
0 0 0
1 0 -1 0 drop last zero for 8 verts
2 10 -1 0
3 10 1 0
4 0 1 0
5 0 -1 0.1
6 10 -1 0.1
10 10 1 0.1
14 0 1 0.1
1 1 2 1 0 -1 0 10 -1 0 0 2 1 -2
2 2 3 2 10 -1 0 10 1 0 0 2 2 -3
3 3 4 keep keep negate
4 4 1 ends original loop I defined
6 5 6 starts opposite/extruded loop and flows
7 6 10
8 10 14
9 14 5 ends extruded loop
11 1 5 extruded edges in order of root face
12 2 6
16 3 10
20 4 14 last edge
1 1 1 0 -1 0 10 1 0 0 4 1 2 3 4
4 ft skip 7 ne etags in loop order (note this one curls in)
1 1
2 1
3 1
4 1 pos->1
13 1 13 0 -1 0 10 -1 0.1 0 4 1 12 -6 -11
4 neg-> 0
1 1
12 1
6 0
11 0
17 1 17 10 -1 0 10 1 0.1 0 4 2 16 -7 -12
4
2 1
16 1
7 0
12 0
21 1 21 0 1 0 10 1 0.1 0 4 3 20 -8 -16
4
3 1
20 1
8 0
16 0
25 1 25 0 -1 0 0 1 0.1 0 4 4 11 -9 -20
4
4 1
11 1
9 0
20 0
26 1 26 0 -1 0.1 10 1 0.1 0 4 6 7 8 9
4
6 1
7 1
8 1
9 1 nf pos-> 1
1 1 1 0 -1 0 10 1 0.1 0 6 -1 26 13 17 21 25
6 neg-> 0
1 0
26 1
13 1
17 1
21 1
25 1
(base) kjansen@portal1: /projects/tools/SCOREC-core/core (gmshv4Load2MDS)$ git commit -am "made a branch for the not yet finished attempt at a reader from .msh directly into mds making a dmg along the way"
[gmshv4Load2MDS cbfc9ce] made a branch for the not yet finished attempt at a reader from .msh directly into mds making a dmg along the way
2 files changed, 150 insertions(+), 7 deletions(-)
create mode 100644 test/gmshUT.cc
In case you would rather track and comment on my efforts here (note however this should be diffed withExtrusionTagsOffDev
Which I don't think has been merged onto develop of SCOREC/core yet even though I did make my changes off of the dev branch at the time (as the name implies).
@cwsmith had mentioned keeping the option to read a V2 gmsh.
That would not be hard for the specific workflow that is described here which builds the dmg and the gmsh .geo file from cpp primitive commands but, this is limited and has no developer so it may be confusing to support that and this direct from .msh approach in the same code. Specifically, v2 does not have the model information in the .msh file that is needed to build the dmg file unless they are built from the same cpp source.
I may not have time to fully dive into this, but it may help to point out that there is Gmsh reader code in Omega_h as well:
https://github.com/sandialabs/omega_h/blob/main/src/Omega_h_gmsh.cpp
It seems to have support for format V4. Perhaps copying this to SCOREC/core would be a path forward?
Soooo the code that I put into PR#372 satisfies verify but based on PHASTAs reaction to the code I am fairly certain that mesh vertices that should be classified on model vertices are not getting that classification.
Further, and possibly the same issue, when I assign a BC to a model face, normal chef protocol is to reconcile through inheritance what should happen on the closure of that model face (e.g., for a model edge it looks at the BCs on both model faces, then for a model vertex, it looks at the BCs on all the model edges incident on that vertex and then through inheritance rules applies a BC). In the model in question, when I do a single layer extrusion of an x-y box in z, the mesh edges at the box corners match model edges (no mesh vertices classified on them) and the mesh vertices (destination and extrusion source) also match model vertices. If I put a comp3 (no slip all three components of velocity) on the faces at the top and bottom of the box, inheritance should apply that same boundary condition to all 8 model vertices each of which should have one mesh vertex classified on it. This is not happening with the committed code. Further it is not happening when bypass inheritance and request in the spj file to put a comp3 on those 8 model vertices directly. Below I will begin to blog my understanding of what the code is doing but I will also pose a more expedient question:
Has anyone tested the original code for its ability to classify mesh on model vertices?
and a second question
Does verify actually check to see if the mesh vertices that are located on model vertices do get classified against them?
If the answer to both is NO, then I think we have to assume the original code is unverified in this capability and I could sure use some help writing some testing code to see if they are correct or not. Or, if it is easier to stare at the code and answer these questions thats fine too.
I might be making a tiny bit of headway understanding what is wrong but I am in so far over my head in terms of understanding the code that does this that the following is likely littered with conceptual miss-understandings. I guess thats what happens when a fortran person tries to use C/C++ code. Hoping someone can see what is going wrong:
- in the .msh file of v4 of gmsh write a section called Entities that has model topology information. For the topological cube case I am testing there are 27 model entities: 8 vertices, 12 edges, 6 faces and a region. gmsh assumes you know the dim of your entity and lets you repeat tag numbers. This may or may not have been my problem but, thinking it was, I created a map to unique tag numbers as I parsed the $ENTITIES section and WROTE a dmg file with model verts tagged 1..8, model edges tagged 9..20, model faces tagged 21..26, and my sole model region tagged 27.
- v4 of gmsh writes NODE information in blocks of each of the 27 model tags (exposing its view of mesh classification). To be clear it has 8 blocks for the 8 model vertices (with 1 mesh vert per block), 12 blocks for the 12 model edges(with a mesh dependent number of mesh verts on each of those 12 model edges) and so on.
- v4 of gmsh also write ELEMENT information in blocks of each of the 27 model tags.
- I am totally unclear how from_gmsh (compiled from test/gmsh.cc) managed to build an mds mesh from gmsh v2 format but here is what I have been trying (only partially successful so far) to do with v4.
- As noted in 1. I read the ENTITIES section and create a map from the non-unique model adjacencies/tags for the 27 model entities (general I think but being concrete to the current topological cube) and write that to a dmg file and save the tag map. This is done in
gmshFindDmg
which is called fromloadMdsDmgFromGmsh
. loadMdsDmgFromGmsh
then callsmakeEmptyMdsMesh(gmi_load(fnameDmg),0,false);
that loads the dmg file thatgmshFindDmg
just wrote. Since I have visually inspected/checked the dmg file and since that call is the same as the v2.0 version, I have no reason to suspect that I should have needed to change any of that (just moved from test/gmsh.cc down to this level so that is done after the dmg file is created).loadMdsDmgFromGmsh
then callsreadGmsh(m,filename,emap);
. Note the new, third argumentemap
is a array of integers that stores the gmsh tag for every unique dmg tag. For now I have a hard limit of 100 dmg tags (stored from 0..99) and then use the 4 entries in location 100+d to store how many model d dimensional model entities there are in the model for this mesh. This information allows me to invert the map by doing a while loop search over the appropriate range when I know the gmsh tag and its dim.- within
readGmsh
, I first callinitReader
(unchanged) - Then I call
readNodes
. Other than changing the format for how v4 changes the way the data is written and thus must be read there is no change. Specifically, though v4 does have model classification information in each block of nodes it writes, I am not currently using that to classify the mesh while reading the nodes because this was not done in the v2 version of this code and I doubted my ability to do that correctly. Instead, I hoped I could extend what v2 did which was to classify the code during the reading of ELEMENTS (below). Of course I did follow v2's practice of storing the coordinates at n.point and thenodeMap[gmshNode#]=n
which is used in ELEMENTS below. This should be unchanged though it was a little tricky since v4 writes ALL the gmshNode#s classified on a given model entity right after the header that holds the info for that model entity's block of nodes and THEN writes all the coordinate triples after that whereas v2 had that gmshNode# an triple on the same line. I am confident I did this right as the mesh passes verify. readGmsh
thenreadElements(&r,emap)
. New argument emap is needed to be sure that as I loop over the model entities, each with their own header block that contains
sscanf(r->line, "%ld %ld %ld %ld", &Edim, >ag, &gmshType, &Elements_Block);
dim, gmshtag, type of element, and number of elements in this block, I need emap to map gtag to what I wrote in the dmg file and have currently loaded as described in 6. above. Again, I am confident that this is correct to the level that verify checks.- Now we are to the place I am thinking there is a problem. What I describe in 10 is for EVERY model entity type. That is gmsh thinks model vertices have elements of zero dimensions so there is an element list for each model vertex. If I skip those elements by just advancing the line until I get to model edges and then use my v4 modified readElement function for each element line for 1D elements listed after each model edge header (in v4...I don't think v2 did this), and then do the same for the 2D elements listed after each model face header, and then finally do the same for the 3D element listed after the each of the model region headers (I only have one in this model but I hopefully wrote it general), I get from_gmsh to load and verify passes on this mesh. As noted above, PHASTA fails to get BCs set on model vertices which is why I am still blogging here hoping for help finding the problem. Note at the top of this issue, my first attempt was to also skip model edges blocks and model face blocks (on the grounds that I did not see that these were even present to process in the v2 version of this code but maybe I am wrong about that???). This failed to verify as noted near the top of this issue.
- so, logically, my current theory is that I was wrong to skip over those dim=0 gmsh "elements" which are mesh nodes on model vertices. Latest attempt is to process them and this is failing as follows.
13 Here is my current code for readElement (inmds/mdsGmsh.cc
)
323 void readElement(Reader* r, long gmshType,long gtag, long skippedLDE)
324 {
325 long id = getLong(r); // tag in 2 and 4
326 id=id-skippedLDE; // id only used for quadratic so this is probably not necessary until that is ready
327 if (isQuadratic(gmshType))
328 r->isQuadratic = true;
329 int apfType = apfFromGmsh(gmshType);
330 PCU_ALWAYS_ASSERT(0 <= apfType);
331 int nverts = apf::Mesh::adjacentCount[apfType][0];
332 int dim = apf::Mesh::typeDimension[apfType];
333 apf::ModelEntity* g = r->mesh->findModelEntity(dim, gtag);
334 apf::Downward verts;
335 for (int i = 0; i < nverts; ++i) {
336 long nid = getLong(r);
337 verts[i] = lookupVert(r, nid, g);
338 }
339 if (dim != 0) {
340 if (dim > r->mesh->getDimension())
341 apf::changeMdsDimension(r->mesh, dim);
342 apf::MeshEntity* ent = apf::buildElement(r->mesh, g, apfType, verts);
343 if (r->isQuadratic)
344 r->entMap[dim][id] = ent;
345 }
346 getLine(r);
347 }
For the first model vertex "element" line 333 does what I would expect. I feed it dim=0 an gtag=1 and it returns g with 1 if I am interpreting TotalView correctly. Great..maybe??. The next model vertex feeds it dim=0 and gtag=2 but now it returns g with a 5. Next gtag=3 returns g with a "9". Then gtag 4 returns g with a d. So it is jumping by 4?? Maybe this is not wrong but I notice that lookupVert(r,nid,g) on line 337 is thinking that this is node 19 if I am interpreting TotalView correctly. Here is a Screenshot of that.
This version, which does process the 0 dim elements but does not add them as elements to the mesh (due to line 339 that was in the original V2 code) does run and pass verify. It runs through Chef as well. PHASTA however does not apply BCs to model vertices which tells me the classification is wrong I think.
Anybody see the problem?
PHASTA seems fine with the current commit for a simple box. However, gmsh dumps construction vertices into the .msh file and when these are not mesh nodes classified on model vertices, the whole premise of verify (and probably SCOREC/core adjacency as a whole) is violated. I confirmed that this can be "fixed" by hand deleting all the spurious entities from mode $Entities, as $Nodes, and as $Elements but this is not a viable solution. I am in touch with Jean Francois Remacle who says the solution is to add Physical Groups. When gmsh sees no Physical Groups, it dumps EVERYTHING. When it sees physical groups, it only dumps the mesh for them.
HOWEVER, while I figure out how to do that I would REALLY appreciate someone to put eyes on readElements and readElement to figure out what that code really needs. Specifically, It obviously needs the highest dimensional elements (volumes in my case) but, can it construct everything else from that or does it also need faces on every model face, edges on every model face and nodes on every model node as I have been giving it (and it has been working with). If so I guess the user will need to make physical groups in the GUI or .geo file for all of these things. If not, I guess the second question is whether the inferred classification that code will create MATCHES the dmg file which is of course required for the downstream workflow.
Another alternative I was considering is to read an auxiliary file that lists the spurious model/mesh vertices that need to be removed and then, at each stage (Entities, Nodes, Elements) check to see if they are "banned" from the mesh and skip over them if they are. This effectively does what I did by deleting them from the .msh file. Tedious and ugly code to be sure. Another solution in the same vein is to scan the elements first to identify the spurious model/mesh vertices (they will be nodes not part of any 1D, 2D, or 3D elements while part of "0D" elements that I have only seen in gmsh).
Yet another solution (might take longer if I don't have help) is to NOT derive model node classification from the $Elements group but instead set it while reading the $Nodes section since it is available there. This would allow us to limit the correct vs. banned check only there as we would only process lines from the $Elements section for dim > 0 and those banned nodes don't appear there. Actually they would still have to not be added to the dmg file in the $Entities section.
Lots of possible solutions but maybe it is good for users to get acquainted with their mesh and set Physical Groups in the GUI or .geo file anyway as some day we can probably replace the spj file with something far more intuitive.