Skip to content
Merged

Dev #25

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
afcd50d
Change ShapeClassification to check for json data
adrianarce-elemwave Sep 30, 2025
c0c2e57
Added graph for pecs in Shape Classification
adrianarce-elemwave Oct 1, 2025
304dabd
Added json files
adrianarce-elemwave Oct 2, 2025
8472cfc
Add label to exported json
adrianarce-elemwave Oct 2, 2025
98ee095
Changed physical model to use mapped graph
adrianarce-elemwave Oct 2, 2025
f817b9e
minor, assertion fix
adrianarce-elemwave Oct 2, 2025
0aa8466
Merge pull request #22 from OpenSEMBA/21-change-step2gmsh-input-reque…
lmdiazangulo Oct 2, 2025
0c965a3
Adds realistic bundle case
lmdiazangulo Oct 12, 2025
78c6258
Reverts order 1 to order 3.
lmdiazangulo Oct 13, 2025
cc1a97d
Includes test to check that there are no duplicate nodes.
lmdiazangulo Oct 13, 2025
f9de401
Add duplicate node checks in mesh tests
lmdiazangulo Oct 13, 2025
545c816
Fixes problem with duplicated nodes
lmdiazangulo Oct 13, 2025
bde2f5b
Minor | Added complex nested cas and updated doc
adrianarce-elemwave Oct 14, 2025
5806255
Changes near region size to 1.15
lmdiazangulo Oct 15, 2025
1dcf29b
[WIP] Possibility of defining open boundary
lmdiazangulo Oct 15, 2025
d1680ad
Refactor vacuum domain handling and update physical model construction
lmdiazangulo Oct 15, 2025
46802b5
Debugging issue with dielectrics
lmdiazangulo Oct 16, 2025
06768d4
Fixes problem reading layers with more than one entity
lmdiazangulo Oct 16, 2025
c80ddcd
refactors getters
lmdiazangulo Oct 16, 2025
b4e5483
Minor | Readme update
adrianarce-elemwave Oct 16, 2025
2da30d4
[WIP] Ensuring conductor areas are computed correctly
lmdiazangulo Oct 16, 2025
297b191
Merge pull request #24 from OpenSEMBA/minor-add-nested-test-and-updat…
lmdiazangulo Oct 16, 2025
f82fe1a
[WIP] Ensuring conductor areas are computed correctly
lmdiazangulo Oct 16, 2025
277e036
[WIP] Testing area exporting for just conductors of general shape
lmdiazangulo Oct 16, 2025
12c17e4
Fixes merge
lmdiazangulo Oct 17, 2025
ae81022
Passes tests
lmdiazangulo Oct 17, 2025
21f9f47
Merge remote-tracking branch 'origin/dev' into realistic-case
lmdiazangulo Oct 17, 2025
a9463f3
Merge pull request #23 from OpenSEMBA/realistic-case
lmdiazangulo Oct 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
*.FCStd1
*.pyc
*.msh
.vscode/
.pytest_cache/
.idea/
tmpFolder/
venv/
gmshDoc/

*.msh
*.vtk
*.areas.json
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,26 @@ Install requirements with

## Usage

Step2gmsh requires two diferent files:
- A json file where material properties are described for each geometry
- A step file with all the geometry info

Both files must have the same label and share folder path.
An example of those files can be found in [Five wires case](testData/five_wires/)

Launch from command line as

```shell
python src/step2gmsh.py <-i path_to_step_file>
```

The tested input step files have been generated with [FreeCAD](https://www.freecad.org/). The geometrical entities within the step file must be separated in layers. The operations which are performed of the different layers depend on their name.
The tested input step files have been generated with [FreeCAD](https://www.freecad.org/). The geometrical entities within the step file must be separated in layers. The operations performed on the different layers depend on their material asignment registered in the json file.

- A layer named `Conductor_N` with `N` being an integer represents a perfect conductor. `Conductor_0` is a special case of which represents the ground and defines the global domain. For layers named `Conductor_N` with `N` different to zero their areas will be substracted from the computational domain and removed.
- Layers named as `Dielectric_N` are used to identify regions which will have a material assigned.
- A layer with a `PEC material`, represent a perfect conductor. In case one of the layers surrounds the rest of elements, it will be asigned as ground and defines the global domain for the rest of conductors. Internally, this will be represented as Conductor_0. The areas of the rest of conductors different to zero will be substracted from the computational domain and removed. In open cases, Conductor_0 is just another conductor and the domain is defined using the bounding box of the layers.
- Layers registered as `Dielectric` are used to identify regions which will have a material assigned.
- Open and semi-open problems can be defined using a single layer called `OpenBoundary`.

Below is shown an example of a closed case with 6 conductors and 5 dielectrics, the external boundary corresponds to `Conductor_0`. The case is modeled with FreeCAD and can be found in the `testData/five_wires` folder together with the exported as a step file. The resulting mesh after applying `step2gmsh` is shown below.
Below is shown an example of a closed case with 6 conductors and 5 dielectrics, the external boundary corresponds to `Conductor_0`. The case is modeled with FreeCAD and can be found in the [testData/five_wires](testData/five_wires/) folder together with the exported as a step file. The resulting mesh after applying `step2gmsh` is shown below.

![Five wires example as modeled with FreeCAD](doc/fig/five_wires_freecad.png)
![Five wires example meshed with gmsh](doc/fig/five_wires_gmsh.png)
Expand Down
20 changes: 0 additions & 20 deletions partially_filled_coax.areas.json

This file was deleted.

39 changes: 27 additions & 12 deletions src/AreaExporterService.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,42 @@ def __init__(self):
"geometries": []
}

def addComputedArea(self, geometry:str, area:float):
def addComputedArea(self, geometry:str, label:str, area:float):
geometry:Dict ={
"geometry": geometry,
"area": area,
"label": label,
"area": round(area,6),
}
self.computedAreas['geometries'].append(geometry)

def addPhysicalModelOfDimension(self, dimension=2):
physicalGroups = gmsh.model.getPhysicalGroups(dimension)
def addPhysicalModelForConductors(self, mappedElements:Dict[str,str]):
physicalGroups = gmsh.model.getPhysicalGroups(1)
for physicalGroup in physicalGroups:
entityTags = gmsh.model.getEntitiesForPhysicalGroup(*physicalGroup)
geometryName = gmsh.model.getPhysicalName(*physicalGroup)
for tag in entityTags:
if dimension == 1:
rad = gmsh.model.occ.getMass(dimension, tag) / (2*np.pi)
area = rad*rad*np.pi
if dimension == 2:
area = gmsh.model.occ.getMass(dimension, tag)
if geometryName != AreaExporterService._EMPTY_NAME_CASE:
self.addComputedArea(geometryName, area)
if not geometryName.startswith("Conductor_"):
continue

label = ''
for key, geometry in mappedElements.items():
if geometry == geometryName:
label = key
break

# Find surface that has these curves as boundaries
allSurfaces = gmsh.model.getEntities(2)
foundSurface = None
for surface in allSurfaces:
boundary = gmsh.model.getBoundary([surface], oriented=False, recursive=False)
boundaryTags = set(tag for dim, tag in boundary)
if set(entityTags) == boundaryTags:
foundSurface = surface
break

if foundSurface:
area = gmsh.model.occ.getMass(2, foundSurface[1])
self.addComputedArea(geometryName, label, area)

def exportToJson(self, exportFileName:str):
with open(exportFileName + ".areas.json", 'w') as f:
json.dump(self.computedAreas, f, indent=3)
118 changes: 118 additions & 0 deletions src/Graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from collections import defaultdict, deque
from typing import Dict, List, Tuple

class Graph:
def __init__(self):
self._nodes:List = []
self._edges:List[Tuple] = []

@property
def roots(self) -> List:
roots:List = []
for node in self._nodes:
isChild = False
for edge in self._edges:
if edge[-1] == node:
isChild = True
continue
if isChild == False:
roots.append(node)
return roots.copy()

@property
def nodes(self) -> List:
return self._nodes.copy()

@property
def edges(self) -> List:
return self._edges.copy()

@nodes.setter
def nodes(self, nodes):
self._nodes = list(nodes)

@edges.setter
def edges(self, edges):
self._edges = [tuple(e) for e in edges]

def add_node(self, node):
if node not in self._nodes:
self._nodes.append(node)

def add_edge(self, source, destination):
if source not in self._nodes:
self.add_node(source)
if destination not in self._nodes:
self.add_node(destination)
if (source, destination) not in self._edges:
self._edges.append((source, destination))

def get_connections(self):
connections = {node: [] for node in self._nodes}
for source, destination in self._edges:
connections[source].append(destination)
return connections

def getParentNodes(self) -> List:
return [edge[0] for edge in self._edges]

def getChildNodes(self) -> List:
return [edge[-1] for edge in self._edges]

def prune_to_longest_paths(self):
connections = self.get_connections()
roots = [n for n in self._nodes if n not in self.getChildNodes()]

longest_paths = []

def dfs(node, path):
path = path + [node]
if node not in connections or not connections[node]:
longest_paths.append(path)
return
for child in connections[node]:
dfs(child, path)

for root in roots:
dfs(root, [])

leaf_to_path = {}
for path in longest_paths:
leaf = path[-1]
if leaf not in leaf_to_path or len(path) > len(leaf_to_path[leaf]):
leaf_to_path[leaf] = path

new_nodes = set()
new_edges = set()
for path in leaf_to_path.values():
new_nodes.update(path)
new_edges.update([(path[i], path[i+1]) for i in range(len(path)-1)])

self._nodes = list(new_nodes)
self._edges = list(new_edges)

def getAdyacencyTree(self) -> Dict:
tree = defaultdict(list)
for root in self.roots:
tree[''].append(root)
for parent, child in self._edges:
tree[parent].append(child)
return tree

def getNodesByLevels(self) -> List:
adyacencyTree = self.getAdyacencyTree()
qeue = deque([('',0)])
nodeList = []
while qeue:
node,level = qeue.popleft()
nodeList.append(node)
for child in adyacencyTree[node]:
qeue.append((child, level+1))
return nodeList[1:] #Removes case 0 that is not part of nodes

def _reorderData(self) -> None:
self._edges = sorted(self._edges)
self._nodes = sorted(self._nodes)

def __str__(self):
return f"Graph(Nodes: {self._nodes},\n Edges: {self._edges})"
Loading