diff --git a/api/python/default.nix b/api/python/default.nix new file mode 100644 index 0000000..80f771a --- /dev/null +++ b/api/python/default.nix @@ -0,0 +1,39 @@ +{ lib +, buildPythonPackage +, hatchling +, openemsh +, openems +, appcsxcad +}: + +buildPythonPackage { + pname = "python-${openemsh.pname}"; + inherit (openemsh) version; + + src = lib.nix-filter { + root = ../..; + include = [ + "api/python" + "CHANGELOG" + "README.md" + ]; + }; + + sourceRoot = "source/api/python"; + + pyproject = true; + + build-system = [ + hatchling + ]; + + dependencies = [ + openemsh + openems + appcsxcad + ]; + + meta = openemsh.meta // { + description = "Python API for OpenEMSH, OpenEMS mesher"; + }; +} diff --git a/api/python/openemsh.py b/api/python/openemsh.py new file mode 100644 index 0000000..c3c566b --- /dev/null +++ b/api/python/openemsh.py @@ -0,0 +1,51 @@ +import os +import shutil +import subprocess +from openEMS import openEMS + +def run_openemsh(fdtd: openEMS, csx_file: str, args: list[str] = ["-Gv"]): + """ + Invokes OpenEMSH mesher + + :param csx_file: CSX XML file path. + """ + fdtd.Write2XML(csx_file) + subprocess.run(["openemsh", "-i", csx_file] + args, capture_output=False, text=True, check=True, stderr=subprocess.STDOUT) + fdtd.ReadFromXML(csx_file) + +def run_appcsxcad(fdtd: openEMS, csx_file: str, edit: bool = False, render_disc_material: bool = False): + """ + Invokes AppCSXCAD + + :param csx_file: CSX XML file path. + :param edit: Edit mode, `--disableEdit`. + :param render_disc_material: Render discrete material, `--RenderDiscMaterial`. + """ + fdtd.Write2XML(csx_file) + command = ["AppCSXCAD"] + if render_disc_material: + command += ["--RenderDiscMaterial"] + if not edit: + command += ["--disableEdit"] + command += [csx_file] + subprocess.run( + command, + capture_output=False, + text=True, + check=False) + if edit: + fdtd.ReadFromXML(csx_file) + +def ensure_sim_path(sim_path: str, cleanup: bool = False): + """ + Ensure simulation path exists, optionaly cleanup old simulation files, if any. + Use this instead of regular openEMS.Run cleanup param, before writing CSX file. + + :param sim_path: Simulation path. + :param cleanup: Remove old result in simulation path. + """ + if cleanup and os.path.exists(sim_path): + shutil.rmtree(sim_path, ignore_errors=True) + os.mkdir(sim_path) + if not os.path.exists(sim_path): + os.mkdir(sim_path) diff --git a/api/python/pyproject.toml b/api/python/pyproject.toml new file mode 100644 index 0000000..e289567 --- /dev/null +++ b/api/python/pyproject.toml @@ -0,0 +1,38 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +dynamic = ["version"] +name = "openemsh" +description = "API for OpenEMSH, openEMS mesher" +dependencies = [] +license = "GPL-3.0-or-later" +readme = "../../README.md" +keywords = ["openems", "fdtd", "mesh"] +authors = [ + {name = "Thomas Lepoix", email = "thomas.lepoix@protonmail.ch"} +] +maintainers = [ + {name = "Thomas Lepoix", email = "thomas.lepoix@protonmail.ch"} +] + +[project.urls] +homepage = "https://github.com/Open-RFlab/openemsh" +source = "https://github.com/Open-RFlab/openemsh" +changelog = "https://github.com/Open-RFlab/openemsh/blob/main/CHANGELOG" +releasenotes = "https://github.com/Open-RFlab/openemsh/releases" +#documentation = "https://github.com/Open-RFlab/openemsh" +issues = "https://github.com/Open-RFlab/openemsh/issues" +funding = "https://liberapay.com/thomaslepoix" + +[project.scripts] +spam-cli = "spam:main_cli" + +[project.gui-scripts] +spam-gui = "spam:main_gui" + +[tool.hatch.version] +path = "../../CHANGELOG" +# TODO + in version are not fine +pattern = "^[^ ]+ \\((?P.*)\\)$" diff --git a/flake.nix b/flake.nix index 35ab78d..87def66 100644 --- a/flake.nix +++ b/flake.nix @@ -129,12 +129,20 @@ packages = [ nixGL.packages.${system}.nixGLDefault pkgs.openems + pkgs.openemsh pkgs.appcsxcad (pkgs.octaveFull.withPackages (p: [ - pkgs.octave-openems pkgs.octave-csxcad + pkgs.octave-openems pkgs.octave-openems-hll ])) + (pkgs.python3.withPackages (p: [ + p.numpy + p.matplotlib + pkgs.python-csxcad + pkgs.python-openems + pkgs.python-openemsh + ])) ]; }; @@ -219,12 +227,12 @@ }) {}; csxcad = (prev.csxcad.overrideAttrs (new: old: { - version = "0.6.3"; + version = "0116fc1"; src = prev.fetchFromGitHub { owner = "thliebig"; repo = "CSXCAD"; - rev = "v${new.version}"; - hash = "sha256-SSV5ulx3rCJg99I/oOQbqe+gOSs+BfcCo6UkWHVhnSs="; + rev = new.version; + hash = "sha256-/xd4GyHEzX9ZstrAPgHeo7xrp/srycfdMAuDq82D4z8="; }; prePatch = '' substituteInPlace CMakeLists.txt --replace-fail 'INSTALL(DIRECTORY matlab DESTINATION share/CSXCAD)' "" @@ -235,6 +243,13 @@ }; qcsxcad = (prev.qcsxcad.overrideAttrs (new: old: { + version = "63ac6f8"; + src = prev.fetchFromGitHub { + owner = "thliebig"; + repo = "QCSXCAD"; + rev = new.version; + hash = "sha256-kJZcuY2CSaaFAQiBemIqLrABUBpym047ISnmwg3kKXQ="; + }; nativeBuildInputs = old.nativeBuildInputs ++ [ prev.libsForQt5.wrapQtAppsHook ]; @@ -244,6 +259,14 @@ }; appcsxcad = (prev.appcsxcad.overrideAttrs (new: old: { + version = "731d2dc"; + src = prev.fetchFromGitHub { + owner = "thliebig"; + repo = "AppCSXCAD"; + rev = new.version; + hash = "sha256-RcXLgh+Czs3TdvsX+OtDMrqqcBN/QCCJu644Fwz3RLE="; + }; + nativeBuildInputs = old.nativeBuildInputs ++ [ prev.libsForQt5.wrapQtAppsHook ]; @@ -253,12 +276,12 @@ }; openems = (prev.openems.overrideAttrs (new: old: { - version = "0.0.36"; + version = "fc15273"; src = prev.fetchFromGitHub { owner = "thliebig"; repo = "openEMS"; - rev = "v${new.version}"; - hash = "sha256-wdH+Zw7G2ZigzBMX8p3GKdFVx/AhbTNL+P3w+YjI/dc="; + rev = new.version; + hash = "sha256-JNJU8KS/6qgifn41+ivosofb/IWKPsCGBj9tdehpZBQ="; }; postFixup = ""; # Avoid oct file building / setup.m patching. prePatch = '' @@ -268,6 +291,32 @@ })).override { stdenv = prev.fastStdenv; inherit (final) csxcad qcsxcad; + withQcsxcad = false; + }; + + python-csxcad = (prev.python3Packages.python-csxcad.overrideAttrs (new: old: { + CSXCAD_INSTALL_PATH_IGNORE = true; + })).override { + inherit (final) csxcad openems; + }; + + python-openems = (prev.python3Packages.python-openems.overrideAttrs (new: old: { + OPENEMS_INSTALL_PATH_IGNORE = true; + nativeBuildInputs = old.nativeBuildInputs ++ [ + prev.boost.dev + ]; + setupPyBuildFlags = old.setupPyBuildFlags ++ [ + "-I${prev.boost.dev}/include" + "-L${prev.boost}/lib" + "-R${prev.boost}/lib" + ]; + })).override { + inherit (final) csxcad openems python-csxcad; + }; + + python-openemsh = prev.python3Packages.callPackage ./api/python/default.nix { + inherit lib; + inherit (final) appcsxcad openemsh; }; octave-csxcad = prev.octavePackages.buildOctavePackage rec { diff --git a/src/ui/cli/logger.cpp b/src/ui/cli/logger.cpp index 1efaa4a..d4ba9db 100644 --- a/src/ui/cli/logger.cpp +++ b/src/ui/cli/logger.cpp @@ -52,6 +52,7 @@ string to_string(Logger::UserAction action) noexcept { case Logger::UserAction::CANCEL: return "n"; case Logger::UserAction::OK: return "y"; case Logger::UserAction::SAVE: return "s"; + case Logger::UserAction::CLOSE: return "c"; case Logger::UserAction::ABORT: return "a"; default: ::unreachable(); } @@ -71,6 +72,7 @@ Logger::UserAction from_string(string const& str) noexcept { if(str == "n") return Logger::UserAction::CANCEL; else if(str == "y") return Logger::UserAction::OK; else if(str == "s") return Logger::UserAction::SAVE; + else if(str == "c") return Logger::UserAction::CLOSE; else if(str == "a") return Logger::UserAction::ABORT; else return Logger::UserAction::NOTHING; } diff --git a/src/ui/qt/logger.cpp b/src/ui/qt/logger.cpp index ba15f8e..24e6f24 100644 --- a/src/ui/qt/logger.cpp +++ b/src/ui/qt/logger.cpp @@ -23,6 +23,7 @@ Logger::UserAction from_qt(int button) noexcept { case QMessageBox::Cancel: return Logger::UserAction::CANCEL; case QMessageBox::Ok: return Logger::UserAction::OK; case QMessageBox::Save: return Logger::UserAction::SAVE; + case QMessageBox::Close: return Logger::UserAction::CLOSE; case QMessageBox::Abort: return Logger::UserAction::ABORT; } } @@ -34,6 +35,7 @@ QMessageBox::StandardButton to_qt(Logger::UserAction action) noexcept { case Logger::UserAction::CANCEL: return QMessageBox::Cancel; case Logger::UserAction::OK: return QMessageBox::Ok; case Logger::UserAction::SAVE: return QMessageBox::Save; + case Logger::UserAction::CLOSE: return QMessageBox::Close; case Logger::UserAction::ABORT: return QMessageBox::Abort; default: ::unreachable(); } diff --git a/src/ui/qt/main_window.cpp b/src/ui/qt/main_window.cpp index 7fe2ff0..bdd3960 100644 --- a/src/ui/qt/main_window.cpp +++ b/src/ui/qt/main_window.cpp @@ -41,6 +41,7 @@ MainWindow::MainWindow(app::OpenEMSH& oemsh, QWidget* parent) , ui(std::make_unique()) , oemsh(oemsh) , dock_layout_order(false) +, is_unsaved(false) , csx_file(oemsh.get_params().input.empty() ? QString() : QString::fromStdString(oemsh.get_params().input.generic_string())) @@ -350,14 +351,15 @@ void MainWindow::save_csx_file() { QGuiApplication::setOverrideCursor(Qt::WaitCursor); if(auto res = oemsh.write() - ; res.has_value()) + ; res.has_value()) { + is_unsaved = false; log({ .level = Logger::Level::INFO, .message = std::format( "Saved file \"{}\"", csx_file.toStdString()) }); - else + } else { log({ .level = Logger::Level::ERROR, .user_actions = { Logger::UserAction::OK }, @@ -366,7 +368,7 @@ void MainWindow::save_csx_file() { csx_file.toStdString(), res.error()) }); - + } QGuiApplication::restoreOverrideCursor(); } @@ -479,6 +481,7 @@ void MainWindow::go_to_or_make_current_state() { go_to_current_state(); else make_current_state_view(); + is_unsaved = true; } //****************************************************************************** @@ -523,12 +526,14 @@ void MainWindow::make_current_state_view() { void MainWindow::run(app::Step from) { oemsh.run_from_step(from); make_current_state_view(); + is_unsaved = true; } //****************************************************************************** void MainWindow::run() { oemsh.run_all_steps(); make_current_state_view(); + is_unsaved = true; } //****************************************************************************** @@ -633,4 +638,23 @@ void MainWindow::keyPressEvent(QKeyEvent* event) { } } +//****************************************************************************** +void MainWindow::closeEvent(QCloseEvent* event) { + if(is_unsaved) { + auto res = log({ + .level = Logger::Level::QUESTION, + .user_actions = { Logger::UserAction::CANCEL, Logger::UserAction::SAVE, Logger::UserAction::CLOSE }, + .message = std::format( + "You are about closing a unsaved document, do you want to save it before?") + }); + if(res == Logger::UserAction::CANCEL) { + event->ignore(); + return; + } else if(res == Logger::UserAction::SAVE) { + on_a_file_save_triggered(); + } + } + event->accept(); +} + } // namespace ui::qt diff --git a/src/ui/qt/main_window.hpp b/src/ui/qt/main_window.hpp index 26ae48a..14bfa96 100644 --- a/src/ui/qt/main_window.hpp +++ b/src/ui/qt/main_window.hpp @@ -29,6 +29,7 @@ class MainWindow : public QMainWindow { app::OpenEMSH& oemsh; bool dock_layout_order; + bool is_unsaved; QString csx_file; void set_style(Style const& style); @@ -93,6 +94,7 @@ private slots: protected: void keyPressEvent(QKeyEvent* event) override; + void closeEvent(QCloseEvent* event) override; }; } // namespace ui::qt diff --git a/src/utils/logger.hpp b/src/utils/logger.hpp index 665134c..e529c3f 100644 --- a/src/utils/logger.hpp +++ b/src/utils/logger.hpp @@ -29,6 +29,7 @@ class Logger { CANCEL, OK, SAVE, + CLOSE, ABORT }; diff --git a/test/Simple_Patch_Antenna.py b/test/Simple_Patch_Antenna.py new file mode 100644 index 0000000..fb208f0 --- /dev/null +++ b/test/Simple_Patch_Antenna.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +""" + https://github.com/thliebig/openEMS/raw/refs/heads/master/python/Tutorials/Simple_Patch_Antenna.py + + Simple Patch Antenna Tutorial adapted for OpenEMSH + +""" + +### Import Libraries +import os, tempfile +from pylab import * + +from CSXCAD import ContinuousStructure +from openEMS import openEMS +from openEMS.physical_constants import * +from openemsh import * + +### General parameter setup +post_proc_only = False +use_oemsh = False +if len(sys.argv) > 1 and sys.argv[1] == "openemsh": + use_oemsh = True +print(f"Running in { "OpenEMSH" if use_oemsh else "legacy" } mode\n") +Sim_Path = os.path.join(tempfile.gettempdir(), 'Simp_Patch') +CSX_file = os.path.join(Sim_Path, 'simp_patch.xml') +ensure_sim_path(Sim_Path, cleanup=True) + +# patch width (resonant length) in x-direction +patch_width = 32 # +# patch length in y-direction +patch_length = 40 + +#substrate setup +substrate_epsR = 3.38 +substrate_kappa = 1e-3 * 2*pi*2.45e9 * EPS0*substrate_epsR +substrate_width = 60 +substrate_length = 60 +substrate_thickness = 1.524 +substrate_cells = 4 + +#setup feeding +feed_pos = -6 #feeding position in x-direction +feed_R = 50 #feed resistance + +# size of the simulation box +SimBox = np.array([200, 200, 150]) + +# setup FDTD parameter & excitation function +f0 = 2e9 # center frequency +fc = 1e9 # 20 dB corner frequency + +### FDTD setup +## * Limit the simulation to 30k timesteps +## * Define a reduced end criteria of -40dB +FDTD = openEMS(NrTS=30000, EndCriteria=1e-4) +FDTD2 = openEMS(NrTS=30000, EndCriteria=1e-4) +FDTD.SetGaussExcite( f0, fc ) +FDTD.SetBoundaryCond( ['MUR', 'MUR', 'MUR', 'MUR', 'MUR', 'MUR'] ) + + +CSX = ContinuousStructure() +FDTD.SetCSX(CSX) +mesh = CSX.GetGrid() +mesh.SetDeltaUnit(1e-3) +mesh_res = C0/(f0+fc)/1e-3/20 + +### Generate properties, primitives and mesh-grid +#initialize the mesh with the "air-box" dimensions +mesh.AddLine('x', [-SimBox[0]/2, SimBox[0]/2]) +mesh.AddLine('y', [-SimBox[1]/2, SimBox[1]/2]) +mesh.AddLine('z', [-SimBox[2]/3, SimBox[2]*2/3]) + +# create patch +patch = CSX.AddMetal( 'patch' ) # create a perfect electric conductor (PEC) +start = [-patch_width/2, -patch_length/2, substrate_thickness] +stop = [ patch_width/2 , patch_length/2, substrate_thickness] +patch.AddBox(priority=10, start=start, stop=stop) # add a box-primitive to the metal property 'patch' +if not use_oemsh: + FDTD.AddEdges2Grid(dirs='xy', properties=patch, metal_edge_res=mesh_res/2) + +# create substrate +substrate = CSX.AddMaterial( 'substrate', epsilon=substrate_epsR, kappa=substrate_kappa) +start = [-substrate_width/2, -substrate_length/2, 0] +stop = [ substrate_width/2, substrate_length/2, substrate_thickness] +substrate.AddBox( priority=0, start=start, stop=stop ) + +# add extra cells to discretize the substrate thickness +if not use_oemsh: + mesh.AddLine('z', linspace(0,substrate_thickness,substrate_cells+1)) + +# create ground (same size as substrate) +gnd = CSX.AddMetal( 'gnd' ) # create a perfect electric conductor (PEC) +start[2]=0 +stop[2] =0 +gnd.AddBox(start, stop, priority=10) +if not use_oemsh: + FDTD.AddEdges2Grid(dirs='xy', properties=gnd) + +# apply the excitation & resist as a current source +start = [feed_pos, 0, 0] +stop = [feed_pos, 0, substrate_thickness] +port = FDTD.AddLumpedPort(1, feed_R, start, stop, 'z', 1.0, priority=5, edges2grid='xy') + + +### Mesh generation +if use_oemsh: + run_openemsh(FDTD, CSX_file, ["-Gvf", "--integrate-old-mesh=true", "--dmax=5"]) +else: + mesh.SmoothMeshLines('all', mesh_res, 1.4) + +# Add the nf2ff recording box +nf2ff = FDTD.CreateNF2FFBox() + +### Run the simulation +run_appcsxcad(FDTD, CSX_file) +if not post_proc_only: + FDTD.Run(Sim_Path) + + +### Post-processing and plotting +f = np.linspace(max(1e9,f0-fc),f0+fc,401) +port.CalcPort(Sim_Path, f) +s11 = port.uf_ref/port.uf_inc +s11_dB = 20.0*np.log10(np.abs(s11)) +figure() +plot(f/1e9, s11_dB, 'k-', linewidth=2, label=r'$S_{11}$') +grid() +legend() +ylabel('S-Parameter (dB)') +xlabel('Frequency (GHz)') + +idx = np.where((s11_dB<-10) & (s11_dB==np.min(s11_dB)))[0] +if not len(idx)==1: + print('No resonance frequency found for far-field calulation') +else: + f_res = f[idx[0]] + theta = np.arange(-180.0, 180.0, 2.0) + phi = [0., 90.] + nf2ff_res = nf2ff.CalcNF2FF(Sim_Path, f_res, theta, phi, center=[0,0,1e-3]) + + figure() + E_norm = 20.0*np.log10(nf2ff_res.E_norm[0]/np.max(nf2ff_res.E_norm[0])) + 10.0*np.log10(nf2ff_res.Dmax[0]) + plot(theta, np.squeeze(E_norm[:,0]), 'k-', linewidth=2, label='xz-plane') + plot(theta, np.squeeze(E_norm[:,1]), 'r--', linewidth=2, label='yz-plane') + grid() + ylabel('Directivity (dBi)') + xlabel('Theta (deg)') + title('Frequency: {} GHz'.format(f_res/1e9)) + legend() + +Zin = port.uf_tot/port.if_tot +figure() +plot(f/1e9, np.real(Zin), 'k-', linewidth=2, label=r'$\Re\{Z_{in}\}$') +plot(f/1e9, np.imag(Zin), 'r--', linewidth=2, label=r'$\Im\{Z_{in}\}$') +grid() +legend() +ylabel('Zin (Ohm)') +xlabel('Frequency (GHz)') + +show()