Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CaseMaker and path to define problems from VTKs #126

Merged
merged 3 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion doc/smbjson.md
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ If not `magnitudeFile` is specified and only one `source` is defined, the `magni
"type": "farField",
"elementIds": [4],
"theta": {"initial": 0, "final": 180, "step": 10},
"phi": {"initial": 0, "final": 0, "step": 0}
"phi": {"initial": 0, "final": 0, "step": 0},
"domain": {
"type": "frequency",
"initialFrequency": 1e6,
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ matplotlib
pandas
pyvista
h5py
scikit-rf
scikit-rf
vtk
279 changes: 279 additions & 0 deletions src_pyWrapper/pyWrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
import re
import pandas as pd
import numpy as np
import vtk
from vtk.util.numpy_support import vtk_to_numpy
from itertools import product
import copy

DEFAULT_SEMBA_FDTD_PATH = '/build/bin/semba-fdtd'

Expand Down Expand Up @@ -329,3 +333,278 @@ def getMaterialProperties(self, materialName):
for idx, element in enumerate(self._input['materials']):
if element["name"] == materialName:
return self._input['materials'][idx]


class CaseMaker():
# A collection of helper functions to create FDTD cases.
def __init__(self):
self.input = {}

def __getitem__(self, key):
return self.input[key]

def setNumberOfTimeSteps(self, steps):
if 'general' not in self.input:
self.input['general'] = {}

self.input['general']['numberOfSteps'] = steps

def setGridFromVTK(self, path_to_grid):
assert os.path.isfile(path_to_grid)

reader = vtk.vtkXMLPolyDataReader()
reader.SetFileName(path_to_grid)
reader.Update()

points = vtk_to_numpy(reader.GetOutput().GetPoints().GetData())

if 'mesh' not in self.input:
self.input['mesh'] = {}

if 'grid' not in self.input['mesh']:
self.input['mesh']['grid'] = {}

grid = self.input['mesh']['grid']
if 'steps' not in grid:
grid['steps'] = {}

steps = grid['steps']
grid['numberOfCells'] = []
grid['origin'] = []

index = 0
for x in ['x', 'y', 'z']:
steps[x] = np.diff(np.unique(points[:, index])).tolist()
grid['numberOfCells'].append(len(steps[x]))
grid['origin'].append(float(points[0, index]))
index += 1

def setAllBoundaries(self, boundary_type):
self.input['boundary'] = {
"all": boundary_type
}

def addCellElementsFromVTK(self, path_to_vtk):
assert os.path.isfile(path_to_vtk)

reader = vtk.vtkXMLPolyDataReader()
reader.SetFileName(path_to_vtk)
reader.Update()
polyData = reader.GetOutput()

vtkGroups = vtk_to_numpy(polyData.GetCellData().GetArray('group'))
if len(np.unique(vtkGroups)) > 1:
raise ValueError("Different groups are not supported.")

# Stores in case input
if 'mesh' not in self.input:
self.input['mesh'] = {}

if 'elements' not in self.input['mesh']:
self.input['mesh']['elements'] = []
elements = self.input['mesh']['elements']

id = len(elements) + 1
elements.append({
"id": id,
"type": "cell",
"intervals": self._getCellsIntervals(polyData),
})

return id

def addCellElementBox(self, boundingBox):
''' boundingBox is assumed to be in absolute coordinates.'''
if 'mesh' not in self.input:
self.input['mesh'] = {}

if 'elements' not in self.input['mesh']:
self.input['mesh']['elements'] = []
elements = self.input['mesh']['elements']

rel = self._absoluteToRelative(
np.array([boundingBox[0], boundingBox[1]]))

id = len(elements) + 1
elements.append({
"id": id,
"type": "cell",
"intervals": [
[rel[0].tolist(), rel[1, :].tolist()]
]
})

return id

def addNodeElement(self, position):
''' position is assumed to be in absolute coordinates.'''
if isinstance(position, list):
pos = np.array(position).reshape((1, 3))
elif isinstance(position, np.ndarray):
pos = position.reshape((1, 3))
else:
raise ValueError("Invalid type for position")
relative = self._absoluteToRelative(pos)

if 'mesh' not in self.input:
self.input['mesh'] = {}

if 'coordinates' not in self.input['mesh']:
self.input['mesh']['coordinates'] = []

coordId = len(self.input['mesh']['coordinates']) + 1
self.input['mesh']['coordinates'].append({
"id": coordId,
"relativePosition": relative[0].tolist()
})

if 'elements' not in self.input['mesh']:
self.input['mesh']['elements'] = []
elementId = len(self.input['mesh']['elements']) + 1
self.input['mesh']['elements'].append({
"id": elementId,
"type": "node",
"coordinateIds": [coordId]
})

return elementId

def addPECMaterial(self):
if 'materials' not in self.input:
self.input['materials'] = []

id = len(self.input['materials']) + 1
self.input['materials'].append({
"id": id,
"type": "pec",
})

return id

def addMaterialAssociation(self, materialId: int, elementIds: list):
if 'materials' not in self.input:
raise ValueError("No materials defined.")
if 'mesh' not in self.input:
raise ValueError("No mesh defined.")
if 'elements' not in self.input['mesh']:
raise ValueError("No elements defined.")
if len(elementIds) == 0:
raise ValueError("No elements to associate.")

if 'materialAssociations' not in self.input:
self.input['materialAssociations'] = []

self.input['materialAssociations'].append({
'materialId': materialId,
'elementIds': elementIds
})

def addPlanewaveSource(self, elementId, magnitudeFile, direction, polarization):
assert os.path.isfile(magnitudeFile)

if 'sources' not in self.input:
self.input['sources'] = []

self.input['sources'].append({
'type': 'planewave',
'magnitudeFile': magnitudeFile,
'elementIds': [elementId],
'direction': direction,
'polarization': polarization
})

def addPointProbe(self, elementId, name):
if 'probes' not in self.input:
self.input['probes'] = []

self.input['probes'].append({
"name": name,
"type": "point",
"elementIds": [elementId]
})

def addFarFieldProbe(self, elementId, name, theta, phi, domain):
if 'probes' not in self.input:
self.input['probes'] = []

self.input['probes'].append({
"name": name,
"type": "farField",
"elementIds": [elementId],
"theta": theta,
"phi": phi,
"domain": domain
})

def exportCase(self, case_name):
with open(case_name + '.fdtd.json', 'w') as f:
json.dump(self.input, f)

def _buildGridLines(self):
gridLines = []
grid = self.input['mesh']['grid']
index = 0
for x in ['x', 'y', 'z']:
newLines = np.cumsum(
np.insert(grid['steps'][x], 0, grid['origin'][index]))
index += 1
gridLines.append(newLines)
return gridLines

def _relativeToAbsolute(self, relativeCoordinates):
gridLines = self._buildGridLines()

res = np.empty_like(relativeCoordinates, dtype=float)
for x in range(3):
res[:, x] = gridLines[x][relativeCoordinates[:, x]]

return res

def _absoluteToRelative(self, absoluteCoordinates):
gridLines = self._buildGridLines()

res = np.empty_like(absoluteCoordinates, dtype=int)
for x in range(3):
for i in range(absoluteCoordinates.shape[0]):
array = gridLines[x]
value = absoluteCoordinates[i, x]
idx = np.searchsorted(array, value, side="left")
if idx > 0 and (idx == len(array) or np.abs(value - array[idx-1]) < np.abs(value - array[idx])):
res[i, x] = idx-1
else:
res[i, x] = idx

if np.any(absoluteCoordinates - self._relativeToAbsolute(res) > 1e-10):
raise ValueError(
"Error in conversion from absolute to relative coordinates.")

return res

def _getCellsIntervals(self, polyData):
relative = self._absoluteToRelative(
vtk_to_numpy(polyData.GetPoints().GetData()))

# Precounts
numberOfIntervals = 0
for i in range(polyData.GetNumberOfCells()):
cellType = polyData.GetCellType(i)
if cellType == vtk.VTK_QUAD or vtk.VTK_LINE:
numberOfIntervals += 1
res = [None] * numberOfIntervals

# Writes as list
c = 0
for i in range(polyData.GetNumberOfCells()):
cellType = polyData.GetCellType(i)
if cellType == vtk.VTK_QUAD:
p1 = polyData.GetCell(i).GetPointId(0)
p2 = polyData.GetCell(i).GetPointId(2)
elif cellType == vtk.VTK_LINE:
p1 = polyData.GetCell(i).GetPointId(0)
p2 = polyData.GetCell(i).GetPointId(1)
else:
continue
res[c] = [relative[p1].tolist(), relative[p2].tolist()]
c += 1

return res
73 changes: 73 additions & 0 deletions test/pyWrapper/test_pyWrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,76 @@ def test_fdtd_get_used_files():
assert len(used_files) == 2
assert used_files[0] == 'spice_4port_pulse_start_75.exc'
assert used_files[1] == 'opamp.model'


def test_casemaker_setGridFromVTK():
cm = CaseMaker()
cm.setGridFromVTK(GEOMETRIES_FOLDER + 'sphere.grid.vtp')

grid = cm['mesh']['grid']

steps = grid['steps']
assert np.allclose(np.array(steps['x']), 2.0)
assert np.allclose(np.array(steps['y']), 2.0)
assert np.allclose(np.array(steps['z']), 2.0)
assert np.array(steps['x']).size == 100
assert np.array(steps['y']).size == 100
assert np.array(steps['z']).size == 100

assert grid['numberOfCells'] == [100, 100, 100]
assert grid['origin'] == [-100.0, -100.0, -100.0]


def test_casemaker_sphere_rcs_case(tmp_path):
os.chdir(tmp_path)
cm = CaseMaker()

cm.setNumberOfTimeSteps(1)

cm.setAllBoundaries("mur")

cm.setGridFromVTK(GEOMETRIES_FOLDER + "sphere.grid.vtp")
sphereId = cm.addCellElementsFromVTK(
GEOMETRIES_FOLDER + "buggy_sphere.str.vtp")

pecId = cm.addPECMaterial()
cm.addMaterialAssociation(pecId, [sphereId])

planewaveBoxId = cm.addCellElementBox(
[[-75.0, -75.0, -75.0], [75.0, 75.0, 75.0]])
direction = {"theta": np.pi/2, "phi": 0.0}
polarization = {"theta": np.pi/2, "phi": np.pi/2}
dt = 1e-12
w0 = 0.1e-9 # ~ 2 GHz bandwidth
t0 = 10*w0
t = np.arange(0, t0+20*w0, dt)
data = np.empty((len(t), 2))
data[:,0] = t
data[:,1] = np.exp( -np.power(t-t0,2)/ w0**2 )
np.savetxt('gauss.exc', data)
cm.addPlanewaveSource(planewaveBoxId, 'gauss.exc', direction, polarization)

pointProbeNodeId = cm.addNodeElement([-65.0, 0.0, 0.0])
cm.addPointProbe(pointProbeNodeId, name="front")

n2ffBoxId = cm.addCellElementBox(
[[-85.0, -85.0, -85.0], [85.0, 85.0, 85.0]])
theta = {"initial": np.pi/2, "final": np.pi/2, "step": 0.0}
phi = {"initial": np.pi, "final": np.pi, "step": 0.0}
domain = {
"type": "frequency",
"initialFrequency": 10e6,
"finalFrequency": 1e9,
"numberOfFrequencies": 10,
"frequencySpacing": "logarithmic"
}
cm.addFarFieldProbe(n2ffBoxId, "n2ff", theta, phi, domain)

case_name = 'sphere_rcs'
cm.exportCase(case_name)

solver = FDTD(
case_name + ".fdtd.json",
path_to_exe=SEMBA_EXE)
solver.run()
assert solver.hasFinishedSuccessfully()
2 changes: 1 addition & 1 deletion test/pyWrapper/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
EXCITATIONS_FOLDER = os.path.join(TEST_DATA_FOLDER, 'excitations/')
OUTPUTS_FOLDER = os.path.join(TEST_DATA_FOLDER, 'outputs/')
SPINIT_FOLDER = os.path.join(TEST_DATA_FOLDER, 'spinit/')

GEOMETRIES_FOLDER = os.path.join(TEST_DATA_FOLDER, 'geometries/')

def getCase(case):
return json.load(open(CASES_FOLDER + case + '.fdtd.json'))
Expand Down
Loading