Source code for prismo.io.lumerical.material_db

"""
Lumerical material database import.

This module provides functionality to import material definitions from
Lumerical's material database files.
"""

import json
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Any, Optional

import numpy as np

from prismo.materials.dispersion import (
    DispersiveMaterial,
    DrudeMaterial,
    LorentzMaterial,
    LorentzPole,
)


[docs] class LumericalMaterialDB: """ Parser for Lumerical material database files. Lumerical stores material data in various formats including: - Text files with (n, k) data - XML files with dispersion model parameters - MDF (Material Data Format) files Parameters ---------- db_path : Path Path to Lumerical material database directory or file. """
[docs] def __init__(self, db_path: Path): self.db_path = Path(db_path) self.materials: dict[str, Any] = {}
[docs] def load_material(self, material_name: str) -> Optional[DispersiveMaterial]: """ Load a material from the database. Parameters ---------- material_name : str Name of material to load. Returns ------- DispersiveMaterial or None Material object if found. """ # Search for material file # Lumerical uses various extensions: .txt, .xml, .mdf for ext in [".txt", ".xml", ".mdf", ".json"]: material_file = self.db_path / f"{material_name}{ext}" if material_file.exists(): return self._parse_material_file(material_file) return None
def _parse_material_file(self, file_path: Path) -> Optional[DispersiveMaterial]: """Parse a material data file.""" if file_path.suffix == ".txt": return self._parse_nk_data(file_path) elif file_path.suffix == ".xml": return self._parse_xml_material(file_path) elif file_path.suffix == ".json": return self._parse_json_material(file_path) else: return None def _parse_nk_data(self, file_path: Path) -> Optional[DispersiveMaterial]: """ Parse (n, k) data file. Format: wavelength(nm) n k """ try: # Read data data = np.loadtxt(file_path, skiprows=1) wavelength_nm = data[:, 0] n = data[:, 1] k = data[:, 2] if data.shape[1] > 2 else np.zeros_like(n) # Convert to permittivity epsilon = (n + 1j * k) ** 2 # Fit to Lorentz model (simplified - should use proper fitting) # For now, use constant permittivity at a reference wavelength ref_idx = len(wavelength_nm) // 2 # Middle wavelength epsilon_static = epsilon[ref_idx].real # Create simple material (no dispersion for now) # Full implementation would fit to Lorentz poles material_name = file_path.stem return LorentzMaterial( epsilon_inf=epsilon_static, poles=[], # Would fit poles from n-k data name=material_name, ) except Exception as e: print(f"Error parsing {file_path}: {e}") return None def _parse_xml_material(self, file_path: Path) -> Optional[DispersiveMaterial]: """Parse XML material definition.""" try: tree = ET.parse(file_path) root = tree.getroot() # Extract dispersion model parameters # Lumerical XML format varies - this is simplified model_type = ( root.find(".//model").get("type") if root.find(".//model") is not None else None ) if model_type == "Lorentz": return self._parse_lorentz_xml(root) elif model_type == "Drude": return self._parse_drude_xml(root) else: return None except Exception as e: print(f"Error parsing XML {file_path}: {e}") return None def _parse_lorentz_xml(self, root: ET.Element) -> Optional[LorentzMaterial]: """Parse Lorentz model from XML.""" try: epsilon_inf = float(root.find(".//epsilon_inf").text) poles = [] for pole_elem in root.findall(".//pole"): pole = LorentzPole( omega_0=float(pole_elem.find("omega_0").text), delta_epsilon=float(pole_elem.find("delta_epsilon").text), gamma=float(pole_elem.find("gamma").text), ) poles.append(pole) return LorentzMaterial( epsilon_inf=epsilon_inf, poles=poles, name=root.get("name", "imported") ) except Exception: return None def _parse_drude_xml(self, root: ET.Element) -> Optional[DrudeMaterial]: """Parse Drude model from XML.""" try: epsilon_inf = float(root.find(".//epsilon_inf").text) omega_p = float(root.find(".//omega_p").text) gamma = float(root.find(".//gamma").text) return DrudeMaterial( epsilon_inf=epsilon_inf, omega_p=omega_p, gamma=gamma, name=root.get("name", "imported"), ) except Exception: return None def _parse_json_material(self, file_path: Path) -> Optional[DispersiveMaterial]: """Parse JSON material definition.""" try: with open(file_path) as f: data = json.load(f) model_type = data.get("type", "") if model_type == "Lorentz": poles = [ LorentzPole(**pole_data) for pole_data in data.get("poles", []) ] return LorentzMaterial( epsilon_inf=data["epsilon_inf"], poles=poles, name=data.get("name", "imported"), ) elif model_type == "Drude": return DrudeMaterial( epsilon_inf=data["epsilon_inf"], omega_p=data["omega_p"], gamma=data["gamma"], name=data.get("name", "imported"), ) except Exception as e: print(f"Error parsing JSON {file_path}: {e}") return None
[docs] def import_lumerical_material( file_path: Path, material_name: Optional[str] = None ) -> Optional[DispersiveMaterial]: """ Import a single material from a Lumerical file. Parameters ---------- file_path : Path Path to material data file. material_name : str, optional Material name to use. If None, uses filename. Returns ------- DispersiveMaterial or None Imported material. Examples -------- >>> mat = import_lumerical_material('path/to/Silicon.txt') >>> prismo.add_material('Silicon_imported', mat) """ file_path = Path(file_path) if not file_path.exists(): raise FileNotFoundError(f"Material file not found: {file_path}") # Use database parser db = LumericalMaterialDB(file_path.parent) material = db._parse_material_file(file_path) if material and material_name: material.name = material_name return material
def convert_lumerical_to_prismo_units(lumerical_value: float, quantity: str) -> float: """ Convert Lumerical units to Prismo (SI) units. Parameters ---------- lumerical_value : float Value in Lumerical units. quantity : str Physical quantity ('length', 'frequency', 'power', etc.). Returns ------- float Value in SI units. """ # Lumerical often uses μm for length, THz for frequency conversion = { "length": 1e-6, # μm → m "frequency": 1e12, # THz → Hz "wavelength": 1e-9, # nm → m "power": 1e-3, # mW → W } return lumerical_value * conversion.get(quantity, 1.0)