Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ wheels/
# Virtual environments
.venv
.env
tmp
pytest.log
11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ simulate = [
requires = ["uv_build>=0.9.15,<0.10.0"]
build-backend = "uv_build"

[tool.poe.tasks.precommit]
sequence = [
{cmd = "uv sync --all-groups --all-extras"},
{cmd = "uvx ruff format"},
{cmd = "uvx ruff check"},
{cmd = "uvx ty check"},
{cmd = "pytest --basetemp=tmp"},
]


[tool.pytest.ini_options]
testpaths = ["tests"]
log_cli_level = "ERROR"
Expand All @@ -27,5 +37,6 @@ log_file = "./pytest.log"

[dependency-groups]
dev = [
"poethepoet>=0.40.0",
"pytest>=9.0.2",
]
189 changes: 189 additions & 0 deletions src/pyxems/csx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
from dataclasses import dataclass, field
from typing import Literal, get_args, Protocol

Axes = Literal["X", "Y", "Z"]


@dataclass(frozen=True)
class Line:
axe: Axes
position: list[float] = field(default_factory=list)

def to_xml(self) -> str:
xml = f'<{self.axe.upper()}Lines Qty="{len(self.position)}">'
xml += ",".join([f"{value}" for value in self.position])
xml += f"</{self.axe.upper()}Lines>"
return xml


@dataclass()
class Physical:
value: tuple[float, float, float]

def __init__(self, value: float | int | tuple[float, float, float]):
if isinstance(value, float) or isinstance(value, int):
self.value = (value, 1.0, 1.0)
else:
self.value = value

def __str__(self, short=True) -> str:
if short:
return f"{self.value[0]:g}"
else:
return ",".join([f"{v:e}" for v in self.value])


@dataclass()
class Material:
name: str
epsilon_r: Physical
mu_r: Physical
kappa: Physical = field(default_factory=lambda: Physical((0, 0, 0)))
sigma: Physical = field(default_factory=lambda: Physical((0, 0, 0)))
density: float = 0.0

def to_xml(self, short=True) -> str:
if short:
return f'<{self.name} Epsilon="{self.epsilon_r}" Mue="{self.mu_r}" Kappa="{self.kappa}" Sigma="{self.sigma}" />'
else:
return f'<{self.name} Epsilon="{self.epsilon_r.__str__(short=False)}" Mue="{self.mu_r.__str__(short=False)}" Kappa="{self.kappa.__str__(short=False)}" Sigma="{self.sigma.__str__(short=False)}" Density="{self.density:e}" />'


@dataclass(frozen=True)
class Color:
r: int
g: int
b: int
a: int = 255

def to_xml(self) -> str:
return f'R="{self.r}" G="{self.g}" B="{self.b}" a="{self.a}"'


class Primitive(Protocol):
def to_xml(self) -> str: ...


point = tuple[float, float, float]


@dataclass(frozen=True)
class Box(Primitive):
start: point
stop: point
priority: int = 0

def to_xml(self) -> str:
xml = f'<Box Priority="{self.priority}">\n'
xml += f' <P1 X="{self.start[0]:8e}" Y="{self.start[1]:8e}" Z="{self.start[2]:8e}" />\n'
xml += f' <P2 X="{self.stop[0]:8e}" Y="{self.stop[1]:8e}" Z="{self.stop[2]:8e}" />\n'
xml += "</Box>"
return xml


@dataclass(frozen=True)
class Property:
name: str
id: int
kind: Literal["Metal", "Material"] = "Material"
fillcolor: Color = field(default_factory=lambda: Color(255, 255, 255, 255))
edgecolor: Color = field(default_factory=lambda: Color(0, 0, 0, 255))
material: Material = field(
default_factory=lambda: Material("Property", Physical(1.0), Physical(1.0))
)
weight: Material = field(
default_factory=lambda: Material(
"Weight",
Physical(1.0),
Physical(1.0),
Physical((1.0, 1.0, 1.0)),
Physical((1.0, 1.0, 1.0)),
1.0,
)
)
_primitive: list[Primitive] = field(default_factory=list)

def to_xml(self) -> str:
iso = ' Isotropy="1"' if self.kind == "Material" else ""
xml = f'<{self.kind} ID="{self.id}" Name="{self.name}"{iso}>\n'
xml += f" <FillColor {self.fillcolor.to_xml()} />\n"
xml += f" <EdgeColor {self.edgecolor.to_xml()} />\n"
xml += " <Primitives>\n"
for primitive in self._primitive:
for line in primitive.to_xml().splitlines():
xml += f" {line}\n"
xml += " </Primitives>\n"
if self.kind == "Material":
xml += f" {self.material.to_xml(False)}\n"
xml += f" {self.weight.to_xml(False)}\n"
xml += f"</{self.kind}>\n"
return xml


@dataclass(frozen=True)
class ContinousStructure:
coordinates_system: int = 0
lines: dict[Axes, Line] = field(default_factory=dict)
background_material: Material = field(
default_factory=lambda: Material(
"BackgroundMaterial", Physical(1.0), Physical(1.0)
)
)
properties: list[Property] = field(default_factory=list)

def __post_init__(self):
for axe in get_args(Axes):
self.lines[axe] = Line(axe.upper())

def add_line(self, axe: Axes, position: float):
self.lines[axe].position.append(position)

def add_property(
self,
kind: Literal["Metal", "Material"],
name: str,
fillcolor: Color = Color(255, 255, 255, 255),
edgecolor: Color = Color(0, 0, 0, 255),
eps: float = 1.0,
mu: float = 1.0,
kappa: float = 0.0,
sigma: float = 0.0,
):
id = len(self.properties)
prop = Property(
name,
id,
kind,
fillcolor,
edgecolor,
material=Material(
"Property",
Physical(eps),
Physical(mu),
Physical((kappa, 0, 0)),
Physical((sigma, 0, 0)),
),
)
self.properties.append(prop)

def add_box(
self, start: point, stop: point, priority: int = 0, property_id: int = 0
):
box = Box(start, stop, priority)
self.properties[property_id]._primitive.append(box)

def to_xml(self) -> str:
xml = f'<ContinuousStructure CoordSystem="{self.coordinates_system}">\n'
xml += ' <RectilinearGrid DeltaUnit="0.001" CoordSystem="0">\n'
for line in self.lines.values():
xml += f" {line.to_xml()}\n"
xml += " </RectilinearGrid>\n"
xml += f" {self.background_material.to_xml()}\n"
xml += " <ParameterSet />\n"
xml += " <Properties>\n"
for property in self.properties:
for line in property.to_xml().splitlines():
xml += f" {line}\n"
xml += " </Properties>\n"
xml += "</ContinuousStructure>\n"
return xml
35 changes: 35 additions & 0 deletions src/pyxems/fdtd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from dataclasses import dataclass, field
from typing import Literal

Boundary = Literal["MUR", "PEC", "PMC", "PML"]


@dataclass(frozen=True)
class BoundaryCond:
xmin: Boundary = "MUR"
xmax: Boundary = "MUR"
ymin: Boundary = "MUR"
ymax: Boundary = "MUR"
zmin: Boundary = "MUR"
zmax: Boundary = "MUR"

def to_xml(self) -> str:
elem = "<BoundaryCond "
for dim, value in self.__dict__.items():
elem += f'{dim}="{value}" '
elem += "/>"
return elem


@dataclass(frozen=True)
class FDTDConfig:
max_time_step: int = 1_000_000
boundary_cond: BoundaryCond = field(default_factory=BoundaryCond)
exitation: int = 0

def to_xml(self) -> str:
xml = f'<FDTD MaxTimeStep="{self.max_time_step}">\n'
xml += f" {self.boundary_cond.to_xml()}\n"
xml += f" <Excitation Type={self.exitation} />\n"
xml += "</FDTD>\n"
return xml
89 changes: 2 additions & 87 deletions src/pyxems/main.py
Original file line number Diff line number Diff line change
@@ -1,93 +1,8 @@
from pathlib import Path
from typing import Literal, get_args
from dataclasses import dataclass, field

Boundary = Literal["MUR", "PEC", "PMC", "PML"]


@dataclass(frozen=True)
class Material:
name: str
epsilon_r: float
mu_r: float
kappa: float = 0.0
sigma: float = 0.0

def to_xml(self) -> str:
return f'<{self.name} Epsilon="{self.epsilon_r:g}" Mue="{self.mu_r:g}" Kappa="{self.kappa:g}" Sigma="{self.sigma:g}" />'


@dataclass(frozen=True)
class BoundaryCond:
xmin: Boundary = "MUR"
xmax: Boundary = "MUR"
ymin: Boundary = "MUR"
ymax: Boundary = "MUR"
zmin: Boundary = "MUR"
zmax: Boundary = "MUR"

def to_xml(self) -> str:
elem = "<BoundaryCond "
for dim, value in self.__dict__.items():
elem += f'{dim}="{value}" '
elem += "/>"
return elem


@dataclass(frozen=True)
class FDTDConfig:
max_time_step: int = 1_000_000
boundary_cond: BoundaryCond = field(default_factory=BoundaryCond)
exitation: int = 0

def to_xml(self) -> str:
xml = f'<FDTD MaxTimeStep="{self.max_time_step}">\n'
xml += f" {self.boundary_cond.to_xml()}\n"
xml += f" <Excitation Type={self.exitation} />\n"
xml += "</FDTD>\n"
return xml


Axes = Literal["X", "Y", "Z"]


@dataclass(frozen=True)
class Line:
axe: Axes
position: list[float] = field(default_factory=list)

def to_xml(self) -> str:
xml = f'<{self.axe.upper()}Lines Qty="{len(self.position)}">'
xml += ",".join([f"{value}" for value in self.position])
xml += f"</{self.axe.upper()}Lines>"
return xml


@dataclass(frozen=True)
class ContinousStructure:
coordinates_system: int = 0
lines: dict[Axes, Line] = field(default_factory=dict)
background_material: Material = field(
default_factory=lambda: Material("BackgroundMaterial", 1.0, 1.0)
)
# TODO: add parameters settings for the structure

def __post_init__(self):
for axe in get_args(Axes):
self.lines[axe] = Line(axe.upper())

def add_line(self, axe: Axes, position: float):
self.lines[axe].position.append(position)

def to_xml(self) -> str:
xml = f'<ContinuousStructure CoordSystem="{self.coordinates_system}">\n'
xml += ' <RectilinearGrid DeltaUnit="0.001" CoordSystem="0">\n'
for line in self.lines.values():
xml += f" {line.to_xml()}\n"
xml += " </RectilinearGrid>\n"
xml += f" {self.background_material.to_xml()}\n"
xml += "</ContinuousStructure>\n"
return xml
from pyxems.fdtd import FDTDConfig
from pyxems.csx import ContinousStructure


@dataclass(frozen=True)
Expand Down
27 changes: 21 additions & 6 deletions src/pyxems/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,29 @@
app = cyclopts.App(__name__)


def find_openems_executable() -> Optional[Path]:
if which("openEMS") is not None:
return Path(which("openEMS")) # type: ignore
load_dotenv()
if "OPENEMS_PATH" in os.environ:
openems_path = Path(os.environ["OPENEMS_PATH"]) / "openEMS"
if openems_path.is_file():
return openems_path
return None


def check_config() -> bool:
openems_executable = find_openems_executable()
return openems_executable is not None


@app.command()
def simulate(config_path: Path, run_dir: Optional[Path]) -> CompletedProcess:
if which("openEMS") is None:
load_dotenv()
if "OPENEMS_PATH" in os.environ:
openems_path = Path(os.environ["OPENEMS_PATH"]) / "openEMS"
else:
openems_path = "openEMS"
openems_path = find_openems_executable()
if openems_path is None:
raise FileNotFoundError(
"OpenEMS executable not found. Please ensure it is installed and in your PATH, or set the OPENEMS_PATH environment variable."
)
config_path = config_path.resolve()
if run_dir is None:
run_dir = config_path.parent
Expand Down
Loading
Loading