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

Pythonize/open ems #153

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
146 changes: 78 additions & 68 deletions python/openEMS/openEMS.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#

import os, sys, shutil
from pathlib import Path
import numpy as np
cimport openEMS
from openEMS import ports, nf2ff, automesh
Expand Down Expand Up @@ -50,8 +51,8 @@ cdef class openEMS:
>>>
>>> FDTD.Run(sim_path='/tmp/test')

:param NrTS: max. number of timesteps to simulate (e.g. default=1e9)
:param EndCriteria: end criteria, e.g. 1e-5, simulations stops if energy has decayed by this value (<1e-4 is recommended, default=1e-5)
:param number_of_time_steps: Maximum number of timesteps to simulate.
:param energy_end_criterion: End the simulation if the energy has decayed by this value. For example, `energy_end_criterion=1e-5` will make the simulation stop if energy has decayed by a factor of `1e5`. `energy_end_criterion`<1e-4 is recommended.
:param MaxTime: max. real time in seconds to simulate
:param OverSampling: nyquist oversampling of time domain dumps
:param CoordSystem: choose coordinate system (0 Cartesian, 1 Cylindrical)
Expand All @@ -68,18 +69,23 @@ cdef class openEMS:
"""
_openEMS.WelcomeScreen()

def __cinit__(self, *args, **kw):
def __cinit__(self, number_of_time_steps:int|float=1e9, energy_end_criterion:float=1e-5, **kw):
self.thisptr = new _openEMS()
self.__CSX = None

if 'NrTS' in kw:
if 'NrTS' in kw: # This is kept for backwards compatibility, but using `number_of_time_steps` should be preferred.
self.SetNumberOfTimeSteps(kw['NrTS'])
del kw['NrTS']
else:
self.SetNumberOfTimeSteps(1e9)
if 'EndCriteria' in kw:
else: # Use `number_of_time_steps`:
self.SetNumberOfTimeSteps(number_of_time_steps)


if 'EndCriteria' in kw: # This is kept for backwards compatibility, but using `energy_end_criterion` should be preferred.
self.SetEndCriteria(kw['EndCriteria'])
del kw['EndCriteria']
else: # Use `energy_end_criterion`:
self.SetEndCriteria(energy_end_criterion)

if 'MaxTime' in kw:
self.SetMaxTime(kw['MaxTime'])
del kw['MaxTime']
Expand Down Expand Up @@ -112,19 +118,19 @@ cdef class openEMS:
if self.__CSX is not None:
self.__CSX.thisptr = NULL

def SetNumberOfTimeSteps(self, val):
""" SetNumberOfTimeSteps(val)

Set the number of timesteps. E.g. 5e4 (default is 1e9)
"""
self.thisptr.SetNumberOfTimeSteps(val)

def SetEndCriteria(self, val):
""" SetEndCriteria(val)
def SetNumberOfTimeSteps(self, time_steps:int|float):
"""Set the number of timesteps."""
if not isinstance(time_steps, (int,float)):
raise TypeError(f'`time_steps` must be an instance of `int` or `float`, received object of type {type(time_steps)}. ')
if time_steps <= 0:
raise ValueError(f'`time_steps` must be > 0, received {time_steps}. ')
self.thisptr.SetNumberOfTimeSteps(int(time_steps))

Set the end criteria value. E.g. 1e-6 for -60dB
"""
self.thisptr.SetEndCriteria(val)
def SetEndCriteria(self, energy_decay_factor:float):
"""Set the end criterion value. For example, `energy_decay_factor=1e-5` will make the simulation stop if energy has decayed by a factor of `1e5`. `1e-6` for -60dB."""
if not isinstance(energy_decay_factor, float):
raise TypeError(f'`energy_decay_factor` must be an instance of `float`, received object of type {type(energy_decay_factor)}. ')
self.thisptr.SetEndCriteria(float(energy_decay_factor))

def SetOverSampling(self, val):
""" SetOverSampling(val)
Expand Down Expand Up @@ -250,13 +256,13 @@ cdef class openEMS:
"""
self.thisptr.SetDiracExcite(f_max)

def SetStepExcite(self, f_max):
""" SetStepExcite(f_max)

Set a step function as excitation signal.
def SetStepExcite(self, f_max:float):
"""Set a step function as excitation signal.

:param f_max: float -- maximum simulated frequency in Hz.
:param f_max: Maximum simulated frequency in Hz.
"""
if not isinstance(f_max, (int,float)) or f_max<0:
raise ValueError(f'`f_max` must be a number >0, received {f_max}. ')
self.thisptr.SetStepExcite(f_max)

def SetCustomExcite(self, _str, f0, fmax):
Expand All @@ -271,22 +277,26 @@ cdef class openEMS:
"""
self.thisptr.SetCustomExcite(_str, f0, fmax)

def SetBoundaryCond(self, BC):
""" SetBoundaryCond(BC)
def SetBoundaryCond(self, x_min:str|int, x_max:str|int, y_min:str|int, y_max:str|int, z_min:str|int, z_max:str|int):
"""Set the boundary conditions for all six FDTD directions.

Set the boundary conditions for all six FDTD directions.
Options for all the arguments `x_min`, `x_max`, ... are:
* 'PEC': Perfect electric conductor.
* 'PMC': Perfect magnetic conductor, useful for symmetries.
* 'MUR': Simple MUR absorbing boundary conditions.
* 'PML_n: PML absorbing boundary conditions, with PML size `n` between 4 and 50.

Options:
Note: Each argument `x_min`, `x_max`, ... can take integer values 0, 1, 2 or 3. This is kept for backwards compatibility but its usage is discouraged.
"""
POSSIBLE_VALUES_FOR_BOUNDARY_CONDITIONS = {'PEC','PMC','MUR'} | {f'PML_{x}' for x in range(4,51)}
POSSIBLE_VALUES_FOR_BOUNDARY_CONDITIONS |= {0,1,2,3} # This is for backwards compatibility, but passing numbers should be avoided.

* 0 or 'PEC' : perfect electric conductor (default)
* 1 or 'PMC' : perfect magnetic conductor, useful for symmetries
* 2 or 'MUR' : simple MUR absorbing boundary conditions
* 3 or 'PML_8' : PML absorbing boundary conditions
boundary_conditions = dict(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, z_min=z_min, z_max=z_max)
for name,val in boundary_conditions.items():
if val not in POSSIBLE_VALUES_FOR_BOUNDARY_CONDITIONS:
raise ValueError(f'`{name}` is not one of the allowed boundary conditions, which are "PEC", "PMC", "MUR" or "PML_n" with "n" between 4 and 50. Received {repr(val)}. ')

:param BC: (8,) array or list -- see options above
"""
if not len(BC)==6:
raise Exception('Invalid boundary condition size!')
BC = [boundary_conditions[name] for name in ['x_min','x_max','y_min','y_max','z_min','z_max']]
for n in range(len(BC)):
if type(BC[n])==int:
self.thisptr.Set_BC_Type(n, BC[n])
Expand All @@ -300,19 +310,16 @@ cdef class openEMS:
continue
raise Exception('Unknown boundary condition')

def AddLumpedPort(self, port_nr, R, start, stop, p_dir, excite=0, **kw):
""" AddLumpedPort(port_nr, R, start, stop, p_dir, excite=0, **kw)

Add a lumped port with the given values and location.
def AddLumpedPort(self, port_nr, R, start, stop, p_dir, excite=0, edges2grid=None, **kw)->ports.LumpedPort:
"""Add a lumped port with the given values and location.

See Also
--------
openEMS.ports.LumpedPort
"""
if self.__CSX is None:
raise Exception('AddLumpedPort: CSX is not set!')
port = ports.LumpedPort(self.__CSX, port_nr, R, start, stop, p_dir, excite, **kw)
edges2grid = kw.get('edges2grid', None)
raise RuntimeError('CSX is not yet set!')
port = ports.LumpedPort(CSX=self.__CSX, port_nr=port_nr, R=R, start=start, stop=stop, exc_dir=p_dir, excite=excite, **kw)
if edges2grid is not None:
grid = self.__CSX.GetGrid()
for n in GetMultiDirs(edges2grid):
Expand Down Expand Up @@ -458,26 +465,29 @@ cdef class openEMS:
continue
grid.AddLine(n, hint[n])

def Run(self, sim_path, cleanup=False,setup_only=False, debug_material=False, debug_pec=False,
debug_operator=False, debug_boxes=False, debug_csx=False, verbose=None, **kw):
""" Run(sim_path, cleanup=False, setup_only=False, verbose=None)

Run the openEMS FDTD simulation.
def Run(self, sim_path:str|Path, cleanup:bool=False, setup_only:bool=False, debug_material:bool=False, debug_pec:bool=False, debug_operator:bool=False, debug_boxes:bool=False, debug_csx:bool=False, verbose:int=0, numThreads:int=0):
"""Run the openEMS FDTD simulation.

:param sim_path: str -- path to run in and create result data
:param cleanup: bool -- remove existing sim_path to cleanup old results
:param setup_only: bool -- only perform FDTD setup, do not run simulation
:param verbose: int -- set the openEMS verbosity level 0..3

Additional keyword parameter:
:param numThreads: int -- set the number of threads (default 0 --> max)
:param sim_path: Path to run in and create result data.
:param cleanup: Remove existing sim_path to cleanup old results.
:param setup_only: Only perform FDTD setup, do not run simulation.
:param verbose: Set the openEMS verbosity level, form 0 to 3.
:param numThreads: Set the number of threads.
"""
if cleanup and os.path.exists(sim_path):
shutil.rmtree(sim_path, ignore_errors=True)
os.mkdir(sim_path)
if not os.path.exists(sim_path):
os.mkdir(sim_path)
sim_path = Path(sim_path) # Check that whatever we receive can be interpreted as a path.
if verbose not in {0,1,2,3}:
raise ValueError(f'`verbose` must be 0, 1, 2 or 3, received {repr(verbose)}. ')
if not isinstance(numThreads, int) or numThreads<0:
raise ValueError(f'`numThreads` must be an int >= 0, received {repr(numThreads)}')
for name,val in dict(cleanup=cleanup, setup_only=setup_only, debug_material=debug_material, debug_pec=debug_pec, debug_operator=debug_operator, debug_boxes=debug_boxes, debug_csx=debug_csx).items():
if val not in {True,False}: # Not sure why, but Cython complains with `isinstance(val, bool)`.
raise TypeError(f'`{name}` must be a bool, received object of type {type(val)}. ')

if cleanup and sim_path.is_dir():
shutil.rmtree(str(sim_path), ignore_errors=True)
sim_path.mkdir(parents=True, exist_ok=False)
os.chdir(sim_path)

if verbose is not None:
self.thisptr.SetVerboseLevel(verbose)
if debug_material:
Expand All @@ -495,17 +505,17 @@ cdef class openEMS:
if debug_csx:
with nogil:
self.thisptr.DebugCSX()
if 'numThreads' in kw:
self.thisptr.SetNumberOfThreads(int(kw['numThreads']))
if numThreads is not None:
self.thisptr.SetNumberOfThreads(int(numThreads))
assert os.getcwd() == os.path.realpath(sim_path)
_openEMS.WelcomeScreen()
cdef int EC
cdef int error_code
with nogil:
EC = self.thisptr.SetupFDTD()
if EC!=0:
print('Run: Setup failed, error code: {}'.format(EC))
if setup_only or EC!=0:
return EC
error_code = self.thisptr.SetupFDTD()
if error_code!=0:
raise RuntimeError(f'Setup failed, error code: {error_code}')
if setup_only:
return
with nogil:
self.thisptr.RunFDTD()

Expand Down
44 changes: 19 additions & 25 deletions python/openEMS/ports.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,33 +54,25 @@ class Port(object):
:param port_nr: int -- port number
:param R: float -- port reference impedance, e.g. 50 (Ohms)
:param start, stop: (3,) array -- Start/Stop box coordinates
:param p_dir: int -- port direction
:param excite: float -- port excitation amplitude
:param priority: int -- priority of all contained primtives
:param PortNamePrefix: str -- a prefix for all ports-names
:param delay: float -- a positive delay value to e.g. emulate a phase shift
"""
def __init__(self, CSX, port_nr, start, stop, excite, **kw):
def __init__(self, CSX, port_nr:int, start:list, stop:list, excite:float=0, priority:int=0, PortNamePrefix:str=None, delay:float=0, U_filenames:list=None, I_filenames:list=None, R:float=None):
self.CSX = CSX
self.number = port_nr
self.excite = excite
self.start = np.array(start, np.double)
self.stop = np.array(stop, np.double)
self.Z_ref = None
self.U_filenames = kw.get('U_filenames', [])
self.I_filenames = kw.get('I_filenames', [])

self.priority = 0
if 'priority' in kw:
self.priority = kw['priority']

self.prefix = ''
if 'PortNamePrefix' in kw:
self.prefix = kw['PortNamePrefix']
self.delay = 0
self.R = R
self.U_filenames = U_filenames if U_filenames is not None else []
self.I_filenames = I_filenames if I_filenames is not None else []

if 'delay' in kw:
self.delay = kw['delay']
self.priority = priority
self.prefix = '' if PortNamePrefix is None else PortNamePrefix
self.delay = delay

self.lbl_temp = self.prefix + 'port_{}' + '_{}'.format(self.number)

Expand Down Expand Up @@ -144,46 +136,48 @@ class LumpedPort(Port):
"""
The lumped port.

:param exc_dir: Coordinate for the excitation direction.
:param kwargs: Keyword parameters passed to parent class `Port.__init__`.

See Also
--------
Port
"""
def __init__(self, CSX, port_nr, R, start, stop, exc_dir, excite=0, **kw):
super(LumpedPort, self).__init__(CSX, port_nr=port_nr, start=start, stop=stop, excite=excite, **kw)
self.R = R
def __init__(self, exc_dir:int|str, **kwargs):
super().__init__(**kwargs)
self.exc_ny = CheckNyDir(exc_dir)

self.direction = np.sign(self.stop[self.exc_ny]-self.start[self.exc_ny])
if not self.start[self.exc_ny]!=self.stop[self.exc_ny]:
raise Exception('LumpedPort: start and stop may not be identical in excitation direction')

if self.R > 0:
lumped_R = CSX.AddLumpedElement(self.lbl_temp.format('resist'), ny=self.exc_ny, caps=True, R=self.R)
lumped_R = self.CSX.AddLumpedElement(self.lbl_temp.format('resist'), ny=self.exc_ny, caps=True, R=self.R)
elif self.R==0:
lumped_R = CSX.AddMetal(self.lbl_temp.format('resist'))
lumped_R = self.CSX.AddMetal(self.lbl_temp.format('resist'))

lumped_R.AddBox(self.start, self.stop, priority=self.priority)

if excite!=0:
if self.excite!=0:
exc_vec = np.zeros(3)
exc_vec[self.exc_ny] = -1*self.direction*excite
exc = CSX.AddExcitation(self.lbl_temp.format('excite'), exc_type=0, exc_val=exc_vec, delay=self.delay)
exc_vec[self.exc_ny] = -1*self.direction*self.excite
exc = self.CSX.AddExcitation(self.lbl_temp.format('excite'), exc_type=0, exc_val=exc_vec, delay=self.delay)
exc.AddBox(self.start, self.stop, priority=self.priority)

self.U_filenames = [self.lbl_temp.format('ut'), ]
u_start = 0.5*(self.start+self.stop)
u_start[self.exc_ny] = self.start[self.exc_ny]
u_stop = 0.5*(self.start+self.stop)
u_stop[self.exc_ny] = self.stop[self.exc_ny]
u_probe = CSX.AddProbe(self.U_filenames[0], p_type=0, weight=-1)
u_probe = self.CSX.AddProbe(self.U_filenames[0], p_type=0, weight=-1)
u_probe.AddBox(u_start, u_stop)

self.I_filenames = [self.lbl_temp.format('it'), ]
i_start = np.array(self.start)
i_start[self.exc_ny] = 0.5*(self.start[self.exc_ny]+self.stop[self.exc_ny])
i_stop = np.array(self.stop)
i_stop[self.exc_ny] = 0.5*(self.start[self.exc_ny]+self.stop[self.exc_ny])
i_probe = CSX.AddProbe(self.I_filenames[0], p_type=1, weight=self.direction, norm_dir=self.exc_ny)
i_probe = self.CSX.AddProbe(self.I_filenames[0], p_type=1, weight=self.direction, norm_dir=self.exc_ny)
i_probe.AddBox(i_start, i_stop)

def CalcPort(self, sim_path, freq, ref_impedance=None, ref_plane_shift=None, signal_type='pulse'):
Expand Down