From c99ca29307a3e71fafacaeb2dc9f993b8b66497c Mon Sep 17 00:00:00 2001 From: David Lurz Date: Wed, 16 Sep 2026 11:54:45 +0200 Subject: [PATCH 1/3] COBRA ruff rules + automatic ruff fixes --- examples/main.py | 2 +- examples/slurm_runs/main.py | 3 +- pyproject.toml | 112 ++++++++++++++++-- src/orca/__init__.py | 10 +- src/orca/__main__.py | 2 + src/orca/geometry/base_geometry.py | 3 +- src/orca/geometry/cells/inductor.py | 3 +- src/orca/geometry/cells/transformer.py | 14 ++- src/orca/geometry/input_parameters.py | 3 +- src/orca/geometry/presets/inductor_octa.py | 10 +- src/orca/geometry/presets/tf_octa_c_ports.py | 9 +- src/orca/gui/app.py | 7 +- src/orca/gui/pipeline_window.py | 78 ++++++------ src/orca/gui/theme.py | 2 +- src/orca/gui/utils.py | 25 ++-- src/orca/gui/widgets/geometry_selector.py | 47 +++++--- src/orca/gui/widgets/stage_widget.py | 70 +++++------ src/orca/logger.py | 3 +- src/orca/orca.py | 21 ++-- src/orca/pipeline/context.py | 2 +- src/orca/pipeline/export_onnx_stage.py | 10 +- src/orca/pipeline/gds_conversion_stage.py | 11 +- src/orca/pipeline/gds_gen_stage.py | 14 ++- src/orca/pipeline/pipeline_stage.py | 5 +- src/orca/pipeline/simulation_stage.py | 16 +-- src/orca/pipeline/test_model_stage.py | 11 +- src/orca/pipeline/training_stage.py | 12 +- src/orca/simulation/combine_snp_results.py | 21 ++-- src/orca/simulation/gds_converter.py | 7 +- src/orca/simulation/simulate.py | 3 +- src/orca/training/basis_expansion.py | 11 +- src/orca/training/datasets/base_dataset.py | 4 +- src/orca/training/datasets/geo_to_ntwk.py | 13 +- src/orca/training/datasets/geo_to_s_param.py | 2 +- .../datasets/geo_to_s_param_single_f.py | 2 +- src/orca/training/guarantees.py | 4 +- src/orca/training/losses.py | 2 +- src/orca/training/models/base_model.py | 9 +- src/orca/training/models/mlp.py | 2 +- src/orca/training/models/transformer.py | 2 +- src/orca/training/normalize.py | 9 +- src/orca/training/onnx_wrapper.py | 2 + src/orca/training/trainer.py | 13 +- src/orca/training/tuner.py | 10 +- src/orca/utils/class_finder.py | 6 +- src/orca/utils/postprocessing.py | 7 +- 46 files changed, 393 insertions(+), 241 deletions(-) diff --git a/examples/main.py b/examples/main.py index 4e3accd..4557eb2 100644 --- a/examples/main.py +++ b/examples/main.py @@ -1,6 +1,6 @@ -import orca import numpy as np +import orca from orca.geometry.presets.tf_octa_c_ports import TransformerOcta PLOT = False diff --git a/examples/slurm_runs/main.py b/examples/slurm_runs/main.py index 626f75e..c2effdd 100644 --- a/examples/slurm_runs/main.py +++ b/examples/slurm_runs/main.py @@ -1,8 +1,9 @@ -import orca import numpy as np +import orca from orca.geometry.presets.tf_octa_c_ports import TransformerOcta + def main(): # Use predefined geometry from examples np.random.seed(40) diff --git a/pyproject.toml b/pyproject.toml index 65c67ed..dd9d031 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,8 +36,13 @@ authors = [ ] classifiers = [ "Development Status :: 4 - Beta", + "Environment :: Console", + "Environment :: X11 Applications :: Qt", "Intended Audience :: Developers", - "Operating System :: Unix", + "Intended Audience :: Science/Research", + "Intended Audience :: Education", + "Natural Language :: English", + "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", @@ -134,10 +139,103 @@ orca = "src/orca" [tool.setuptools.package-data] "orca" = ["**/*.simcfg", "**/*.xml"] -[tool.black] -line-length = 79 -target-version = ['py311', 'py312', 'py313'] +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-ra --strict-markers --strict-config" +xfail_strict = true -[tool.isort] -profile = "black" -line_length = 79 +[tool.coverage.run] +source = ["src/cobra"] +branch = true + +[tool.coverage.report] +show_missing = true +exclude_also = [ + "if TYPE_CHECKING:", + "raise NotImplementedError", + "@(abc\\.)?abstractmethod", +] + + +[tool.ruff] +line-length = 100 +target-version = "py313" +# Resolve first-party imports out of the src layout. +src = ["src"] +# Scratch/vendored trees that are not part of the package. +extend-exclude = ["Ignore"] + +[tool.ruff.lint] +select = [ + "ALL", # include all the rules, including new ones +] +ignore = [ + #### modules + "ANN", # flake8-annotations: full annotation coverage is not a project goal + "COM", # flake8-commas: trailing commas are the formatter's job + "C90", # mccabe complexity: PLR0912/PLR0915 cover this better + "CPY", # flake8-copyright: no per-file copyright headers in this project + "DJ", # django + "EM", # exception messages inline, consistent with TRY003 below + "EXE", # flake8-executable + "FBT", # boolean traps: unavoidable in Qt slots and config flags + "PD", # pandas-vet: false positives on numpy/scikit-rf `.values` + "T10", # debugger + "TID", # flake8-tidy-imports + + #### specific rules + "D100", # ignore missing docs + "D101", + "D102", + "D103", + "D104", + "D105", + "D106", + "D107", + "D200", + "D203", # incompatible with D211; D211 is the convention here + "D205", + "D212", # summary placement is not enforced; both styles exist in the tree + "D400", + "D401", + "D415", + "E402", # false positives for local imports + "E501", # line too long + "N802", # RF symbols keep their conventional casing (S, Z, Y, N, K, ...) + "N803", + "N806", + "PLC0415", # imports are deliberately local to keep Palace/ORCA/GUI optional + "PLR0911", # simulation and GUI setup code is legitimately branch-heavy + "PLR0912", + "PLR0913", + "PLR0915", + "PLR0917", + "PLR2004", # magic-value comparison is noise in numeric/engineering code + "PTH", # os.path is used throughout; migrating is a separate refactor + "RUF001", # ambiguous unicode: units and symbols (µ, Ω, °) are intentional + "RUF002", + "RUF003", + "S603", # the Xyce subprocess call is the point of the simulator + "TRY003", # external messages in exceptions are too verbose + "TD002", + "TD003", + "FIX002", # too verbose descriptions of todos +] + +[tool.ruff.lint.per-file-ignores] +# Examples are standalone scripts, not an importable package. +"examples/*" = ["INP001", "T201"] +# Tests assert, reach into private helpers deliberately, and use fixtures that +# look like shadowed or unused arguments to the linter. +"tests/*" = [ + "S101", # assert is how pytest works + "ARG001", + "ARG002", + "PLR6301", + "SLF001", # pinning private behaviour (_tell, _parse_target) is the point + "TC003", + "T201", # printing is how a test script reports +] + +[tool.ruff.lint.pydocstyle] +convention = "google" diff --git a/src/orca/__init__.py b/src/orca/__init__.py index fad95cb..c6ffe52 100644 --- a/src/orca/__init__.py +++ b/src/orca/__init__.py @@ -1,15 +1,15 @@ # Import stuff here so that they are available at the package level (i.e. from orca import ORCA, BaseGeometry, InputParameters) +from .geometry.base_geometry import BaseGeometry +from .geometry.input_parameters import InputParameterIterator from .orca import ORCA from .pipeline.context import PipelineContext -from .pipeline.pipeline_stage import PipelineStage -from .pipeline.gds_gen_stage import GDSGenerator from .pipeline.gds_conversion_stage import GDSConverter +from .pipeline.gds_gen_stage import GDSGenerator +from .pipeline.pipeline_stage import PipelineStage from .pipeline.simulation_stage import PalaceSimulator -from .geometry.base_geometry import BaseGeometry -from .geometry.input_parameters import InputParameterIterator +from .training.codecs import FlatReImCodec, OutputCodec, UpperTriangleReImCodec from .training.guarantees import PhysicsGuarantees -from .training.codecs import OutputCodec, FlatReImCodec, UpperTriangleReImCodec from .training.spec import FrequencyMode, IOSpec # Everything that needs PyTorch (the "train" extra) is imported on first access diff --git a/src/orca/__main__.py b/src/orca/__main__.py index 9bd6117..39f13f4 100644 --- a/src/orca/__main__.py +++ b/src/orca/__main__.py @@ -1,6 +1,8 @@ import sys + from orca.gui.app import run_gui + def main(): """ Main entry point for ORCA. diff --git a/src/orca/geometry/base_geometry.py b/src/orca/geometry/base_geometry.py index a9c3e8d..5796b1b 100644 --- a/src/orca/geometry/base_geometry.py +++ b/src/orca/geometry/base_geometry.py @@ -1,8 +1,9 @@ from __future__ import annotations + from abc import ABC, abstractmethod from dataclasses import dataclass from functools import cached_property -from typing import Any, TYPE_CHECKING +from typing import TYPE_CHECKING, Any from orca.geometry.input_parameters import InputParameterIterator diff --git a/src/orca/geometry/cells/inductor.py b/src/orca/geometry/cells/inductor.py index 4c3a57f..7a12dd5 100644 --- a/src/orca/geometry/cells/inductor.py +++ b/src/orca/geometry/cells/inductor.py @@ -172,8 +172,7 @@ def get_min_outer_diameter(N, w, s): min_crossover_size = (2 * s + w) * (math.sqrt(2) - 1) + (s + w) + 2 * overlap_size - if crossover_size < min_crossover_size: - crossover_size = min_crossover_size + crossover_size = max(crossover_size, min_crossover_size) if N < 3: inner_segment_size = crossover_size diff --git a/src/orca/geometry/cells/transformer.py b/src/orca/geometry/cells/transformer.py index 894fc38..cf37b1b 100644 --- a/src/orca/geometry/cells/transformer.py +++ b/src/orca/geometry/cells/transformer.py @@ -1,14 +1,16 @@ import gdsfactory as gf import klayout.db as kdb import numpy as np + from orca.geometry.layers import SG13G2 GRID_NM = 10 # 10 nm manufacturing grid = 0.01 µm def _ensure_active_pdk() -> None: - """gdsfactory refuses to extrude paths without an active PDK. We only use it as a - polygon generator with explicit SG13G2 layer tuples, so its generic PDK is enough.""" + """Gdsfactory refuses to extrude paths without an active PDK. We only use it as a + polygon generator with explicit SG13G2 layer tuples, so its generic PDK is enough. + """ try: gf.get_active_pdk() except ValueError: @@ -120,18 +122,18 @@ def tf_octa_c( # Check if linewidth is too large for winding diameter if bottom_linewidth > bottom_winding_diameter / 3.0: raise ValueError("bottom_linewidth is too large for input_winding_diameter.") - elif top_linewidth > top_winding_diameter / 3.0: + if top_linewidth > top_winding_diameter / 3.0: raise ValueError("upper_linewidth is too large for output_winding_diameter.") # Check if center tap width is too large for winding diameter of the other winding if bottom_centertap_width > top_winding_diameter / 3.0: raise ValueError( "bottom_center_tap_width is too large for output_winding_diameter." ) - elif top_centertap_width > bottom_winding_diameter / 3.0: + if top_centertap_width > bottom_winding_diameter / 3.0: raise ValueError( "upper_center_tap_width is too large for input_winding_diameter." ) - elif abs(bottom_winding_diameter - top_winding_diameter) > 40.0: + if abs(bottom_winding_diameter - top_winding_diameter) > 40.0: raise ValueError( "input_winding_diameter and output_winding_diameter difference is too large. No sufficient coupling." ) @@ -463,4 +465,4 @@ def add_port_marker(center, width, layer, orientation): # so the cutout geometry and reference offsets stay untouched. _snap_inplace(c) - return c \ No newline at end of file + return c diff --git a/src/orca/geometry/input_parameters.py b/src/orca/geometry/input_parameters.py index fac3ac9..bc852f2 100644 --- a/src/orca/geometry/input_parameters.py +++ b/src/orca/geometry/input_parameters.py @@ -1,6 +1,7 @@ import threading from itertools import product from typing import Any + import numpy as np @@ -90,6 +91,7 @@ def get_min_max_values(self) -> tuple[list[float], list[float]]: """ Returns the minimum and maximum values for each input parameter. Useful for normalization purposes. + Returns: tuple: A tuple containing two lists - (min_values, max_values). """ @@ -131,7 +133,6 @@ def uniform_grid(self): This method creates a grid by uniformly sampling each parameter's range by selecting values such that the total number of samples is approximately equal to num_samples. """ - # Calculate number of steps per parameter (nth root of n_samples) steps_per_param = int(np.ceil(self.n_samples ** (1 / self.n_inputs))) diff --git a/src/orca/geometry/presets/inductor_octa.py b/src/orca/geometry/presets/inductor_octa.py index 7d59202..15ddf14 100644 --- a/src/orca/geometry/presets/inductor_octa.py +++ b/src/orca/geometry/presets/inductor_octa.py @@ -1,10 +1,9 @@ -from dataclasses import dataclass, field -from typing import Any, TYPE_CHECKING -import numpy as np import os +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any from orca import BaseGeometry -from orca.geometry.cells.inductor import symmetric_octa_IHP, get_min_outer_diameter +from orca.geometry.cells.inductor import get_min_outer_diameter, symmetric_octa_IHP from orca.geometry.input_parameters import InputParameterIterator if TYPE_CHECKING: @@ -79,8 +78,7 @@ def create_gds_file(name: str, output_path: str, params: dict[str, Any]) -> str: # clamp the outer diameter to the minimum buildable (DRC-valid) value do_min = get_min_outer_diameter(N, w, s) - if D < do_min: - D = do_min + D = max(D, do_min) symmetric_octa_IHP( N=N, D=D, w=w, s=s, diff --git a/src/orca/geometry/presets/tf_octa_c_ports.py b/src/orca/geometry/presets/tf_octa_c_ports.py index 3de4458..cf92b1a 100644 --- a/src/orca/geometry/presets/tf_octa_c_ports.py +++ b/src/orca/geometry/presets/tf_octa_c_ports.py @@ -1,7 +1,6 @@ -from dataclasses import dataclass, field -from typing import Any, TYPE_CHECKING -import numpy as np import os +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any from orca import BaseGeometry from orca.geometry.cells.transformer import tf_octa_c @@ -105,7 +104,7 @@ def create_gds_file(name: str, output_path: str, params: dict[str, Any]) -> str: # c.show() c.write_gds(output_path, with_metadata=False) return output_path - + # def postprocess_outputs(self, output, frequency_points=None): # """ # Converts model outputs (Re/Im) into a .sNp Touchstone file format. @@ -143,4 +142,4 @@ def create_gds_file(name: str, output_path: str, params: dict[str, Any]) -> str: # input_params = np.array([70.6, 74.6, 13.2, 6.4, 5.4]) # Example input parameters # onnx_session = onnxruntime.InferenceSession("/home/david/Documents/git/ORCA/output/tf_octa_c_ports/models/tf_octa_c_ports.onnx") # ntwk = geometry.inference_snp(onnx_session, input_params) -# plot_rfic_transformer_metrics(ntwk) \ No newline at end of file +# plot_rfic_transformer_metrics(ntwk) diff --git a/src/orca/gui/app.py b/src/orca/gui/app.py index 4cfd6ae..c4750b9 100644 --- a/src/orca/gui/app.py +++ b/src/orca/gui/app.py @@ -1,11 +1,14 @@ import sys + from PySide6.QtWidgets import QApplication -from orca.gui.theme import apply_theme + from orca.gui.pipeline_window import PipelineWindow +from orca.gui.theme import apply_theme + def run_gui(): app = QApplication(sys.argv) - + window = PipelineWindow() apply_theme(window) window.show() diff --git a/src/orca/gui/pipeline_window.py b/src/orca/gui/pipeline_window.py index 78061af..e3dc89e 100644 --- a/src/orca/gui/pipeline_window.py +++ b/src/orca/gui/pipeline_window.py @@ -1,16 +1,26 @@ -from PySide6.QtWidgets import ( - QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, - QPushButton, QScrollArea, QLabel, QProgressBar, QMessageBox, QPlainTextEdit -) +import logging + from PySide6.QtCore import QThread, Signal from PySide6.QtGui import QFontDatabase +from PySide6.QtWidgets import ( + QHBoxLayout, + QLabel, + QMainWindow, + QMessageBox, + QPlainTextEdit, + QProgressBar, + QPushButton, + QScrollArea, + QVBoxLayout, + QWidget, +) from orca import ORCA from orca.gui.utils import get_available_stages from orca.gui.widgets.geometry_selector import GeometrySelector from orca.gui.widgets.stage_widget import StageConfigWidget from orca.logger import logger -import logging + class LogSignalHandler(logging.Handler): def __init__(self, signal): @@ -37,7 +47,7 @@ def __init__(self, orca_instance, geometry): def run(self): try: self.orca.run( - geometry=self.geometry, + geometry=self.geometry, progress_callback=self.progress_callback, overwrite_callback=self.overwrite_callback ) @@ -53,10 +63,10 @@ def overwrite_callback(self, base_dir): self._overwrite_result = None self._waiting_for_overwrite = True self.confirm_overwrite.emit(base_dir) - + while self._waiting_for_overwrite: self.msleep(100) - + return self._overwrite_result def set_overwrite_result(self, result): @@ -70,19 +80,19 @@ def __init__(self): super().__init__() self.setWindowTitle("ORCA Pipeline") self.resize(1000, 800) - + self.stages_widgets = [] self._log_handler = None self.init_ui() self.setup_logging() - + def init_ui(self): central_widget = QWidget() self.setCentralWidget(central_widget) - + main_layout = QHBoxLayout() central_widget.setLayout(main_layout) - + # Left Panel: Configuration config_scroll = QScrollArea() config_scroll.setWidgetResizable(True) @@ -90,53 +100,53 @@ def init_ui(self): config_layout = QVBoxLayout() config_widget.setLayout(config_layout) config_scroll.setWidget(config_widget) - + # Geometry Section config_layout.addWidget(QLabel("

1. Geometry Selection

")) self.geometry_selector = GeometrySelector() config_layout.addWidget(self.geometry_selector) - + # Stages Section config_layout.addWidget(QLabel("

2. Pipeline Stages

")) config_layout.addWidget(QLabel("Select active stages and configure parameters:")) - + available_stages = get_available_stages() for stage_cls in available_stages: sw = StageConfigWidget(stage_cls) self.stages_widgets.append(sw) config_layout.addWidget(sw) - + config_layout.addStretch() - + # Right Panel: Logs and button right_panel = QWidget() right_layout = QVBoxLayout() right_panel.setLayout(right_layout) - + # Log output self.log_output = QPlainTextEdit() self.log_output.setReadOnly(True) self.log_output.setPlaceholderText("Logs will appear here...") self.log_output.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap) self.log_output.setFont(QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)) - + # Progress bar that uses the callbacks self.progress_bar = QProgressBar() self.progress_label = QLabel("Ready") - + # Run Button self.btn_run = QPushButton("Run Pipeline") self.btn_run.setMinimumHeight(50) self.btn_run.setStyleSheet("font-size: 16px; font-weight: bold;") self.btn_run.clicked.connect(self.run_pipeline) - + # Right side: "Console"-style log output and progress right_layout.addWidget(QLabel("

Logs & Status

")) right_layout.addWidget(self.log_output) right_layout.addWidget(self.progress_label) right_layout.addWidget(self.progress_bar) right_layout.addWidget(self.btn_run) - + # Add sub-widgets to main layout main_layout.addWidget(config_scroll, 1) main_layout.addWidget(right_panel, 1) @@ -157,38 +167,38 @@ def run_pipeline(self): if not geometry: QMessageBox.warning(self, "Invalid Geometry", "Please select a valid geometry first.") return - + stages = [] for sw in self.stages_widgets: stage_instance = sw.get_instance() if stage_instance: stages.append(stage_instance) - + if not stages: QMessageBox.warning(self, "No Stages", "Please select at least one pipeline stage.") return - + # Instantiate ORCA orca_instance = ORCA(stages) - + # Disable button self.btn_run.setEnabled(False) self.log_output.clear() self.log_output.appendPlainText("Starting pipeline...") - + self.worker = PipelineWorker(orca_instance, geometry) self.worker.progress.connect(self.update_progress) self.worker.confirm_overwrite.connect(self.handle_overwrite_confirmation) self.worker.finished.connect(self.pipeline_finished) self.worker.error.connect(self.pipeline_error) self.worker.start() - + def handle_overwrite_confirmation(self, base_dir): reply = QMessageBox.question( - self, - "Overwrite Confirmation", + self, + "Overwrite Confirmation", f"Output directory {base_dir} already exists. Stages may overwrite existing files. Continue?", - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) self.worker.set_overwrite_result(reply == QMessageBox.StandardButton.Yes) @@ -199,19 +209,19 @@ def update_progress(self, stage_name, current, total, message): self.progress_bar.setValue(int(current / total * 100)) else: self.progress_bar.setValue(0) - + def pipeline_finished(self): self.btn_run.setEnabled(True) self.progress_label.setText("Pipeline Completed Successfully") self.progress_bar.setValue(100) QMessageBox.information(self, "Success", "ORCA Pipeline finished successfully!") - + def pipeline_error(self, error_msg): self.btn_run.setEnabled(True) self.progress_label.setText("Error Occurred") self.log_output.appendPlainText(f"ERROR: {error_msg}") QMessageBox.critical(self, "Pipeline Error", f"An error occurred:\n{error_msg}") - + def closeEvent(self, event): if self._log_handler is not None: logger.removeHandler(self._log_handler) diff --git a/src/orca/gui/theme.py b/src/orca/gui/theme.py index 944c5f2..7c4bcee 100644 --- a/src/orca/gui/theme.py +++ b/src/orca/gui/theme.py @@ -109,4 +109,4 @@ def apply_theme(widget) -> None: - widget.setStyleSheet(THEME_STYLESHEET) \ No newline at end of file + widget.setStyleSheet(THEME_STYLESHEET) diff --git a/src/orca/gui/utils.py b/src/orca/gui/utils.py index d79adf0..194fb1f 100644 --- a/src/orca/gui/utils.py +++ b/src/orca/gui/utils.py @@ -1,27 +1,27 @@ import importlib.util -import sys import inspect +import sys from pathlib import Path -from typing import List, Type, Any +from typing import Any from orca.geometry.base_geometry import BaseGeometry from orca.geometry.presets.inductor_octa import InductorOcta from orca.logger import logger -from orca.pipeline.pipeline_stage import PipelineStage +from orca.pipeline.gds_conversion_stage import GDSConverter # Import all pipeline stages from orca.pipeline.gds_gen_stage import GDSGenerator -from orca.pipeline.gds_conversion_stage import GDSConverter +from orca.pipeline.pipeline_stage import PipelineStage from orca.pipeline.simulation_stage import PalaceSimulator # The training stages need PyTorch (the "train" extra). Without it the GUI still # offers the GDS generation, conversion and simulation stages. try: - from orca.pipeline.training_stage import ModelTrainer from orca.pipeline.export_onnx_stage import OnnxExporter from orca.pipeline.test_model_stage import ModelTester + from orca.pipeline.training_stage import ModelTrainer except ModuleNotFoundError as e: - _TRAINING_STAGES: List[Type[PipelineStage]] = [] + _TRAINING_STAGES: list[type[PipelineStage]] = [] logger.warning( f"Training stages are unavailable ({e}). Install ORCA's optional " "training dependencies with `pip install -e '.[train]'` to enable them." @@ -29,10 +29,11 @@ else: _TRAINING_STAGES = [ModelTrainer, OnnxExporter, ModelTester] -# Import all preset geometries +# Import all preset geometries from orca.geometry.presets.tf_octa_c_ports import TransformerOcta -def load_class_from_file(file_path: str, base_class: Type) -> Type[Any] | None: + +def load_class_from_file(file_path: str, base_class: type) -> type[Any] | None: """ Loads a class that inherits from `base_class` from a given file path. """ @@ -42,11 +43,11 @@ def load_class_from_file(file_path: str, base_class: Type) -> Type[Any] | None: # Module name from file name module_name = path.stem - + spec = importlib.util.spec_from_file_location(module_name, file_path) if not spec or not spec.loader: return None - + module = importlib.util.module_from_spec(spec) sys.modules[module_name] = module try: @@ -62,13 +63,13 @@ def load_class_from_file(file_path: str, base_class: Type) -> Type[Any] | None: return obj return None -def get_available_stages() -> List[Type[PipelineStage]]: +def get_available_stages() -> list[type[PipelineStage]]: """ Returns a list of available PipelineStage subclasses. """ return [GDSGenerator, GDSConverter, PalaceSimulator, *_TRAINING_STAGES] -def get_preset_geometries() -> List[Type[BaseGeometry]]: +def get_preset_geometries() -> list[type[BaseGeometry]]: """ Returns a list of preset BaseGeometry subclasses. """ diff --git a/src/orca/gui/widgets/geometry_selector.py b/src/orca/gui/widgets/geometry_selector.py index bc4d1bb..251b52d 100644 --- a/src/orca/gui/widgets/geometry_selector.py +++ b/src/orca/gui/widgets/geometry_selector.py @@ -1,9 +1,18 @@ from PySide6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QComboBox, - QPushButton, QLabel, QLineEdit, QFileDialog, QMessageBox + QComboBox, + QFileDialog, + QHBoxLayout, + QLabel, + QLineEdit, + QMessageBox, + QPushButton, + QVBoxLayout, + QWidget, ) -from orca.gui.utils import get_preset_geometries, load_class_from_file + from orca.geometry.base_geometry import BaseGeometry +from orca.gui.utils import get_preset_geometries, load_class_from_file + class GeometrySelector(QWidget): """ @@ -14,55 +23,55 @@ def __init__(self, parent=None): super().__init__(parent) self.current_geometry_class = None self.current_geometry_instance = None - + self.init_ui() self.load_presets() - + def init_ui(self): layout = QVBoxLayout() self.setLayout(layout) - + # Selection Mode mode_layout = QHBoxLayout() self.combo_presets = QComboBox() self.combo_presets.currentIndexChanged.connect(self.on_preset_changed) - + self.btn_load_custom = QPushButton("Load Custom .py") self.btn_load_custom.clicked.connect(self.load_custom_file) - + layout.addWidget(QLabel("Select Geometry:")) layout.addLayout(mode_layout) mode_layout.addWidget(self.combo_presets) mode_layout.addWidget(self.btn_load_custom) - + # Name Override name_layout = QHBoxLayout() self.name_input = QLineEdit() self.name_input.setPlaceholderText("Geometry Name") - + name_layout.addWidget(QLabel("Geometry Name:")) name_layout.addWidget(self.name_input) layout.addLayout(name_layout) - + self.lbl_status = QLabel("No geometry loaded") layout.addWidget(self.lbl_status) - + def load_presets(self): self.combo_presets.clear() self.combo_presets.addItem("Select a preset...", None) - + presets = get_preset_geometries() for cls in presets: self.combo_presets.addItem(cls.__name__, cls) - + def on_preset_changed(self, index): if index == 0: return - + cls = self.combo_presets.currentData() if cls and issubclass(cls, BaseGeometry): self.load_geometry_from_class(cls) - + def load_custom_file(self): file_path, _ = QFileDialog.getOpenFileName( self, "Open Geometry File", "", "Python Files (*.py)" @@ -75,7 +84,7 @@ def load_custom_file(self): self.lbl_status.setText("Error loading geometry") return self.load_geometry_from_class(cls) - + def load_geometry_from_class(self, cls): try: # Instantiate with default arguments @@ -91,10 +100,10 @@ def load_geometry_from_class(self, cls): def get_geometry(self): if not self.current_geometry_instance: return None - + # Update name new_name = self.name_input.text() if new_name: self.current_geometry_instance.name = new_name - + return self.current_geometry_instance diff --git a/src/orca/gui/widgets/stage_widget.py b/src/orca/gui/widgets/stage_widget.py index 5d2950f..78a3158 100644 --- a/src/orca/gui/widgets/stage_widget.py +++ b/src/orca/gui/widgets/stage_widget.py @@ -1,72 +1,78 @@ import inspect import json -from typing import Type from PySide6.QtWidgets import ( - QWidget, QVBoxLayout, QCheckBox, QLabel, QLineEdit, - QSpinBox, QDoubleSpinBox, QFormLayout, QGroupBox + QCheckBox, + QDoubleSpinBox, + QFormLayout, + QGroupBox, + QLabel, + QLineEdit, + QSpinBox, + QVBoxLayout, + QWidget, ) from orca.pipeline.pipeline_stage import PipelineStage + class StageConfigWidget(QWidget): """ This widget represents a single PipelineStage configuration panel. """ - def __init__(self, stage_class: Type[PipelineStage], parent=None): + def __init__(self, stage_class: type[PipelineStage], parent=None): super().__init__(parent) self.stage_class = stage_class self.parameter_inputs = {} - + self.init_ui() - + def init_ui(self): layout = QVBoxLayout() self.setLayout(layout) - + self.group_box = QGroupBox(self.stage_class.__name__) self.group_box.setCheckable(True) self.group_box.setChecked(True) - + form_layout = QFormLayout() self.group_box.setLayout(form_layout) - + # Introspect __init__ sig = inspect.signature(self.stage_class.__init__) - + for name, param in sig.parameters.items(): if name == "self": continue - + label = QLabel(name) input_widget = self.create_input_widget(param) - + self.parameter_inputs[name] = {"widget": input_widget, "type": param.annotation} form_layout.addRow(label, input_widget) - + # Set default value if available if param.default is not param.empty: self.set_widget_value(input_widget, param.default) - + layout.addWidget(self.group_box) - + def create_input_widget(self, param: inspect.Parameter): annotation = param.annotation - + # Handle simple types if annotation is int: widget = QSpinBox() widget.setRange(-999999, 999999) return widget - elif annotation is float: + if annotation is float: widget = QDoubleSpinBox() widget.setRange(-999999.0, 999999.0) return widget - elif annotation is bool: + if annotation is bool: return QCheckBox() - else: - # Fallback for str, complex types, or unannotated - return QLineEdit() + # Fallback for str, complex types, or unannotated + return QLineEdit() def set_widget_value(self, widget, value): if isinstance(widget, QSpinBox): @@ -80,23 +86,21 @@ def set_widget_value(self, widget, value): widget.setText(json.dumps(value)) else: widget.setText(str(value)) - + def get_widget_value(self, widget, annotation): - if isinstance(widget, QSpinBox): - return widget.value() - elif isinstance(widget, QDoubleSpinBox): + if isinstance(widget, QSpinBox) or isinstance(widget, QDoubleSpinBox): return widget.value() - elif isinstance(widget, QCheckBox): + if isinstance(widget, QCheckBox): return widget.isChecked() - elif isinstance(widget, QLineEdit): + if isinstance(widget, QLineEdit): text = widget.text() if annotation is int: return int(text) - elif annotation is float: + if annotation is float: return float(text) - elif annotation is bool: + if annotation is bool: return text.lower() == "true" - elif annotation is str: + if annotation is str: return text # Try to parse JSON for lists/dicts if it looks like one if (text.startswith("[") and text.endswith("]")) or (text.startswith("{") and text.endswith("}")): @@ -109,16 +113,16 @@ def get_widget_value(self, widget, annotation): def is_enabled(self): return self.group_box.isChecked() - + def get_instance(self) -> PipelineStage | None: if not self.is_enabled(): return None - + kwargs = {} for name, info in self.parameter_inputs.items(): widget = info["widget"] annotation = info["type"] kwargs[name] = self.get_widget_value(widget, annotation) - + return self.stage_class(**kwargs) diff --git a/src/orca/logger.py b/src/orca/logger.py index a1569ab..61cf6eb 100644 --- a/src/orca/logger.py +++ b/src/orca/logger.py @@ -1,7 +1,8 @@ import logging -import colorlog import sys +import colorlog + logger = logging.getLogger("ORCA") stdout = colorlog.StreamHandler(stream=sys.stdout) diff --git a/src/orca/orca.py b/src/orca/orca.py index 6398f5e..97c8414 100644 --- a/src/orca/orca.py +++ b/src/orca/orca.py @@ -1,11 +1,12 @@ import json import multiprocessing import os -from typing import Callable, Optional -from orca.pipeline.context import PipelineContext -from orca.pipeline.pipeline_stage import PipelineStage +from collections.abc import Callable + from orca.geometry.base_geometry import BaseGeometry from orca.logger import logger +from orca.pipeline.context import PipelineContext +from orca.pipeline.pipeline_stage import PipelineStage def default_process_count() -> int: @@ -33,14 +34,14 @@ def __init__(self, stages: list[PipelineStage]): def run( self, geometry: BaseGeometry, - num_processes: Optional[int] = None, + num_processes: int | None = None, force_overwrite: bool = False, - progress_callback: Optional[Callable[[str, int, int, str], None]] = None, - overwrite_callback: Optional[Callable[[str], bool]] = None, - base_dir: Optional[str] = None, - result_dir: Optional[str] = None, - result_csv: Optional[str] = None, - ) -> Optional[PipelineContext]: + progress_callback: Callable[[str, int, int, str], None] | None = None, + overwrite_callback: Callable[[str], bool] | None = None, + base_dir: str | None = None, + result_dir: str | None = None, + result_csv: str | None = None, + ) -> PipelineContext | None: """ Runs the ORCA pipeline with the specified geometry and CPU cores. diff --git a/src/orca/pipeline/context.py b/src/orca/pipeline/context.py index 672d9f7..9dd4fff 100644 --- a/src/orca/pipeline/context.py +++ b/src/orca/pipeline/context.py @@ -19,7 +19,7 @@ if TYPE_CHECKING: import pandas as pd - import torch.nn as nn + from torch import nn from orca.geometry.base_geometry import BaseGeometry from orca.training.datasets.base_dataset import BaseDataset diff --git a/src/orca/pipeline/export_onnx_stage.py b/src/orca/pipeline/export_onnx_stage.py index 4f46f6a..4a6c630 100644 --- a/src/orca/pipeline/export_onnx_stage.py +++ b/src/orca/pipeline/export_onnx_stage.py @@ -1,12 +1,14 @@ import json -from typing import Optional, Callable, TYPE_CHECKING import os -import torch +from collections.abc import Callable +from typing import TYPE_CHECKING + import onnx +import torch -from orca.pipeline.pipeline_stage import PipelineStage from orca.geometry.base_geometry import BaseGeometry from orca.logger import logger +from orca.pipeline.pipeline_stage import PipelineStage from orca.training.onnx_wrapper import ONNXWrapper if TYPE_CHECKING: @@ -24,7 +26,7 @@ def __init__(self): def run( self, context: "PipelineContext", - progress_callback: Optional[Callable[[str, int, int, str], None]] = None, + progress_callback: Callable[[str, int, int, str], None] | None = None, ) -> "PipelineContext": geometry: BaseGeometry = context.geometry diff --git a/src/orca/pipeline/gds_conversion_stage.py b/src/orca/pipeline/gds_conversion_stage.py index 3ebb249..592c541 100644 --- a/src/orca/pipeline/gds_conversion_stage.py +++ b/src/orca/pipeline/gds_conversion_stage.py @@ -1,14 +1,15 @@ +import os +from collections.abc import Callable from concurrent.futures import as_completed -import multiprocessing +from typing import TYPE_CHECKING, Any + import pandas as pd -import os import tqdm from pebble import ProcessPool -from typing import Any, Callable, Optional, TYPE_CHECKING from orca.geometry.base_geometry import BaseGeometry -from orca.pipeline.pipeline_stage import PipelineStage from orca.logger import logger +from orca.pipeline.pipeline_stage import PipelineStage from orca.simulation.gds_converter import create_palace_model_from_gds if TYPE_CHECKING: @@ -38,7 +39,7 @@ def __init__(self, timeout: float = 60.0): def run( self, context: "PipelineContext", - progress_callback: Optional[Callable[[str, int, int, str], None]] = None, + progress_callback: Callable[[str, int, int, str], None] | None = None, ) -> "PipelineContext": geometry: BaseGeometry = context.geometry cpu_cores: int = context.num_processes diff --git a/src/orca/pipeline/gds_gen_stage.py b/src/orca/pipeline/gds_gen_stage.py index 9a8665d..c4b0be4 100644 --- a/src/orca/pipeline/gds_gen_stage.py +++ b/src/orca/pipeline/gds_gen_stage.py @@ -1,13 +1,15 @@ +import os +from collections.abc import Callable from concurrent.futures import ProcessPoolExecutor, as_completed from concurrent.futures.process import BrokenProcessPool +from typing import TYPE_CHECKING, Any + import pandas as pd -from typing import Any, Callable, Optional, TYPE_CHECKING +import tqdm + from orca.geometry.base_geometry import BaseGeometry -from orca.pipeline.pipeline_stage import PipelineStage -import multiprocessing from orca.logger import logger -import tqdm -import os +from orca.pipeline.pipeline_stage import PipelineStage if TYPE_CHECKING: from orca.pipeline.context import PipelineContext @@ -25,7 +27,7 @@ def __init__(self, num_samples: int = 1000): def run( self, context: "PipelineContext", - progress_callback: Optional[Callable[[str, int, int, str], None]] = None, + progress_callback: Callable[[str, int, int, str], None] | None = None, ) -> "PipelineContext": geometry: BaseGeometry = context.geometry cpu_cores: int = context.num_processes diff --git a/src/orca/pipeline/pipeline_stage.py b/src/orca/pipeline/pipeline_stage.py index 8e7af3e..be6cd08 100644 --- a/src/orca/pipeline/pipeline_stage.py +++ b/src/orca/pipeline/pipeline_stage.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod -from typing import Callable, Optional, TYPE_CHECKING +from collections.abc import Callable +from typing import TYPE_CHECKING from orca.logger import logger @@ -17,7 +18,7 @@ def __init__(self, name: str, index: int = 0): def run( self, context: "PipelineContext", - progress_callback: Optional[Callable[[str, int, int, str], None]] = None, + progress_callback: Callable[[str, int, int, str], None] | None = None, ) -> "PipelineContext": """ Execute the pipeline stage. diff --git a/src/orca/pipeline/simulation_stage.py b/src/orca/pipeline/simulation_stage.py index 3c4af94..7619d23 100644 --- a/src/orca/pipeline/simulation_stage.py +++ b/src/orca/pipeline/simulation_stage.py @@ -1,16 +1,18 @@ -from concurrent.futures import ThreadPoolExecutor, as_completed -from queue import Queue -from typing import Optional, Any, Callable, TYPE_CHECKING import json import os +from collections.abc import Callable +from concurrent.futures import ThreadPoolExecutor, as_completed +from queue import Queue +from typing import TYPE_CHECKING, Any + import pandas as pd import tqdm -from orca.pipeline.pipeline_stage import PipelineStage from orca.logger import logger +from orca.pipeline.pipeline_stage import PipelineStage +from orca.simulation.combine_snp_results import touchstone_filename from orca.simulation.launchers import BIND_CHOICES, LocalLauncher, SimulationLauncher from orca.simulation.simulate import run_palace -from orca.simulation.combine_snp_results import touchstone_filename if TYPE_CHECKING: from orca.pipeline.context import PipelineContext @@ -97,7 +99,7 @@ def _create_launcher(self) -> SimulationLauncher: def run( self, context: "PipelineContext", - progress_callback: Optional[Callable[[str, int, int, str], None]] = None, + progress_callback: Callable[[str, int, int, str], None] | None = None, ) -> "PipelineContext": num_processes: int = context.num_processes output_dir = context.result_dir @@ -235,7 +237,7 @@ def _patch_palace_config(self, config_path: str) -> None: Args: config_path (str): Path to the Palace config.json file to patch. """ - with open(config_path, "r") as f: + with open(config_path) as f: config = json.load(f) config.setdefault("Problem", {})["OutputFormats"] = {"Paraview": False} diff --git a/src/orca/pipeline/test_model_stage.py b/src/orca/pipeline/test_model_stage.py index bcd2c9c..3f73519 100644 --- a/src/orca/pipeline/test_model_stage.py +++ b/src/orca/pipeline/test_model_stage.py @@ -1,5 +1,6 @@ -from typing import Optional, Any, Dict, Callable, TYPE_CHECKING import os +from collections.abc import Callable +from typing import TYPE_CHECKING, Any import numpy as np import pandas as pd @@ -34,7 +35,7 @@ class ModelTester(PipelineStage): physical units, on geometries the model was never trained on. """ - def __init__(self, n_test_samples: Optional[int] = None, plot: bool = False): + def __init__(self, n_test_samples: int | None = None, plot: bool = False): """ Args: n_test_samples: Limit the evaluation to the first N held-out geometries. @@ -49,7 +50,7 @@ def __init__(self, n_test_samples: Optional[int] = None, plot: bool = False): def run( self, context: "PipelineContext", - progress_callback: Optional[Callable[[str, int, int, str], None]] = None, + progress_callback: Callable[[str, int, int, str], None] | None = None, ) -> "PipelineContext": result_dir = context.result_dir test_df = context.test_df @@ -123,8 +124,8 @@ def test_model( self, test_dataset: GeoToNtwkDataset, predictor: NetworkPredictor, - progress_callback: Optional[Callable[[str, int, int, str], None]] = None, - ) -> Dict[str, Any]: + progress_callback: Callable[[str, int, int, str], None] | None = None, + ) -> dict[str, Any]: """ Evaluates the predictor on the test dataset. diff --git a/src/orca/pipeline/training_stage.py b/src/orca/pipeline/training_stage.py index 939adcc..b8be9a0 100644 --- a/src/orca/pipeline/training_stage.py +++ b/src/orca/pipeline/training_stage.py @@ -1,13 +1,15 @@ -from typing import Optional, Any, Callable, TYPE_CHECKING import os +from collections.abc import Callable +from typing import TYPE_CHECKING, Any + import pandas as pd from sklearn.model_selection import train_test_split -from orca.pipeline.pipeline_stage import PipelineStage from orca.geometry.base_geometry import BaseGeometry from orca.logger import logger -from orca.training.datasets.base_dataset import BaseDataset +from orca.pipeline.pipeline_stage import PipelineStage from orca.training.basis_expansion import BasisExpansion, get_basis_class +from orca.training.datasets.base_dataset import BaseDataset from orca.training.models.base_model import OrcaModel, get_model_class from orca.training.trainer import Trainer, TrainingConfig from orca.training.tuner import HyperparameterTuner @@ -27,7 +29,7 @@ def __init__( basis: str | type[BasisExpansion] | None = None, hyperparameters: dict[str, Any] | None = None, test_frac: float = 0.15, - n_train_samples: Optional[int] = None, + n_train_samples: int | None = None, n_fold_cv: int = 5, n_trials: int = 200, ): @@ -61,7 +63,7 @@ def __init__( def run( self, context: "PipelineContext", - progress_callback: Optional[Callable[[str, int, int, str], None]] = None, + progress_callback: Callable[[str, int, int, str], None] | None = None, ) -> "PipelineContext": geometry: BaseGeometry = context.geometry result_dir = context.result_dir diff --git a/src/orca/simulation/combine_snp_results.py b/src/orca/simulation/combine_snp_results.py index 2a9ea23..315df86 100644 --- a/src/orca/simulation/combine_snp_results.py +++ b/src/orca/simulation/combine_snp_results.py @@ -9,15 +9,16 @@ # updated 13-Nov-2025 Mue: added simple de-embedding of parasitic port inductance (flat ribbon calculation) # updated 26-Nov-2025 Mue: also read Elmer FEM files -import os -import re import json import math +import os +import re + +import numpy as np import skrf as rf from skrf.network import connect -import numpy as np -from orca.logger import logger +from orca.logger import logger #: Filename suffix written for each touchstone_type, appended before the .sNp extension. #: "all" writes every variant; the fully corrected one is treated as canonical. @@ -67,7 +68,7 @@ def parse_elmer_results(found_filename, freq, S_dB, S_arg): # Parse column names column_names = [] - with open(names_filename, "r") as namesfile: + with open(names_filename) as namesfile: for line in namesfile: if ":" in line and line.strip()[0].isdigit(): parts = line.split(":") @@ -232,9 +233,7 @@ def traverse_directories(path, level=0): if os.path.isdir(item_path): traverse_directories(item_path, level + 1) - elif item == "port-S.csv": - found_datafiles.append(item_path) - elif item == "scalar_results.names": + elif item == "port-S.csv" or item == "scalar_results.names": found_datafiles.append(item_path) except PermissionError: @@ -320,7 +319,7 @@ def port_deembedding(snp_filename, port_info_available, port_info_data): # convert the dict with port L into a list, to have the final values in correct order L_values = [] - for key in Lport.keys(): + for key in Lport: L_values.append(-Lport[key]) # load SnP data and apply negative series L at each port @@ -395,7 +394,7 @@ def convert_to_touchstone(workdir, output_dir, touchstone_type: str): ) # Load the JSON data - with open(port_info_filename, "r") as f: + with open(port_info_filename) as f: port_info_data = json.load(f) # Extract all Z0 values @@ -406,7 +405,7 @@ def convert_to_touchstone(workdir, output_dir, touchstone_type: str): Z0_string = str(Z0_values[0]) for Z in Z0_values: - if Z != Z0_values[0]: + if Z0_values[0] != Z: Z0_string = Z0_string + " " + str(Z) # If string is filled, we have a Z0 parameter for Touchstone header line. # For mixed port impedance, we have multiple values there diff --git a/src/orca/simulation/gds_converter.py b/src/orca/simulation/gds_converter.py index 063235d..d62d5e4 100644 --- a/src/orca/simulation/gds_converter.py +++ b/src/orca/simulation/gds_converter.py @@ -1,9 +1,10 @@ -from gds2palace import gds_reader, stackup_reader, utilities, simulation_setup import os -from contextlib import redirect_stdout, ExitStack +from contextlib import ExitStack, redirect_stdout +from typing import Any import gmsh -from typing import Any +from gds2palace import gds_reader, simulation_setup, stackup_reader, utilities + from orca.simulation.simulate import read_simconfig diff --git a/src/orca/simulation/simulate.py b/src/orca/simulation/simulate.py index e53307c..1669dd6 100644 --- a/src/orca/simulation/simulate.py +++ b/src/orca/simulation/simulate.py @@ -79,10 +79,11 @@ def read_simconfig(simconfig_filename: str) -> dict: Args: simconfig_filename (str): Path to the simulation configuration file. + Returns: dict: A dictionary containing simulation configuration parameters. """ - with open(simconfig_filename, "r") as file: + with open(simconfig_filename) as file: simconfig = json.load(file) # Add e9 suffix to frequency values if they are in GHz diff --git a/src/orca/training/basis_expansion.py b/src/orca/training/basis_expansion.py index 46ebca0..6690423 100644 --- a/src/orca/training/basis_expansion.py +++ b/src/orca/training/basis_expansion.py @@ -23,11 +23,12 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, Callable +from collections.abc import Callable +from typing import Any import optuna import torch -import torch.nn as nn +from torch import nn from orca.training.spec import IOSpec @@ -50,7 +51,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: @classmethod @abstractmethod - def from_spec(cls, spec: IOSpec, hyperparameters: dict[str, Any]) -> "BasisExpansion": + def from_spec(cls, spec: IOSpec, hyperparameters: dict[str, Any]) -> BasisExpansion: """Build a basis expansion for ``spec``, configured by ``hyperparameters``. Implementations must tolerate extra keys: the trainer's and the model's @@ -108,7 +109,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: return x @classmethod - def from_spec(cls, spec: IOSpec, hyperparameters: dict[str, Any]) -> "IdentityBasis": + def from_spec(cls, spec: IOSpec, hyperparameters: dict[str, Any]) -> IdentityBasis: return cls() @@ -168,7 +169,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: return torch.cat([x, torch.stack(terms[1:], dim=1)], dim=1) @classmethod - def from_spec(cls, spec: IOSpec, hyperparameters: dict[str, Any]) -> "ChebyshevBasis": + def from_spec(cls, spec: IOSpec, hyperparameters: dict[str, Any]) -> ChebyshevBasis: column = hyperparameters.get("basis_column") if column is None: if "frequency" not in spec.input_names: diff --git a/src/orca/training/datasets/base_dataset.py b/src/orca/training/datasets/base_dataset.py index 353a3ae..00387bc 100644 --- a/src/orca/training/datasets/base_dataset.py +++ b/src/orca/training/datasets/base_dataset.py @@ -30,7 +30,7 @@ def __init__( input_normalizer (Normalizer|None): Normalizer for input parameters. output_normalizer (Normalizer|None): Normalizer for output parameters. """ - super(BaseDataset, self).__init__() + super().__init__() self.codec = codec self.samples: list[tuple[torch.Tensor, torch.Tensor]] = [] @@ -143,7 +143,6 @@ def load_samples(self, directory: str, data_df: pd.DataFrame) -> None: Load samples from the dataset. This method should be implemented by subclasses to load data specific from its self.data_dir. """ - pass def __getitem__(self, idx) -> tuple[torch.Tensor, torch.Tensor]: return self.samples[idx] @@ -165,6 +164,7 @@ def new_split( normalizers. Pass True for the training split and False for the validation and test splits, which must reuse the training statistics. + Returns: BaseDataset: New dataset split instance. """ diff --git a/src/orca/training/datasets/geo_to_ntwk.py b/src/orca/training/datasets/geo_to_ntwk.py index 9ad1e75..fe0b1fb 100644 --- a/src/orca/training/datasets/geo_to_ntwk.py +++ b/src/orca/training/datasets/geo_to_ntwk.py @@ -1,11 +1,11 @@ -import pandas as pd -import skrf as rf import os + import numpy as np +import pandas as pd +import skrf as rf import torch import tqdm -from orca.training.normalize import Normalizer from orca.logger import logger @@ -46,11 +46,10 @@ def load_single_sample( self, sparam_path: str, geometry_params: np.ndarray ) -> list[tuple[np.ndarray, rf.Network]]: """Load S-parameter data from a Touchstone file.""" - return [(geometry_params, rf.Network(sparam_path, f_unit="Hz"))] - + def __len__(self): return len(self.samples) - + def __getitem__(self, idx): - return self.samples[idx] \ No newline at end of file + return self.samples[idx] diff --git a/src/orca/training/datasets/geo_to_s_param.py b/src/orca/training/datasets/geo_to_s_param.py index 8136f8f..e6013bd 100644 --- a/src/orca/training/datasets/geo_to_s_param.py +++ b/src/orca/training/datasets/geo_to_s_param.py @@ -27,7 +27,7 @@ def __init__( input_normalizer: Normalizer | None = None, output_normalizer: Normalizer | None = None, ): - super(GeoToSParamDataset, self).__init__( + super().__init__( codec, input_normalizer, output_normalizer ) diff --git a/src/orca/training/datasets/geo_to_s_param_single_f.py b/src/orca/training/datasets/geo_to_s_param_single_f.py index f99939c..70788e3 100644 --- a/src/orca/training/datasets/geo_to_s_param_single_f.py +++ b/src/orca/training/datasets/geo_to_s_param_single_f.py @@ -28,7 +28,7 @@ def __init__( input_normalizer: Normalizer | None = None, output_normalizer: Normalizer | None = None, ): - super(GeoToSParamDatasetSingleFrequency, self).__init__( + super().__init__( codec, input_normalizer, output_normalizer ) diff --git a/src/orca/training/guarantees.py b/src/orca/training/guarantees.py index a43e642..41e736a 100644 --- a/src/orca/training/guarantees.py +++ b/src/orca/training/guarantees.py @@ -10,7 +10,7 @@ from __future__ import annotations -from dataclasses import dataclass, asdict, fields +from dataclasses import asdict, dataclass, fields @dataclass(frozen=True) @@ -38,7 +38,7 @@ class PhysicsGuarantees: causal: bool = False stable: bool = False - def __or__(self, other: "PhysicsGuarantees") -> "PhysicsGuarantees": + def __or__(self, other: PhysicsGuarantees) -> PhysicsGuarantees: """Combine two sets of guarantees, keeping every property either one promises. Used to merge what the architecture guarantees with what the output diff --git a/src/orca/training/losses.py b/src/orca/training/losses.py index 6312350..b767895 100644 --- a/src/orca/training/losses.py +++ b/src/orca/training/losses.py @@ -10,7 +10,7 @@ import math import torch -import torch.nn as nn +from torch import nn class ComplexMSELoss(nn.Module): diff --git a/src/orca/training/models/base_model.py b/src/orca/training/models/base_model.py index b0b52b8..372d4e7 100644 --- a/src/orca/training/models/base_model.py +++ b/src/orca/training/models/base_model.py @@ -11,14 +11,15 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, Callable, ClassVar +from collections.abc import Callable +from typing import Any, ClassVar import numpy as np import skrf as rf import torch -import torch.nn as nn +from torch import nn -from orca.training.basis_expansion import IdentityBasis, BasisExpansion +from orca.training.basis_expansion import BasisExpansion, IdentityBasis from orca.training.guarantees import PhysicsGuarantees from orca.training.spec import FrequencyMode, IOSpec @@ -70,7 +71,7 @@ def from_spec( spec: IOSpec, hyperparameters: dict[str, Any], basis: BasisExpansion | None = None, - ) -> "OrcaModel": + ) -> OrcaModel: """Build a model sized for ``spec`` and configured by ``hyperparameters``. Implementations must tolerate extra keys in ``hyperparameters`` (the diff --git a/src/orca/training/models/mlp.py b/src/orca/training/models/mlp.py index b17b378..6598894 100644 --- a/src/orca/training/models/mlp.py +++ b/src/orca/training/models/mlp.py @@ -2,7 +2,7 @@ import optuna import torch -import torch.nn as nn +from torch import nn from orca.training.basis_expansion import BasisExpansion from orca.training.models.base_model import OrcaModel, register_model diff --git a/src/orca/training/models/transformer.py b/src/orca/training/models/transformer.py index 59d0347..629c40e 100644 --- a/src/orca/training/models/transformer.py +++ b/src/orca/training/models/transformer.py @@ -1,5 +1,5 @@ -import torch.nn as nn import torch +from torch import nn class SParamTransformer(nn.Module): diff --git a/src/orca/training/normalize.py b/src/orca/training/normalize.py index 93eb28d..f0bcc6b 100644 --- a/src/orca/training/normalize.py +++ b/src/orca/training/normalize.py @@ -1,7 +1,8 @@ -import torch -import torch.nn as nn from abc import ABC, abstractmethod + import numpy as np +import torch +from torch import nn from orca.geometry.input_parameters import InputParameterIterator from orca.logger import logger @@ -21,6 +22,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: Args: x (torch.Tensor): Input tensor of shape (batch_size, n_input_parameters). + Returns: torch.Tensor: Normalized tensor of shape (batch_size, n_input_parameters). """ @@ -32,6 +34,7 @@ def denormalize(self, x: torch.Tensor) -> torch.Tensor: Args: x (torch.Tensor): Normalized tensor of shape (batch_size, n_output_parameters). + Returns: torch.Tensor: Denormalized tensor of shape (batch_size, n_output_parameters). """ @@ -42,6 +45,7 @@ def normalize(self, x: torch.Tensor) -> torch.Tensor: Args: x (torch.Tensor): Input tensor of shape (batch_size, n_input_parameters). + Returns: torch.Tensor: Normalized tensor of shape (batch_size, n_input_parameters). """ @@ -113,7 +117,6 @@ def process_samples(self, samples: list): Args: samples (list): List of output parameter samples. """ - pass class MinMaxNormalizer(InputNormalizer): diff --git a/src/orca/training/onnx_wrapper.py b/src/orca/training/onnx_wrapper.py index b6207ac..3dce928 100644 --- a/src/orca/training/onnx_wrapper.py +++ b/src/orca/training/onnx_wrapper.py @@ -1,4 +1,5 @@ import torch + from orca.training.normalize import Normalizer @@ -12,6 +13,7 @@ class ONNXWrapper(torch.nn.Module): Args: model: The PyTorch model to be wrapped. + Returns: A torch.nn.Module that can be passed to torch.onnx.export. """ diff --git a/src/orca/training/trainer.py b/src/orca/training/trainer.py index d6fcd93..c56645b 100644 --- a/src/orca/training/trainer.py +++ b/src/orca/training/trainer.py @@ -10,13 +10,14 @@ from __future__ import annotations import copy +from collections.abc import Callable from dataclasses import dataclass, field -from typing import Any, Callable, Optional +from typing import Any import optuna import torch -import torch.nn as nn import tqdm +from torch import nn from torch.optim import AdamW, Optimizer from torch.utils.data import DataLoader, Dataset @@ -52,7 +53,7 @@ class TrainingConfig: device: torch.device = field(default_factory=default_device) @classmethod - def from_hyperparameters(cls, hyperparameters: dict[str, Any], **overrides) -> "TrainingConfig": + def from_hyperparameters(cls, hyperparameters: dict[str, Any], **overrides) -> TrainingConfig: """Build a config from a hyperparameter dict, ignoring architecture keys. Args: @@ -121,8 +122,8 @@ class Trainer: def __init__( self, config: TrainingConfig | None = None, - criterion: Optional[Callable[[torch.Tensor, torch.Tensor], torch.Tensor]] = None, - progress_callback: Optional[Callable[[str, int, int, str], None]] = None, + criterion: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] | None = None, + progress_callback: Callable[[str, int, int, str], None] | None = None, stage_name: str = "Training", verbose: bool = True, ): @@ -219,7 +220,7 @@ def evaluate( self, model: nn.Module, dataset: Dataset, - criterion: Optional[Callable] = None, + criterion: Callable | None = None, batch_size: int | None = None, ) -> float: """Mean loss of ``model`` over ``dataset``, without updating any weights.""" diff --git a/src/orca/training/tuner.py b/src/orca/training/tuner.py index 442ff52..9940dd8 100644 --- a/src/orca/training/tuner.py +++ b/src/orca/training/tuner.py @@ -8,7 +8,7 @@ from __future__ import annotations import traceback -from typing import Any, Optional +from typing import Any import optuna from sklearn.model_selection import KFold @@ -44,7 +44,7 @@ def suggest_hyperparameters(trial: optuna.Trial, search_space: dict[str, Any]) - step=distribution.step, log=distribution.log, ) - elif isinstance(distribution, optuna.distributions.FloatDistribution): + elif isinstance(distribution, optuna.distributions.FloatDistribution): values[key] = trial.suggest_float( key, distribution.low, @@ -83,12 +83,12 @@ def __init__( self, model_cls: type[OrcaModel], dataset: Dataset, - basis_cls: Optional[type[BasisExpansion]] = None, + basis_cls: type[BasisExpansion] | None = None, n_fold_cv: int = 5, n_trials: int = 200, seed: int = 42, - sampler: Optional[optuna.samplers.BaseSampler] = None, - pruner: Optional[optuna.pruners.BasePruner] = None, + sampler: optuna.samplers.BaseSampler | None = None, + pruner: optuna.pruners.BasePruner | None = None, ): self.model_cls = model_cls self.dataset = dataset diff --git a/src/orca/utils/class_finder.py b/src/orca/utils/class_finder.py index 5419175..27c4b45 100644 --- a/src/orca/utils/class_finder.py +++ b/src/orca/utils/class_finder.py @@ -6,17 +6,17 @@ import inspect import os from pathlib import Path -from typing import Type, Dict, Any +from typing import Any from orca.logger import logger def discover_classes( - base_class: Type, + base_class: type, search_dir: str, module_prefix: str, extract_default_params: bool = False, -) -> Dict[str, Dict[str, Any]]: +) -> dict[str, dict[str, Any]]: """ Automatically discover and load classes that inherit from a specified base class. diff --git a/src/orca/utils/postprocessing.py b/src/orca/utils/postprocessing.py index cbc001b..03065a3 100644 --- a/src/orca/utils/postprocessing.py +++ b/src/orca/utils/postprocessing.py @@ -1,6 +1,6 @@ -import skrf as rf -import numpy as np import matplotlib.pyplot as plt +import numpy as np +import skrf as rf def to_mixed_mode(ntwk): @@ -280,8 +280,10 @@ def single_ended_to_mixed_mode(ntwk: rf.Network) -> rf.Network: """ Converts a 4-port single-ended network to a 2-port mixed-mode network using rf.se2gmm. Usually port 1 and 2 are considered differential pair 1, and port 3 and 4 differential pair 2. + Args: network (rf.Network): 4-port single-ended network. + Returns: rf.Network: 2-port mixed-mode network. """ @@ -292,6 +294,7 @@ def single_ended_to_mixed_mode(ntwk: rf.Network) -> rf.Network: def plot_diff_s_params_and_k(ntwk: rf.Network): """ Plots the differential S-parameters and coupling factor k for a 4-port single-ended network. + Args: network (rf.Network): 4-port single-ended network. """ From a8538380e6bfc1b239a7278e40a465be4d4a9ec8 Mon Sep 17 00:00:00 2001 From: David Lurz Date: Wed, 16 Sep 2026 14:32:29 +0200 Subject: [PATCH 2/3] Fixed remaining ruff + ty errors --- README.md | 2 +- docs/pipeline.md | 2 +- examples/main.py | 15 +- examples/slurm_runs/main.py | 5 +- pyproject.toml | 6 +- src/orca/__init__.py | 17 ++ src/orca/__main__.py | 2 +- src/orca/geometry/__init__.py | 0 src/orca/geometry/base_geometry.py | 3 +- src/orca/geometry/cells/__init__.py | 0 src/orca/geometry/cells/inductor.py | 72 +++----- src/orca/geometry/cells/transformer.py | 171 ++++++++---------- src/orca/geometry/input_parameters.py | 52 +++--- src/orca/geometry/presets/__init__.py | 0 src/orca/geometry/presets/inductor_octa.py | 5 +- src/orca/geometry/presets/tf_octa_c_ports.py | 57 ------ src/orca/gui/__init__.py | 0 src/orca/gui/pipeline_window.py | 2 +- src/orca/gui/utils.py | 15 +- src/orca/gui/widgets/__init__.py | 0 src/orca/gui/widgets/geometry_selector.py | 2 +- src/orca/gui/widgets/stage_widget.py | 4 +- src/orca/pipeline/__init__.py | 0 src/orca/pipeline/context.py | 4 +- src/orca/pipeline/export_onnx_stage.py | 5 +- src/orca/pipeline/gds_conversion_stage.py | 25 +-- src/orca/pipeline/gds_gen_stage.py | 19 +- src/orca/pipeline/test_model_stage.py | 4 +- src/orca/pipeline/training_stage.py | 2 +- src/orca/simulation/__init__.py | 0 src/orca/simulation/combine_snp_results.py | 86 +++------ src/orca/simulation/gds_converter.py | 8 +- src/orca/simulation/launchers.py | 2 +- src/orca/simulation/slurm.py | 7 +- src/orca/training/__init__.py | 0 src/orca/training/basis_expansion.py | 18 +- src/orca/training/datasets/__init__.py | 0 src/orca/training/datasets/base_dataset.py | 28 ++- src/orca/training/datasets/geo_to_ntwk.py | 12 +- src/orca/training/datasets/geo_to_s_param.py | 2 +- .../datasets/geo_to_s_param_single_f.py | 6 +- src/orca/training/models/__init__.py | 0 src/orca/training/models/base_model.py | 22 ++- src/orca/training/normalize.py | 25 ++- src/orca/training/onnx_wrapper.py | 8 +- src/orca/training/predictors.py | 20 +- src/orca/training/spec.py | 6 +- src/orca/training/trainer.py | 46 ++--- src/orca/training/tuner.py | 23 ++- src/orca/utils/__init__.py | 0 src/orca/utils/class_finder.py | 4 +- src/orca/utils/postprocessing.py | 25 +-- 52 files changed, 385 insertions(+), 454 deletions(-) create mode 100644 src/orca/geometry/__init__.py create mode 100644 src/orca/geometry/cells/__init__.py create mode 100644 src/orca/geometry/presets/__init__.py create mode 100644 src/orca/gui/__init__.py create mode 100644 src/orca/gui/widgets/__init__.py create mode 100644 src/orca/pipeline/__init__.py create mode 100644 src/orca/simulation/__init__.py create mode 100644 src/orca/training/__init__.py create mode 100644 src/orca/training/datasets/__init__.py create mode 100644 src/orca/training/models/__init__.py create mode 100644 src/orca/utils/__init__.py diff --git a/README.md b/README.md index b17e2e4..1d10e8c 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ ORCA runs a linear pipeline. Each stage receives a context dictionary and adds i ### Stage 1 — GDS generation (`GDSGenerator`) -The geometry class's `input_parameter_iterator` samples parameter combinations (randomly or on a grid). For each combination, `create_gds_file()` is called to produce a GDS layout file. The number of samples is set by `num_samples`. +The geometry class's `input_parameter_iterator` samples parameter combinations (randomly or on a grid). For each combination, `create_gds_file()` is called to produce a GDS layout file. The number of samples is set by `num_samples`; `seed` makes the `"random"` picking strategy reproducible. ### Stage 2 — GDS conversion (`GDSConverter`) diff --git a/docs/pipeline.md b/docs/pipeline.md index 18c27be..bf43508 100644 --- a/docs/pipeline.md +++ b/docs/pipeline.md @@ -21,7 +21,7 @@ flowchart TB ## Stage 1 — GDS generation (`GDSGenerator`) -The geometry class's `input_parameter_iterator` samples parameter combinations (randomly or on a grid). For each combination, `create_gds_file()` is called to produce a GDS layout file. The number of samples is set by `num_samples`. +The geometry class's `input_parameter_iterator` samples parameter combinations (randomly or on a grid). For each combination, `create_gds_file()` is called to produce a GDS layout file. The number of samples is set by `num_samples`; `seed` makes the `"random"` picking strategy reproducible. ## Stage 2 — GDS conversion (`GDSConverter`) diff --git a/examples/main.py b/examples/main.py index 4557eb2..0fa1b32 100644 --- a/examples/main.py +++ b/examples/main.py @@ -1,18 +1,12 @@ -import numpy as np - import orca from orca.geometry.presets.tf_octa_c_ports import TransformerOcta PLOT = False -### Example of using a custom geometry -# geometry = MyCustomGeometry( # Python class that inherits from BaseGeometry -# name = "my_geometry", -# stackup_xml = "/path/to/stackup.xml", # XML file defining the physical layer stackup -# simconfig_filename = "/path/to/simconfig.simcfg" # Simulation configuration file generated manually or with setupEM -# ) +# To run the pipeline on your own geometry, subclass BaseGeometry (name, stackup_xml, +# simconfig_filename, input_parameter_iterator, create_gds_file) and pass an instance +# to orca_instance.run() below; see docs/custom_class.md for a complete example. -# hyperparameters = {'learning_rate': 0.0008166998266605425, 'batch_size': 128, 'epochs': 10, 'num_layers': 4, 'hidden_size': 512, 'activation_function': 'GELU'} hyperparameters = { "learning_rate": 0.0005, "batch_size": 256, @@ -24,7 +18,6 @@ def main(): # Use predefined geometry from examples - np.random.seed(40) try: import torch # optional: only installed with ORCA's "train" extra torch.manual_seed(40) @@ -34,7 +27,7 @@ def main(): orca_instance = orca.ORCA( [ - orca.GDSGenerator(num_samples=6000), + orca.GDSGenerator(num_samples=6000, seed=40), orca.GDSConverter(), #orca.PalaceSimulator(palace_executable="apptainer exec ~/Documents/git/palace/palace.sif palace"), #orca.ModelTrainer(model=orca.OrcaMLP, hyperparameters=hyperparameters, n_train_samples=1000), diff --git a/examples/slurm_runs/main.py b/examples/slurm_runs/main.py index c2effdd..22a7960 100644 --- a/examples/slurm_runs/main.py +++ b/examples/slurm_runs/main.py @@ -1,12 +1,9 @@ -import numpy as np - import orca from orca.geometry.presets.tf_octa_c_ports import TransformerOcta def main(): # Use predefined geometry from examples - np.random.seed(40) try: import torch # optional: only installed with ORCA's "train" extra torch.manual_seed(40) @@ -16,7 +13,7 @@ def main(): orca_instance = orca.ORCA( [ - orca.GDSGenerator(num_samples=6000), + orca.GDSGenerator(num_samples=6000, seed=40), orca.GDSConverter(), # launcher="slurm" runs the simulations as srun job steps on the nodes of this allocation # (#SBATCH --nodes in the job script). bind="numa" with num_parallel_sims=0 runs one diff --git a/pyproject.toml b/pyproject.toml index dd9d031..2d30bb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -159,7 +159,6 @@ exclude_also = [ [tool.ruff] line-length = 100 -target-version = "py313" # Resolve first-party imports out of the src layout. src = ["src"] # Scratch/vendored trees that are not part of the package. @@ -223,8 +222,9 @@ ignore = [ ] [tool.ruff.lint.per-file-ignores] -# Examples are standalone scripts, not an importable package. -"examples/*" = ["INP001", "T201"] +# Examples are standalone scripts, not an importable package. Optional pipeline +# stages are left in as commented-out lines to be enabled by the reader. +"examples/*" = ["INP001", "T201", "ERA001"] # Tests assert, reach into private helpers deliberately, and use fixtures that # look like shadowed or unused arguments to the linter. "tests/*" = [ diff --git a/src/orca/__init__.py b/src/orca/__init__.py index c6ffe52..343a1f8 100644 --- a/src/orca/__init__.py +++ b/src/orca/__init__.py @@ -12,6 +12,23 @@ from .training.guarantees import PhysicsGuarantees from .training.spec import FrequencyMode, IOSpec +__all__ = [ + "ORCA", + "BaseGeometry", + "FlatReImCodec", + "FrequencyMode", + "GDSConverter", + "GDSGenerator", + "IOSpec", + "InputParameterIterator", + "OutputCodec", + "PalaceSimulator", + "PhysicsGuarantees", + "PipelineContext", + "PipelineStage", + "UpperTriangleReImCodec", +] + # Everything that needs PyTorch (the "train" extra) is imported on first access # instead of here, so that `import orca` works in a simulation-only install, e.g. # on an HPC cluster. The names stay importable as `orca.ModelTrainer` etc.; a diff --git a/src/orca/__main__.py b/src/orca/__main__.py index 39f13f4..b71a386 100644 --- a/src/orca/__main__.py +++ b/src/orca/__main__.py @@ -12,7 +12,7 @@ def main(): if len(sys.argv) == 1 or (len(sys.argv) > 1 and sys.argv[1] == "--gui"): run_gui() else: - print("CLI usage not yet implemented. Use --gui to launch the graphical interface.") + sys.exit("CLI usage not yet implemented. Use --gui to launch the graphical interface.") if __name__ == "__main__": main() diff --git a/src/orca/geometry/__init__.py b/src/orca/geometry/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/orca/geometry/base_geometry.py b/src/orca/geometry/base_geometry.py index 5796b1b..c62759d 100644 --- a/src/orca/geometry/base_geometry.py +++ b/src/orca/geometry/base_geometry.py @@ -5,9 +5,8 @@ from functools import cached_property from typing import TYPE_CHECKING, Any -from orca.geometry.input_parameters import InputParameterIterator - if TYPE_CHECKING: + from orca.geometry.input_parameters import InputParameterIterator from orca.training.datasets.base_dataset import BaseDataset diff --git a/src/orca/geometry/cells/__init__.py b/src/orca/geometry/cells/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/orca/geometry/cells/inductor.py b/src/orca/geometry/cells/inductor.py index 7a12dd5..37d8118 100644 --- a/src/orca/geometry/cells/inductor.py +++ b/src/orca/geometry/cells/inductor.py @@ -125,7 +125,7 @@ def add_poly(all_geometries_list, layer, purpose, points): return p -def add_via(all_geometries_list, layer, purpose, p1, p2, forEM): +def add_via(all_geometries_list, layer, purpose, p1, p2): draw_via_array(all_geometries_list, layer, purpose, p1, p2) @@ -180,15 +180,14 @@ def get_min_outer_diameter(N, w, s): feedline_spacing = crossover_size + w + 2 * s inner_segment_size = feedline_spacing - if N > 1: - Di_min = inner_segment_size * (1 + math.sqrt(2)) - else: - Di_min = 2 * (w + s) * (1 + math.sqrt(2)) # for single turn inductor + # single turn inductors have no inner segment + Di_min = ( + inner_segment_size * (1 + math.sqrt(2)) if N > 1 else 2 * (w + s) * (1 + math.sqrt(2)) + ) Do_min = (Di_min + 2 * N * w + 2 * (N - 1) * s) # round to 2 decimal digits - Do_min = math.ceil(100 * Do_min) / 100 - return Do_min + return math.ceil(100 * Do_min) / 100 def calculate_octa_diameter(N, w, s, Ltarget, K1=2.15522, K2=3.61868, L0=0): @@ -202,8 +201,7 @@ def calculate_octa_diameter(N, w, s, Ltarget, K1=2.15522, K2=3.61868, L0=0): p = -(b + Lsyn / c) q = b * b / 4 - Lsyn * b * (K2 - 1) / (2 * c) Dout = (-p / 2 + math.sqrt(p * p / 4 - q)) / um # output is in micron - Dout = math.ceil(Dout * 100) / 100 - return Dout + return math.ceil(Dout * 100) / 100 # ==================== @@ -234,7 +232,7 @@ def symmetric_octa_IHP(N, D, w, s, includeCenterTap=False, LBE=False, forEM=Fals try: cell = lib.new_cell(cellname, overwrite_duplicate=True) - except Exception: + except ValueError: cell = lib.new_cell("final_" + cellname, overwrite_duplicate=True) # list with all geometries that we created @@ -267,29 +265,17 @@ def symmetric_octa_IHP(N, D, w, s, includeCenterTap=False, LBE=False, forEM=Fals # Ground-ring geometry (only present when forEM). Computed up front so the # feed length can reach the ring. - if ring_width is None: - frame_width = min(20, gridsnap(5 * w)) - else: - frame_width = gridsnap(ring_width) - if ring_spacing is None: - frame_margin = gridsnap(D / 2) - else: - frame_margin = gridsnap(ring_spacing) + frame_width = min(20, gridsnap(5 * w)) if ring_width is None else gridsnap(ring_width) + frame_margin = gridsnap(D / 2) if ring_spacing is None else gridsnap(ring_spacing) # Feed length: when forEM, extend the feedlines so the pins/ports always # land on the OUTER edge of the ground ring; otherwise keep the default. - if forEM: - feed_length = gridsnap(frame_margin + frame_width) - else: - feed_length = 30 + feed_length = gridsnap(frame_margin + frame_width) if forEM else 30 # --- Feedline drawing --- - if N == 1: - # for single turn, we draw everything on single layer TopMetal2 - feed_layer = SPIRAL_LAYER_NUM - else: - # for multi turn, we draw trace on TopMetal2 and feedline on TopMetal1 - feed_layer = CROSSOVER_LAYER_NUM + # for single turn, we draw everything on single layer TopMetal2; + # for multi turn, we draw trace on TopMetal2 and feedline on TopMetal1 + feed_layer = SPIRAL_LAYER_NUM if N == 1 else CROSSOVER_LAYER_NUM add_box(all_geometries_list, layer=feed_layer, purpose=PURPOSE_DRAWING, p1=(x0 - w / 2 - feedline_spacing / 2, y0 - Di / 2), @@ -303,13 +289,11 @@ def symmetric_octa_IHP(N, D, w, s, includeCenterTap=False, LBE=False, forEM=Fals # for all N except single turn, we need via from TopMetal1 feedline to TopMetal2 trace add_via(all_geometries_list, layer=VIA_LAYER_NUM, purpose=PURPOSE_DRAWING, p1=(x0 - w / 2 - feedline_spacing / 2, y0 - Di / 2), - p2=(x0 + w / 2 - feedline_spacing / 2, y0 - Di / 2 - w), - forEM=forEM) + p2=(x0 + w / 2 - feedline_spacing / 2, y0 - Di / 2 - w)) add_via(all_geometries_list, layer=VIA_LAYER_NUM, purpose=PURPOSE_DRAWING, p1=(x0 - w / 2 + feedline_spacing / 2, y0 - Di / 2), - p2=(x0 + w / 2 + feedline_spacing / 2, y0 - Di / 2 - w), - forEM=forEM) + p2=(x0 + w / 2 + feedline_spacing / 2, y0 - Di / 2 - w)) # create pin label in IHP PDK-style on layer IND.text cell.add(gdspy.Label("LA", (x0 - feedline_spacing / 2, y0 - D / 2 - feed_length + w / 4), @@ -492,22 +476,18 @@ def symmetric_octa_IHP(N, D, w, s, includeCenterTap=False, LBE=False, forEM=Fals # add vias also add_via(all_geometries_list, layer=VIA_LAYER_NUM, purpose=PURPOSE_DRAWING, p1=(x0 - crossover_size / 2, y0 - D / 2 + 2 * i * (w + s) - s), - p2=(x0 - crossover_size / 2 + via_size, y0 - D / 2 + 2 * i * (w + s) - w - s), - forEM=forEM) + p2=(x0 - crossover_size / 2 + via_size, y0 - D / 2 + 2 * i * (w + s) - w - s)) add_via(all_geometries_list, layer=VIA_LAYER_NUM, purpose=PURPOSE_DRAWING, p1=(x0 + crossover_size / 2, y0 - D / 2 + (2 * i + 1) * (w + s) - s), - p2=(x0 + crossover_size / 2 - via_size, y0 - D / 2 + (2 * i + 1) * (w + s) - w - s), - forEM=forEM) + p2=(x0 + crossover_size / 2 - via_size, y0 - D / 2 + (2 * i + 1) * (w + s) - w - s)) else: # add vias also add_via(all_geometries_list, layer=VIA_LAYER_NUM, purpose=PURPOSE_DRAWING, p1=(x0 - crossover_size / 2, y0 - D / 2 - w - s + (2 * i - 1) * (w + s)), - p2=(x0 - crossover_size / 2 + via_size, y0 - D / 2 + (2 * i - 1) * (w + s) - s), - forEM=forEM) + p2=(x0 - crossover_size / 2 + via_size, y0 - D / 2 + (2 * i - 1) * (w + s) - s)) add_via(all_geometries_list, layer=VIA_LAYER_NUM, purpose=PURPOSE_DRAWING, p1=(x0 + crossover_size / 2, y0 - D / 2 - w - s + (2 * i) * (w + s)), - p2=(x0 + crossover_size / 2 - via_size, y0 - D / 2 + (2 * i) * (w + s) - s), - forEM=forEM) + p2=(x0 + crossover_size / 2 - via_size, y0 - D / 2 + (2 * i) * (w + s) - s)) # top side for i in range(1, num_top + 1): @@ -548,22 +528,18 @@ def symmetric_octa_IHP(N, D, w, s, includeCenterTap=False, LBE=False, forEM=Fals # add via also add_via(all_geometries_list, layer=VIA_LAYER_NUM, purpose=PURPOSE_DRAWING, p1=(x0 - crossover_size / 2, y0 + D / 2 - (2 * i - 1) * (w + s) + w + s), - p2=(x0 - crossover_size / 2 + via_size, y0 + D / 2 - (2 * i - 1) * (w + s) + s), - forEM=forEM) + p2=(x0 - crossover_size / 2 + via_size, y0 + D / 2 - (2 * i - 1) * (w + s) + s)) add_via(all_geometries_list, layer=VIA_LAYER_NUM, purpose=PURPOSE_DRAWING, p1=(x0 + crossover_size / 2, y0 + D / 2 - (2 * i) * (w + s) + w + s), - p2=(x0 + crossover_size / 2 - via_size, y0 + D / 2 - (2 * i) * (w + s) + s), - forEM=forEM) + p2=(x0 + crossover_size / 2 - via_size, y0 + D / 2 - (2 * i) * (w + s) + s)) else: # add via also add_via(all_geometries_list, layer=VIA_LAYER_NUM, purpose=PURPOSE_DRAWING, p1=(x0 - crossover_size / 2, y0 + D / 2 - w - s - (2 * i - 1) * (w + s) + w + s), - p2=(x0 - crossover_size / 2 + via_size, y0 + D / 2 - w - s - (2 * i - 1) * (w + s) + s), - forEM=forEM) + p2=(x0 - crossover_size / 2 + via_size, y0 + D / 2 - w - s - (2 * i - 1) * (w + s) + s)) add_via(all_geometries_list, layer=VIA_LAYER_NUM, purpose=PURPOSE_DRAWING, p1=(x0 + crossover_size / 2, y0 + D / 2 - w - s - (2 * i) * (w + s) + w + s), - p2=(x0 + crossover_size / 2 - via_size, y0 + D / 2 - w - s - (2 * i) * (w + s) + s), - forEM=forEM) + p2=(x0 + crossover_size / 2 - via_size, y0 + D / 2 - w - s - (2 * i) * (w + s) + s)) # one straight segment at outer turn if is_even(N): diff --git a/src/orca/geometry/cells/transformer.py b/src/orca/geometry/cells/transformer.py index cf37b1b..f08d02c 100644 --- a/src/orca/geometry/cells/transformer.py +++ b/src/orca/geometry/cells/transformer.py @@ -63,20 +63,24 @@ def tf_octa_c( Octagon Transformer Component with Feed Extensions and Overlap Checks. Args: - di: Diameter of input (lower/bot) winding (trace center to center). - do: Diameter of output (upper/top) winding (trace center to center). - dis: Displacement (offset between winding centers). - wi: Trace width of lower winding. - wic: Trace width of lower center tap. - fi: Feed type lower (0=no, 1=fwd, 2=rev, 3=both). - wo: Trace width of upper winding. - woc: Trace width of upper center tap. - fo: Feed type upper (0=no, 1=fwd, 2=rev, 3=both). - fs: Feedline spacing (gap between inner sides of feed lines). - ro: Ring spacing on upper winding side. - ri: Ring spacing on lower winding side. - rs: Ring spacing at side. - rw: Ring width. + name: Name of the generated cell. + bottom_winding_diameter: Diameter of the input (lower/bot) winding (trace center to center). + top_winding_diameter: Diameter of the output (upper/top) winding (trace center to center). + center_displacement: Displacement (offset between winding centers). + bottom_linewidth: Trace width of the lower winding. + bottom_center_tap_width: Trace width of the lower center tap (<= 0.1 uses bottom_linewidth). + lower_feed_type: Center tap of the lower winding: 0 = none, 1 = tap from the back of + the winding to the right ring edge (port ``ico`` on layer 206). 2 (tap through + the winding's own feed gap) and 3 (both) are not implemented. + top_linewidth: Trace width of the upper winding. + upper_center_tap_width: Trace width of the upper center tap (<= 0.1 uses top_linewidth). + upper_feed_type: Center tap of the upper winding, same encoding as lower_feed_type + (port ``oci`` on layer 205 when set to 1). + feedline_spacing: Feedline spacing (gap between inner sides of feed lines). + gnd_upper_spacing: Ring spacing on the upper winding side. + gnd_lower_spacing: Ring spacing on the lower winding side. + gnd_side_spacing: Ring spacing at the side. + gnd_ring_width: Ring width. """ _ensure_active_pdk() @@ -94,17 +98,22 @@ def tf_octa_c( top_centertap_width = ( upper_center_tap_width if upper_center_tap_width > 0.1 else top_linewidth ) - fo_int = int(round(upper_feed_type)) - fi_int = int(round(lower_feed_type)) + for label, feed_type in (("lower_feed_type", lower_feed_type), ("upper_feed_type", upper_feed_type)): + if feed_type not in (0, 1): + raise NotImplementedError( + f"{label}={feed_type}: only 0 (no center tap) and 1 (center tap) are implemented." + ) + draw_bottom_tap = lower_feed_type == 1 + draw_top_tap = upper_feed_type == 1 # --- Safety Check: Octagon Opening Width --- # Top Winding Gap is on the RIGHT. - # Bot Center Tap (if fi_int & 1) goes RIGHT. It crosses Top Gap. - fs_top = max(feedline_spacing, bottom_centertap_width) + # Bot Center Tap goes RIGHT. It crosses Top Gap. + fs_top = max(feedline_spacing, bottom_centertap_width) if draw_bottom_tap else feedline_spacing # Bot Winding Gap is on the LEFT. - # Top Center Tap (if fo_int & 2) goes LEFT. It crosses Bot Gap. - fs_bot = max(feedline_spacing, top_centertap_width) + # Top Center Tap goes LEFT. It crosses Bot Gap. + fs_bot = max(feedline_spacing, top_centertap_width) if draw_top_tap else feedline_spacing # Geometry Limits tf_y = max(top_winding_diameter, bottom_winding_diameter) / 2.0 + gnd_side_spacing @@ -125,11 +134,11 @@ def tf_octa_c( if top_linewidth > top_winding_diameter / 3.0: raise ValueError("upper_linewidth is too large for output_winding_diameter.") # Check if center tap width is too large for winding diameter of the other winding - if bottom_centertap_width > top_winding_diameter / 3.0: + if draw_bottom_tap and bottom_centertap_width > top_winding_diameter / 3.0: raise ValueError( "bottom_center_tap_width is too large for output_winding_diameter." ) - if top_centertap_width > bottom_winding_diameter / 3.0: + if draw_top_tap and top_centertap_width > bottom_winding_diameter / 3.0: raise ValueError( "upper_center_tap_width is too large for input_winding_diameter." ) @@ -151,10 +160,12 @@ def create_octa_winding( rotation_deg, feed_target_x, centertap_target_x, + centertap_width, ): """ Creates octagon winding AND the feed extension lines (rectangles) to the port. gap_size: spacing between inner edges of feed lines. + centertap_target_x: x of the center tap port, or None for no center tap. """ r = diameter / 2.0 # Feed Y positions (Trace Centers) @@ -221,13 +232,14 @@ def transform(pt): path_l = gf.Path([start_lo, end_lo]) c << path_l.extrude(width=width, layer=layer) - # Create center tap - find point - p_center_local = (round(x_end + width / 2.0, 2), round(center_y, 2)) - start_ct = transform(p_center_local) - end_ct = (round(centertap_target_x, 2), round(center_y, 2)) + # Center tap (optional): from the back of the winding straight out to its port + if centertap_target_x is not None: + p_center_local = (round(x_end + width / 2.0, 2), round(center_y, 2)) + start_ct = transform(p_center_local) + end_ct = (round(centertap_target_x, 2), round(center_y, 2)) - path_ct = gf.Path([start_ct, end_ct]) - c << path_ct.extrude(width=width, layer=layer) + path_ct = gf.Path([start_ct, end_ct]) + c << path_ct.extrude(width=centertap_width, layer=layer) return start_up, start_lo # Return actual start points for reference if needed @@ -245,7 +257,8 @@ def transform(pt): center_y=0, rotation_deg=0, feed_target_x=port_xr - gnd_ring_width, - centertap_target_x=port_xl + gnd_ring_width, + centertap_target_x=port_xl + gnd_ring_width if draw_top_tap else None, + centertap_width=top_centertap_width, ) # Bot Winding (Rot 180, Gap Left -> connects to port_xl) @@ -258,7 +271,8 @@ def transform(pt): center_y=0, rotation_deg=180, feed_target_x=port_xl + gnd_ring_width, - centertap_target_x=port_xr - gnd_ring_width, + centertap_target_x=port_xr - gnd_ring_width if draw_bottom_tap else None, + centertap_width=bottom_centertap_width, ) # ------------------------------------------------- @@ -289,7 +303,7 @@ def add_port_marker(center, width, layer, orientation): c.shapes(layer_index).insert(kdb.Path([start, end], 0)) ### TOP LAYER (ports on the RIGHT) -> Port 1 and 2 -> Layer 201, 202 - # OP (Top, Right, Upper) + # OP: top winding, right side, upper port c.add_port( name="op", center=(round(port_xr - gnd_ring_width, 2), round(y_top_p, 2)), @@ -303,7 +317,7 @@ def add_port_marker(center, width, layer, orientation): (201, 0), 0, ) - # ON (Top, Right, Lower) + # ON: top winding, right side, lower port c.add_port( name="on", center=(round(port_xr - gnd_ring_width, 2), round(y_top_n, 2)), @@ -317,20 +331,21 @@ def add_port_marker(center, width, layer, orientation): (202, 0), 0, ) - # Center Tap (Top, Center) - c.add_port( - name="oci", - center=(round(port_xl + gnd_ring_width, 2), 0.0), - width=top_centertap_width, - orientation=180, - layer=(205, 0), - ) - add_port_marker( - (round(port_xl + gnd_ring_width, 2), 0.0), top_centertap_width, (205, 0), 180 - ) + # Center Tap (Top, Center) -> Layer 205 + if draw_top_tap: + c.add_port( + name="oci", + center=(round(port_xl + gnd_ring_width, 2), 0.0), + width=top_centertap_width, + orientation=180, + layer=(205, 0), + ) + add_port_marker( + (round(port_xl + gnd_ring_width, 2), 0.0), top_centertap_width, (205, 0), 180 + ) ### BOT LAYER (ports on the LEFT) -> Port 3 and 4 -> Layer 203, 204 - # IP (Bot, Left, Upper) + # IP: bottom winding, left side, upper port c.add_port( name="ip", center=(round(port_xl + gnd_ring_width, 2), round(y_bot_p, 2)), @@ -344,7 +359,7 @@ def add_port_marker(center, width, layer, orientation): (203, 0), 180, ) - # IN (Bot, Left, Lower) + # IN: bottom winding, left side, lower port c.add_port( name="in", center=(round(port_xl + gnd_ring_width, 2), round(y_bot_n, 2)), @@ -358,60 +373,18 @@ def add_port_marker(center, width, layer, orientation): (204, 0), 180, ) - # Center Tap (Bot, Center) - c.add_port( - name="ico", - center=(round(port_xr - gnd_ring_width, 2), 0.0), - width=bottom_centertap_width, - orientation=0, - layer=(206, 0), - ) - add_port_marker( - (round(port_xr - gnd_ring_width, 2), 0.0), bottom_centertap_width, (206, 0), 0 - ) - - # ------------------------------------------------- - # 5. Center Taps (ici, ico, oci, oco) - # ------------------------------------------------- - # Top Winding (Layer Top) - # "Left" edge of Top winding (Back of C-shape) - # top_back_x = center_displacement/2.0 - output_winding_diameter/2.0 - - # OCI (Top, goes Left?) - # if fo_int & 1: - - # else: - # # Default placeholder port - # c.add_port(name="oci", center=(top_back_x - upper_linewidth/2.0 , 0), width=0.25, orientation=180, layer=(205, 0)) - # add_port_marker((top_back_x - upper_linewidth/2.0 , 0), 0.25, (205, 0)) - - # # OCO (Top, goes Right) - # if fo_int & 2: - # c << gf.Path([(top_back_x, 0), (port_xr, 0)]).extrude(width=woc_int, layer=LAYER_TOP) - # c.add_port(name="oco", center=(port_xr, 0), width=woc_int, orientation=0, layer=(207, 0)) - # add_port_marker((port_xr, 0), woc_int, (207, 0)) - # else: - # c.add_port(name="oco", center=(top_back_x + upper_linewidth/2.0, 0), width=0.25, orientation=0, layer=(207, 0)) - # add_port_marker((top_back_x + upper_linewidth/2.0, 0), 0.25, (207, 0)) - - # Bot Winding (Layer Bot) - # bot_back_x = -center_displacement/2.0 + input_winding_diameter/2 - - # ICO (Bot, goes Right) - # if fi_int & 1: - - # else: - # c.add_port(name="ico", center=(bot_back_x + bottom_linewidth/2.0, 0), width=0.25, orientation=0, layer=(206, 0)) - # add_port_marker((bot_back_x + bottom_linewidth/2.0, 0), 0.25, (206, 0)) - - # # ICI (Bot, goes Left) - # if fi_int & 2: - # c << gf.Path([(port_xl, 0), (bot_back_x, 0)]).extrude(width=wic_int, layer=LAYER_BOT) - # c.add_port(name="ici", center=(port_xl, 0), width=wic_int, orientation=180, layer=(208, 0)) - # add_port_marker((port_xl, 0), wic_int, (208, 0)) - # else: - # c.add_port(name="ici", center=(bot_back_x - bottom_linewidth/2.0, 0), width=0.25, orientation=180, layer=(208, 0)) - # add_port_marker((bot_back_x - bottom_linewidth/2.0, 0), 0.25, (208, 0)) + # Center Tap (Bot, Center) -> Layer 206 + if draw_bottom_tap: + c.add_port( + name="ico", + center=(round(port_xr - gnd_ring_width, 2), 0.0), + width=bottom_centertap_width, + orientation=0, + layer=(206, 0), + ) + add_port_marker( + (round(port_xr - gnd_ring_width, 2), 0.0), bottom_centertap_width, (206, 0), 0 + ) # ------------------------------------------------- # 6. Ground Ring diff --git a/src/orca/geometry/input_parameters.py b/src/orca/geometry/input_parameters.py index bc852f2..f345ba6 100644 --- a/src/orca/geometry/input_parameters.py +++ b/src/orca/geometry/input_parameters.py @@ -1,9 +1,14 @@ +from __future__ import annotations + import threading from itertools import product -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np +if TYPE_CHECKING: + from collections.abc import Iterator + class InputParameterIterator: """ @@ -17,6 +22,7 @@ def __init__( self, picking_strategy: str = "grid", frequency: list | range | np.ndarray | None = None, + seed: int | None = None, **input_values, ): """ @@ -25,16 +31,19 @@ def __init__( Args: picking_strategy (str): Strategy for picking parameters ('grid', 'random', etc.). frequency (list|range|np.ndarray|None): Optional frequency values to include as an additional input dimension. Does not get returned by __next__ (since it's handled by palace) but is considered for min/max calculations. + seed (int|None): Seed for the 'random' picking strategy. None draws fresh entropy on every set_sample_count() call. + **input_values: One list, range or numpy array of possible values per geometry parameter. """ # Check if all input_values are lists or ranges for name, values in input_values.items(): if not isinstance(values, (list, range, np.ndarray)): - raise ValueError( + raise TypeError( f"Input kwarg '{name}' must be a list, range, or numpy array of possible values. Found type: {type(values)} (value: {values})" ) self.picking_strategy = picking_strategy self.frequency = frequency + self.seed = seed self.n_inputs = len(input_values) self.input_values = input_values self.input_names = list(input_values.keys()) @@ -42,21 +51,24 @@ def __init__( self._lock = threading.Lock() # For thread-safe iteration # Created after set_sample_count is called - self._iterator = None + self._iterator: Iterator[Any] | None = None - def set_sample_count(self, n_samples: int): + def set_sample_count(self, n_samples: int, seed: int | None = None): """ Sets the number of samples to generate. This is used for strategies that depend on the total number of samples, such as 'uniform_grid' and 'random'. Args: n_samples (int): Number of samples to generate. + seed (int|None): Overrides the seed given to __init__ for the 'random' strategy. """ self.n_samples = n_samples + if seed is not None: + self.seed = seed # Reinitialize the iterator based on the picking strategy if self.picking_strategy == "step_grid": self._iterator = self.step_grid() - elif self.picking_strategy == "uniform_grid" or self.picking_strategy == "grid": + elif self.picking_strategy in ("uniform_grid", "grid"): self._iterator = self.uniform_grid() elif self.picking_strategy == "random": self._iterator = self.random_sampling() @@ -73,19 +85,18 @@ def __iter__(self): def __next__(self) -> dict[str, Any]: with self._lock: # Ensure thread-safe access + if self._iterator is None: + raise RuntimeError( + "set_sample_count() must be called before iterating over the input parameters." + ) self.n_geometries_created += 1 # May be used for logging or tracking - try: - params = next( - self._iterator - ) # Raises StopIteration when exhausted, which is propagated to this iterator - # Convert numpy types to Python native types to avoid type issues with downstream libraries - params = [ - param.item() if isinstance(param, np.generic) else param - for param in params - ] - return dict(zip(self.input_names, params)) - except StopIteration: - raise + # Raises StopIteration when exhausted, which is propagated to this iterator + params = next(self._iterator) + # Convert numpy types to Python native types to avoid type issues with downstream libraries + params = [ + param.item() if isinstance(param, np.generic) else param for param in params + ] + return dict(zip(self.input_names, params, strict=True)) def get_min_max_values(self) -> tuple[list[float], list[float]]: """ @@ -147,9 +158,7 @@ def uniform_grid(self): sampled_values = values[indices] param_samples.append(sampled_values) - result = product(*param_samples) - - return result + return product(*param_samples) def random_sampling(self): """ @@ -158,10 +167,11 @@ def random_sampling(self): Given a dict of {"name": range(start, end)}, it randomly picks values from each range and returns a dict of {"name": value} for each sample. """ + rng = np.random.default_rng(self.seed) for _ in range(self.n_samples): sampled_params = [] for name in self.input_names: values = self.input_values[name] - sampled_value = np.random.choice(values) + sampled_value = rng.choice(values) sampled_params.append(sampled_value) yield sampled_params diff --git a/src/orca/geometry/presets/__init__.py b/src/orca/geometry/presets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/orca/geometry/presets/inductor_octa.py b/src/orca/geometry/presets/inductor_octa.py index 15ddf14..4adcda8 100644 --- a/src/orca/geometry/presets/inductor_octa.py +++ b/src/orca/geometry/presets/inductor_octa.py @@ -32,7 +32,6 @@ def _input_parameters() -> InputParameterIterator: ) - @dataclass class InductorOcta(BaseGeometry): """ @@ -70,8 +69,8 @@ def create_dataset(self) -> "BaseDataset": ) @staticmethod - def create_gds_file(name: str, output_path: str, params: dict[str, Any]) -> str: - N = int(round(params["turns"])) + def create_gds_file(name: str, output_path: str, params: dict[str, Any]) -> str: # noqa: ARG004 - the cell name is derived from the parameters + N = round(params["turns"]) w = float(params["width"]) s = float(params["space"]) D = float(params["diameter"]) diff --git a/src/orca/geometry/presets/tf_octa_c_ports.py b/src/orca/geometry/presets/tf_octa_c_ports.py index cf92b1a..99e2e1c 100644 --- a/src/orca/geometry/presets/tf_octa_c_ports.py +++ b/src/orca/geometry/presets/tf_octa_c_ports.py @@ -5,26 +5,10 @@ from orca import BaseGeometry from orca.geometry.cells.transformer import tf_octa_c from orca.geometry.input_parameters import InputParameterIterator -from orca.utils.postprocessing import * if TYPE_CHECKING: from orca.training.datasets.base_dataset import BaseDataset -# import gdstk -# def _snap_gds_inplace(path: str, grid_nm: int = 10) -> None: -# """Snap all polygon vertices in a GDS file to the nearest grid_nm grid. - -# gf.Path.extrude() produces off-grid vertices for angled octagon segments -# (e.g. width/2 * sin(22.5°) = 0.957 µm is not on the 10 nm grid). -# gdstk is always available as a gdsfactory dependency. -# """ -# grid_um = grid_nm / 1000.0 -# lib = gdstk.read_gds(path) -# for cell in lib.cells: -# for poly in cell.polygons: -# poly.points = np.round(poly.points / grid_um) * grid_um -# lib.write_gds(path) - # Built per instance rather than shared as a class attribute: a dataclass default # holds one object for every instance of the class, so two geometries would share # one iterator. The dataset is built per instance too, by create_dataset(). @@ -48,7 +32,6 @@ def _input_parameters() -> InputParameterIterator: ) - @dataclass class TransformerOcta(BaseGeometry): """ @@ -101,45 +84,5 @@ def create_gds_file(name: str, output_path: str, params: dict[str, Any]) -> str: gnd_side_spacing=40, gnd_ring_width=20, ) - # c.show() c.write_gds(output_path, with_metadata=False) return output_path - - # def postprocess_outputs(self, output, frequency_points=None): - # """ - # Converts model outputs (Re/Im) into a .sNp Touchstone file format. - # Plots the S-parameters for visualization. - - # Parameters - # ---------- - # output : dict - # Dictionary containing S-parameters split into real and imaginary parts. - # Example keys: 'S11_real', 'S11_imag', ..., 'SNN_real', 'SNN_imag'. - # Each value is a 1D array of length equal to len(f). - # f : array-like - # 1D array of frequencies corresponding to the S-parameters. - # filename : str, optional - # Name of the Touchstone file to save, default "output.sNp". - # """ - # # Frequency points are just from 1 to 200 in 1 GHz steps - # if frequency_points is None: - # frequency_points = np.arange(1, 201) # 1 GHz to 200 GHz - # N, ntwk, output_dict = s_param_dict_to_network(output, frequency_points) - # filename = f"{self.name}.s{N}p" - # ntwk.write_touchstone(filename) - - # # N, ntwk = single_ended_to_mixed_mode(ntwk) - # plot_rfic_transformer_metrics(ntwk) - # # plot_diff_s_params_and_k(ntwk) - - # # Write Touchstone - # print(f"Touchstone file saved as {filename}") - - # return output_dict - -# if __name__ == "__main__": -# geometry = TransformerOcta() -# input_params = np.array([70.6, 74.6, 13.2, 6.4, 5.4]) # Example input parameters -# onnx_session = onnxruntime.InferenceSession("/home/david/Documents/git/ORCA/output/tf_octa_c_ports/models/tf_octa_c_ports.onnx") -# ntwk = geometry.inference_snp(onnx_session, input_params) -# plot_rfic_transformer_metrics(ntwk) diff --git a/src/orca/gui/__init__.py b/src/orca/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/orca/gui/pipeline_window.py b/src/orca/gui/pipeline_window.py index e3dc89e..0888c90 100644 --- a/src/orca/gui/pipeline_window.py +++ b/src/orca/gui/pipeline_window.py @@ -52,7 +52,7 @@ def run(self): overwrite_callback=self.overwrite_callback ) self.finished.emit() - except Exception as e: + except Exception as e: # noqa: BLE001 - worker thread: report every failure to the GUI import traceback self.error.emit(str(e) + "\n" + traceback.format_exc()) diff --git a/src/orca/gui/utils.py b/src/orca/gui/utils.py index 194fb1f..af8b6f0 100644 --- a/src/orca/gui/utils.py +++ b/src/orca/gui/utils.py @@ -38,7 +38,7 @@ def load_class_from_file(file_path: str, base_class: type) -> type[Any] | None: Loads a class that inherits from `base_class` from a given file path. """ path = Path(file_path) - if not path.exists() or not path.suffix == ".py": + if not path.exists() or path.suffix != ".py": return None # Module name from file name @@ -52,15 +52,14 @@ def load_class_from_file(file_path: str, base_class: type) -> type[Any] | None: sys.modules[module_name] = module try: spec.loader.exec_module(module) - except Exception as e: - print(f"Error loading module {file_path}: {e}") + except Exception as e: # noqa: BLE001 - a broken user file must not take the GUI down + logger.error(f"Error loading module {file_path}: {e}") return None - for name, obj in inspect.getmembers(module, inspect.isclass): - if issubclass(obj, base_class) and obj is not base_class: - # Avoid importing abstract classes or the base class itself if it's imported in the file - if not inspect.isabstract(obj): - return obj + for _name, obj in inspect.getmembers(module, inspect.isclass): + # Avoid importing abstract classes or the base class itself if it's imported in the file + if issubclass(obj, base_class) and obj is not base_class and not inspect.isabstract(obj): + return obj return None def get_available_stages() -> list[type[PipelineStage]]: diff --git a/src/orca/gui/widgets/__init__.py b/src/orca/gui/widgets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/orca/gui/widgets/geometry_selector.py b/src/orca/gui/widgets/geometry_selector.py index 251b52d..eb6b929 100644 --- a/src/orca/gui/widgets/geometry_selector.py +++ b/src/orca/gui/widgets/geometry_selector.py @@ -93,7 +93,7 @@ def load_geometry_from_class(self, cls): self.current_geometry_instance = instance self.name_input.setText(instance.name) self.lbl_status.setText(f"Loaded: {cls.__name__}") - except Exception as e: + except Exception as e: # noqa: BLE001 - user-supplied class: show the error instead of crashing QMessageBox.critical(self, "Error", f"Failed to instantiate geometry class: {e}") self.lbl_status.setText("Error instantiating class") diff --git a/src/orca/gui/widgets/stage_widget.py b/src/orca/gui/widgets/stage_widget.py index 78a3158..7e8e36f 100644 --- a/src/orca/gui/widgets/stage_widget.py +++ b/src/orca/gui/widgets/stage_widget.py @@ -88,7 +88,7 @@ def set_widget_value(self, widget, value): widget.setText(str(value)) def get_widget_value(self, widget, annotation): - if isinstance(widget, QSpinBox) or isinstance(widget, QDoubleSpinBox): + if isinstance(widget, (QSpinBox, QDoubleSpinBox)): return widget.value() if isinstance(widget, QCheckBox): return widget.isChecked() @@ -106,7 +106,7 @@ def get_widget_value(self, widget, annotation): if (text.startswith("[") and text.endswith("]")) or (text.startswith("{") and text.endswith("}")): try: return json.loads(text) - except: + except json.JSONDecodeError: return text return text return None diff --git a/src/orca/pipeline/__init__.py b/src/orca/pipeline/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/orca/pipeline/context.py b/src/orca/pipeline/context.py index 9dd4fff..02db491 100644 --- a/src/orca/pipeline/context.py +++ b/src/orca/pipeline/context.py @@ -19,10 +19,10 @@ if TYPE_CHECKING: import pandas as pd - from torch import nn from orca.geometry.base_geometry import BaseGeometry from orca.training.datasets.base_dataset import BaseDataset + from orca.training.models.base_model import OrcaModel from orca.training.trainer import EpochResult #: Fields left out of :meth:`PipelineContext.to_json_dict`, because they are @@ -87,7 +87,7 @@ class PipelineContext: """Parameter table for the generated Palace models.""" # --- Written by ModelTrainer -------------------------------------------- - trained_model: nn.Module | None = None + trained_model: OrcaModel | None = None """Best model from the training run, restored to its best-validation weights.""" dataset: BaseDataset | None = None """The training split, carrying the feature pipeline and fitted normalizers. diff --git a/src/orca/pipeline/export_onnx_stage.py b/src/orca/pipeline/export_onnx_stage.py index 4a6c630..bc3d16c 100644 --- a/src/orca/pipeline/export_onnx_stage.py +++ b/src/orca/pipeline/export_onnx_stage.py @@ -6,12 +6,12 @@ import onnx import torch -from orca.geometry.base_geometry import BaseGeometry from orca.logger import logger from orca.pipeline.pipeline_stage import PipelineStage from orca.training.onnx_wrapper import ONNXWrapper if TYPE_CHECKING: + from orca.geometry.base_geometry import BaseGeometry from orca.pipeline.context import PipelineContext @@ -96,5 +96,8 @@ def run( onnx.save(onnx_model, output_path) + if progress_callback: + progress_callback(self.name, 1, 1, f"Exported ONNX model to {output_path}.") + context.model_path = output_path return context diff --git a/src/orca/pipeline/gds_conversion_stage.py b/src/orca/pipeline/gds_conversion_stage.py index 592c541..0fdd86c 100644 --- a/src/orca/pipeline/gds_conversion_stage.py +++ b/src/orca/pipeline/gds_conversion_stage.py @@ -7,12 +7,12 @@ import tqdm from pebble import ProcessPool -from orca.geometry.base_geometry import BaseGeometry from orca.logger import logger from orca.pipeline.pipeline_stage import PipelineStage from orca.simulation.gds_converter import create_palace_model_from_gds if TYPE_CHECKING: + from orca.geometry.base_geometry import BaseGeometry from orca.pipeline.context import PipelineContext @@ -66,7 +66,7 @@ def run( ) as pool: futures = {} gds_dir = os.path.dirname(gds_csv) - for i, row in gds_data.iterrows(): + for _, row in gds_data.iterrows(): # CSV layout: # name,input_winding_diameter,output_winding_diameter,center_displacement,bottom_linewidth,upper_linewidth # everything after name is input parameters @@ -78,15 +78,15 @@ def run( # Submit GDS conversion tasks future = pool.schedule( create_palace_model_from_gds, - kwargs=dict( - geometry_name=name, - params=params, - output_dir=base_dir, - gds_filename=gds_path, - stackup_xml=geometry.stackup_xml, - simconfig_filename=geometry.simconfig_filename, - show_mesh_results=False, - ), + kwargs={ + "geometry_name": name, + "params": params, + "output_dir": base_dir, + "gds_filename": gds_path, + "stackup_xml": geometry.stackup_xml, + "simconfig_filename": geometry.simconfig_filename, + "show_mesh_results": False, + }, timeout=self.timeout, ) futures[future] = name @@ -110,7 +110,7 @@ def run( f"GDS conversion of {name} exceeded {self.timeout:g} s " "(gmsh did not finish meshing); skipping this sample." ) - except Exception as e: + except Exception as e: # noqa: BLE001 - one bad sample must not abort the batch logger.error(f"GDS conversion failed for {name} with error: {e}") finally: # and call progress_callback even on failure if progress_callback: @@ -144,6 +144,7 @@ def _save_csv( params (dict[str, Any]): Input parameters to save. data_dir (str): Directory where data is stored. sim_path (str): Path to the simulation file. + config_name (str): Name of the Palace config file written for this sample. """ df = pd.DataFrame( [ diff --git a/src/orca/pipeline/gds_gen_stage.py b/src/orca/pipeline/gds_gen_stage.py index c4b0be4..c5c957d 100644 --- a/src/orca/pipeline/gds_gen_stage.py +++ b/src/orca/pipeline/gds_gen_stage.py @@ -7,22 +7,29 @@ import pandas as pd import tqdm -from orca.geometry.base_geometry import BaseGeometry from orca.logger import logger from orca.pipeline.pipeline_stage import PipelineStage if TYPE_CHECKING: + from orca.geometry.base_geometry import BaseGeometry from orca.pipeline.context import PipelineContext class GDSGenerator(PipelineStage): """ Pipeline stage for generating GDS files from trained models. + + Args: + num_samples (int): Number of parameter combinations to draw and lay out. + seed (int|None): Seed for the geometry's 'random' picking strategy, so that + a run can be reproduced. None keeps the seed set on the geometry's + input parameter iterator (fresh entropy by default). """ - def __init__(self, num_samples: int = 1000): + def __init__(self, num_samples: int = 1000, seed: int | None = None): super().__init__(name="GDS Generator", index=0) self.num_samples = num_samples + self.seed = seed def run( self, @@ -46,7 +53,7 @@ def run( os.makedirs(output_dir) # Tell the input parameter iterator the number of samples to generate - geometry.input_parameter_iterator.set_sample_count(self.num_samples) + geometry.input_parameter_iterator.set_sample_count(self.num_samples, seed=self.seed) futures = [] with ProcessPoolExecutor(max_workers=cpu_cores) as executor: @@ -72,7 +79,7 @@ def run( ) ): try: - gds_path, name, params = future.result() + _gds_path, name, params = future.result() # Save instance name + input parameters to CSV self._save_csv(gds_csv, name, params) @@ -80,7 +87,7 @@ def run( # A worker died (e.g. the calling script re-ran the pipeline on # import); nothing else will finish, so abort instead of skipping raise - except Exception as e: + except Exception as e: # noqa: BLE001 - one bad sample must not abort the batch logger.debug(f"Worker task failed: {e}") finally: if progress_callback: @@ -104,7 +111,7 @@ def _generate_gds_file( This is used for multiprocessing to avoid pickling issues with instance methods. Args: - geometry_class (Type[BaseGeometry]): The geometry class to instantiate. + gds_method (Callable): The geometry's create_gds_file(name, output_path, params). name (str): The name of the GDS file to create. output_dir (str): The directory to save the GDS file. params (dict[str, Any]): The input parameters for the geometry. diff --git a/src/orca/pipeline/test_model_stage.py b/src/orca/pipeline/test_model_stage.py index 3f73519..1caaf6a 100644 --- a/src/orca/pipeline/test_model_stage.py +++ b/src/orca/pipeline/test_model_stage.py @@ -7,7 +7,6 @@ import tqdm from sklearn.model_selection import train_test_split -from orca.geometry.base_geometry import BaseGeometry from orca.logger import logger from orca.pipeline.pipeline_stage import PipelineStage from orca.training.datasets.geo_to_ntwk import GeoToNtwkDataset @@ -23,6 +22,7 @@ ) if TYPE_CHECKING: + from orca.geometry.base_geometry import BaseGeometry from orca.pipeline.context import PipelineContext @@ -156,7 +156,7 @@ def test_model( try: predicted = calculate_electrical_parameters(ntwk_pred) reference = calculate_electrical_parameters(ntwk_gt) - except Exception as e: + except Exception as e: # noqa: BLE001 - skip samples whose metrics cannot be derived logger.debug(f"Could not compute electrical parameters for sample {i}: {e}") continue diff --git a/src/orca/pipeline/training_stage.py b/src/orca/pipeline/training_stage.py index b8be9a0..1300d77 100644 --- a/src/orca/pipeline/training_stage.py +++ b/src/orca/pipeline/training_stage.py @@ -5,7 +5,6 @@ import pandas as pd from sklearn.model_selection import train_test_split -from orca.geometry.base_geometry import BaseGeometry from orca.logger import logger from orca.pipeline.pipeline_stage import PipelineStage from orca.training.basis_expansion import BasisExpansion, get_basis_class @@ -15,6 +14,7 @@ from orca.training.tuner import HyperparameterTuner if TYPE_CHECKING: + from orca.geometry.base_geometry import BaseGeometry from orca.pipeline.context import PipelineContext diff --git a/src/orca/simulation/__init__.py b/src/orca/simulation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/orca/simulation/combine_snp_results.py b/src/orca/simulation/combine_snp_results.py index 315df86..2c0d560 100644 --- a/src/orca/simulation/combine_snp_results.py +++ b/src/orca/simulation/combine_snp_results.py @@ -13,6 +13,7 @@ import math import os import re +import sys import numpy as np import skrf as rf @@ -99,17 +100,13 @@ def parse_elmer_results(found_filename, freq, S_dB, S_arg): logger.debug( "Incorrect number of values in data file, does not match port count" ) - exit(1) + sys.exit(1) # read data file data = np.loadtxt(data_filename) - if data.ndim == 2: - # we have multiple frequencies - omegalist = data[:, omega_column] - else: - # we have only one freqiency point - omegalist = [data[omega_column]] + # 2D data holds multiple frequencies, 1D data a single frequency point + omegalist = data[:, omega_column] if data.ndim == 2 else [data[omega_column]] for omega in omegalist: freq.append(omega / (1e9 * 2 * math.pi)) @@ -233,7 +230,7 @@ def traverse_directories(path, level=0): if os.path.isdir(item_path): traverse_directories(item_path, level + 1) - elif item == "port-S.csv" or item == "scalar_results.names": + elif item in ("port-S.csv", "scalar_results.names"): found_datafiles.append(item_path) except PermissionError: @@ -256,19 +253,13 @@ def extrapolate_to_DC(snp_filename): if nw.frequency.npoints > 20: if nw.frequency.start <= 1e9: # extrapolate to DC - extrapolated = nw.extrapolate_to_dc( - points=None, dc_sparam=None, kind="cubic", coords="polar" - ) - filename, file_extension = os.path.splitext(snp_filename) + extrapolated = nw.extrapolate_to_dc(kind="cubic", coords="polar") + filename, _ = os.path.splitext(snp_filename) out_filename = filename + "_dc" # without extension - extrapolated.write_touchstone( - out_filename, - skrf_comment="DC point added by extrapolation", - form="db", - write_noise=True, - ) + extrapolated.comments = "DC point added by extrapolation" + extrapolated.write_touchstone(out_filename, form="db", write_noise=True) returnval = out_filename - logger.debug("Created file with DC extrapolation: ", out_filename, "\n") + logger.debug(f"Created file with DC extrapolation: {out_filename}") else: logger.debug("No data at low frequency, skipping DC extrapolation") else: @@ -318,16 +309,14 @@ def port_deembedding(snp_filename, port_info_available, port_info_data): Lport[str(portnum)] = L # convert the dict with port L into a list, to have the final values in correct order - L_values = [] - for key in Lport: - L_values.append(-Lport[key]) + L_values = [-L for L in Lport.values()] # load SnP data and apply negative series L at each port ntwk = rf.Network(snp_filename) freq = ntwk.frequency - # Create a Media object (needed for Media.inductor) - media = rf.media.DefinedGammaZ0(frequency=freq, z0=50) + # Create a Media object (needed for Media.inductor), z0 defaults to 50 Ohm + media = rf.media.DefinedGammaZ0(frequency=freq) for n, L in enumerate(L_values): logger.debug(f"Cascading L= {L * 1e12:.2f} pH at port {n + 1}") @@ -338,19 +327,11 @@ def port_deembedding(snp_filename, port_info_available, port_info_data): # after iterating over all ports we have the correct order again ntwk = connect(inductor, 0, ntwk, 0) - filename, file_extension = os.path.splitext(snp_filename) + filename, _ = os.path.splitext(snp_filename) out_filename = filename + "_deembedded" # without extension - ntwk.write_touchstone( - out_filename, - skrf_comment="De-embedded by adding negative series L at ports", - form="db", - write_noise=True, - ) - logger.debug( - "Created file with de-embedding (cascaded negative port L): ", - out_filename, - "\n", - ) + ntwk.comments = "De-embedded by adding negative series L at ports" + ntwk.write_touchstone(out_filename, form="db", write_noise=True) + logger.debug(f"Created file with de-embedding (cascaded negative port L): {out_filename}") else: logger.debug( "Skipping port de-embedding, not port geometry information available" @@ -367,8 +348,6 @@ def convert_to_touchstone(workdir, output_dir, touchstone_type: str): # evaluate the found data files for found_filename in found_datafiles: - # logger.debug(str(f)) - # Before we evaluate S-parameters, also check if we have a file port_information.json port_info_available = False # Get the directory two levels up @@ -438,23 +417,18 @@ def convert_to_touchstone(workdir, output_dir, touchstone_type: str): ) else: logger.debug("Invalid file, exit") - exit(1) + sys.exit(1) data_lines = [] for frequency in freq: - # line = str(frequency) - index = freq.index(frequency) data_line = [frequency] for i in range(1, num_ports + 1): for j in range(1, num_ports + 1): # special case 2-port data: the output is S11 S21 S12 S22 - if num_ports == 2: - param = str(j) + " " + str(i) - else: - param = str(i) + " " + str(j) + param = f"{j} {i}" if num_ports == 2 else f"{i} {j}" found_params = S_dB[index].keys() # assume that we also have phase data then @@ -491,22 +465,18 @@ def convert_to_touchstone(workdir, output_dir, touchstone_type: str): if not os.path.exists(output_dir): os.makedirs(output_dir) - output_file = open(output_filename, "w") - # write Touchstone header line - output_file.write(f"# {freq_unit.upper()} S DB R {Z0_string}\n") + with open(output_filename, "w") as output_file: + # write Touchstone header line + output_file.write(f"# {freq_unit.upper()} S DB R {Z0_string}\n") - for data_line in data_lines: - line = "" - for value in data_line: - line = line + " " + str(value) - output_file.write(line + "\n") + for data_line in data_lines: + line = "" + for value in data_line: + line = line + " " + str(value) + output_file.write(line + "\n") - output_file.close() logger.debug( - "Created combined S-parameter file for ", - num_ports, - "ports, filename: ", - output_filename, + f"Created combined S-parameter file for {num_ports} ports, filename: {output_filename}" ) if not port_info_available: diff --git a/src/orca/simulation/gds_converter.py b/src/orca/simulation/gds_converter.py index d62d5e4..1494670 100644 --- a/src/orca/simulation/gds_converter.py +++ b/src/orca/simulation/gds_converter.py @@ -24,11 +24,17 @@ def create_palace_model_from_gds( Based on: https://github.com/VolkerMuehlhaus/gds2palace_ihp_sg13g2/blob/main/workflow/palace_L2n0.py Args: + geometry_name (str): Name of the sample; passed through to the result for bookkeeping. + params (dict[str, Any]): Input parameters of the sample; passed through to the result. + output_dir (str): Directory in which the Palace model directory is created. gds_filename (str): Path to the GDS file. + stackup_xml (str): Path to the XML file describing the layer stackup. simconfig_filename (str): Path to the simulation configuration file (json). + show_mesh_results (bool): Show the gmsh GUI with the mesh and keep gds2palace's console output. Returns: - tuple[str, str]: Palace config name and data directory of the created Palace model. + tuple[str, dict, str, str, str]: geometry_name, params, Palace config name, simulation + directory and data directory of the created Palace model. """ # ExitStack is used to suppress stdout output in the conversion worker processes to avoid cluttering the console with ExitStack() as stack: diff --git a/src/orca/simulation/launchers.py b/src/orca/simulation/launchers.py index 59bd415..b4498d0 100644 --- a/src/orca/simulation/launchers.py +++ b/src/orca/simulation/launchers.py @@ -127,7 +127,7 @@ def ranks_per_simulation(self, requested: int) -> int: def command(self, slot: str, palace_executable: str, num_processes: int, config_name: str) -> str: """Shell command that runs `config_name` with `num_processes` MPI ranks in `slot`.""" - def check(self, num_processes: int) -> None: + def check(self, num_processes: int) -> None: # noqa: B027 - optional hook, not abstract """ Pre-flight check run once before the first simulation, so a broken launch setup fails fast with a clear message instead of a hanging or failing simulation. No-op by default. diff --git a/src/orca/simulation/slurm.py b/src/orca/simulation/slurm.py index 3307d36..558425c 100644 --- a/src/orca/simulation/slurm.py +++ b/src/orca/simulation/slurm.py @@ -33,7 +33,10 @@ def slurm_allocation() -> tuple[list[str], int]: # Expand the compact node list (e.g. "f[0101-0102,0110]") into hostnames ret = subprocess.run( - ["scontrol", "show", "hostnames", nodelist], capture_output=True, text=True, check=False + ["scontrol", "show", "hostnames", nodelist], # noqa: S607 - scontrol comes from the Slurm PATH + capture_output=True, + text=True, + check=False, ) if ret.returncode != 0: raise RuntimeError(f"Could not expand Slurm node list '{nodelist}': {ret.stderr.strip()}") @@ -186,7 +189,7 @@ def check(self, num_processes: int, timeout: float = 120) -> None: RuntimeError: If any test step fails or does not complete within `timeout`. """ procs = { - slot: subprocess.Popen( + slot: subprocess.Popen( # noqa: S602 - the srun prefix is built by this launcher f"{self._srun_prefix(slot, num_processes)} hostname", shell=True, stdout=subprocess.PIPE, diff --git a/src/orca/training/__init__.py b/src/orca/training/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/orca/training/basis_expansion.py b/src/orca/training/basis_expansion.py index 6690423..ba712c9 100644 --- a/src/orca/training/basis_expansion.py +++ b/src/orca/training/basis_expansion.py @@ -23,14 +23,16 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Callable -from typing import Any +from typing import TYPE_CHECKING, Any, TypeVar import optuna import torch from torch import nn -from orca.training.spec import IOSpec +if TYPE_CHECKING: + from collections.abc import Callable + + from orca.training.spec import IOSpec class BasisExpansion(nn.Module, ABC): @@ -66,11 +68,15 @@ def hyperparameter_search_space() -> dict[str, Any]: _BASIS_REGISTRY: dict[str, type[BasisExpansion]] = {} +# The decorator hands back the very class it was given, so the type checker +# keeps seeing e.g. ``IdentityBasis`` and not just ``type[BasisExpansion]``. +_BasisT = TypeVar("_BasisT", bound=BasisExpansion) + -def register_basis(name: str) -> Callable[[type[BasisExpansion]], type[BasisExpansion]]: +def register_basis(name: str) -> Callable[[type[_BasisT]], type[_BasisT]]: """Class decorator registering a basis expansion under a short name.""" - def decorator(cls: type[BasisExpansion]) -> type[BasisExpansion]: + def decorator(cls: type[_BasisT]) -> type[_BasisT]: if name in _BASIS_REGISTRY and _BASIS_REGISTRY[name] is not cls: raise ValueError(f"Basis expansion '{name}' is already registered.") _BASIS_REGISTRY[name] = cls @@ -109,7 +115,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: return x @classmethod - def from_spec(cls, spec: IOSpec, hyperparameters: dict[str, Any]) -> IdentityBasis: + def from_spec(cls, spec: IOSpec, hyperparameters: dict[str, Any]) -> IdentityBasis: # noqa: ARG003 - nothing to configure return cls() diff --git a/src/orca/training/datasets/__init__.py b/src/orca/training/datasets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/orca/training/datasets/base_dataset.py b/src/orca/training/datasets/base_dataset.py index 00387bc..9c68653 100644 --- a/src/orca/training/datasets/base_dataset.py +++ b/src/orca/training/datasets/base_dataset.py @@ -10,7 +10,7 @@ from orca.training.spec import FrequencyMode, IOSpec -class BaseDataset(ABC, torch.utils.data.Dataset): +class BaseDataset(ABC, torch.utils.data.Dataset[tuple[torch.Tensor, torch.Tensor]]): #: Frequency layout of the samples this dataset produces. frequency_mode: ClassVar[FrequencyMode] @@ -105,7 +105,7 @@ def _load_samples_and_normalize( "directory and the CSV describe the same set of simulations." ) - inputs, outputs = zip(*self.samples) + inputs, outputs = zip(*self.samples, strict=True) # The normalizers are shared across the splits of a run, so only the split # that owns them fits; the rest reuse those statistics unchanged. @@ -123,19 +123,13 @@ def _load_samples_and_normalize( "training split first with new_split(..., fit_normalizers=True)." ) - self.samples = list( - map( - lambda s: ( - self.input_normalizer.normalize(s[0]) - if self.input_normalizer is not None - else s[0], - self.output_normalizer.normalize(s[1]) - if self.output_normalizer is not None - else s[1], - ), - self.samples, + self.samples = [ + ( + self.input_normalizer.normalize(x) if self.input_normalizer is not None else x, + self.output_normalizer.normalize(y) if self.output_normalizer is not None else y, ) - ) + for x, y in self.samples + ] @abstractmethod def load_samples(self, directory: str, data_df: pd.DataFrame) -> None: @@ -144,8 +138,8 @@ def load_samples(self, directory: str, data_df: pd.DataFrame) -> None: This method should be implemented by subclasses to load data specific from its self.data_dir. """ - def __getitem__(self, idx) -> tuple[torch.Tensor, torch.Tensor]: - return self.samples[idx] + def __getitem__(self, index) -> tuple[torch.Tensor, torch.Tensor]: + return self.samples[index] def __len__(self): return len(self.samples) @@ -173,5 +167,5 @@ def new_split( input_normalizer=self.input_normalizer, output_normalizer=self.output_normalizer, ) - new_dataset._load_samples_and_normalize(directory, data_df, fit_normalizers) + new_dataset._load_samples_and_normalize(directory, data_df, fit_normalizers) # noqa: SLF001 - same class return new_dataset diff --git a/src/orca/training/datasets/geo_to_ntwk.py b/src/orca/training/datasets/geo_to_ntwk.py index fe0b1fb..6abc4f4 100644 --- a/src/orca/training/datasets/geo_to_ntwk.py +++ b/src/orca/training/datasets/geo_to_ntwk.py @@ -9,9 +9,9 @@ from orca.logger import logger -class GeoToNtwkDataset(torch.utils.data.Dataset): +class GeoToNtwkDataset(torch.utils.data.Dataset[tuple[np.ndarray, rf.Network]]): """ - This dataset class is designed to load a scikit-rf Network object from a .snp file and associate it with the corresponding geometry parameters. + This dataset class is designed to load a scikit-rf Network object from a .snp file and associate it with the corresponding geometry parameters. This can be used in the testing stage to compare the predicted S-parameters with the actual S-parameters from the .snp file. """ @@ -20,7 +20,7 @@ def __init__( directory: str, data_df: pd.DataFrame, ): - self.samples = [] + self.samples: list[tuple[np.ndarray, rf.Network]] = [] self.load_samples(directory, data_df) @@ -28,7 +28,7 @@ def load_samples(self, directory: str, data_df: pd.DataFrame) -> None: self.input_param_names = list(data_df.columns) self.input_param_names.remove("name") # Remove 'name' column - for idx, row in tqdm.tqdm( + for _, row in tqdm.tqdm( data_df.iterrows(), total=len(data_df), desc="Loading test network samples" ): snp_path = os.path.join(directory, row["name"]) @@ -51,5 +51,5 @@ def load_single_sample( def __len__(self): return len(self.samples) - def __getitem__(self, idx): - return self.samples[idx] + def __getitem__(self, index) -> tuple[np.ndarray, rf.Network]: + return self.samples[index] diff --git a/src/orca/training/datasets/geo_to_s_param.py b/src/orca/training/datasets/geo_to_s_param.py index e6013bd..a24c011 100644 --- a/src/orca/training/datasets/geo_to_s_param.py +++ b/src/orca/training/datasets/geo_to_s_param.py @@ -35,7 +35,7 @@ def load_samples(self, directory: str, data_df: pd.DataFrame) -> None: self.input_param_names = list(data_df.columns) self.input_param_names.remove("name") # Remove 'name' column - for idx, row in data_df.iterrows(): + for _, row in data_df.iterrows(): snp_path = os.path.join(directory, row["name"]) if not os.path.exists(snp_path): diff --git a/src/orca/training/datasets/geo_to_s_param_single_f.py b/src/orca/training/datasets/geo_to_s_param_single_f.py index 70788e3..4f70977 100644 --- a/src/orca/training/datasets/geo_to_s_param_single_f.py +++ b/src/orca/training/datasets/geo_to_s_param_single_f.py @@ -33,10 +33,10 @@ def __init__( ) def load_samples(self, directory: str, data_df: pd.DataFrame) -> None: - self.input_param_names = list(data_df.columns) + ["frequency"] + self.input_param_names = [*data_df.columns, "frequency"] self.input_param_names.remove("name") # Remove 'name' column - for idx, row in tqdm.tqdm( + for _, row in tqdm.tqdm( data_df.iterrows(), total=len(data_df), desc="Loading samples" ): snp_path = os.path.join(directory, row["name"]) @@ -60,7 +60,7 @@ def load_single_sample( samples = [] for i in range(len(freq)): - # Input = geometry + frequency + # the model input is the geometry vector with the frequency appended x = np.hstack((geometry_params, freq[i])).astype(np.float32) x, y = ( diff --git a/src/orca/training/models/__init__.py b/src/orca/training/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/orca/training/models/base_model.py b/src/orca/training/models/base_model.py index 372d4e7..6e32905 100644 --- a/src/orca/training/models/base_model.py +++ b/src/orca/training/models/base_model.py @@ -11,17 +11,21 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Callable -from typing import Any, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar, TypeVar -import numpy as np -import skrf as rf import torch from torch import nn from orca.training.basis_expansion import BasisExpansion, IdentityBasis from orca.training.guarantees import PhysicsGuarantees -from orca.training.spec import FrequencyMode, IOSpec + +if TYPE_CHECKING: + from collections.abc import Callable + + import numpy as np + import skrf as rf + + from orca.training.spec import FrequencyMode, IOSpec class OrcaModel(nn.Module, ABC): @@ -106,15 +110,19 @@ def to_network(self, raw: np.ndarray, frequencies: np.ndarray) -> rf.Network: _MODEL_REGISTRY: dict[str, type[OrcaModel]] = {} +# The decorator hands back the very class it was given, so the type checker +# keeps seeing e.g. ``OrcaMLP`` and not just ``type[OrcaModel]``. +_ModelT = TypeVar("_ModelT", bound=OrcaModel) + -def register_model(name: str) -> Callable[[type[OrcaModel]], type[OrcaModel]]: +def register_model(name: str) -> Callable[[type[_ModelT]], type[_ModelT]]: """Class decorator registering a model under a short name. Args: name (str): Name to register the model under, e.g. ``"mlp"``. """ - def decorator(cls: type[OrcaModel]) -> type[OrcaModel]: + def decorator(cls: type[_ModelT]) -> type[_ModelT]: if name in _MODEL_REGISTRY and _MODEL_REGISTRY[name] is not cls: raise ValueError(f"Model name '{name}' is already registered.") _MODEL_REGISTRY[name] = cls diff --git a/src/orca/training/normalize.py b/src/orca/training/normalize.py index f0bcc6b..1d0996c 100644 --- a/src/orca/training/normalize.py +++ b/src/orca/training/normalize.py @@ -1,6 +1,5 @@ from abc import ABC, abstractmethod -import numpy as np import torch from torch import nn @@ -110,7 +109,7 @@ def is_fitted(self) -> bool: return getattr(self, "_fitted", False) @abstractmethod - def process_samples(self, samples: list): + def process_samples(self, samples: list[torch.Tensor]): """ Process the samples to compute normalization statistics. @@ -124,6 +123,10 @@ class MinMaxNormalizer(InputNormalizer): A normalizer that applies min-max normalization to input parameters. """ + # Buffers registered in __init__; declared here so they are typed as tensors + input_mins: torch.Tensor + input_maxs: torch.Tensor + def __init__(self, input_parameter_iterator: InputParameterIterator): """ Applies component-wise min-max normalization to the input tensor. @@ -154,12 +157,16 @@ class OutputMinMaxNormalizer(OutputNormalizer): A normalizer that applies min-max normalization to output parameters. """ - def process_samples(self, samples: list[tuple[np.ndarray, np.ndarray]]): + # Buffers registered by process_samples; declared here so they are typed as tensors + output_mins: torch.Tensor + output_maxs: torch.Tensor + + def process_samples(self, samples: list[torch.Tensor]): output_mins, output_maxs = self.get_output_min_max(samples) self.register_buffer("output_mins", output_mins) self.register_buffer("output_maxs", output_maxs) - def get_output_min_max(self, samples) -> tuple[list[float], list[float]]: + def get_output_min_max(self, samples: list[torch.Tensor]) -> tuple[torch.Tensor, torch.Tensor]: """Calculate min and max of output parameters for normalization.""" stacked_samples = torch.vstack(samples) mins = torch.min(stacked_samples, dim=0).values @@ -180,12 +187,18 @@ class StandardNormalizer(OutputNormalizer): A normalizer that applies standard score normalization to input parameters. """ - def process_samples(self, samples: list[tuple[np.ndarray, np.ndarray]]): + # Buffers registered by process_samples; declared here so they are typed as tensors + input_means: torch.Tensor + input_stds: torch.Tensor + + def process_samples(self, samples: list[torch.Tensor]): input_means, input_stds = self.get_output_means_stds(samples) self.register_buffer("input_means", input_means) self.register_buffer("input_stds", input_stds) - def get_output_means_stds(self, samples) -> tuple[list[float], list[float]]: + def get_output_means_stds( + self, samples: list[torch.Tensor] + ) -> tuple[torch.Tensor, torch.Tensor]: """Calculate means and standard deviations of output parameters for normalization.""" stacked_samples = torch.vstack(samples) means = torch.mean(stacked_samples, dim=0) diff --git a/src/orca/training/onnx_wrapper.py b/src/orca/training/onnx_wrapper.py index 3dce928..7594704 100644 --- a/src/orca/training/onnx_wrapper.py +++ b/src/orca/training/onnx_wrapper.py @@ -13,6 +13,8 @@ class ONNXWrapper(torch.nn.Module): Args: model: The PyTorch model to be wrapped. + input_normalizer: Applied to the concatenated inputs before the model, if given. + output_denormalizer: Its ``denormalize`` is applied to the model output, if given. Returns: A torch.nn.Module that can be passed to torch.onnx.export. @@ -20,9 +22,9 @@ class ONNXWrapper(torch.nn.Module): def __init__( self, - model, - input_normalizer: Normalizer, - output_denormalizer: Normalizer, + model: torch.nn.Module, + input_normalizer: Normalizer | None, + output_denormalizer: Normalizer | None, ): super().__init__() self.model = model diff --git a/src/orca/training/predictors.py b/src/orca/training/predictors.py index 088c752..135a238 100644 --- a/src/orca/training/predictors.py +++ b/src/orca/training/predictors.py @@ -10,15 +10,19 @@ from __future__ import annotations from abc import ABC, abstractmethod +from typing import TYPE_CHECKING import numpy as np -import skrf as rf import torch -from orca.training.codecs import OutputCodec -from orca.training.models.base_model import OrcaModel from orca.training.spec import FrequencyMode +if TYPE_CHECKING: + import skrf as rf + + from orca.training.codecs import OutputCodec + from orca.training.models.base_model import OrcaModel + class NetworkPredictor(ABC): """Predicts the response of a geometry over a frequency grid.""" @@ -101,14 +105,14 @@ def __init__(self, session, codec: OutputCodec): def predict(self, params: np.ndarray, frequencies: np.ndarray) -> rf.Network: frequencies = np.asarray(frequencies) - params = iter(np.asarray(params, dtype=np.float32)) + # Geometry parameters are consumed in model-input order; frequency is the sweep + param_values = iter(np.asarray(params, dtype=np.float32)) feed = {} for name in self.input_names: - if name == "frequency": - column = frequencies - else: - column = np.full(len(frequencies), next(params)) + column = ( + frequencies if name == "frequency" else np.full(len(frequencies), next(param_values)) + ) feed[name] = column.reshape(-1, 1).astype(np.float32) outputs = self.session.run(self.output_names, feed) diff --git a/src/orca/training/spec.py b/src/orca/training/spec.py index 19f3e6e..873d020 100644 --- a/src/orca/training/spec.py +++ b/src/orca/training/spec.py @@ -9,10 +9,12 @@ from dataclasses import dataclass from enum import Enum, auto +from typing import TYPE_CHECKING -import numpy as np +if TYPE_CHECKING: + import numpy as np -from orca.training.codecs import OutputCodec + from orca.training.codecs import OutputCodec class FrequencyMode(Enum): diff --git a/src/orca/training/trainer.py b/src/orca/training/trainer.py index c56645b..60ff27d 100644 --- a/src/orca/training/trainer.py +++ b/src/orca/training/trainer.py @@ -10,18 +10,19 @@ from __future__ import annotations import copy -from collections.abc import Callable from dataclasses import dataclass, field -from typing import Any +from typing import TYPE_CHECKING, Any import optuna import torch import tqdm -from torch import nn from torch.optim import AdamW, Optimizer from torch.utils.data import DataLoader, Dataset -from orca.training.models.base_model import OrcaModel +if TYPE_CHECKING: + from collections.abc import Callable + + from orca.training.models.base_model import OrcaModel def default_device() -> torch.device: @@ -37,7 +38,7 @@ class TrainingConfig: batch_size: Mini-batch size for both training and validation. learning_rate: Initial learning rate handed to the optimizer. patience: Epochs without validation improvement before stopping early. - optimizer_cls: Optimizer class, constructed as ``optimizer_cls(params, lr=...)``. + optimizer_cls: Optimizer factory, called as ``optimizer_cls(params, lr=...)``. scheduler_factor: Factor by which ReduceLROnPlateau scales the learning rate. scheduler_patience: Plateau length, in epochs, before the scheduler reacts. device: Device to train on. @@ -47,7 +48,7 @@ class TrainingConfig: batch_size: int = 128 learning_rate: float = 1e-3 patience: int = 10 - optimizer_cls: type[Optimizer] = AdamW + optimizer_cls: Callable[..., Optimizer] = AdamW scheduler_factor: float = 0.5 scheduler_patience: int = 10 device: torch.device = field(default_factory=default_device) @@ -96,7 +97,7 @@ class TrainingResult: stopped_early: Whether early stopping ended the run before ``epochs``. """ - model: nn.Module + model: OrcaModel best_loss: float history: list[EpochResult] stopped_early: bool @@ -133,24 +134,22 @@ def __init__( self.stage_name = stage_name self.verbose = verbose - def resolve_criterion(self, model: nn.Module) -> Callable: + def resolve_criterion(self, model: OrcaModel) -> Callable: """The configured loss, or the one the model asks to be trained with.""" if self.criterion is not None: return self.criterion - if isinstance(model, OrcaModel): - return model.default_loss() - return nn.L1Loss() + return model.default_loss() def fit( self, - model: nn.Module, + model: OrcaModel, train_dataset: Dataset, val_dataset: Dataset, ) -> TrainingResult: """Train ``model``, keeping the weights with the lowest validation loss. Args: - model (nn.Module): Model to train. Moved to the configured device. + model (OrcaModel): Model to train. Moved to the configured device. train_dataset (Dataset): Samples to optimize on. val_dataset (Dataset): Samples used for early stopping and scheduling. @@ -201,12 +200,13 @@ def fit( message = f"Train: {train_loss:.4f} | Val: {val_loss:.4f}" self._report(epoch + 1, message) if self.verbose: - print(f"Epoch {epoch + 1:4d} | {message}") + # tqdm.write keeps the line clear of the epoch progress bar + tqdm.tqdm.write(f"Epoch {epoch + 1:4d} | {message}") if patience_counter >= config.patience: stopped_early = True if self.verbose: - print("Early stopping triggered") + tqdm.tqdm.write("Early stopping triggered") break if best_state is not None: @@ -218,7 +218,7 @@ def fit( def evaluate( self, - model: nn.Module, + model: OrcaModel, dataset: Dataset, criterion: Callable | None = None, batch_size: int | None = None, @@ -233,9 +233,11 @@ def _train_epoch(self, model, criterion, optimizer, loader) -> float: model.train() total = 0.0 - for x, y in tqdm.tqdm(loader, desc="Training", leave=False, disable=not self.verbose): - x = x.to(self.config.device) - y = y.to(self.config.device) + for batch_x, batch_y in tqdm.tqdm( + loader, desc="Training", leave=False, disable=not self.verbose + ): + x = batch_x.to(self.config.device) + y = batch_y.to(self.config.device) optimizer.zero_grad() loss = criterion(model(x), y) @@ -258,9 +260,9 @@ def _run_eval(self, model, criterion, loader, desc: str, show_progress: bool = T iterator = tqdm.tqdm(loader, desc=desc, leave=False, disable=not self.verbose) with torch.no_grad(): - for x, y in iterator: - x = x.to(self.config.device) - y = y.to(self.config.device) + for batch_x, batch_y in iterator: + x = batch_x.to(self.config.device) + y = batch_y.to(self.config.device) total += criterion(model(x), y).item() return total / len(loader) diff --git a/src/orca/training/tuner.py b/src/orca/training/tuner.py index 9940dd8..4d9a79a 100644 --- a/src/orca/training/tuner.py +++ b/src/orca/training/tuner.py @@ -8,17 +8,20 @@ from __future__ import annotations import traceback -from typing import Any +from typing import TYPE_CHECKING, Any import optuna from sklearn.model_selection import KFold -from torch.utils.data import Dataset, Subset +from torch.utils.data import Subset from orca.logger import logger -from orca.training.basis_expansion import BasisExpansion -from orca.training.models.base_model import OrcaModel from orca.training.trainer import Trainer, TrainingConfig +if TYPE_CHECKING: + from orca.training.basis_expansion import BasisExpansion + from orca.training.datasets.base_dataset import BaseDataset + from orca.training.models.base_model import OrcaModel + def suggest_hyperparameters(trial: optuna.Trial, search_space: dict[str, Any]) -> dict[str, Any]: """Draw one value per entry of a search space from an optuna trial. @@ -67,8 +70,8 @@ class HyperparameterTuner: Args: model_cls (type[OrcaModel]): Architecture to tune. - dataset (Dataset): Train/validation data, split into folds internally. - Must expose an ``io_spec`` (any :class:`~orca.training.datasets.base_dataset.BaseDataset`). + dataset (BaseDataset): Train/validation data, split into folds internally; + its ``io_spec`` sizes the models. basis_cls (type[BasisExpansion] | None): Basis expansion to build each model with. Its search space is tuned alongside the model's. ``None`` trains on the raw inputs. @@ -82,7 +85,7 @@ class HyperparameterTuner: def __init__( self, model_cls: type[OrcaModel], - dataset: Dataset, + dataset: BaseDataset, basis_cls: type[BasisExpansion] | None = None, n_fold_cv: int = 5, n_trials: int = 200, @@ -159,15 +162,15 @@ def _run_fold(self, trial, config, hyperparameters, fold_idx, train_indices, val train_dataset=Subset(self.dataset, list(train_indices)), val_dataset=Subset(self.dataset, list(val_indices)), ) - except Exception: + except Exception as e: traceback.print_exc() - raise optuna.exceptions.TrialPruned() + raise optuna.exceptions.TrialPruned from e logger.info(f"{fold_label} | Val Loss: {result.best_loss:.4f}") # Report once per fold, using the fold index as the pruning step trial.report(result.best_loss, fold_idx) if trial.should_prune(): - raise optuna.exceptions.TrialPruned() + raise optuna.exceptions.TrialPruned return result.best_loss diff --git a/src/orca/utils/__init__.py b/src/orca/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/orca/utils/class_finder.py b/src/orca/utils/class_finder.py index 27c4b45..6fb2dad 100644 --- a/src/orca/utils/class_finder.py +++ b/src/orca/utils/class_finder.py @@ -85,10 +85,10 @@ def discover_classes( f"Discovered {base_class.__name__} subclass: {display_name}" ) - except Exception as e: + except Exception as e: # noqa: BLE001 - skip the class, keep discovering logger.warning(f"Could not process {name}: {e}") - except Exception as e: + except Exception as e: # noqa: BLE001 - a broken plugin file must not stop discovery logger.warning(f"Failed to load classes from {py_file.name}: {e}") if not classes: diff --git a/src/orca/utils/postprocessing.py b/src/orca/utils/postprocessing.py index 03065a3..d75048f 100644 --- a/src/orca/utils/postprocessing.py +++ b/src/orca/utils/postprocessing.py @@ -2,6 +2,8 @@ import numpy as np import skrf as rf +from orca.logger import logger + def to_mixed_mode(ntwk): """Mixed-mode view of a single-ended network, as a copy. @@ -38,10 +40,8 @@ def calculate_electrical_parameters(ntwk): Qs = np.imag(z_d22) / np.real(z_d22) k = np.abs(np.imag(z_d12)) / np.sqrt(np.abs(np.imag(z_d11) * np.imag(z_d22))) - #srf_idx = np.where(np.diff(np.sign(np.imag(z_d11))))[0] - #srf_f = freq_ghz[srf_idx[0]] if len(srf_idx) > 0 else None im = np.imag(z_d11) - cross = np.where(im[:-1] * im[1:] < 0)[0] # echte Vorzeichenwechsel + cross = np.where(im[:-1] * im[1:] < 0)[0] # actual sign changes only f_min = 20.0 cross = cross[freq_ghz[cross] >= f_min] @@ -59,8 +59,6 @@ def calculate_electrical_parameters(ntwk): srf_f = float(f0 - y0 * (f1 - f0) / (y1 - y0)) return { - #"mm_ntwk": mm_ntwk, - #"freq_ghz": freq_ghz, "Lp": np.array(Lp), "Ls": np.array(Ls), "Rp": np.array(Rp), @@ -261,7 +259,7 @@ def s_param_list_to_network(s_param_list: np.ndarray) -> tuple[int, list[rf.Netw # Assume s_param_list shape is (batch_size, num_params) num_params = s_param_list.shape[1] N = int(np.sqrt(num_params // 2)) # number of ports - print(f"Number of ports inferred: {N}") + logger.debug(f"Number of ports inferred: {N}") # Create a network for each sample in the batch ntwk_list = [] for sample in s_param_list: @@ -282,13 +280,13 @@ def single_ended_to_mixed_mode(ntwk: rf.Network) -> rf.Network: Usually port 1 and 2 are considered differential pair 1, and port 3 and 4 differential pair 2. Args: - network (rf.Network): 4-port single-ended network. + ntwk (rf.Network): 4-port single-ended network, converted in place. Returns: rf.Network: 2-port mixed-mode network. """ ntwk.se2gmm(p=2) - return ntwk.nports, ntwk + return ntwk def plot_diff_s_params_and_k(ntwk: rf.Network): @@ -296,7 +294,7 @@ def plot_diff_s_params_and_k(ntwk: rf.Network): Plots the differential S-parameters and coupling factor k for a 4-port single-ended network. Args: - network (rf.Network): 4-port single-ended network. + ntwk (rf.Network): 4-port single-ended network. """ # Calculate k z = ntwk.z @@ -314,9 +312,12 @@ def plot_diff_s_params_and_k(ntwk: rf.Network): # Primary Y-Axis (S-parameters) ax1.set_xlabel("Frequency") ax1.set_ylabel("S-Parameters (dB)") - ntwk.plot_s_db(m=1, n=0, ax=ax1, label="Insertion Loss ($S_{d2d1}$)") - ntwk.plot_s_db(m=0, n=0, ax=ax1, label="Return Loss ($S_{d1d1}$)") - ntwk.plot_s_db(m=3, n=0, ax=ax1, label="Mode Conversion ($S_{c2d1}$)") + # Plotted against the same frequency axis as k below (ntwk.f in Hz), which + # Network.plot_s_db would not do: it scales the axis to the network's unit. + ax1.plot(freq_ghz, ntwk.s_db[:, 1, 0], label="Insertion Loss ($S_{d2d1}$)") + ax1.plot(freq_ghz, ntwk.s_db[:, 0, 0], label="Return Loss ($S_{d1d1}$)") + ax1.plot(freq_ghz, ntwk.s_db[:, 3, 0], label="Mode Conversion ($S_{c2d1}$)") + ax1.legend(loc="lower left") # Secondary Y-Axis (k) ax2 = ax1.twinx() From c4dcfe3316d2cdfcd4d21c0be4ffc1b267353ca5 Mon Sep 17 00:00:00 2001 From: David Lurz Date: Wed, 16 Sep 2026 14:52:46 +0200 Subject: [PATCH 3/3] ruff/ty github action --- .github/workflows/tests.yml | 33 ++++++++++++++++++++ pyproject.toml | 9 +++++- uv.lock | 62 +++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..b2a5352 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,33 @@ +name: tests + +on: + push: + branches: + - main + pull_request: + +concurrency: + group: tests-${{ github.ref }} + cancel-in-progress: true + +env: + UV_PYTHON: "3.12" + +jobs: + lint: + name: lint and type-check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: uv.lock + - name: Install dependencies + # The train extra is needed for ty to resolve torch & co.; the cpu selector + # keeps that to the small CPU-only wheel, which setup-uv caches. + run: uv sync --locked --extra train --extra cpu + - name: Ruff + run: uv run ruff check . + - name: Ty + run: uv run ty check diff --git a/pyproject.toml b/pyproject.toml index 2d30bb4..d809d47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,6 +88,13 @@ cpu = ["torch"] cu126 = ["torch"] cu130 = ["torch"] +[dependency-groups] +# Installed by `uv sync` (not by `pip install`): the linters CI runs, pinned by uv.lock. +dev = [ + "ruff", + "ty", +] + [tool.uv] conflicts = [ [{ extra = "cpu" }, { extra = "cu126" }], @@ -145,7 +152,7 @@ addopts = "-ra --strict-markers --strict-config" xfail_strict = true [tool.coverage.run] -source = ["src/cobra"] +source = ["src/orca"] branch = true [tool.coverage.report] diff --git a/uv.lock b/uv.lock index dd1c740..2c20a8a 100644 --- a/uv.lock +++ b/uv.lock @@ -2271,6 +2271,12 @@ train = [ { name = "torch", version = "2.14.0+cu130", source = { registry = "https://download.pytorch.org/whl/cu130" }, marker = "(extra == 'extra-4-orca-cpu' and extra == 'extra-4-orca-cu126') or (extra != 'extra-4-orca-cu126' and extra == 'extra-4-orca-cu130') or (extra != 'extra-4-orca-cpu' and extra == 'extra-4-orca-cu130')" }, ] +[package.dev-dependencies] +dev = [ + { name = "ruff" }, + { name = "ty" }, +] + [package.metadata] requires-dist = [ { name = "colorlog" }, @@ -2297,6 +2303,12 @@ requires-dist = [ ] provides-extras = ["train", "cpu", "cu126", "cu130"] +[package.metadata.requires-dev] +dev = [ + { name = "ruff" }, + { name = "ty" }, +] + [[package]] name = "orjson" version = "3.12.0" @@ -3212,6 +3224,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/2a/b508237a7fcceab8a8724405480eb55d527523419c9dbcde369f954656ad/ruamel.yaml.string-0.1.1-py3-none-any.whl", hash = "sha256:eb146bcb42b116216638034a434e9cf3ae2a5d3933aa37183a9854b5f3ff42de", size = 4118, upload-time = "2023-05-02T05:37:20.332Z" }, ] +[[package]] +name = "ruff" +version = "0.16.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/bb/5a449b9162e49b139d72f61672bd3ac1d790221f796d3304e2241fff4c58/ruff-0.16.7.tar.gz", hash = "sha256:5f71d004ac1263b22fa39462ac5ae618a4b77d58981af2cc79bf79a29c12b1a6", size = 4924184, upload-time = "2026-09-10T18:04:06.336Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/b2/c80aeeb7f9e469c0d63a85d2f1ab6e1ebfbe10ea7a8d2438b7e09e3ff09e/ruff-0.16.7-py3-none-linux_armv6l.whl", hash = "sha256:727307773e7c7f9181d3ed3a2484186e56c1fa1874255911c74585eb2c7c19f9", size = 10048917, upload-time = "2026-09-10T18:03:30.28Z" }, + { url = "https://files.pythonhosted.org/packages/7b/96/20bb7bcae008004df52afcb7ac83432d4a467f2c17b672fe46d26be231c5/ruff-0.16.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9d61c258deabf58f34c67bd4bb4d939c7f2e6b5f0e59c1cdd1cf771b11cde929", size = 10242929, upload-time = "2026-09-10T18:03:32.706Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f184b0d5abec02db69cfd7e49b688ae0237554528ca777136c613bf36bee/ruff-0.16.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ab81118df8945e0193d0240712aa4496573595b75185c3636ed825592a0f728", size = 9847245, upload-time = "2026-09-10T18:03:34.509Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2d/db1633a641866ed801e34cc6b60ef236c5e16f9b2124ab1d49cc24a5fe4f/ruff-0.16.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c196c968874fc8019da8e7163de7a1a370f111e2309b4b7dfea0fce950198d0", size = 9961780, upload-time = "2026-09-10T18:03:36.618Z" }, + { url = "https://files.pythonhosted.org/packages/4d/98/edea21e1a3e38dbbc3bf6bb068b863b3b06184cf8533a4c7dbbe208a89d5/ruff-0.16.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac8c3bd0a7e10ad31e6ce51e7a99f3cb772e69aecdd6b9ea7e99b362f62a62c0", size = 9866337, upload-time = "2026-09-10T18:03:38.805Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/a15e60d4c87b214646f116ca9d204475bf993ee1047459bc9a360fd4d6d1/ruff-0.16.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398d3988edde000b5c75dc1b3f584708da9bc990de069c18909142580fec1af9", size = 10562512, upload-time = "2026-09-10T18:03:40.71Z" }, + { url = "https://files.pythonhosted.org/packages/29/42/eaff4c9b6d0c7cdf56df313a17e89ae854f5bbc0b0c8f9cce19be0ab7a8f/ruff-0.16.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce05b62b770a8217c4646a9c4139fca00efe8fe5d71f87df2b243ff20d4584d1", size = 11302938, upload-time = "2026-09-10T18:03:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/c75aa59a4ec181fe2ec06cab30e198c1c6d107229a9f008ae3a7c16cabd8/ruff-0.16.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af1b576fddb9d9ef2ececfb5fadcd6a624b25070ed85e3cfcfe449fc3ff6a7b9", size = 10840857, upload-time = "2026-09-10T18:03:44.604Z" }, + { url = "https://files.pythonhosted.org/packages/21/33/81f3da371942ea031105ba679d8d6e28ec1660ccd690a45f42d381161356/ruff-0.16.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce7f8f22df67c93ed96c717f9128eadb797144ac2bad475cf536f31d6100c55", size = 10370001, upload-time = "2026-09-10T18:03:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/fa/0b/6345fb4dbf6dd0ed1cfe5d18391dc9c3f59cc81622a7b0a65b84b3e730ba/ruff-0.16.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:06d0e93d04f392996435ebd600c153f65b47d73fbec2415aa99c5ee5756b3a5f", size = 10548735, upload-time = "2026-09-10T18:03:48.658Z" }, + { url = "https://files.pythonhosted.org/packages/3f/4d/c5576adf511f92a328e5569dda190ecdd430da51f1a649f3a4a2fd73e21e/ruff-0.16.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:142151a5e7b93c1b11111337142f89dd2fbfee92161225c99a97222f22e32656", size = 10108496, upload-time = "2026-09-10T18:03:50.563Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8c/667d83c16199a17a56adc6b0bd4c3beb5b767a2babcd16a56f76f9be7fd6/ruff-0.16.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e6651f97a342d8b35d54d8991544ca22169b86dc54111cb604666940c431b750", size = 9860136, upload-time = "2026-09-10T18:03:52.621Z" }, + { url = "https://files.pythonhosted.org/packages/99/75/78d401106731999a1dd20cc5a6961e37e1eb9397a3b589f73f3a5ce146a3/ruff-0.16.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ef140c6eb935fa9a84c9c607dfb2cb1b85843c192e79265b0c54f35f557ea8e5", size = 10286290, upload-time = "2026-09-10T18:03:55.207Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/56f9c3a8b755df93a0ad318b2147bf4ef5dae9a7e5ec61c460109c67957f/ruff-0.16.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:53e39506a730fadeee0d998ed5946f30671f0db240c6c7c73bdabbe33604bb6f", size = 10745048, upload-time = "2026-09-10T18:03:57.299Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ea/7f9b938a63ece4bec677ad7f9f7fa02df3383db1949ed93a382441c09a87/ruff-0.16.7-py3-none-win32.whl", hash = "sha256:2ea3470fcebcbc5df2fb0c6f3b90333fa9084c534e0111c038fa4a6ab9f1c4b7", size = 10059082, upload-time = "2026-09-10T18:03:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/39/11/480a6973a927aa653e1cead6a6416008640e03a99d05b34c0434b8c6c366/ruff-0.16.7-py3-none-win_amd64.whl", hash = "sha256:7ac26aca826e9e21d0f1cb25b54ac660760a9fdd094d3e4df9848232be98cfc6", size = 10593368, upload-time = "2026-09-10T18:04:01.999Z" }, + { url = "https://files.pythonhosted.org/packages/8b/4b/51327018d056f0dad2c2238f26d1fb0f53707a9d91b75dea6d1b3039f136/ruff-0.16.7-py3-none-win_arm64.whl", hash = "sha256:aab7f39e2c9df6c596216070f98eef1207b94f8516cca20c808826974971855b", size = 10412401, upload-time = "2026-09-10T18:04:04.098Z" }, +] + [[package]] name = "scikit-image" version = "0.26.0" @@ -3942,6 +3979,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/b6/3394d5548404c1cabd1dadadd28d0b3f9478db1dff8180da53bb3f0a1e19/triton-3.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f0497218e26b7d79773ad9c2a3fa3b539ee69f587a13fac2e552b1d322a8015", size = 247975122, upload-time = "2026-08-28T15:56:04.112Z" }, ] +[[package]] +name = "ty" +version = "0.0.81" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/b7/c9d736f48585f5a711ea47bb97a353d3771834f89481d747ea9687b74fa9/ty-0.0.81.tar.gz", hash = "sha256:ef721aa649bf41d665ba86e1ea726fd3feab6800e2c4887a062a704baf304ca8", size = 7225871, upload-time = "2026-09-15T02:05:29.536Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/70/649a5ec8fd6cc9dfa731c905523b19e677ad36dd9dcc4fcadfed8235507f/ty-0.0.81-py3-none-linux_armv6l.whl", hash = "sha256:8e9e3dd6edb1462633ddba2bf337d1c05023a5a13c2f315e445565c7ad43d247", size = 13679646, upload-time = "2026-09-15T02:04:51.436Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d0/3769b760918c3dde1bbaeee87422cd442fc550baadb5c8621e479b363282/ty-0.0.81-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:649c350873e4b3e937d856512ee57b8a345d80079cbead73a5308c2cdafe594c", size = 13211367, upload-time = "2026-09-15T02:04:54.18Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5f/b8fe3ab7eee87bbf1f5ff1b65c55526a3f31a9604630bc9e7d84199bbac5/ty-0.0.81-py3-none-macosx_11_0_arm64.whl", hash = "sha256:480c78706ba78239f901d68cf1ba53e98b84aa51e19b9241daa0fb62fdecd2e2", size = 13084312, upload-time = "2026-09-15T02:04:56.133Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d8/ce22278014f14ae8205d2f9622696d39c623a0c9e80056c258bb89bb8322/ty-0.0.81-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cdbfa4b634b1d21542eedc11e424dedcc4871c2128d0d46b55b0089d3220492", size = 13127965, upload-time = "2026-09-15T02:04:58.306Z" }, + { url = "https://files.pythonhosted.org/packages/82/d0/4e75c61f6a241a0804f25117af4f91a5aaa4a48408b57f8d4c34d8a3d91e/ty-0.0.81-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f7a34743a21669d4f3d5fda98c36189cb4d9dceee1415a8a42d7426d55995db", size = 13432523, upload-time = "2026-09-15T02:05:00.471Z" }, + { url = "https://files.pythonhosted.org/packages/0b/02/e0b6b783418515c8ba40c13d5e333da9e0576a7a69582fc2b5831bb7d913/ty-0.0.81-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:742eab849947bc10b4c09810d37bd743afe9502e114b7bcfd6389f0ae3f425aa", size = 14211082, upload-time = "2026-09-15T02:05:02.455Z" }, + { url = "https://files.pythonhosted.org/packages/6b/af/85cb68d13ff617f2b213212d674e1266b933f2d051b63a1e5116e8ca0bfc/ty-0.0.81-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b4cef395eee7007449aa3fc3fc8dc6bc58fe7a8956d56db078569061741bf52", size = 14692204, upload-time = "2026-09-15T02:05:04.705Z" }, + { url = "https://files.pythonhosted.org/packages/24/c6/791891f3fa8fec91e9ec3e8c9d6774ab903297defe847a3e5d7e047360ad/ty-0.0.81-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80cf8c34973a1da2ec2a043efcba8f4c0a984d45210957a8ddd2c16f2b8f8e85", size = 14371010, upload-time = "2026-09-15T02:05:06.63Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b2/c9fba5b4ed7f63f26e83432fa2062ca04606b61dce7bbacc1238232af649/ty-0.0.81-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f37a8f66c444eea29f17adfe0664f42062520773f18031fd4913f28eb56d2966", size = 13758537, upload-time = "2026-09-15T02:05:08.722Z" }, + { url = "https://files.pythonhosted.org/packages/77/e6/09c41e96bbcbe53dd18c9a1a6dbd2e456b0d03b94440a4bd9bab55a4e19a/ty-0.0.81-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e8134cc03e84a27f409877b9b62053fd4aaac9c3ce0ce0fdace8b5ed58d78e04", size = 14280789, upload-time = "2026-09-15T02:05:10.719Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d1/87eb8e01fb17602cde5911cbadb9b11141a04f728dbda0a2a00243f337ca/ty-0.0.81-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb6f269c5f14a07047e05cbf3d06dd9a64a048050f736afcf744a180a272f034", size = 13169638, upload-time = "2026-09-15T02:05:12.806Z" }, + { url = "https://files.pythonhosted.org/packages/94/3c/137ceb792858f408c48f20a066d12ee76108e5f28a13242720879c024172/ty-0.0.81-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:38355a5e6294ebd3cee616ed581d051963d61b293a1b3351c248747f73a45b74", size = 13449856, upload-time = "2026-09-15T02:05:15.505Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4a9b31fadfc80a2141f3862316eb31944843e01bc1f6533794291cce767e/ty-0.0.81-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59a35e128a9831423aee19db804aa8a1358ee3ce615b3fc0dec20d40bfd0d733", size = 13675303, upload-time = "2026-09-15T02:05:17.537Z" }, + { url = "https://files.pythonhosted.org/packages/e3/16/dbcd2c10ab80b015a227110a4ec75314468b6720d800261098521df4eee9/ty-0.0.81-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f0f4219fbc068ea3414fa804c6bd57ef4229a11884ab9ca71cd8308072520aad", size = 14050298, upload-time = "2026-09-15T02:05:20.424Z" }, + { url = "https://files.pythonhosted.org/packages/95/ba/a70ced0c0c078ff09a3a80224d045c4267d07811dbaf125c749af5f72606/ty-0.0.81-py3-none-win32.whl", hash = "sha256:82ff952e64ba4c4da1c0c89723e326c768ae4f9ff20624dee04a51c3f4e4b96c", size = 12866650, upload-time = "2026-09-15T02:05:22.824Z" }, + { url = "https://files.pythonhosted.org/packages/3f/51/4b79453df93dfb9edecb1bd6cb04e772277d8c7260693165358a033b6ed1/ty-0.0.81-py3-none-win_amd64.whl", hash = "sha256:ef82788744da0b7f2a598e4e9776f9c5998df4eef7e2750ae3a5537e7c734b69", size = 13572976, upload-time = "2026-09-15T02:05:24.907Z" }, + { url = "https://files.pythonhosted.org/packages/0a/56/09594d55a0c49543eeac149c0dbc80b9b04409cc26ca48f1c1a9595c0916/ty-0.0.81-py3-none-win_arm64.whl", hash = "sha256:bdb1563990c5d3abfe5a09e7512733f302773ede6a5d3e4e5c6ad46393d8df4d", size = 13364408, upload-time = "2026-09-15T02:05:27.247Z" }, +] + [[package]] name = "typer" version = "0.24.2"