emerge 0.6.11__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of emerge might be problematic. Click here for more details.
- emerge/__init__.py +2 -2
- emerge/_emerge/bc.py +8 -3
- emerge/_emerge/cacherun.py +79 -0
- emerge/_emerge/geo/__init__.py +1 -1
- emerge/_emerge/geo/pcb.py +160 -71
- emerge/_emerge/geo/pcb_tools/dxf.py +360 -0
- emerge/_emerge/geo/polybased.py +21 -15
- emerge/_emerge/geo/shapes.py +31 -16
- emerge/_emerge/geometry.py +147 -21
- emerge/_emerge/material.py +2 -1
- emerge/_emerge/mesh3d.py +39 -12
- emerge/_emerge/periodic.py +19 -17
- emerge/_emerge/physics/microwave/assembly/assembler.py +12 -12
- emerge/_emerge/physics/microwave/microwave_3d.py +29 -6
- emerge/_emerge/physics/microwave/microwave_bc.py +22 -9
- emerge/_emerge/physics/microwave/microwave_data.py +3 -0
- emerge/_emerge/plot/pyvista/display.py +20 -6
- emerge/_emerge/plot/pyvista/display_settings.py +2 -1
- emerge/_emerge/plot/simple_plots.py +4 -1
- emerge/_emerge/selection.py +10 -8
- emerge/_emerge/settings.py +12 -0
- emerge/_emerge/simmodel.py +182 -48
- emerge/_emerge/solver.py +9 -2
- emerge/beta/dxf.py +1 -0
- emerge/lib.py +4 -1
- emerge/materials/__init__.py +1 -0
- emerge/materials/isola.py +294 -0
- emerge/materials/rogers.py +58 -0
- {emerge-0.6.11.dist-info → emerge-1.0.1.dist-info}/METADATA +6 -8
- {emerge-0.6.11.dist-info → emerge-1.0.1.dist-info}/RECORD +33 -26
- {emerge-0.6.11.dist-info → emerge-1.0.1.dist-info}/WHEEL +0 -0
- {emerge-0.6.11.dist-info → emerge-1.0.1.dist-info}/entry_points.txt +0 -0
- {emerge-0.6.11.dist-info → emerge-1.0.1.dist-info}/licenses/LICENSE +0 -0
emerge/_emerge/simmodel.py
CHANGED
|
@@ -26,7 +26,9 @@ from .logsettings import LOG_CONTROLLER
|
|
|
26
26
|
from .plot.pyvista import PVDisplay
|
|
27
27
|
from .dataset import SimulationDataset
|
|
28
28
|
from .periodic import PeriodicCell
|
|
29
|
-
from .
|
|
29
|
+
from .cacherun import get_build_section, get_run_section
|
|
30
|
+
from .settings import DEFAULT_SETTINGS, Settings
|
|
31
|
+
from .solver import EMSolver, Solver
|
|
30
32
|
from typing import Literal, Generator, Any
|
|
31
33
|
from loguru import logger
|
|
32
34
|
import numpy as np
|
|
@@ -94,21 +96,26 @@ class Simulation:
|
|
|
94
96
|
|
|
95
97
|
self.mesh: Mesh3D = Mesh3D(self.mesher)
|
|
96
98
|
self.select: Selector = Selector()
|
|
99
|
+
|
|
100
|
+
self.settings: Settings = DEFAULT_SETTINGS
|
|
97
101
|
|
|
102
|
+
## Display
|
|
103
|
+
self.display: PVDisplay = PVDisplay(self.mesh)
|
|
104
|
+
|
|
105
|
+
## Dataset
|
|
106
|
+
self.data: SimulationDataset = SimulationDataset()
|
|
107
|
+
|
|
98
108
|
## STATES
|
|
99
109
|
self.__active: bool = False
|
|
100
110
|
self._defined_geometries: bool = False
|
|
101
111
|
self._cell: PeriodicCell | None = None
|
|
102
|
-
|
|
103
|
-
self.display: PVDisplay = PVDisplay(self.mesh)
|
|
104
|
-
|
|
105
112
|
self.save_file: bool = save_file
|
|
106
113
|
self.load_file: bool = load_file
|
|
107
|
-
|
|
108
|
-
self.
|
|
109
|
-
|
|
114
|
+
self._cache_run: bool = False
|
|
115
|
+
self._file_lines: str = ''
|
|
116
|
+
|
|
110
117
|
## Physics
|
|
111
|
-
self.mw: Microwave3D = Microwave3D(self.mesher, self.data.mw)
|
|
118
|
+
self.mw: Microwave3D = Microwave3D(self.mesher, self.settings, self.data.mw)
|
|
112
119
|
|
|
113
120
|
self._initialize_simulation()
|
|
114
121
|
|
|
@@ -216,14 +223,6 @@ class Simulation:
|
|
|
216
223
|
def _update_data(self) -> None:
|
|
217
224
|
"""Writes the stored physics data to each phyics class insatnce"""
|
|
218
225
|
self.mw.data = self.data.mw
|
|
219
|
-
|
|
220
|
-
def all_geometries(self) -> list[GeoObject]:
|
|
221
|
-
"""Returns all geometries stored in the simulation file."""
|
|
222
|
-
return [obj for obj in self.data.sim.default.values() if isinstance(obj, GeoObject)]
|
|
223
|
-
|
|
224
|
-
def all_bcs(self) -> list[BoundaryCondition]:
|
|
225
|
-
"""Returns all boundary condition objects stored in the simulation file"""
|
|
226
|
-
return [obj for obj in self.data.sim.default.values() if isinstance(obj, BoundaryCondition)]
|
|
227
226
|
|
|
228
227
|
def _set_mesh(self, mesh: Mesh3D) -> None:
|
|
229
228
|
"""Set the current model mesh to a given mesh."""
|
|
@@ -235,34 +234,141 @@ class Simulation:
|
|
|
235
234
|
# PUBLIC FUNCTIONS #
|
|
236
235
|
############################################################
|
|
237
236
|
|
|
238
|
-
def
|
|
239
|
-
"""
|
|
237
|
+
def cache_build(self) -> bool:
|
|
238
|
+
"""Checks if all the lines inside this if statement block are the same as those
|
|
239
|
+
stored from a previous run. If so, then it returns false. Else it returns True.
|
|
240
240
|
|
|
241
|
-
|
|
241
|
+
Can be used to capture an entire model simulation.
|
|
242
242
|
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
Example:
|
|
244
|
+
|
|
245
|
+
>>> if model.cache_build():
|
|
246
|
+
>>> box = em.geo.Box(...)
|
|
247
|
+
>>> # Other lines
|
|
248
|
+
>>> model.mw.run_sweep()
|
|
249
|
+
>>> data = model.data.mw
|
|
245
250
|
|
|
246
|
-
|
|
247
|
-
|
|
251
|
+
Returns:
|
|
252
|
+
bool: If the code is not the same
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
self.save_file = True
|
|
256
|
+
self._cache_run = True
|
|
257
|
+
filestr = get_build_section()
|
|
258
|
+
self._file_lines = filestr
|
|
259
|
+
cachepath = self.modelpath / 'pylines.txt'
|
|
260
|
+
|
|
261
|
+
# If there is not pylines file, simulate (can't have been run).
|
|
262
|
+
if not cachepath.exists():
|
|
263
|
+
logger.info('No cached data detected, running file')
|
|
264
|
+
return True
|
|
265
|
+
|
|
266
|
+
with open(cachepath, 'r') as file:
|
|
267
|
+
lines = file.read()
|
|
268
|
+
|
|
269
|
+
if lines==filestr:
|
|
270
|
+
logger.info('Cached data detected! Loading data!')
|
|
271
|
+
self.load()
|
|
272
|
+
return False
|
|
273
|
+
logger.info('Different cached data detected, rebuilding file.')
|
|
274
|
+
return True
|
|
275
|
+
|
|
276
|
+
def cache_run(self) -> bool:
|
|
277
|
+
"""Checks if all the lines before this call are the same as the lines
|
|
278
|
+
stored from a previous run. If so, then it returns false. Else it returns True.
|
|
279
|
+
|
|
280
|
+
Can be used to capture a run_sweep() call.
|
|
281
|
+
|
|
282
|
+
Example:
|
|
283
|
+
|
|
284
|
+
>>> if model.cache_run():
|
|
285
|
+
>>> model.mw.run_sweep()
|
|
286
|
+
>>> data = model.data.mw
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
bool: If the code is not the same
|
|
290
|
+
"""
|
|
291
|
+
self.save_file = True
|
|
292
|
+
self._cache_run = True
|
|
293
|
+
filestr = get_run_section()
|
|
294
|
+
self._file_lines = filestr
|
|
295
|
+
cachepath = self.modelpath / 'pylines.txt'
|
|
296
|
+
|
|
297
|
+
# If there is not pylines file, simulate (can't have been run).
|
|
298
|
+
if not cachepath.exists():
|
|
299
|
+
logger.info('No cached data detected, running simulation!')
|
|
300
|
+
return True
|
|
301
|
+
|
|
302
|
+
with open(cachepath, 'r') as file:
|
|
303
|
+
lines = file.read()
|
|
304
|
+
|
|
305
|
+
if lines==filestr:
|
|
306
|
+
logger.info('Cached data detected! Loading data!')
|
|
307
|
+
self.load()
|
|
308
|
+
return False
|
|
309
|
+
logger.info('Different cached data detected, rerunning simulation.')
|
|
310
|
+
return True
|
|
311
|
+
|
|
312
|
+
def check_version(self, target_version: str, *, log: bool = False) -> None:
|
|
313
|
+
"""
|
|
314
|
+
Ensure the script targets an EMerge version compatible with the current runtime.
|
|
315
|
+
|
|
316
|
+
Parameters
|
|
317
|
+
----------
|
|
318
|
+
target_version : str
|
|
319
|
+
The EMerge version this script was written for (e.g. "1.4.0").
|
|
320
|
+
log : bool, optional
|
|
321
|
+
If True and a `logger` is available, emit a single WARNING with the same
|
|
322
|
+
message as the exception. Defaults to False.
|
|
323
|
+
|
|
324
|
+
Raises
|
|
325
|
+
------
|
|
326
|
+
VersionError
|
|
327
|
+
If the script's target version differs from the running EMerge version.
|
|
248
328
|
"""
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
329
|
+
try:
|
|
330
|
+
from packaging.version import Version as _V
|
|
331
|
+
v_script = _V(target_version)
|
|
332
|
+
v_runtime = _V(__version__)
|
|
333
|
+
newer = v_script > v_runtime
|
|
334
|
+
older = v_script < v_runtime
|
|
335
|
+
except Exception:
|
|
336
|
+
def _parse(v: str):
|
|
337
|
+
try:
|
|
338
|
+
return tuple(int(p) for p in v.split("."))
|
|
339
|
+
except Exception:
|
|
340
|
+
# Last-resort: compare as strings to avoid crashing the check itself
|
|
341
|
+
return tuple(v.split("."))
|
|
342
|
+
v_script = _parse(target_version)
|
|
343
|
+
v_runtime = _parse(__version__)
|
|
344
|
+
newer = v_script > v_runtime
|
|
345
|
+
older = v_script < v_runtime
|
|
346
|
+
|
|
347
|
+
if not newer and not older:
|
|
348
|
+
return # exact match
|
|
349
|
+
|
|
350
|
+
if newer:
|
|
351
|
+
msg = (
|
|
352
|
+
f"Script targets EMerge {target_version}, but runtime is {__version__}. "
|
|
353
|
+
"The script may rely on features added after your installed version. "
|
|
354
|
+
"Recommended: upgrade EMerge (`pip install --upgrade emerge`). "
|
|
355
|
+
"If you know the script is compatible, you may remove this check."
|
|
356
|
+
)
|
|
357
|
+
else: # older
|
|
358
|
+
msg = (
|
|
359
|
+
f"Script targets EMerge {target_version}, but runtime is {__version__}. "
|
|
360
|
+
"APIs may have changed since the targeted version. "
|
|
361
|
+
"Recommended: update the script for the current EMerge, or run a matching older release. "
|
|
362
|
+
"If you know the script is compatible, you may remove this check."
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
if log:
|
|
366
|
+
try:
|
|
367
|
+
logger.warning(msg)
|
|
368
|
+
except Exception:
|
|
369
|
+
pass
|
|
370
|
+
|
|
371
|
+
raise VersionError(msg)
|
|
266
372
|
|
|
267
373
|
def save(self) -> None:
|
|
268
374
|
"""Saves the current model in the provided project directory."""
|
|
@@ -289,6 +395,12 @@ class Simulation:
|
|
|
289
395
|
data_path = self.modelpath / 'simdata.emerge'
|
|
290
396
|
with open(str(data_path), "wb") as f_out:
|
|
291
397
|
cloudpickle.dump(dataset, f_out)
|
|
398
|
+
|
|
399
|
+
if self._cache_run:
|
|
400
|
+
cachepath = self.modelpath / 'pylines.txt'
|
|
401
|
+
with open(str(cachepath), 'w') as f_out:
|
|
402
|
+
f_out.write(self._file_lines)
|
|
403
|
+
|
|
292
404
|
logger.info(f"Saved simulation data to: {data_path}")
|
|
293
405
|
|
|
294
406
|
def load(self) -> None:
|
|
@@ -334,7 +446,8 @@ class Simulation:
|
|
|
334
446
|
use_gmsh: bool = False,
|
|
335
447
|
plot_mesh: bool = False,
|
|
336
448
|
volume_mesh: bool = True,
|
|
337
|
-
opacity: float | None = None
|
|
449
|
+
opacity: float | None = None,
|
|
450
|
+
labels: bool = False) -> None:
|
|
338
451
|
"""View the current geometry in either the BaseDisplay object (PVDisplay only) or
|
|
339
452
|
the GMSH viewer.
|
|
340
453
|
|
|
@@ -344,21 +457,21 @@ class Simulation:
|
|
|
344
457
|
plot_mesh (bool, optional): If the mesh should be plot instead of the object. Defaults to False.
|
|
345
458
|
volume_mesh (bool, optional): If the internal mesh should be plot instead of only the surface boundary mesh. Defaults to True
|
|
346
459
|
opacity (float | None, optional): The object/mesh opacity. Defaults to None.
|
|
347
|
-
|
|
460
|
+
labels: (bool, optional): If geometry name labels should be shown. Defaults to False.
|
|
348
461
|
"""
|
|
349
462
|
if not (self.display is not None and self.mesh.defined) or use_gmsh:
|
|
350
463
|
gmsh.model.occ.synchronize()
|
|
351
464
|
gmsh.fltk.run()
|
|
352
465
|
return
|
|
353
466
|
for geo in _GEOMANAGER.all_geometries():
|
|
354
|
-
self.display.add_object(geo, mesh=plot_mesh, opacity=opacity, volume_mesh=volume_mesh)
|
|
467
|
+
self.display.add_object(geo, mesh=plot_mesh, opacity=opacity, volume_mesh=volume_mesh, label=labels)
|
|
355
468
|
if selections:
|
|
356
|
-
[self.display.add_object(sel, color='red', opacity=0.3) for sel in selections]
|
|
469
|
+
[self.display.add_object(sel, color='red', opacity=0.3, label=labels) for sel in selections]
|
|
357
470
|
self.display.show()
|
|
358
471
|
|
|
359
472
|
return None
|
|
360
473
|
|
|
361
|
-
def set_periodic_cell(self, cell: PeriodicCell,
|
|
474
|
+
def set_periodic_cell(self, cell: PeriodicCell, included_faces: FaceSelection | None = None):
|
|
362
475
|
"""Set the given periodic cell object as the simulations peridicity.
|
|
363
476
|
|
|
364
477
|
Args:
|
|
@@ -367,6 +480,7 @@ class Simulation:
|
|
|
367
480
|
"""
|
|
368
481
|
self.mw.bc._cell = cell
|
|
369
482
|
self._cell = cell
|
|
483
|
+
self._cell.included_faces = included_faces
|
|
370
484
|
|
|
371
485
|
def commit_geometry(self, *geometries: GeoObject | list[GeoObject]) -> None:
|
|
372
486
|
"""Finalizes and locks the current geometry state of the simulation.
|
|
@@ -380,11 +494,19 @@ class Simulation:
|
|
|
380
494
|
else:
|
|
381
495
|
geometries_parsed = unpack_lists(geometries + tuple([item for item in self.data.sim.default.values() if isinstance(item, GeoObject)]))
|
|
382
496
|
|
|
383
|
-
self.data.sim['
|
|
497
|
+
self.data.sim['geos'] = {geo.name: geo for geo in geometries_parsed}
|
|
384
498
|
self.mesher.submit_objects(geometries_parsed)
|
|
385
499
|
self._defined_geometries = True
|
|
386
500
|
self.display._facetags = [dt[1] for dt in gmsh.model.get_entities(2)]
|
|
387
|
-
|
|
501
|
+
|
|
502
|
+
def all_geos(self) -> list[GeoObject]:
|
|
503
|
+
"""Returns all geometries in a list
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
list[GeoObject]: A list of all GeoObjects
|
|
507
|
+
"""
|
|
508
|
+
return _GEOMANAGER.all_geometries()
|
|
509
|
+
|
|
388
510
|
def generate_mesh(self) -> None:
|
|
389
511
|
"""Generate the mesh.
|
|
390
512
|
This can only be done after commit_geometry(...) is called and if frequencies are defined.
|
|
@@ -402,7 +524,7 @@ class Simulation:
|
|
|
402
524
|
if self._cell is not None:
|
|
403
525
|
self.mesher.set_periodic_cell(self._cell)
|
|
404
526
|
|
|
405
|
-
self.mw._initialize_bcs()
|
|
527
|
+
self.mw._initialize_bcs(_GEOMANAGER.get_surfaces())
|
|
406
528
|
|
|
407
529
|
# Check if frequencies are defined: TODO: Replace with a more generic check
|
|
408
530
|
if self.mw.frequencies is None:
|
|
@@ -424,9 +546,11 @@ class Simulation:
|
|
|
424
546
|
logger.error('GMSH Mesh error detected.')
|
|
425
547
|
print(_GMSH_ERROR_TEXT)
|
|
426
548
|
raise
|
|
549
|
+
|
|
427
550
|
self.mesh.update(self.mesher._get_periodic_bcs())
|
|
428
551
|
self.mesh.exterior_face_tags = self.mesher.domain_boundary_face_tags
|
|
429
552
|
gmsh.model.occ.synchronize()
|
|
553
|
+
|
|
430
554
|
self._set_mesh(self.mesh)
|
|
431
555
|
|
|
432
556
|
def parameter_sweep(self, clear_mesh: bool = True, **parameters: np.ndarray) -> Generator[tuple[float,...], None, None]:
|
|
@@ -490,6 +614,16 @@ class Simulation:
|
|
|
490
614
|
filename (str): The filename
|
|
491
615
|
"""
|
|
492
616
|
gmsh.write(filename)
|
|
617
|
+
|
|
618
|
+
def set_solver(self, solver: EMSolver | Solver):
|
|
619
|
+
"""Set a given Solver class instance as the main solver.
|
|
620
|
+
Solvers will be checked on validity for the given problem.
|
|
621
|
+
|
|
622
|
+
Args:
|
|
623
|
+
solver (EMSolver | Solver): The solver objects
|
|
624
|
+
"""
|
|
625
|
+
self.mw.solveroutine.set_solver(solver)
|
|
626
|
+
|
|
493
627
|
############################################################
|
|
494
628
|
# DEPRICATED FUNCTIONS #
|
|
495
629
|
############################################################
|
emerge/_emerge/solver.py
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
# along with this program; if not, see
|
|
16
16
|
# <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
|
-
|
|
19
18
|
from __future__ import annotations
|
|
19
|
+
|
|
20
20
|
from scipy.sparse import csr_matrix # type: ignore
|
|
21
21
|
from scipy.sparse.csgraph import reverse_cuthill_mckee # type: ignore
|
|
22
22
|
from scipy.sparse.linalg import bicgstab, gmres, gcrotmk, eigs, splu # type: ignore
|
|
@@ -286,6 +286,7 @@ class Solver:
|
|
|
286
286
|
"""
|
|
287
287
|
real_only: bool = False
|
|
288
288
|
req_sorter: bool = False
|
|
289
|
+
released_gil: bool = False
|
|
289
290
|
|
|
290
291
|
def __init__(self):
|
|
291
292
|
self.own_preconditioner: bool = False
|
|
@@ -479,7 +480,8 @@ class SolverSuperLU(Solver):
|
|
|
479
480
|
""" Implements Scipi's direct SuperLU solver."""
|
|
480
481
|
req_sorter: bool = False
|
|
481
482
|
real_only: bool = False
|
|
482
|
-
|
|
483
|
+
released_gil: bool = True
|
|
484
|
+
|
|
483
485
|
def __init__(self):
|
|
484
486
|
super().__init__()
|
|
485
487
|
self.atol = 1e-5
|
|
@@ -502,6 +504,7 @@ class SolverSuperLU(Solver):
|
|
|
502
504
|
|
|
503
505
|
def solve(self, A, b, precon, reuse_factorization: bool = False, id: int = -1) -> tuple[np.ndarray, SolveReport]:
|
|
504
506
|
logger.info(f'[ID={id}] Calling SuperLU Solver.')
|
|
507
|
+
|
|
505
508
|
self.single = True
|
|
506
509
|
if not reuse_factorization:
|
|
507
510
|
logger.trace('Computing LU-Decomposition')
|
|
@@ -903,6 +906,10 @@ class SolveRoutine:
|
|
|
903
906
|
bool: If the solver is legal
|
|
904
907
|
"""
|
|
905
908
|
if any(isinstance(solver, solvertype) for solvertype in self.disabled_solver):
|
|
909
|
+
logger.warning(f'The selected solver {solver} cannot be used as it is disabled.')
|
|
910
|
+
return False
|
|
911
|
+
if self.parallel=='MT' and not solver.released_gil:
|
|
912
|
+
logger.warning(f'The selected solver {solver} cannot be used in MultiThreading as it does not release the GIL')
|
|
906
913
|
return False
|
|
907
914
|
return True
|
|
908
915
|
|
emerge/beta/dxf.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .._emerge.geo.pcb_tools.dxf import import_dxf
|
emerge/lib.py
CHANGED
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
║ Verify critical values independently before use. ║
|
|
13
13
|
╚══════════════════════════════════════════════════════════════════════╝
|
|
14
14
|
"""
|
|
15
|
-
from ._emerge.material import Material, AIR, COPPER
|
|
15
|
+
from ._emerge.material import Material, AIR, COPPER, PEC
|
|
16
16
|
from ._emerge.const import C0, Z0, PI, EPS0, MU0
|
|
17
|
+
from .materials import isola
|
|
18
|
+
from .materials import rogers
|
|
17
19
|
|
|
18
20
|
EISO: float = (Z0/(2*PI))**0.5
|
|
19
21
|
EOMNI = (3*Z0/(4*PI))**0.5
|
|
@@ -280,6 +282,7 @@ DIEL_XT_Duroid_8100 = Material(er=3.54, tand=0.0049, color="#21912b
|
|
|
280
282
|
DIEL_XT_Duroid_81000_004IN_Thick = Material(er=3.32, tand=0.0038, color="#21912b", opacity=0.3, name="Rogers XT/duroid 81000 0.004in")
|
|
281
283
|
DIEL_TEFLON = Material(er=2.1, tand=0.0003, color='#eeeeee', opacity=0.3, name="Teflon")
|
|
282
284
|
|
|
285
|
+
DIEL_IS420_approx = Material(er=4.5, tand=0.018, color="#CCDE30", opacity=0.3, name="ISOLA420 approximate")
|
|
283
286
|
|
|
284
287
|
# Legacy FR Materials
|
|
285
288
|
DIEL_FR1 = Material(er=4.8, tand=0.025, color="#3c9747", opacity=0.3, name="FR-1 (Paper Phenolic)")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from . import isola
|