gsim 0.0.0__py3-none-any.whl → 0.0.2__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.
gsim/__init__.py CHANGED
@@ -9,7 +9,7 @@ Currently includes:
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
- __version__ = "0.0.0"
12
+ __version__ = "0.0.2"
13
13
 
14
14
  __all__ = [
15
15
  "__version__",
@@ -0,0 +1,61 @@
1
+ """Common components shared between Palace and FDTD solvers.
2
+
3
+ This module provides shared data models and utilities that can be used
4
+ across different electromagnetic solvers (Palace, FDTD, etc.).
5
+
6
+ Classes:
7
+ Geometry: Wrapper for gdsfactory Component with computed properties
8
+ LayerStack: Layer stack data model with extraction from PDK
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from gsim.common.geometry import Geometry
14
+ from gsim.common.stack import (
15
+ MATERIALS_DB,
16
+ Layer,
17
+ LayerStack,
18
+ MaterialProperties,
19
+ StackLayer,
20
+ ValidationResult,
21
+ extract_from_pdk,
22
+ extract_layer_stack,
23
+ get_material_properties,
24
+ get_stack,
25
+ load_stack_yaml,
26
+ material_is_conductor,
27
+ material_is_dielectric,
28
+ parse_layer_stack,
29
+ plot_stack,
30
+ print_stack,
31
+ print_stack_table,
32
+ )
33
+
34
+ # Alias for backward compatibility
35
+ Stack = LayerStack
36
+
37
+ __all__ = [
38
+ # Geometry
39
+ "Geometry",
40
+ # Stack (LayerStack is the primary name, Stack is an alias)
41
+ "LayerStack",
42
+ "Stack",
43
+ "Layer",
44
+ "StackLayer",
45
+ "ValidationResult",
46
+ # Stack functions
47
+ "get_stack",
48
+ "load_stack_yaml",
49
+ "extract_from_pdk",
50
+ "extract_layer_stack",
51
+ "parse_layer_stack",
52
+ "print_stack",
53
+ "print_stack_table",
54
+ "plot_stack",
55
+ # Materials
56
+ "MATERIALS_DB",
57
+ "MaterialProperties",
58
+ "get_material_properties",
59
+ "material_is_conductor",
60
+ "material_is_dielectric",
61
+ ]
@@ -0,0 +1,76 @@
1
+ """Geometry wrapper for gdsfactory components.
2
+
3
+ This module provides a Geometry class that wraps gdsfactory Components
4
+ with computed properties useful for simulation setup.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from functools import cached_property
10
+ from typing import Any
11
+
12
+ from pydantic import BaseModel, ConfigDict
13
+
14
+
15
+ class Geometry(BaseModel):
16
+ """Shared geometry wrapper for gdsfactory Component.
17
+
18
+ This class wraps a gdsfactory Component and provides computed properties
19
+ that are useful for simulation setup (bounds, ports, etc.).
20
+
21
+ Attributes:
22
+ component: The wrapped gdsfactory Component
23
+
24
+ Example:
25
+ >>> from gdsfactory.components import straight
26
+ >>> c = straight(length=100)
27
+ >>> geom = Geometry(component=c)
28
+ >>> print(geom.bounds)
29
+ (0.0, -0.25, 100.0, 0.25)
30
+ """
31
+
32
+ model_config = ConfigDict(arbitrary_types_allowed=True)
33
+
34
+ component: Any # gdsfactory Component (Any to avoid import issues)
35
+
36
+ @cached_property
37
+ def bounds(self) -> tuple[float, float, float, float]:
38
+ """Get bounding box (xmin, ymin, xmax, ymax) in um."""
39
+ bbox = self.component.dbbox()
40
+ return (bbox.left, bbox.bottom, bbox.right, bbox.top)
41
+
42
+ @cached_property
43
+ def ports(self) -> list:
44
+ """Get list of ports on the component."""
45
+ return list(self.component.ports)
46
+
47
+ @cached_property
48
+ def port_names(self) -> list[str]:
49
+ """Get list of port names."""
50
+ return [p.name for p in self.component.ports]
51
+
52
+ @property
53
+ def width(self) -> float:
54
+ """Get width (x-extent) of geometry in um."""
55
+ xmin, _, xmax, _ = self.bounds
56
+ return xmax - xmin
57
+
58
+ @property
59
+ def height(self) -> float:
60
+ """Get height (y-extent) of geometry in um."""
61
+ _, ymin, _, ymax = self.bounds
62
+ return ymax - ymin
63
+
64
+ def get_port(self, name: str) -> Any:
65
+ """Get a port by name.
66
+
67
+ Args:
68
+ name: Port name to find
69
+
70
+ Returns:
71
+ Port object if found, None otherwise
72
+ """
73
+ for port in self.component.ports:
74
+ if port.name == name:
75
+ return port
76
+ return None
@@ -1,8 +1,11 @@
1
- """Layer stack extraction and parsing for Palace EM simulation.
1
+ """Layer stack extraction and parsing for EM simulation.
2
+
3
+ This module provides stack extraction functionality that can be shared
4
+ between different solvers (Palace, FDTD, etc.).
2
5
 
3
6
  Usage:
4
7
  # From PDK module (preferred, no file needed)
5
- from gsim.palace.stack import get_stack
8
+ from gsim.common.stack import get_stack
6
9
  stack = get_stack(pdk=ihp)
7
10
 
8
11
  # From YAML file (for custom/tweaked stacks)
@@ -19,21 +22,21 @@ from pathlib import Path
19
22
  import gdsfactory as gf
20
23
  import yaml
21
24
 
22
- from gsim.palace.stack.extractor import (
25
+ from gsim.common.stack.extractor import (
23
26
  Layer,
24
27
  LayerStack,
25
28
  ValidationResult,
26
29
  extract_from_pdk,
27
30
  extract_layer_stack,
28
31
  )
29
- from gsim.palace.stack.materials import (
32
+ from gsim.common.stack.materials import (
30
33
  MATERIALS_DB,
31
34
  MaterialProperties,
32
35
  get_material_properties,
33
36
  material_is_conductor,
34
37
  material_is_dielectric,
35
38
  )
36
- from gsim.palace.stack.visualization import (
39
+ from gsim.common.stack.visualization import (
37
40
  StackLayer,
38
41
  parse_layer_stack,
39
42
  plot_stack,
@@ -7,15 +7,15 @@ that can be used for Palace EM simulation.
7
7
  from __future__ import annotations
8
8
 
9
9
  import logging
10
- from dataclasses import dataclass, field
11
10
  from pathlib import Path
12
- from typing import Any
11
+ from typing import Any, Literal
13
12
 
14
13
  import yaml
15
14
  from gdsfactory.technology import LayerLevel
16
15
  from gdsfactory.technology import LayerStack as GfLayerStack
16
+ from pydantic import BaseModel, ConfigDict, Field
17
17
 
18
- from gsim.palace.stack.materials import (
18
+ from gsim.common.stack.materials import (
19
19
  MATERIALS_DB,
20
20
  get_material_properties,
21
21
  )
@@ -23,20 +23,18 @@ from gsim.palace.stack.materials import (
23
23
  logger = logging.getLogger(__name__)
24
24
 
25
25
 
26
- @dataclass
27
- class Layer:
26
+ class Layer(BaseModel):
28
27
  """Layer information for Palace simulation."""
29
28
 
29
+ model_config = ConfigDict(validate_assignment=True)
30
+
30
31
  name: str
31
32
  gds_layer: tuple[int, int] # (layer, datatype)
32
33
  zmin: float # um
33
34
  zmax: float # um
34
35
  thickness: float # um
35
36
  material: str
36
- layer_type: str # "conductor", "via", "dielectric", "substrate"
37
-
38
- # Mesh resolution control
39
- # Options: "fine", "medium", "coarse" or a float in um
37
+ layer_type: Literal["conductor", "via", "dielectric", "substrate"]
40
38
  mesh_resolution: str | float = "medium"
41
39
 
42
40
  def get_mesh_size(self, base_size: float = 1.0) -> float:
@@ -56,7 +54,7 @@ class Layer:
56
54
  "medium": base_size,
57
55
  "coarse": base_size * 2.0,
58
56
  }
59
- return resolution_map.get(self.mesh_resolution, base_size)
57
+ return resolution_map.get(str(self.mesh_resolution), base_size)
60
58
 
61
59
  def to_dict(self) -> dict:
62
60
  """Convert to dictionary for YAML output."""
@@ -71,13 +69,14 @@ class Layer:
71
69
  }
72
70
 
73
71
 
74
- @dataclass
75
- class ValidationResult:
72
+ class ValidationResult(BaseModel):
76
73
  """Result of stack validation."""
77
74
 
75
+ model_config = ConfigDict(validate_assignment=True)
76
+
78
77
  valid: bool
79
- errors: list[str] = field(default_factory=list)
80
- warnings: list[str] = field(default_factory=list)
78
+ errors: list[str] = Field(default_factory=list)
79
+ warnings: list[str] = Field(default_factory=list)
81
80
 
82
81
  def __bool__(self) -> bool:
83
82
  return self.valid
@@ -97,18 +96,19 @@ class ValidationResult:
97
96
  return "\n".join(lines)
98
97
 
99
98
 
100
- @dataclass
101
- class LayerStack:
99
+ class LayerStack(BaseModel):
102
100
  """Complete layer stack for Palace simulation."""
103
101
 
104
- pdk_name: str
102
+ model_config = ConfigDict(validate_assignment=True)
103
+
104
+ pdk_name: str = "unknown"
105
105
  units: str = "um"
106
- layers: dict[str, Layer] = field(default_factory=dict)
107
- materials: dict[str, dict] = field(default_factory=dict)
108
- dielectrics: list[dict] = field(default_factory=list)
109
- simulation: dict = field(default_factory=dict)
106
+ layers: dict[str, Layer] = Field(default_factory=dict)
107
+ materials: dict[str, dict] = Field(default_factory=dict)
108
+ dielectrics: list[dict] = Field(default_factory=list)
109
+ simulation: dict = Field(default_factory=dict)
110
110
 
111
- def validate(self, tolerance: float = 0.001) -> ValidationResult:
111
+ def validate_stack(self, tolerance: float = 0.001) -> ValidationResult:
112
112
  """Validate the layer stack for simulation readiness.
113
113
 
114
114
  Checks:
@@ -132,7 +132,6 @@ class LayerStack:
132
132
  # Collect materials from layers
133
133
  for name, layer in self.layers.items():
134
134
  materials_used.add(layer.material)
135
- # Check for invalid thickness
136
135
  if layer.thickness < 0:
137
136
  errors.append(
138
137
  f"Layer '{name}' has negative thickness: {layer.thickness}"
@@ -165,10 +164,8 @@ class LayerStack:
165
164
 
166
165
  # 2. Check z-axis continuity of dielectrics
167
166
  if self.dielectrics:
168
- # Sort dielectrics by zmin
169
167
  sorted_dielectrics = sorted(self.dielectrics, key=lambda d: d["zmin"])
170
168
 
171
- # Check for gaps between dielectric regions
172
169
  for i in range(len(sorted_dielectrics) - 1):
173
170
  current = sorted_dielectrics[i]
174
171
  next_d = sorted_dielectrics[i + 1]
@@ -181,13 +178,11 @@ class LayerStack:
181
178
  f"(zmin={next_d['zmin']:.4f}): gap={gap:.4f} um"
182
179
  )
183
180
  elif gap < -tolerance:
184
- # Overlap - this might be intentional
185
181
  warnings.append(
186
182
  f"Z-axis overlap between '{current['name']}' and "
187
183
  f"'{next_d['name']}': overlap={-gap:.4f} um"
188
184
  )
189
185
 
190
- # Get overall dielectric envelope
191
186
  z_min_dielectric = sorted_dielectrics[0]["zmin"]
192
187
  z_max_dielectric = sorted_dielectrics[-1]["zmax"]
193
188
  else:
@@ -281,61 +276,43 @@ class LayerStack:
281
276
 
282
277
 
283
278
  def _get_gds_layer_tuple(layer_level: LayerLevel) -> tuple[int, int]:
284
- """Extract GDS layer tuple from LayerLevel.
285
-
286
- The layer can be specified in various ways in gdsfactory:
287
- - tuple: (8, 0)
288
- - int: 8
289
- - LogicalLayer with nested layer enum
290
- - kfactory LayerEnum with .layer and .datatype
291
- """
279
+ """Extract GDS layer tuple from LayerLevel."""
292
280
  layer: Any = layer_level.layer
293
281
 
294
- # Handle different layer specifications
295
282
  if isinstance(layer, tuple):
296
283
  return (int(layer[0]), int(layer[1]))
297
284
 
298
285
  if isinstance(layer, int):
299
286
  return (int(layer), 0)
300
287
 
301
- # Handle LogicalLayer (gdsfactory.technology.layer_stack.LogicalLayer)
302
- # Structure: layer_level.layer (LogicalLayer) -> .layer (Enum) -> .layer/.datatype
303
- # (int)
304
288
  if hasattr(layer, "layer"):
305
289
  inner = layer.layer
306
- # Check if inner has layer/datatype (kfactory/gdsfactory enum)
307
290
  if hasattr(inner, "layer") and hasattr(inner, "datatype"):
308
291
  return (int(inner.layer), int(inner.datatype))
309
- # Check if inner itself has the values
310
292
  if isinstance(inner, int):
311
293
  datatype = getattr(layer, "datatype", 0)
312
294
  return (int(inner), int(datatype) if datatype else 0)
313
- # Recurse one more level if needed
314
295
  if hasattr(inner, "layer"):
315
296
  innermost = inner.layer
316
297
  if isinstance(innermost, int):
317
298
  datatype = getattr(inner, "datatype", 0)
318
299
  return (int(innermost), int(datatype) if datatype else 0)
319
300
 
320
- # Direct enum with layer/datatype
321
301
  if hasattr(layer, "layer") and hasattr(layer, "datatype"):
322
302
  return (int(layer.layer), int(layer.datatype))
323
303
 
324
- # Enum with tuple value
325
304
  if hasattr(layer, "value"):
326
305
  if isinstance(layer.value, tuple):
327
306
  return (int(layer.value[0]), int(layer.value[1]))
328
307
  if isinstance(layer.value, int):
329
308
  return (int(layer.value), 0)
330
309
 
331
- # String format "8/0"
332
310
  if isinstance(layer, str):
333
311
  if "/" in layer:
334
312
  parts = layer.split("/")
335
313
  return (int(parts[0]), int(parts[1]))
336
314
  return (0, 0)
337
315
 
338
- # Fallback
339
316
  try:
340
317
  return (int(layer), 0)
341
318
  except (TypeError, ValueError):
@@ -343,34 +320,24 @@ def _get_gds_layer_tuple(layer_level: LayerLevel) -> tuple[int, int]:
343
320
  return (0, 0)
344
321
 
345
322
 
346
- def _classify_layer_type(layer_name: str, material: str) -> str:
347
- """Classify a layer as conductor, via, dielectric, or substrate.
348
-
349
- Args:
350
- layer_name: Name of the layer (e.g., "metal1", "via1", "substrate")
351
- material: Material name
352
-
353
- Returns:
354
- Layer type string
355
- """
323
+ def _classify_layer_type(
324
+ layer_name: str, material: str
325
+ ) -> Literal["conductor", "via", "dielectric", "substrate"]:
326
+ """Classify a layer as conductor, via, dielectric, or substrate."""
356
327
  name_lower = layer_name.lower()
357
328
  material_lower = material.lower()
358
329
 
359
- # Check for via layers
360
330
  if "via" in name_lower:
361
331
  return "via"
362
332
 
363
- # Check for metal layers
364
333
  if any(
365
334
  m in name_lower for m in ["metal", "topmetal", "m1", "m2", "m3", "m4", "m5"]
366
335
  ):
367
336
  return "conductor"
368
337
 
369
- # Check for substrate
370
338
  if "substrate" in name_lower or name_lower == "sub":
371
339
  return "substrate"
372
340
 
373
- # Check by material
374
341
  props = get_material_properties(material)
375
342
  if props:
376
343
  if props.type == "conductor":
@@ -380,15 +347,12 @@ def _classify_layer_type(layer_name: str, material: str) -> str:
380
347
  if props.type == "dielectric":
381
348
  return "dielectric"
382
349
 
383
- # Check for common conductor materials
384
350
  if material_lower in ["aluminum", "copper", "tungsten", "gold", "al", "cu", "w"]:
385
351
  return "conductor"
386
352
 
387
- # Check for poly
388
353
  if "poly" in name_lower:
389
354
  return "conductor"
390
355
 
391
- # Default to dielectric
392
356
  return "dielectric"
393
357
 
394
358
 
@@ -408,41 +372,28 @@ def extract_layer_stack(
408
372
  substrate_thickness: Thickness of substrate in um (default: 2.0)
409
373
  air_above: Height of air box above top metal in um (default: 200)
410
374
  boundary_margin: Lateral margin from GDS bbox in um (default: 30)
411
- include_substrate: Whether to include lossy substrate (default: False).
412
- When False, matches gds2palace "nosub" behavior for RF simulation.
375
+ include_substrate: Whether to include lossy substrate (default: False)
413
376
 
414
377
  Returns:
415
378
  LayerStack object for Palace simulation
416
379
  """
417
380
  stack = LayerStack(pdk_name=pdk_name)
418
381
 
419
- # Track z-range for dielectric regions
420
382
  z_min_overall = float("inf")
421
383
  z_max_overall = float("-inf")
422
-
423
- # Collect materials used
424
384
  materials_used: set[str] = set()
425
385
 
426
- # Extract each layer
427
386
  for layer_name, layer_level in gf_layer_stack.layers.items():
428
- # Get layer properties
429
387
  zmin = layer_level.zmin if layer_level.zmin is not None else 0.0
430
388
  thickness = layer_level.thickness if layer_level.thickness is not None else 0.0
431
389
  zmax = zmin + thickness
432
390
  material = layer_level.material if layer_level.material else "unknown"
433
-
434
- # Get GDS layer tuple
435
391
  gds_layer = _get_gds_layer_tuple(layer_level)
436
-
437
- # Classify layer type
438
392
  layer_type = _classify_layer_type(layer_name, material)
439
393
 
440
- # Skip substrate layers when include_substrate=False (matches gds2palace
441
- # "nosub").
442
394
  if layer_type == "substrate" and not include_substrate:
443
395
  continue
444
396
 
445
- # Create layer
446
397
  layer = Layer(
447
398
  name=layer_name,
448
399
  gds_layer=gds_layer,
@@ -456,34 +407,27 @@ def extract_layer_stack(
456
407
  stack.layers[layer_name] = layer
457
408
  materials_used.add(material)
458
409
 
459
- # Update z-range (only for non-substrate layers to get metal stack extent)
460
410
  if layer_type != "substrate":
461
411
  z_min_overall = min(z_min_overall, zmin)
462
412
  z_max_overall = max(z_max_overall, zmax)
463
413
 
464
- # Add material properties
465
414
  for material in materials_used:
466
415
  props = get_material_properties(material)
467
416
  if props:
468
417
  stack.materials[material] = props.to_dict()
469
418
  else:
470
- # Unknown material - add placeholder
471
419
  stack.materials[material] = {
472
420
  "type": "unknown",
473
421
  "note": "Material not in database, please add properties manually",
474
422
  }
475
423
 
476
- # Always add air for the air box
477
424
  if "air" not in stack.materials:
478
425
  stack.materials["air"] = MATERIALS_DB["air"].to_dict()
479
426
 
480
- # Add SiO2 for inter-layer dielectric if not present
481
427
  if "SiO2" not in stack.materials:
482
428
  stack.materials["SiO2"] = MATERIALS_DB["SiO2"].to_dict()
483
429
 
484
- # Generate dielectric regions
485
430
  if include_substrate:
486
- # 1. Substrate below z=0 (lossy silicon)
487
431
  stack.dielectrics.append(
488
432
  {
489
433
  "name": "substrate",
@@ -496,12 +440,8 @@ def extract_layer_stack(
496
440
  stack.materials["silicon"] = MATERIALS_DB["silicon"].to_dict()
497
441
  oxide_zmin = 0.0
498
442
  else:
499
- # No substrate - extend oxide down slightly (matches gds2palace "nosub")
500
- # This provides a dielectric spacing below the bottom metal
501
443
  oxide_zmin = -substrate_thickness
502
444
 
503
- # 2. Inter-layer dielectric (simplified: one big oxide region)
504
- # In reality, should fill gaps between metals
505
445
  stack.dielectrics.append(
506
446
  {
507
447
  "name": "oxide",
@@ -511,8 +451,7 @@ def extract_layer_stack(
511
451
  }
512
452
  )
513
453
 
514
- # 3. Passivation layer on top of oxide (matches gds2palace IHP SG13G2)
515
- passive_thickness = 0.4 # um, from gds2palace XML
454
+ passive_thickness = 0.4
516
455
  stack.dielectrics.append(
517
456
  {
518
457
  "name": "passive",
@@ -524,7 +463,6 @@ def extract_layer_stack(
524
463
  if "passive" not in stack.materials:
525
464
  stack.materials["passive"] = MATERIALS_DB["passive"].to_dict()
526
465
 
527
- # 4. Air above passivation
528
466
  stack.dielectrics.append(
529
467
  {
530
468
  "name": "air_box",
@@ -534,7 +472,6 @@ def extract_layer_stack(
534
472
  }
535
473
  )
536
474
 
537
- # Simulation settings
538
475
  stack.simulation = {
539
476
  "boundary_margin": boundary_margin,
540
477
  "air_above": air_above,
@@ -560,42 +497,31 @@ def extract_from_pdk(
560
497
  Returns:
561
498
  LayerStack object for Palace simulation
562
499
  """
563
- # Get PDK name - handle both module and Pdk object
564
500
  pdk_name = "unknown"
565
501
 
566
- # Direct .name attribute (gdsfactory Pdk object)
567
502
  if hasattr(pdk_module, "name") and isinstance(pdk_module.name, str):
568
503
  pdk_name = pdk_module.name
569
- # Module with PDK.name
570
504
  elif hasattr(pdk_module, "PDK") and hasattr(pdk_module.PDK, "name"):
571
505
  pdk_name = pdk_module.PDK.name
572
- # Module __name__
573
506
  elif hasattr(pdk_module, "__name__"):
574
507
  pdk_name = pdk_module.__name__
575
508
 
576
- # Get layer stack from PDK - handle both module and Pdk object
577
509
  gf_layer_stack = None
578
510
 
579
- # Direct layer_stack attribute (gdsfactory Pdk object)
580
511
  if hasattr(pdk_module, "layer_stack") and pdk_module.layer_stack is not None:
581
512
  gf_layer_stack = pdk_module.layer_stack
582
- # Module with LAYER_STACK
583
513
  elif hasattr(pdk_module, "LAYER_STACK"):
584
514
  gf_layer_stack = pdk_module.LAYER_STACK
585
- # Module with get_layer_stack()
586
515
  elif hasattr(pdk_module, "get_layer_stack"):
587
516
  gf_layer_stack = pdk_module.get_layer_stack()
588
- # Module with PDK.layer_stack
589
517
  elif hasattr(pdk_module, "PDK") and hasattr(pdk_module.PDK, "layer_stack"):
590
518
  gf_layer_stack = pdk_module.PDK.layer_stack
591
519
 
592
520
  if gf_layer_stack is None:
593
521
  raise ValueError(f"Could not find layer stack in PDK: {pdk_module}")
594
522
 
595
- # Extract
596
523
  stack = extract_layer_stack(gf_layer_stack, pdk_name=pdk_name, **kwargs)
597
524
 
598
- # Write to file if path provided
599
525
  if output_path:
600
526
  stack.to_yaml(output_path)
601
527
 
@@ -6,17 +6,20 @@ This database provides the EM properties needed for Palace simulation.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from dataclasses import dataclass
9
+ from typing import Literal
10
10
 
11
+ from pydantic import BaseModel, ConfigDict, Field
11
12
 
12
- @dataclass
13
- class MaterialProperties:
13
+
14
+ class MaterialProperties(BaseModel):
14
15
  """EM properties for a material."""
15
16
 
16
- type: str # "conductor", "dielectric", "semiconductor"
17
- conductivity: float | None = None # S/m (for conductors)
18
- permittivity: float | None = None # relative permittivity
19
- loss_tangent: float | None = None # dielectric loss tangent
17
+ model_config = ConfigDict(validate_assignment=True)
18
+
19
+ type: Literal["conductor", "dielectric", "semiconductor"]
20
+ conductivity: float | None = Field(default=None, ge=0) # S/m (for conductors)
21
+ permittivity: float | None = Field(default=None, ge=1.0) # relative permittivity
22
+ loss_tangent: float | None = Field(default=None, ge=0, le=1)
20
23
 
21
24
  def to_dict(self) -> dict[str, object]:
22
25
  """Convert to dictionary for YAML output."""
@@ -29,6 +32,20 @@ class MaterialProperties:
29
32
  d["loss_tangent"] = self.loss_tangent
30
33
  return d
31
34
 
35
+ @classmethod
36
+ def conductor(cls, conductivity: float = 5.8e7) -> "MaterialProperties":
37
+ """Create a conductor material."""
38
+ return cls(type="conductor", conductivity=conductivity)
39
+
40
+ @classmethod
41
+ def dielectric(
42
+ cls, permittivity: float, loss_tangent: float = 0.0
43
+ ) -> "MaterialProperties":
44
+ """Create a dielectric material."""
45
+ return cls(
46
+ type="dielectric", permittivity=permittivity, loss_tangent=loss_tangent
47
+ )
48
+
32
49
 
33
50
  # Material properties database
34
51
  # Sources:
@@ -38,7 +55,7 @@ MATERIALS_DB: dict[str, MaterialProperties] = {
38
55
  # Conductors (conductivity in S/m)
39
56
  "aluminum": MaterialProperties(
40
57
  type="conductor",
41
- conductivity=3.77e7, # S/m
58
+ conductivity=3.77e7,
42
59
  ),
43
60
  "copper": MaterialProperties(
44
61
  type="conductor",
@@ -64,7 +81,7 @@ MATERIALS_DB: dict[str, MaterialProperties] = {
64
81
  "SiO2": MaterialProperties(
65
82
  type="dielectric",
66
83
  permittivity=4.1, # Matches gds2palace IHP SG13G2
67
- loss_tangent=0.0, # Matches gds2palace (no loss)
84
+ loss_tangent=0.0,
68
85
  ),
69
86
  "passive": MaterialProperties(
70
87
  type="dielectric",
@@ -100,7 +117,7 @@ MATERIALS_DB: dict[str, MaterialProperties] = {
100
117
  "si": MaterialProperties(
101
118
  type="semiconductor",
102
119
  permittivity=11.9,
103
- conductivity=2.0, # ~50 Ω·cm substrate (matches gds2palace)
120
+ conductivity=2.0,
104
121
  ),
105
122
  }
106
123
 
@@ -130,7 +147,6 @@ def get_material_properties(material_name: str) -> MaterialProperties | None:
130
147
  Returns:
131
148
  MaterialProperties if found, None otherwise
132
149
  """
133
- # Normalize name
134
150
  name_lower = material_name.lower().strip()
135
151
 
136
152
  # Check direct match