Source code for prismo.io.lumerical.fsp_parser

"""
Parser for Lumerical .fsp (FDTD project) files.

This module provides functionality to parse Lumerical FDTD Solutions
project files and convert them to Prismo format.
"""

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


@dataclass
class FSPGeometry:
    """Geometry object from FSP file."""

    name: str
    type: str  # 'rectangle', 'circle', 'polygon', etc.
    center: tuple
    size: tuple
    material: str
    properties: dict[str, Any] = field(default_factory=dict)


@dataclass
class FSPSource:
    """Source definition from FSP file."""

    name: str
    type: str  # 'mode', 'gaussian', 'plane_wave', etc.
    center: tuple
    size: tuple
    properties: dict[str, Any] = field(default_factory=dict)


@dataclass
class FSPMonitor:
    """Monitor definition from FSP file."""

    name: str
    type: str  # 'time', 'frequency', 'mode_expansion', etc.
    center: tuple
    size: tuple
    properties: dict[str, Any] = field(default_factory=dict)


[docs] @dataclass class FSPProject: """ Complete FSP project data. Attributes ---------- filename : Path Original .fsp filename. geometries : List[FSPGeometry] Geometry objects. sources : List[FSPSource] Source definitions. monitors : List[FSPMonitor] Monitor definitions. simulation_region : dict Simulation domain parameters. materials : Dict[str, Any] Custom material definitions. metadata : dict Additional metadata. """ filename: Path geometries: list[FSPGeometry] = field(default_factory=list) sources: list[FSPSource] = field(default_factory=list) monitors: list[FSPMonitor] = field(default_factory=list) simulation_region: dict[str, Any] = field(default_factory=dict) materials: dict[str, Any] = field(default_factory=dict) metadata: dict[str, Any] = field(default_factory=dict)
[docs] class FSPParser: """ Parser for Lumerical .fsp files. Lumerical FDTD Solutions saves projects as .fsp files, which are compressed archives containing XML and binary data. This parser extracts the relevant information for conversion to Prismo. Parameters ---------- fsp_file : Path Path to .fsp file. """
[docs] def __init__(self, fsp_file: Path): self.fsp_file = Path(fsp_file) if not self.fsp_file.exists(): raise FileNotFoundError(f"FSP file not found: {fsp_file}") self.project: Optional[FSPProject] = None
[docs] def parse(self) -> FSPProject: """ Parse the .fsp file. Returns ------- FSPProject Parsed project data. """ # FSP files are compressed archives (similar to ZIP) # They contain XML files and binary data # This is a simplified parser - full implementation would: # 1. Extract .fsp archive # 2. Parse XML structure files # 3. Extract geometry definitions # 4. Extract source/monitor definitions # 5. Extract material assignments project = FSPProject(filename=self.fsp_file) # Placeholder parsing logic # Real implementation would use zipfile to extract and parse try: # Attempt to parse as text/XML directly (some FSP files) self._parse_xml_structure(project) except Exception: # Try archive extraction self._parse_archive(project) self.project = project return project
def _parse_xml_structure(self, project: FSPProject) -> None: """Parse XML structure from FSP file.""" # Placeholder - real implementation would parse actual FSP XML # Example structure parsing project.metadata["parser_note"] = ( "FSP parser is a simplified implementation. " "Full parsing requires reverse engineering of Lumerical's format." ) def _parse_archive(self, project: FSPProject) -> None: """Extract and parse FSP archive.""" import zipfile try: with zipfile.ZipFile(self.fsp_file, "r") as zf: # List files in archive files = zf.namelist() project.metadata["archive_files"] = files # Look for structure files for filename in files: if filename.endswith(".xml"): # Parse XML file with zf.open(filename) as f: xml_content = f.read() self._parse_xml_content(xml_content, project) except zipfile.BadZipFile: # Not a zip archive - might be binary format project.metadata["format"] = "binary" def _parse_xml_content(self, xml_content: bytes, project: FSPProject) -> None: """Parse XML content.""" try: root = ET.fromstring(xml_content) # Extract geometries for geom_elem in root.findall(".//geometry"): geometry = self._parse_geometry_element(geom_elem) if geometry: project.geometries.append(geometry) # Extract sources for source_elem in root.findall(".//source"): source = self._parse_source_element(source_elem) if source: project.sources.append(source) # Extract monitors for monitor_elem in root.findall(".//monitor"): monitor = self._parse_monitor_element(monitor_elem) if monitor: project.monitors.append(monitor) except ET.ParseError: pass def _parse_geometry_element(self, elem: ET.Element) -> Optional[FSPGeometry]: """Parse a geometry element from XML.""" try: return FSPGeometry( name=elem.get("name", "unnamed"), type=elem.get("type", "unknown"), center=(0, 0, 0), # Extract from XML size=(0, 0, 0), # Extract from XML material=elem.get("material", ""), ) except Exception: return None def _parse_source_element(self, elem: ET.Element) -> Optional[FSPSource]: """Parse a source element from XML.""" try: return FSPSource( name=elem.get("name", "unnamed"), type=elem.get("type", "unknown"), center=(0, 0, 0), size=(0, 0, 0), ) except Exception: return None def _parse_monitor_element(self, elem: ET.Element) -> Optional[FSPMonitor]: """Parse a monitor element from XML.""" try: return FSPMonitor( name=elem.get("name", "unnamed"), type=elem.get("type", "unknown"), center=(0, 0, 0), size=(0, 0, 0), ) except Exception: return None
[docs] def to_prismo_simulation(self): """ Convert FSP project to Prismo Simulation. Returns ------- Simulation Prismo simulation object configured from FSP file. """ from prismo import Simulation if self.project is None: raise RuntimeError("Must parse FSP file first") # Extract simulation region # Create Prismo simulation # (Simplified - would need proper parameter mapping) sim = Simulation( size=(10e-6, 5e-6, 0), # Placeholder resolution=50e6, ) # Add geometries, sources, monitors # (Would require full conversion logic) return sim
[docs] def export_summary(self, output_file: Optional[Path] = None) -> Path: """ Export summary of FSP contents to JSON. Parameters ---------- output_file : Path, optional Output file path. Returns ------- Path Path to summary file. """ if self.project is None: raise RuntimeError("Must parse FSP file first") if output_file is None: output_file = self.output_dir / f"{self.fsp_file.stem}_summary.json" summary = { "filename": str(self.fsp_file), "num_geometries": len(self.project.geometries), "num_sources": len(self.project.sources), "num_monitors": len(self.project.monitors), "geometries": [ {"name": g.name, "type": g.type, "material": g.material} for g in self.project.geometries ], "sources": [{"name": s.name, "type": s.type} for s in self.project.sources], "monitors": [ {"name": m.name, "type": m.type} for m in self.project.monitors ], "metadata": self.project.metadata, } with open(output_file, "w") as f: json.dump(summary, f, indent=2) return output_file