diff --git a/.gitignore b/.gitignore index 889367d5..e1e1299a 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,5 @@ __pycache__ .vscode/settings.json .vscode/tasks.json /cmake-build-debug/ + +build-qucs-s-spar-viewer-Desktop-Debug/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 68b666df..eb554291 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,6 +66,7 @@ add_subdirectory( qucs-filter ) add_subdirectory( library ) add_subdirectory( qucs-transcalc ) add_subdirectory( qucs-powercombining ) +add_subdirectory( qucs-s-spar-viewer ) #add_subdirectory( examples ) if(EXISTS ${CMAKE_SOURCE_DIR}/qucsator_rf/CMakeLists.txt) add_subdirectory(qucsator_rf) diff --git a/qucs-s-spar-viewer/CMakeLists.txt b/qucs-s-spar-viewer/CMakeLists.txt new file mode 100644 index 00000000..311dbbc0 --- /dev/null +++ b/qucs-s-spar-viewer/CMakeLists.txt @@ -0,0 +1,168 @@ +PROJECT(qucs-s-spar-viewer CXX C) +CMAKE_MINIMUM_REQUIRED(VERSION 3.10) +cmake_policy(VERSION 3.10) + +SET(QUCS_NAME "qucs-s") + +# use top VERSION file +file (STRINGS ${PROJECT_SOURCE_DIR}/../VERSION QUCS_VERSION) +message(STATUS "Configuring ${PROJECT_NAME} (GUI): VERSION ${QUCS_VERSION}") + +set(PROJECT_VERSION "${QUCS_VERSION}") + +set(PROJECT_VENDOR "Qucs-S team. This program is licensed under the GNU GPL") +set(PROJECT_COPYRIGHT_YEAR "2024") +set(PROJECT_DOMAIN_FIRST "qucs") +set(PROJECT_DOMAIN_SECOND "org") + + +add_compile_definitions(HAVE_CONFIG_H) +SET(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# configure the header config.h +CONFIGURE_FILE ( + "${PROJECT_SOURCE_DIR}/../config.h.cmake" + "${PROJECT_BINARY_DIR}/config.h" +) + +INCLUDE_DIRECTORIES("${PROJECT_BINARY_DIR}") + +if(WITH_QT6) + set(QT_VERSION_MAJOR 6) +else() + set(QT_VERSION_MAJOR 5) +endif() +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Widgets Charts) +include_directories( + ${Qt${QT_VERSION_MAJOR}Core_INCLUDE_DIRS} + ${Qt${QT_VERSION_MAJOR}Gui_INCLUDE_DIRS} + ${Qt${QT_VERSION_MAJOR}Widgets_INCLUDE_DIRS} + ) + + +IF(QT_VERSION_MAJOR EQUAL 6) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +ELSE() +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +ENDIF() + +if (MSVC) + add_compile_options(/Zc:__cplusplus /permissive- /MP /Zc:preprocessor) +else() + # additional warnings + add_compile_options(-Wall -Wextra) +endif() +ADD_DEFINITIONS(${QT_DEFINITIONS}) + +#ADD_SUBDIRECTORY( bitmaps ) -> added as resources + +SET( spar_viewer_sources main.cpp qucs-s-spar-viewer.cpp) + +SET( spar_viewer_moc_headers qucs-s-spar-viewer.h) + +SET(RESOURCES qucs-s-spar-viewer.qrc) + +if(QT_VERSION_MAJOR EQUAL 6) +QT6_WRAP_CPP( spar_viewer_moc_sources ${spar_viewer_moc_headers}spar_viewerf ) +QT6_ADD_RESOURCES(RESOURCES_SRCS ${RESOURCES}) +else() +QT5_WRAP_CPP( spar_viewer_moc_sources ${spar_viewer_moc_headers} ) +QT5_ADD_RESOURCES(RESOURCES_SRCS ${RESOURCES}) +endif() + +IF(APPLE) + # set information on Info.plist file + SET(MACOSX_BUNDLE_INFO_STRING "${PROJECT_NAME} ${PROJECT_VERSION}") + SET(MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_NAME} ${PROJECT_VERSION}") + SET(MACOSX_BUNDLE_LONG_VERSION_STRING "${PROJECT_NAME} ${PROJECT_VERSION}") + SET(MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}") + SET(MACOSX_BUNDLE_COPYRIGHT "${PROJECT_COPYRIGHT_YEAR} ${PROJECT_VENDOR}") + SET(MACOSX_BUNDLE_GUI_IDENTIFIER "${PROJECT_DOMAIN_SECOND}.${PROJECT_DOMAIN_FIRST}") + SET(MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}") + SET(MACOSX_BUNDLE_ICON_FILE qucs-s-spar-viewer.icns) + + # set where in the bundle to put the icns file + SET_SOURCE_FILES_PROPERTIES(${CMAKE_CURRENT_SOURCE_DIR}/../qucs/bitmaps/qucs-s-spar-viewer.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + # include the icns file in the target + SET(spar_viewer_sources ${spar_viewer_sources} ${CMAKE_CURRENT_SOURCE_DIR}/../qucs/bitmaps/qucs-s-spar-viewer.icns) + +ENDIF(APPLE) + +ADD_EXECUTABLE( ${QUCS_NAME}spar-viewer MACOSX_BUNDLE WIN32 + ${spar_viewer_sources} + ${spar_viewer_moc_sources} + ${RESOURCES_SRCS} ) + +TARGET_LINK_LIBRARIES( ${QUCS_NAME}spar-viewer Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Charts ) +SET_TARGET_PROPERTIES(${QUCS_NAME}spar-viewer PROPERTIES POSITION_INDEPENDENT_CODE TRUE) +#INSTALL (TARGETS ${QUCS_NAME}spar-viewer DESTINATION bin) +# +# Prepare the installation +# +SET(plugin_dest_dir bin) +SET(qtconf_dest_dir bin) +SET(APPS "${CMAKE_INSTALL_PREFIX}/bin/${QUCS_NAME}spar-viewer") +IF(APPLE) + SET(plugin_dest_dir ${QUCS_NAME}spar-viewer.app/Contents/MacOS) + SET(qtconf_dest_dir ${QUCS_NAME}spar-viewer.app/Contents/Resources) + SET(APPS "${CMAKE_INSTALL_PREFIX}/bin/${QUCS_NAME}spar-viewer.app") +ENDIF(APPLE) + +IF(WIN32) + SET(APPS "${CMAKE_INSTALL_PREFIX}/bin/${QUCS_NAME}spar-viewer.exe") +ENDIF(WIN32) + +# +# Install the Qucs application, on Apple, the bundle is +# installed as on other platforms it'll go into the bin directory. +# +INSTALL(TARGETS ${QUCS_NAME}spar-viewer + BUNDLE DESTINATION bin COMPONENT Runtime + RUNTIME DESTINATION bin COMPONENT Runtime + ) + + +# +# Install needed Qt plugins by copying directories from the qt installation +# One can cull what gets copied by using 'REGEX "..." EXCLUDE' +# +IF(APPLE AND QT_PLUGINS_DIR) + INSTALL(DIRECTORY "${QT_PLUGINS_DIR}/imageformats" DESTINATION bin/${plugin_dest_dir}/plugins COMPONENT Runtime) +ENDIF() +# +# install a qt.conf file +# this inserts some cmake code into the install script to write the file +# +IF(APPLE) +INSTALL(CODE " + file(WRITE \"\${CMAKE_INSTALL_PREFIX}/bin/${qtconf_dest_dir}/qt.conf\" \"\") + " COMPONENT Runtime) +ENDIF() + +#-------------------------------------------------------------------------------- +# Use BundleUtilities to get all other dependencies for the application to work. +# It takes a bundle or executable along with possible plugins and inspects it +# for dependencies. If they are not system dependencies, they are copied. + +# directories to look for dependencies +IF(APPLE) + SET(DIRS ${QT_LIBRARY_DIRS}) +ENDIF() + +# Now the work of copying dependencies into the bundle/package +# The quotes are escaped and variables to use at install time have their $ escaped +# An alternative is the do a configure_file() on a script and use install(SCRIPT ...). +# Note that the image plugins depend on QtSvg and QtXml, and it got those copied +# over. +IF(APPLE) +INSTALL(CODE " + file(GLOB_RECURSE QTPLUGINS + \"\${CMAKE_INSTALL_PREFIX}/bin/${plugin_dest_dir}/plugins/*${CMAKE_SHARED_LIBRARY_SUFFIX}\") + include(BundleUtilities) + fixup_bundle(\"${APPS}\" \"\${QTPLUGINS}\" \"${DIRS}\") + " COMPONENT Runtime) +ENDIF() + + diff --git a/qucs-s-spar-viewer/bitmaps/CMakeLists.txt b/qucs-s-spar-viewer/bitmaps/CMakeLists.txt new file mode 100644 index 00000000..a0b4c05a --- /dev/null +++ b/qucs-s-spar-viewer/bitmaps/CMakeLists.txt @@ -0,0 +1,19 @@ + +SET(XPMS +) + +# toolbar images +SET(PNGS +att_pi.png +att_tee.png +att_bridge.png +) + +# application images +SET(ICONS +) + +INSTALL(FILES ${XPMS} DESTINATION share/qucs/bitmaps) +INSTALL(FILES ${PNGS} DESTINATION share/qucs/bitmaps) +INSTALL(FILES ${ICONS} DESTINATION share/qucs/bitmaps) + diff --git a/qucs-s-spar-viewer/bitmaps/big.qucs.xpm b/qucs-s-spar-viewer/bitmaps/big.qucs.xpm new file mode 100644 index 00000000..b69901cf --- /dev/null +++ b/qucs-s-spar-viewer/bitmaps/big.qucs.xpm @@ -0,0 +1,225 @@ +/* XPM */ +static char *big_qucs_xpm[] = { +"32 32 190 2", +" c None", +". c #BCA2BC", +"+ c #B4B2D4", +"@ c #BC96BC", +"# c #7C6E9C", +"$ c #ACB2CC", +"% c #9C869C", +"& c #BCA2CC", +"* c #CCC6E4", +"= c #140A1C", +"- c #140A14", +"; c #0C0A14", +"> c #5C5A64", +", c #ACB6C4", +"' c #CCC6D4", +") c #0C0614", +"! c #0C0A1C", +"~ c #1C121C", +"{ c #1C1224", +"] c #0C060C", +"^ c #1C0A1C", +"/ c #2C2234", +"( c #0C0A24", +"_ c #140A24", +": c #1C0E2C", +"< c #240E24", +"[ c #3C323C", +"} c #B49EC4", +"| c #AC9ECC", +"1 c #CCCEE4", +"2 c #84768C", +"3 c #140E24", +"4 c #241634", +"5 c #4C324C", +"6 c #341A3C", +"7 c #645E7C", +"8 c #8472AC", +"9 c #9486BC", +"0 c #D4D6EC", +"a c #4C4664", +"b c #443A64", +"c c #DCEAF4", +"d c #CCBADC", +"e c #1C1634", +"f c #342644", +"g c #7C628C", +"h c #6C567C", +"i c #4C3654", +"j c #2C1634", +"k c #1C0E24", +"l c #645A7C", +"m c #8C7AB4", +"n c #9C92C4", +"o c #D4DEF4", +"p c #ACAECC", +"q c #544A6C", +"r c #A4A2C4", +"s c #E4EEF4", +"t c #CCCAE4", +"u c #DCD2E4", +"v c #540E1C", +"w c #840E14", +"x c #440E1C", +"y c #24264C", +"z c #44365C", +"A c #ACAAC4", +"B c #C4C6DC", +"C c #8C7AAC", +"D c #5C426C", +"E c #3C2A44", +"F c #2C162C", +"G c #A49EB4", +"H c #9492AC", +"I c #8C82A4", +"J c #340E1C", +"K c #7C0E14", +"L c #14122C", +"M c #3C3A64", +"N c #3C3E74", +"O c #ACA6CC", +"P c #D4DEEC", +"Q c #BCB2DC", +"R c #846694", +"S c #5C3E64", +"T c #2C1E3C", +"U c #341E34", +"V c #C4C2CC", +"W c #ECEAEC", +"X c #2C2A4C", +"Y c #242254", +"Z c #1C1A4C", +"` c #7466A4", +" . c #BCBADC", +".. c #CCDEEC", +"+. c #745A84", +"@. c #24162C", +"#. c #3C2A3C", +"$. c #241E2C", +"%. c #1C1E4C", +"&. c #242654", +"*. c #142E64", +"=. c #1C326C", +"-. c #1C265C", +";. c #34366C", +">. c #8476AC", +",. c #CCD2EC", +"'. c #C4CAE4", +"). c #4C4254", +"!. c #44325C", +"~. c #4C2E54", +"{. c #141234", +"]. c #142654", +"^. c #144274", +"/. c #1C3E74", +"(. c #1C2254", +"_. c #1C1E54", +":. c #4C4A7C", +"<. c #A492C4", +"[. c #6C6A7C", +"}. c #64628C", +"|. c #847A8C", +"1. c #644A74", +"2. c #14224C", +"3. c #1C3674", +"4. c #14427C", +"5. c #143E7C", +"6. c #1C3A7C", +"7. c #2C2644", +"8. c #D4E2F4", +"9. c #B4AACC", +"0. c #8C7AA4", +"a. c #140E2C", +"b. c #141634", +"c. c #14366C", +"d. c #1C427C", +"e. c #1C3E7C", +"f. c #141E44", +"g. c #C4CEEC", +"h. c #DCE6EC", +"i. c #C4C6C4", +"j. c #6C5E74", +"k. c #445274", +"l. c #A4B6DC", +"m. c #2C528C", +"n. c #6472AC", +"o. c #14163C", +"p. c #4C4684", +"q. c #BCB6DC", +"r. c #D4CEDC", +"s. c #9C9EA4", +"t. c #B4C2DC", +"u. c #2C365C", +"v. c #5C6A94", +"w. c #8CA2CC", +"x. c #BCBEE4", +"y. c #8C86AC", +"z. c #1C2A64", +"A. c #74769C", +"B. c #CCCADC", +"C. c #C4CEE4", +"D. c #A49EBC", +"E. c #1C162C", +"F. c #8C8AB4", +"G. c #445A8C", +"H. c #A49EC4", +"I. c #B4BAD4", +"J. c #B4B2DC", +"K. c #CCCEEC", +"L. c #CCDAF4", +"M. c #BCC6E4", +"N. c #746A8C", +"O. c #949AB4", +"P. c #9C8EB4", +"Q. c #544E74", +"R. c #342E4C", +"S. c #242A54", +"T. c #6C7AA4", +"U. c #4C5E94", +"V. c #7486B4", +"W. c #9CAAD4", +"X. c #C4D2EC", +"Y. c #D4E2EC", +"Z. c #DCEEF4", +"`. c #D4DAEC", +" + c #C4C6E4", +".+ c #B4AED4", +"++ c #7C7AA4", +"@+ c #9496B4", +"#+ c #9492B4", +"$+ c #8C92AC", +" ", +" ", +" . + ", +" @ # $ ", +" % & * $ ", +" = - ; = = - > , ' ", +" = ; ; = ) = ! ) = ! ", +" ~ ; ) ) ; = ! = ! = { ", +" = ; ] ) ; ) ) = ! = = ^ / ", +" = ; ) ! ) = ! ( ! ! _ : < _ [ } | 1 2 ", +" 3 = ) ! ) ( ( ( ( _ 3 4 5 6 { = 7 8 9 0 a b 0 c d ", +" = ( = = ( = ! _ ! e f g h i j k l m n o p q r s t u ", +" 3 _ v w x ) _ ( _ y z A B C D E j F n r C G H I ", +" _ J w K < _ _ L M b N O P Q R S T E U V V W ", +" _ _ x ^ _ 3 X M Y Z X ` ...| +.@.T #. ", +" $.= _ _ L %.&.*.=.-.Z ;.>.,.'.).!.~. ", +" = ! = 3 {.].^././.*.(._.:.<.[.}.|.1. ", +" > = ! ( L 2.3.4.5.6.*._.Y 7.}.8.9. ", +" 0.@.M 3 = a.b.].c.d.e.3.f.a.&.8 g.h. ", +" i.j.k.l.b ; ( 3 f.*.4.m.n.o.Z %.p.q. ", +" r.s.t.l. = ! 3 u.v.w.x.y.*.z._.A. ", +" B.C.D.t. ! E.x.Q 9.+ F.6.G. ", +" H.I.I. ! J. .K...'. ", +"r.J.A L.M.D.N.O. ", +"P.9. Q.R.%.S.T. ", +" N U.V.W.X. ", +" M.X.Y.c c ", +" c Z.c `. + ", +" B .+n ++@+ ", +" #+$+p A ", +" ", +" "}; diff --git a/qucs-s-spar-viewer/bitmaps/trash.png b/qucs-s-spar-viewer/bitmaps/trash.png new file mode 100644 index 00000000..a613fbeb Binary files /dev/null and b/qucs-s-spar-viewer/bitmaps/trash.png differ diff --git a/qucs-s-spar-viewer/main.cpp b/qucs-s-spar-viewer/main.cpp new file mode 100644 index 00000000..4097c086 --- /dev/null +++ b/qucs-s-spar-viewer/main.cpp @@ -0,0 +1,129 @@ +/**************************************************************************** +** Qucs Attenuator Synthesis +** main.cpp +** +** +** +** +** +** +** +*****************************************************************************/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "qucs-s-spar-viewer.h" + + + +struct tQucsSettings QucsSettings; + +// ######################################################################### +// Loads the settings file and stores the settings. +bool loadSettings() +{ + QSettings settings("qucs","qucs_s"); + settings.beginGroup("QucsAttenuator"); + if(settings.contains("x"))QucsSettings.x=settings.value("x").toInt(); + if(settings.contains("y"))QucsSettings.y=settings.value("y").toInt(); + settings.endGroup(); + if(settings.contains("font"))QucsSettings.font.fromString(settings.value("font").toString()); + if(settings.contains("Language"))QucsSettings.Language=settings.value("Language").toString(); + + return true; +} + + +// ######################################################################### +// Saves the settings in the settings file. +bool saveApplSettings(Qucs_S_SPAR_Viewer *qucs) +{ + QSettings settings ("qucs","qucs_s"); + settings.beginGroup("QucsAttenuator"); + settings.setValue("x", qucs->x()); + settings.setValue("y", qucs->y()); + settings.endGroup(); + return true; + +} + + + +int main( int argc, char ** argv ) +{ + QApplication a( argc, argv ); + + // apply default settings + QucsSettings.x = 200; + QucsSettings.y = 100; + + // is application relocated? + char * var = getenv ("QUCSDIR"); + QDir QucsDir; + if (var != NULL) { + QucsDir = QDir (var); + QString QucsDirStr = QucsDir.canonicalPath (); + QucsSettings.LangDir = + QDir::toNativeSeparators (QucsDirStr + "/share/" QUCS_NAME "/lang/"); + } else { + QString QucsApplicationPath = QCoreApplication::applicationDirPath(); +#ifdef __APPLE__ + QucsDir = QDir(QucsApplicationPath.section("/bin",0,0)); +#else + QucsDir = QDir(QucsApplicationPath); + QucsDir.cdUp(); +#endif + QucsSettings.LangDir = QucsDir.canonicalPath() + "/share/qucs/lang/"; + } + + loadSettings(); + + + QTranslator tor( 0 ); + QString lang = QucsSettings.Language; + if(lang.isEmpty()) + lang = QString(QLocale::system().name()); + tor.load( QString("qucs_") + lang, QucsSettings.LangDir); + a.installTranslator( &tor ); + + Qucs_S_SPAR_Viewer *qucs = new Qucs_S_SPAR_Viewer(); + //a.setMainWidget(qucs); + qucs->raise(); + qucs->move(QucsSettings.x, QucsSettings.y); // position before "show" !!! + qucs->show(); + + QScreen* primaryScreen = QGuiApplication::screens().first(); + + qucs->resize(primaryScreen->availableGeometry().size() * 0.9); + qucs->setGeometry( + QStyle::alignedRect( + Qt::LeftToRight, + Qt::AlignCenter, + qucs->size(), + primaryScreen->availableGeometry() + ) + ); + + + + int result = a.exec(); + saveApplSettings(qucs); + return result; +} diff --git a/qucs-s-spar-viewer/qucs-s-spar-viewer.1 b/qucs-s-spar-viewer/qucs-s-spar-viewer.1 new file mode 100644 index 00000000..8d45892d --- /dev/null +++ b/qucs-s-spar-viewer/qucs-s-spar-viewer.1 @@ -0,0 +1,41 @@ +.TH QucsAttenuator "1" "July 2006" "Debian/GNU Linux" "User Commands" +.SH NAME +QucsAttenuator \- An attenuator synthesis application. +.SH SYNOPSIS +.B qucsattenuator +[\fIOPTION\fR] ... +.SH DESCRIPTION + +\fBQucs\fR is an integrated circuit simulator which means you will be +able to setup a circuit with a graphical user interface (GUI) and +simulate the large-signal, small-signal and noise behaviour of the +circuit. After that simulation has finished you will be able to +present the simulation results on a presentation page or window. + +The software aims to support all kinds of circuit simulation types, +e.g. DC, AC, S-parameter, harmonic balance analysis, noise analysis, +etc. + +\fBQucsAttenuator\fR is the attenuator synthesis tool used by Qucs. +By use of an input dialog the user can create an attenuator which is +then copied into the system-wide clipboard. In \fBQucs\fR the user +opens an empty schematic and presses CTRL-V (paste from +clipboard). The attenuator schematic is now inserted and can be +simulated. + +Available attenuator topologies types are: Tee, Pi and Bridged-Tee. + +.SH AVAILABILITY +The latest version of Qucs can always be obtained from +\fBwww.sourceforge.net\fR or \fBwww.freshmeat.net\fR +.SH "REPORTING BUGS" +Known bugs are documented within the BUGS file. Report bugs to +. +.SH COPYRIGHT +Copyright \(co 2006 Michael Margraf +.PP +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +.SH AUTHORS +Written by Toyoyuki Ishikawa and Michael +Margraf . diff --git a/qucs-s-spar-viewer/qucs-s-spar-viewer.cpp b/qucs-s-spar-viewer/qucs-s-spar-viewer.cpp new file mode 100644 index 00000000..75b6dacd --- /dev/null +++ b/qucs-s-spar-viewer/qucs-s-spar-viewer.cpp @@ -0,0 +1,2161 @@ +/**************************************************************************** +** Qucs Attenuator Synthesis +** qucsattenuator.cpp +** +** +** +** +** +** +** +*****************************************************************************/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "qucs-s-spar-viewer.h" + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +Qucs_S_SPAR_Viewer::Qucs_S_SPAR_Viewer() +{ + + QWidget *centralWidget = new QWidget(this); + setCentralWidget(centralWidget); + + setWindowIcon(QPixmap(":/bitmaps/big.qucs.xpm")); + setWindowTitle("Qucs S-parameter Viewer " PACKAGE_VERSION); + + QMenu *fileMenu = new QMenu(tr("&File")); + + QAction *fileQuit = new QAction(tr("&Quit"), this); + fileQuit->setShortcut(QKeySequence::Quit); + connect(fileQuit, SIGNAL(triggered(bool)), SLOT(slotQuit())); + + fileMenu->addAction(fileQuit); + + QMenu *helpMenu = new QMenu(tr("&Help")); + + QAction *helpHelp = new QAction(tr("&Help"), this); + helpHelp->setShortcut(Qt::Key_F1); + helpMenu->addAction(helpHelp); + connect(helpHelp, SIGNAL(triggered(bool)), SLOT(slotHelpIntro())); + + QAction *helpAbout = new QAction(tr("&About"), this); + helpMenu->addAction(helpAbout); + connect(helpAbout, SIGNAL(triggered(bool)), SLOT(slotHelpAbout())); + + + helpMenu->addSeparator(); + + QAction * helpAboutQt = new QAction(tr("About Qt..."), this); + helpMenu->addAction(helpAboutQt); + connect(helpAboutQt, SIGNAL(triggered(bool)), SLOT(slotHelpAboutQt())); + + menuBar()->addMenu(fileMenu); + menuBar()->addSeparator(); + menuBar()->addMenu(helpMenu); + + // Left panel + QScrollArea *scrollArea_Files = new QScrollArea(); + FileList_Widget = new QWidget(); + QWidget *FilesGroup = new QWidget(); + + FilesGrid = new QGridLayout(FileList_Widget); + + vLayout_Files = new QVBoxLayout(FilesGroup); + + QWidget *Buttons = new QWidget(); + QHBoxLayout *hLayout_Files_Buttons = new QHBoxLayout(Buttons); + + Button_Add_File = new QPushButton("Add file", this); + Button_Add_File->setStyleSheet("QPushButton {background-color: green;\ + border-style: outset;\ + border-width: 2px;\ + border-radius: 10px;\ + border-color: beige;\ + font: bold 14px;\ + color: white;\ + min-width: 10em;\ + padding: 6px;\ + }"); + connect(Button_Add_File, SIGNAL(clicked()), SLOT(addFile())); + + Delete_All_Files = new QPushButton("Delete all", this); + Delete_All_Files->setStyleSheet("QPushButton {background-color: red;\ + border-style: outset;\ + border-width: 2px;\ + border-radius: 10px;\ + border-color: beige;\ + font: bold 14px;\ + color: white;\ + min-width: 10em;\ + padding: 6px;\ + }"); + connect(Delete_All_Files, SIGNAL(clicked()), SLOT(removeAllFiles())); + + + hLayout_Files_Buttons->addWidget(Button_Add_File); + hLayout_Files_Buttons->addWidget(Delete_All_Files); + + scrollArea_Files->setWidget(FileList_Widget); + scrollArea_Files->setWidgetResizable(true); + vLayout_Files->addWidget(scrollArea_Files, Qt::AlignTop); + vLayout_Files->addWidget(Buttons, Qt::AlignBottom); + vLayout_Files->setStretch(0, 3); + vLayout_Files->setStretch(1, 1); + + // Chart settings + chart = new QChart; + chart->createDefaultAxes(); + QChartView *chartView = new QChartView(chart); + chartView->setRenderHint(QPainter::Antialiasing); + setCentralWidget(nullptr); + dockChart = new QDockWidget("Chart", this); + dockChart->setWidget(chartView); + dockChart->setAllowedAreas(Qt::AllDockWidgetAreas); + addDockWidget(Qt::LeftDockWidgetArea, dockChart); + + + + // These are two maximum markers to find the lowest and the highest frequency in the data samples. + // They are used to prevent the user from zooming out too much + f_min = 1e20; + f_max = -1; + y_min = 1e4; + y_max = -1e4; + + // Load default colors + default_colors.append(QColor(Qt::red)); + default_colors.append(QColor(Qt::blue)); + default_colors.append(QColor(Qt::darkGreen)); + + // Right panel + QWidget * SettingsGroup = new QWidget(); + QGridLayout * SettingsGrid = new QGridLayout(SettingsGroup); + SettingsGrid->setSpacing(5); + SettingsGrid->setColumnMinimumWidth(3, 20); + + // First row (min, max, div) + QLabel *axis_min = new QLabel("min"); + SettingsGrid->addWidget(axis_min, 0, 1, Qt::AlignCenter); + + QLabel *axis_max = new QLabel("max"); + SettingsGrid->addWidget(axis_max, 0, 2, Qt::AlignCenter); + + QLabel *axis_div = new QLabel("div"); + SettingsGrid->addWidget(axis_div, 0, 3, Qt::AlignCenter); + + // x-axis + QLabel *x_axis = new QLabel("x-axis"); + SettingsGrid->addWidget(x_axis, 1, 0); + + QSpinBox_x_axis_min = new QDoubleSpinBox(); + QSpinBox_x_axis_min->setMinimum(0.1); + QSpinBox_x_axis_min->setMaximum(1000000); + QSpinBox_x_axis_min->setValue(0); + QSpinBox_x_axis_min->setDecimals(1); + QSpinBox_x_axis_min->setSingleStep(0.1); + connect(QSpinBox_x_axis_min, SIGNAL(valueChanged(double)), SLOT(updatePlot())); + SettingsGrid->addWidget(QSpinBox_x_axis_min, 1, 1); + + QSpinBox_x_axis_max = new QDoubleSpinBox(); + QSpinBox_x_axis_max->setMinimum(0.1); + QSpinBox_x_axis_max->setMaximum(1000000); + QSpinBox_x_axis_max->setValue(1000); + QSpinBox_x_axis_max->setDecimals(1); + QSpinBox_x_axis_max->setSingleStep(0.1); + connect(QSpinBox_x_axis_max, SIGNAL(valueChanged(double)), SLOT(updatePlot())); + SettingsGrid->addWidget(QSpinBox_x_axis_max, 1, 2); + + // Available x-axis div + QComboBox_x_axis_div = new QComboBox(); + available_x_axis_div.clear(); + available_x_axis_div << 2000 << 1000 << 500 << 400 << 200 << 100 << 50 << 25 << 20 << 10 << 5 << 1 << 0.5 << 0.2 << 0.1; + + for (const double &value : available_x_axis_div) { + QComboBox_x_axis_div->addItem(QString::number(value)); + } + + connect(QComboBox_x_axis_div, SIGNAL(currentIndexChanged(int)), SLOT(updatePlot())); + SettingsGrid->addWidget(QComboBox_x_axis_div, 1, 3); + + QCombobox_x_axis_units = new QComboBox(); + QCombobox_x_axis_units->addItem("Hz"); + QCombobox_x_axis_units->addItem("kHz"); + QCombobox_x_axis_units->addItem("MHz"); + QCombobox_x_axis_units->addItem("GHz"); + QCombobox_x_axis_units->setCurrentIndex(2); + connect(QCombobox_x_axis_units, SIGNAL(currentIndexChanged(int)), SLOT(changeFreqUnits())); + SettingsGrid->addWidget(QCombobox_x_axis_units, 1, 4); + + // y-axis + QLabel *y_axis = new QLabel("y-axis"); + SettingsGrid->addWidget(y_axis, 2, 0); + + QSpinBox_y_axis_min = new QDoubleSpinBox(); + QSpinBox_y_axis_min->setMinimum(-150); + QSpinBox_y_axis_min->setValue(-50); + QSpinBox_y_axis_min->setDecimals(1); + connect(QSpinBox_y_axis_min, SIGNAL(valueChanged(double)), SLOT(updatePlot())); + SettingsGrid->addWidget(QSpinBox_y_axis_min, 2, 1); + + QSpinBox_y_axis_max = new QDoubleSpinBox(); + QSpinBox_y_axis_max->setMinimum(-150); + QSpinBox_y_axis_max->setValue(0); + QSpinBox_y_axis_max->setDecimals(1); + connect(QSpinBox_y_axis_max, SIGNAL(valueChanged(double)), SLOT(updatePlot())); + SettingsGrid->addWidget(QSpinBox_y_axis_max, 2, 2); + + // Available x-axis div + QComboBox_y_axis_div = new QComboBox(); + available_y_axis_div.clear(); + available_y_axis_div << 50 << 25 << 20 << 10 << 5 << 2 << 1 << 0.5 << 0.2 << 0.1; + for (const double &value : available_y_axis_div) { + QComboBox_y_axis_div->addItem(QString::number(value)); + } + connect(QComboBox_y_axis_div, SIGNAL(currentIndexChanged(int)), SLOT(updatePlot())); + SettingsGrid->addWidget(QComboBox_y_axis_div, 2, 3); + + /*QCombobox_y_axis_units = new QComboBox(); + QCombobox_y_axis_units->addItem("dB"); + SettingsGrid->addWidget(QCombobox_y_axis_units, 2, 4);*/ + + QLabel *y2_axis = new QLabel("y2-axis"); + SettingsGrid->addWidget(y2_axis, 3, 0); + + QSpinBox_y2_axis_min = new QDoubleSpinBox(); + QSpinBox_y2_axis_min->setMinimum(0); + SettingsGrid->addWidget(QSpinBox_y2_axis_min, 3, 1); + + QSpinBox_y2_axis_max = new QDoubleSpinBox(); + QSpinBox_y2_axis_max->setMinimum(0); + SettingsGrid->addWidget(QSpinBox_y2_axis_max, 3, 2); + + QSpinBox_y2_axis_div = new QDoubleSpinBox(); + QSpinBox_y2_axis_div->setMinimum(0); + SettingsGrid->addWidget(QSpinBox_y2_axis_div, 3, 3); + + /*QCombobox_y2_axis_units = new QComboBox(); + QCombobox_y2_axis_units->addItem("dB"); + SettingsGrid->addWidget(QCombobox_y2_axis_units, 3, 4);*/ + + // Hide y2 axis (temporary) + y2_axis->hide(); + QSpinBox_y2_axis_min->hide(); + QSpinBox_y2_axis_max->hide(); + QSpinBox_y2_axis_div->hide(); + // QCombobox_y2_axis_units->hide(); + + QWidget * TracesGroup = new QWidget(); + QVBoxLayout *Traces_VBox = new QVBoxLayout(TracesGroup); + + // Trace addition box + QWidget * TraceSelection_Widget = new QWidget(); // Add trace + + QGridLayout * DatasetsGrid = new QGridLayout(TraceSelection_Widget); + QLabel *dataset_label = new QLabel("Dataset"); + DatasetsGrid->addWidget(dataset_label, 0, 0, Qt::AlignCenter); + + QLabel *Traces_label = new QLabel("Traces"); + DatasetsGrid->addWidget(Traces_label, 0, 1, Qt::AlignCenter); + + QLabel *empty_label = new QLabel("Empty"); + DatasetsGrid->addWidget(empty_label, 0, 2, Qt::AlignCenter); + empty_label->hide(); + + QCombobox_datasets = new QComboBox(); + DatasetsGrid->addWidget(QCombobox_datasets, 1, 0); + connect(QCombobox_datasets, SIGNAL(currentIndexChanged(int)), SLOT(updateTracesCombo())); // Each time the dataset is changed it is needed to update the traces combo. + // This is needed when the user has data with different number of ports. + + + QCombobox_traces = new QComboBox(); + DatasetsGrid->addWidget(QCombobox_traces, 1, 1); + + Button_add_trace = new QPushButton("Add trace"); + Button_add_trace->setStyleSheet("QPushButton {background-color: green;\ + border-style: outset;\ + border-width: 2px;\ + border-radius: 10px;\ + border-color: beige;\ + font: bold 14px;\ + color: white;\ + min-width: 10em;\ + padding: 6px;\ + }"); + connect(Button_add_trace, SIGNAL(clicked()), SLOT(addTrace())); // Connect button with the handler + + DatasetsGrid->addWidget(Button_add_trace, 1, 2); + + // Trace management + // Titles + TracesList_Widget = new QWidget(); // Panel with the trace settings + QLabel * Label_Name = new QLabel("Name"); + QLabel * Label_Color = new QLabel("Color"); + QLabel * Label_LineStyle = new QLabel("Line Style"); + QLabel * Label_LineWidth = new QLabel("Width"); + QLabel * Label_Remove = new QLabel("Remove"); + + TracesGrid = new QGridLayout(TracesList_Widget); + TracesGrid->addWidget(Label_Name, 0, 0, Qt::AlignCenter); + TracesGrid->addWidget(Label_Color, 0, 1, Qt::AlignCenter); + TracesGrid->addWidget(Label_LineStyle, 0, 2, Qt::AlignCenter); + TracesGrid->addWidget(Label_LineWidth, 0, 3, Qt::AlignCenter); + TracesGrid->addWidget(Label_Remove, 0, 4, Qt::AlignCenter); + + QScrollArea *scrollArea_Traces = new QScrollArea(); + scrollArea_Traces->setWidget(TracesList_Widget); + scrollArea_Traces->setWidgetResizable(true); + + Traces_VBox->addWidget(TraceSelection_Widget); + Traces_VBox->addWidget(scrollArea_Traces); + + // Markers dock + QWidget * MarkersGroup = new QWidget(); + QVBoxLayout *Markers_VBox = new QVBoxLayout(MarkersGroup); + + // Trace addition box + QWidget * MarkerSelection_Widget = new QWidget(); // Add trace + + MarkersGrid = new QGridLayout(MarkerSelection_Widget); + QLabel *Frequency_Marker_Label = new QLabel("Frequency"); + MarkersGrid->addWidget(Frequency_Marker_Label, 0, 0, Qt::AlignCenter); + + + Button_add_marker = new QPushButton("Add marker"); + Button_add_marker->setStyleSheet("QPushButton {background-color: green;\ + border-style: outset;\ + border-width: 2px;\ + border-radius: 10px;\ + border-color: beige;\ + font: bold 14px;\ + color: white;\ + min-width: 10em;\ + padding: 6px;\ + }"); + connect(Button_add_marker, SIGNAL(clicked()), SLOT(addMarker())); // Connect button with the handler + MarkersGrid->addWidget(Button_add_marker, 0, 0); + + Button_Remove_All_Markers = new QPushButton("Remove all"); + Button_Remove_All_Markers->setStyleSheet("QPushButton {background-color: red;\ + border-style: outset;\ + border-width: 2px;\ + border-radius: 10px;\ + border-color: beige;\ + font: bold 14px;\ + color: white;\ + min-width: 10em;\ + padding: 6px;\ + }"); + connect(Button_Remove_All_Markers, SIGNAL(clicked()), SLOT(removeAllMarkers())); // Connect button with the handler + MarkersGrid->addWidget(Button_Remove_All_Markers, 0, 1); + + // Marker management + QWidget * MarkerList_Widget = new QWidget(); // Panel with the trace settings + + QLabel * Label_Marker = new QLabel("Marker"); + QLabel * Label_Freq_Marker = new QLabel("Frequency"); + QLabel * Label_Freq_Scale_Marker = new QLabel("Units"); + QLabel * Label_Remove_Marker = new QLabel("Remove"); + + MarkersGrid = new QGridLayout(MarkerList_Widget); + MarkersGrid->addWidget(Label_Marker, 0, 0, Qt::AlignCenter); + MarkersGrid->addWidget(Label_Freq_Marker, 0, 1, Qt::AlignCenter); + MarkersGrid->addWidget(Label_Freq_Scale_Marker, 0, 2, Qt::AlignCenter); + MarkersGrid->addWidget(Label_Remove_Marker, 0, 3, Qt::AlignCenter); + + + QScrollArea *scrollArea_Marker = new QScrollArea(); + scrollArea_Marker->setWidget(MarkerList_Widget); + scrollArea_Marker->setWidgetResizable(true); + + tableMarkers = new QTableWidget(1, 1, this); + + Markers_VBox->addWidget(MarkerSelection_Widget); + Markers_VBox->addWidget(scrollArea_Marker); + Markers_VBox->addWidget(tableMarkers); + + dockFiles = new QDockWidget("S-parameter files", this); + dockAxisSettings = new QDockWidget("Axis Settings", this); + dockTracesList = new QDockWidget("Traces List", this); + dockMarkers = new QDockWidget("Markers", this); + + // Disable dock closing + dockChart->setFeatures(dockChart->features() & ~QDockWidget::DockWidgetClosable); + dockFiles->setFeatures(dockFiles->features() & ~QDockWidget::DockWidgetClosable); + dockAxisSettings->setFeatures(dockAxisSettings->features() & ~QDockWidget::DockWidgetClosable); + dockTracesList->setFeatures(dockTracesList->features() & ~QDockWidget::DockWidgetClosable); + dockMarkers->setFeatures(dockMarkers->features() & ~QDockWidget::DockWidgetClosable); + + dockAxisSettings->setWidget(SettingsGroup); + dockTracesList->setWidget(TracesGroup); + dockFiles->setWidget(FilesGroup); + dockMarkers->setWidget(MarkersGroup); + + addDockWidget(Qt::RightDockWidgetArea, dockAxisSettings); + addDockWidget(Qt::RightDockWidgetArea, dockTracesList); + addDockWidget(Qt::RightDockWidgetArea, dockFiles); + addDockWidget(Qt::RightDockWidgetArea, dockMarkers); + + splitDockWidget(dockTracesList, dockAxisSettings, Qt::Vertical); + tabifyDockWidget(dockFiles, dockTracesList); + tabifyDockWidget(dockTracesList, dockMarkers); + dockFiles->raise(); + setDockNestingEnabled(true); + + // Set the height of the axis settings widget to its minimum. This makes the layout much clearer + int minHeight = dockAxisSettings->minimumSizeHint().height(); + dockAxisSettings->setFixedHeight(minHeight); + + setAcceptDrops(true);//Enable drag and drop feature to open files +} + +Qucs_S_SPAR_Viewer::~Qucs_S_SPAR_Viewer() +{ +} + +void Qucs_S_SPAR_Viewer::slotHelpIntro() +{ + QMessageBox::about(this, tr("Qucs-S S-parameter Help"), + tr("This is a simple viewer for S-parameter data.\n" + "It can show several .snp files at a time in the " + "same diagram. Trace markers can also be added " + "so that the user can read the trace value at " + "at an specific frequency.")); +} + +void Qucs_S_SPAR_Viewer::slotHelpAboutQt() +{ + QMessageBox::aboutQt(this, tr("About Qt")); +} + +void Qucs_S_SPAR_Viewer::slotHelpAbout() +{ + QMessageBox::about(this, tr("About..."), + "Qucs-S S-parameter Viewer Version " PACKAGE_VERSION+ + tr("\nCopyright (C) 2024 by")+" Andrés Martínez Mera" + "\n" + "\nThis is free software; see the source for copying conditions." + "\nThere is NO warranty; not even for MERCHANTABILITY or " + "\nFITNESS FOR A PARTICULAR PURPOSE.\n\n"); +} + +void Qucs_S_SPAR_Viewer::slotQuit() +{ + int tmp; + tmp = x(); + tmp = y(); + tmp = width(); + tmp = height(); + Q_UNUSED(tmp); + + qApp->quit(); +} + + +void Qucs_S_SPAR_Viewer::addFile() +{ + QFileDialog dialog(this, QString("Select S-parameter data files (.snp)"), QDir::homePath(), + tr("S-Parameter Files (*.s1p *.s2p *.s3p *.s4p);;All Files (*.*)")); + dialog.setFileMode(QFileDialog::ExistingFiles); + + QStringList fileNames; + if (dialog.exec()) + fileNames = dialog.selectedFiles(); + + addFiles(fileNames); +} + +void Qucs_S_SPAR_Viewer::addFiles(QStringList fileNames) +{ + int existing_files = this->datasets.size(); // Get the number of entries in the map + + // Variables for reading the Touchstone data + QString line; + QStringList values; + QString filename; + + if (existing_files == 0){ + // Reset limits + this->f_max = -1; + this->f_min = 1e30; + this->y_min = 1e30; + this->y_max = -1e30; + } + + for (int i = existing_files; i < existing_files+fileNames.length(); i++) + { + // Create the file name label + filename = QFileInfo(fileNames.at(i-existing_files)).fileName(); + QLabel * Filename_Label = new QLabel(filename.left(filename.indexOf('.'))); + Filename_Label->setObjectName(QString("File_") + QString::number(i)); + List_FileNames.append(Filename_Label); + this->FilesGrid->addWidget(List_FileNames.last(), i,0,1,1); + + // Create the "Remove" button + QToolButton * RemoveButton = new QToolButton(); + RemoveButton->setObjectName(QString("Remove_") + QString::number(i)); + QIcon icon(":/bitmaps/trash.png"); // Use a resource path or a relative path + RemoveButton->setIcon(icon); + + RemoveButton->setStyleSheet("QToolButton {background-color: red;\ + border-width: 2px;\ + border-radius: 10px;\ + border-color: beige;\ + font: bold 14px;\ + }"); + + List_RemoveButton.append(RemoveButton); + this->FilesGrid->addWidget(List_RemoveButton.last(), i,1,1,1); + + + connect(RemoveButton, SIGNAL(clicked()), SLOT(removeFile())); // Connect button with the handler to remove the entry. + + // Read the Touchstone file. + // Please see https://ibis.org/touchstone_ver2.0/touchstone_ver2_0.pdf + QList frequency; + QMap> file_data; // Data structure to store the file data + QString frequency_unit, parameter, format; + double freq_scale = 1; // Hz + double Z0; + + // Get the number of ports + QString suffix = QFileInfo(filename).suffix(); + QRegularExpression regex("(?i)[sp]"); + QStringList numberParts = suffix.split(regex); + int number_of_ports = numberParts[1].toInt(); + file_data["n_ports"].append(number_of_ports); + + // 1) Open the file + QFile file(fileNames.at(i-existing_files)); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qDebug() << "Cannot open the file"; + } + + // 2) Read data + QTextStream in(&file); + while (!in.atEnd()) { + QString line = in.readLine(); + line = line.simplified(); + //qDebug() << line; + + if (line.isEmpty()) continue; + if ((line.at(0).isNumber() == false) && (line.at(0) != "#")) { + if (file_data["frequency"].size() == 0){ + // There's still no data + continue; + }else{ + //There's already data, so it's very likely that the S-par data has ended and + //the following lines contain noise data. We must stop at this point. + break; + } + + } + // Check for the option line + if (line.at(0) == "#"){ + + QStringList info = line.split(" "); + frequency_unit = info.at(1); // Specifies the unit of frequency. + // Legal values are Hz, kHz, MHz, and GHz. The default value is GHz. + + frequency_unit = frequency_unit.toLower(); + + if (frequency_unit == "khz"){ + freq_scale = 1e3; + } else { + if (frequency_unit == "mhz"){ + freq_scale = 1e6; + } else { + if (frequency_unit == "ghz"){ + freq_scale = 1e9; + } + } + } + + + parameter = info.at(2); // specifies what kind of network parameter data is contained in the file. Legal + // values are: + // S for Scattering parameters, + // Y for Admittance parameters, + // Z for Impedance parameters, + // H for Hybrid-h parameters, + // G for Hybrid-g parameters. + // The default value is S. + + format = info.at(3); // Specifies the format of the network parameter data pairs. Legal values are: + // DB for decibel-angle (decibel = 20 × log 10|magnitude|) + // MA for magnitude-angle, + // RI for real-imaginary. + // Angles are given in degrees. Note that this format does not apply to noise + // parameters (refer to the “Noise Parameter Data” section later in this + // specification). The default value is MA. + + Z0 = info.at(5).toDouble(); + file_data["Rn"].append(Z0); // Specifies the reference resistance in ohms, where n is a real, positive number of + // ohms. The default reference resistance is 50 ohms. Note that this is overridden + // by the [Reference] keyword, described below, for files of [Version] 2.0 and above + + continue; + } + + // Split line by whitespace + values.clear(); + values = line.split(' '); + + file_data["frequency"].append(values[0].toDouble()*freq_scale); // in Hz + + double S_1, S_2, S_3, S_4; + QString s1, s2, s3, s4; + int index = 1, data_counter = 0; + + for (int i = 1; i<=number_of_ports; i++){ + for (int j = 1; j<=number_of_ports; j++){ + s1 = QString("S") + QString::number(j) + QString::number(i) + QString("_dB"); + s2 = s1.mid(0, s1.length() - 2).append("ang"); + s3 = s1.mid(0, s1.length() - 2).append("re"); + s4 = s1.mid(0, s1.length() - 2).append("im"); + + S_1 = values[index].toDouble(); + S_2 = values[index+1].toDouble(); + + convert_MA_RI_to_dB(&S_1, &S_2, &S_3, &S_4, format); + + file_data[s1].append(S_1);//dB + file_data[s2].append(S_2);//ang + file_data[s3].append(S_3);//re + file_data[s4].append(S_4);//im + index += 2; + data_counter++; + + // Check if the next values are in the new line + if ((index >= values.length()) && (data_counter < number_of_ports*number_of_ports)){ + line = in.readLine(); + line = line.simplified(); + values = line.split(' '); + index = 0; // Reset index (it's a new line) + } + } + } + if (number_of_ports == 1){ + double s11_re = file_data["S11_re"].last(); + double s11_im = file_data["S11_im"].last(); + std::complex s11 (s11_re, s11_im); + + // Calculate Zin and Zout + std::complex Zin = Z0 * (1.0 + s11) / (1.0 - s11); + + file_data["Re{Zin}"].append(Zin.real()); // Re{Zin} + file_data["Im{Zin}"].append(Zin.imag()); // Im{Zin} + + } + if (number_of_ports == 2){ + // Compute delta, K, mus, mup, MAG and MSG + double s11_re = file_data["S11_re"].last(); + double s11_im = file_data["S11_im"].last(); + double s12_re = file_data["S12_re"].last(); + double s12_im = file_data["S12_im"].last(); + double s21_re = file_data["S21_re"].last(); + double s21_im = file_data["S21_im"].last(); + double s22_re = file_data["S22_re"].last(); + double s22_im = file_data["S22_im"].last(); + + std::complex s11 (s11_re, s11_im); + std::complex s11_conj (s11_re, -s11_im); + std::complex s12 (s12_re, s12_im); + std::complex s21 (s21_re, s21_im); + std::complex s22 (s22_re, s22_im); + std::complex s22_conj (s22_re, -s22_im); + + double delta = abs(s11*s22 - s12*s21); // Determinant of the S matrix + double K = (1 - abs(s11)*abs(s11) - abs(s22)*abs(s22) + delta*delta) / (2*abs(s12*s21)); // Rollet factor. + double mu = (1 - abs(s11)*abs(s11)) / (abs(s22-delta*s11_conj) + abs(s12*s21)); + double mu_p = (1 - abs(s22)*abs(s22)) / (abs(s11-delta*s22_conj) + abs(s12*s21)); + double MSG = abs(s21) / abs(s12); + double MAG = MSG * (K - std::sqrt(K * K - 1)); + + // Calculate Zin and Zout + std::complex Zin = Z0 * (1.0 + s11) / (1.0 - s11); + std::complex Zout = Z0 * (1.0 + s22) / (1.0 - s22); + + // Convert MSG and MAG to dB scale + MSG = 10*log10(MSG); + MAG = 10*log10(abs(MAG)); + + file_data["delta"].append(delta); //delta + file_data["K"].append(delta); //K + file_data["mu"].append(mu); //mu + file_data["mu_p"].append(mu_p); //mu_p + file_data["MSG"].append(MSG); //MSG + file_data["MAG"].append(MAG); //MAG + file_data["Re{Zin}"].append(Zin.real()); // Re{Zin} + file_data["Im{Zin}"].append(Zin.imag()); // Im{Zin} + file_data["Re{Zout}"].append(Zout.real()); // Re{Zout} + file_data["Im{Zout}"].append(Zout.imag()); // Im{Zout} + } + } + // 3) Add data to the dataset + filename = filename.left(filename.indexOf('.')); // Remove file extension + datasets[filename] = file_data; + file.close(); + + // 4) Add new dataset to the trace selection combobox + QCombobox_datasets->addItem(filename); + QString current_dataset = QCombobox_datasets->currentText(); + // Update traces + updateTracesCombo(); + } + + // Default behavior: If there's no more data loaded and a single S1P file is selected, then automatically plot S11 + if ((fileNames.length() == 1) && (fileNames.first().toLower().endsWith(".s1p")) && (datasets.size() == 1)){ + this->addTrace(filename, QString("S11"), Qt::red); + + adjust_x_axis_to_file(filename); + adjust_y_axis_to_trace(filename, "S11"); + } + + // Default behavior: If there's no more data loaded and a single S2P file is selected, then automatically plot S21, S11 and S22 + if ((fileNames.length() == 1) && (fileNames.first().toLower().endsWith(".s2p")) && (datasets.size() == 1)){ + this->addTrace(filename, QString("S21"), Qt::red); + this->addTrace(filename, QString("S11"), Qt::blue); + this->addTrace(filename, QString("S22"), Qt::darkGreen); + + adjust_x_axis_to_file(filename); + adjust_y_axis_to_trace(filename, "S11"); + adjust_y_axis_to_trace(filename, "S21"); + } + + // Default behaviour: When adding multiple S2P file, then show the S21 of all traces + if (fileNames.length() > 1){ + bool all_s2p = true; + for (int i = 0; i < fileNames.length(); i++){ + if (!fileNames.at(i).toLower().endsWith(".s2p")){ + all_s2p = false; + break; + } + } + if (all_s2p == true){ + QString filename; + for (int i = 0; i < fileNames.length(); i++){ + filename = QFileInfo(fileNames.at(i)).fileName(); + filename = filename.left(filename.indexOf('.')); + // Pick a random color + QColor trace_color = QColor(QRandomGenerator::global()->bounded(256), QRandomGenerator::global()->bounded(256), QRandomGenerator::global()->bounded(256)); + this->addTrace(filename, QString("S21"), trace_color); + adjust_y_axis_to_trace(filename, "S21"); + } + // Update the frequency setting to fit the last s2p file + adjust_x_axis_to_file(filename); + + } + } + + // Show the trace settings widget + dockTracesList->raise(); +} + +// This function is called whenever a s-par file is intended to be removed from the map of datasets +void Qucs_S_SPAR_Viewer::removeFile() +{ + QString ID = qobject_cast(sender())->objectName(); + //qDebug() << "Clicked button:" << ID; + + //Find the index of the button to remove + int index_to_delete = -1; + for (int i = 0; i < List_RemoveButton.size(); i++) { + if (List_RemoveButton.at(i)->objectName() == ID) { + index_to_delete = i; + break; + } + } + + removeFile(index_to_delete); +} + + +void Qucs_S_SPAR_Viewer::removeFile(int index_to_delete) +{ + + + // Delete the label + QLabel* labelToRemove = List_FileNames.at(index_to_delete); + QString dataset_to_remove = labelToRemove->text(); + FilesGrid->removeWidget(labelToRemove); + List_FileNames.removeAt(index_to_delete); + delete labelToRemove; + + // Delete the button + QToolButton* ButtonToRemove = List_RemoveButton.at(index_to_delete); + FilesGrid->removeWidget(ButtonToRemove); + List_RemoveButton.removeAt(index_to_delete); + delete ButtonToRemove; + + // Look for the widgets associated to the trace and remove them + QList indices_to_remove; + for (int i = 0; i < List_TraceNames.size(); i++){ + QString trace_name = List_TraceNames.at(i)->text(); + QStringList parts = trace_name.split(".");//Trace name = dataset + trace + QString dataset_trace = parts[0]; + if (dataset_trace == dataset_to_remove ){ + QString Label_Object_Name = List_TraceNames.at(i)->objectName(); + + //Find the index of the button to remove + int index_to_delete = -1; + for (int j = 0; j < List_TraceNames.size(); j++) { + if (List_TraceNames.at(j)->objectName() == Label_Object_Name) { + index_to_delete = j; + break; + } + } + indices_to_remove.append(index_to_delete); + } + } + + // Once the list of widgets to remove is known, then remove them on a row + std::sort(indices_to_remove.begin(), indices_to_remove.end(), std::greater()); // Sort the items to avoid segfault when removing the widgets + removeTrace(indices_to_remove); + + // Delete the map entry + datasets.remove(dataset_to_remove); + + // Rebuild the dataset combobox based on the available datasets. + QStringList new_dataset_entries = datasets.keys(); + + disconnect(QCombobox_datasets, SIGNAL(currentIndexChanged(int)), this, SLOT(updateTracesCombo())); // Needed to avoid segfault + QCombobox_datasets->clear(); + QCombobox_datasets->addItems(new_dataset_entries); + connect(QCombobox_datasets, SIGNAL(currentIndexChanged(int)), SLOT(updateTracesCombo())); // Connect the signal again + + // Update the combobox for trace selection + updateTracesCombo(); + + // Now it is needed to readjust the widgets in the grid layout + // Move up all widgets below the removed row + for (int r = index_to_delete+1; r < FilesGrid->rowCount(); r++) { + for (int c = 0; c < FilesGrid->columnCount(); c++) { + QLayoutItem* item = FilesGrid->itemAtPosition(r, c); + if (item) { + int oldRow, oldCol, rowSpan, colSpan; + FilesGrid->getItemPosition(FilesGrid->indexOf(item), &oldRow, &oldCol, &rowSpan, &colSpan); + FilesGrid->removeItem(item); + FilesGrid->addItem(item, oldRow - 1, oldCol, rowSpan, colSpan); + } + } + } + + // Check if there are more files. If not, remove markers + if (datasets.keys().size() == 0) + { + removeAllMarkers(); + } +} + +void Qucs_S_SPAR_Viewer::removeAllFiles() +{ + int n_files = List_RemoveButton.size(); + for (int i = 0; i < n_files; i++) { + removeFile(n_files-i-1); + } +} + + +// This function is called when the user wants to remove a trace from the plot +void Qucs_S_SPAR_Viewer::removeTrace() +{ + QString ID = qobject_cast(sender())->objectName(); + //qDebug() << "Clicked button:" << ID; + + //Find the index of the button to remove + int index_to_delete = -1; + for (int i = 0; i < List_Button_DeleteTrace.size(); i++) { + if (List_Button_DeleteTrace.at(i)->objectName() == ID) { + index_to_delete = i; + break; + } + } + removeTrace(index_to_delete); +} + +// This function is called when the user wants to remove a trace from the plot +void Qucs_S_SPAR_Viewer::removeTrace(QList indices_to_delete) +{ + if (indices_to_delete.isEmpty()) + return; + + for (int i = 0; i < indices_to_delete.size(); i++) + removeTrace(indices_to_delete.at(i)); +} + +void Qucs_S_SPAR_Viewer::removeTrace(int index_to_delete) +{ + // Delete the label + QLabel* labelToRemove = List_TraceNames.at(index_to_delete); + QString trace_name = labelToRemove->text(); + TracesGrid->removeWidget(labelToRemove); + List_TraceNames.removeAt(index_to_delete); + delete labelToRemove; + + // Delete the color button + QPushButton* ColorButtonToRemove = List_Trace_Color.at(index_to_delete); + TracesGrid->removeWidget(ColorButtonToRemove); + List_Trace_Color.removeAt(index_to_delete); + delete ColorButtonToRemove; + + // Delete the linestyle combo + QComboBox* ComboToRemove = List_Trace_LineStyle.at(index_to_delete); + TracesGrid->removeWidget(ComboToRemove); + List_Trace_LineStyle.removeAt(index_to_delete); + delete ComboToRemove; + + // Delete the width spinbox + QSpinBox * SpinToRemove = List_TraceWidth.at(index_to_delete); + TracesGrid->removeWidget(SpinToRemove); + List_TraceWidth.removeAt(index_to_delete); + delete SpinToRemove; + + // Delete the "delete" button + QToolButton* ButtonToRemove = List_Button_DeleteTrace.at(index_to_delete); + TracesGrid->removeWidget(ButtonToRemove); + List_Button_DeleteTrace.removeAt(index_to_delete); + delete ButtonToRemove; + + // Remove the trace from the QMap + trace_list.removeAll(trace_name); + + // Update graphs in QChart plot + removeSeriesByName(chart, trace_name); + + // Update the chart limits. + this->f_max = -1; + this->f_min = 1e30; + + QStringList files = datasets.keys(); + for (int i = 0; i < files.size(); i++){ + adjust_x_axis_to_file(files[i]); + } +} + + +bool Qucs_S_SPAR_Viewer::removeSeriesByName(QChart* chart, const QString& name) +{ + QList seriesList = chart->series(); + for (QAbstractSeries* series : seriesList) { + if (series->name() == name) { + chart->removeSeries(series); + return true; // Series found and removed + } + } + return false; // Series not found +} + + +void Qucs_S_SPAR_Viewer::convert_MA_RI_to_dB(double * S_1, double * S_2, double *S_3, double *S_4, QString format) +{ + double S_dB = *S_1, S_ang =*S_2; + double S_re = *S_3, S_im = *S_4; + if (format == "MA"){ + S_dB = 20*log10(*S_1); + S_ang = *S_2; + S_re = *S_1 * std::cos(*S_2); + S_im = *S_1 * std::sin(*S_2); + }else{ + if (format == "RI"){ + S_dB = 20*log10(sqrt((*S_1)*(*S_1) + (*S_2)*(*S_2))); + S_ang = atan2(*S_2, *S_1) * 180 / M_PI; + S_re = *S_1; + S_im = *S_2; + } else { + // DB format + double r = std::pow(10, *S_1 / 10.0); + double theta = *S_2 * M_PI / 180.0; + S_re = r * std::cos(theta); + S_im = r * std::sin(theta); + } + + } + *S_1 = S_dB; + *S_2 = S_ang; + *S_3 = S_re; + *S_4 = S_im; +} + +// Gets the frequency scale unit from a String lke kHz, MHz, GHz +double Qucs_S_SPAR_Viewer::getFreqScale() +{ + QString frequency_unit = QCombobox_x_axis_units->currentText(); + double freq_scale=1; + if (frequency_unit == "kHz"){ + freq_scale = 1e-3; + } else { + if (frequency_unit == "MHz"){ + freq_scale = 1e-6; + } else { + if (frequency_unit == "GHz"){ + freq_scale = 1e-9; + } + } + } + return freq_scale; +} + +// Gets the frequency scale unit from a String lke kHz, MHz, GHz +double Qucs_S_SPAR_Viewer::getFreqScale(QString frequency_unit) +{ + double freq_scale=1; + if (frequency_unit == "kHz"){ + freq_scale = 1e-3; + } else { + if (frequency_unit == "MHz"){ + freq_scale = 1e-6; + } else { + if (frequency_unit == "GHz"){ + freq_scale = 1e-9; + } + } + } + return freq_scale; +} + + +void Qucs_S_SPAR_Viewer::addTrace() +{ + QString selected_dataset, selected_trace, trace_name; + selected_dataset = this->QCombobox_datasets->currentText(); + selected_trace = this->QCombobox_traces->currentText(); + + // Color settings + QColor trace_color; + QPen pen; + int num_traces = trace_list.size(); + if (num_traces >= 3){ + trace_color = QColor(QRandomGenerator::global()->bounded(256), QRandomGenerator::global()->bounded(256), QRandomGenerator::global()->bounded(256)); + pen.setColor(trace_color); + } + else { + trace_color = this->default_colors.at(num_traces); + } + + addTrace(selected_dataset, selected_trace, trace_color); +} + + +// Read the dataset and trace Comboboxes and add a trace to the display list +void Qucs_S_SPAR_Viewer::addTrace(QString selected_dataset, QString selected_trace, QColor trace_color) +{ + int n_trace = this->trace_list.size()+1; // Number of displayed traces; + // Get the name of the selected dataset + + + // Get the name of the trace to plot + QString trace_name = selected_dataset; + trace_name.append("."); // Separate the dataset from the trace name with a point + trace_name.append(selected_trace); + + if (trace_list.contains(trace_name)){ + QMessageBox::information( + this, + tr("Warning"), + tr("This trace is already shown") ); + return; + } + + // Add the trace to the list of displayed list and create the widgets associated to the trace properties + + // Label + QLabel * new_trace_label = new QLabel(trace_name); + new_trace_label->setObjectName(QString("Trace_Name_") + QString::number(n_trace)); + List_TraceNames.append(new_trace_label); + this->TracesGrid->addWidget(new_trace_label, n_trace, 0); + + // Color picker + QPushButton * new_trace_color = new QPushButton(); + new_trace_color->setObjectName(QString("Trace_Color_") + QString::number(n_trace)); + connect(new_trace_color, SIGNAL(clicked()), SLOT(changeTraceColor())); + List_Trace_Color.append(new_trace_color); + this->TracesGrid->addWidget(new_trace_color, n_trace, 1); + // Set the button color according to the trace color + QPalette palette = new_trace_color->palette(); + palette.setColor(QPalette::Button, trace_color); + new_trace_color->setPalette(palette); + new_trace_color->update(); + + // Line Style + QComboBox * new_trace_linestyle = new QComboBox(); + new_trace_linestyle->setObjectName(QString("Trace_LineStyle_") + QString::number(n_trace)); + new_trace_linestyle->addItem("Solid"); + new_trace_linestyle->addItem("- - - -"); + new_trace_linestyle->addItem("·······"); + new_trace_linestyle->addItem("-·-·-·-"); + new_trace_linestyle->addItem("-··-··-"); + connect(new_trace_linestyle, SIGNAL(currentIndexChanged(int)), SLOT(changeTraceLineStyle())); + List_Trace_LineStyle.append(new_trace_linestyle); + this->TracesGrid->addWidget(new_trace_linestyle, n_trace, 2); + + // Line width + QSpinBox * new_trace_width = new QSpinBox(); + new_trace_width->setObjectName(QString("Trace_Width_") + QString::number(n_trace)); + new_trace_width->setValue(1); + connect(new_trace_width, SIGNAL(valueChanged(int)), SLOT(changeTraceWidth())); + List_TraceWidth.append(new_trace_width); + this->TracesGrid->addWidget(new_trace_width, n_trace, 3); + + + // Remove button + QToolButton * new_trace_removebutton = new QToolButton(); + new_trace_removebutton->setObjectName(QString("Trace_RemoveButton_") + QString::number(n_trace)); + QIcon icon(":/bitmaps/trash.png"); // Use a resource path or a relative path + new_trace_removebutton->setIcon(icon); + new_trace_removebutton->setStyleSheet(R"( + QToolButton { + background-color: #FF0000; + color: white; + border-radius: 20px; + } + )"); + connect(new_trace_removebutton, SIGNAL(clicked()), SLOT(removeTrace())); + List_Button_DeleteTrace.append(new_trace_removebutton); + this->TracesGrid->addWidget(new_trace_removebutton, n_trace, 4, Qt::AlignCenter); + + adjust_x_axis_to_file(selected_dataset); + adjust_y_axis_to_trace(selected_dataset, selected_trace); + + QLineSeries* series = new QLineSeries(); + series->setName(trace_name); + trace_list.append(trace_name); + + // Color settings + QPen pen; + pen.setColor(trace_color); + series->setPen(pen);// Apply the pen to the series + + chart->addSeries(series); + + + updatePlot(); +} + +// This function is used for setting the available traces depending on the selected dataset +void Qucs_S_SPAR_Viewer::updateTracesCombo() +{ + QCombobox_traces->clear(); + QStringList traces; + QString current_dataset = QCombobox_datasets->currentText(); + if (current_dataset.isEmpty()) + return; // No datasets loaded. This happens if the user had one single file and deleted it + int n_ports = datasets[current_dataset]["n_ports"].at(0); + + for (int i=1; i<=n_ports; i++){ + for (int j=1; j<=n_ports; j++){ + traces.append(QString("S") + QString::number(i) + QString::number(j)); + } + } + + if(n_ports == 1){ + // Additional traces + traces.append("Re{Zin}"); + traces.append("Im{Zin}"); + } + + if(n_ports == 2){ + // Additional traces + traces.append(QString("|%1|").arg(QChar(0x0394))); + traces.append("K"); + traces.append(QString("%1%2").arg(QChar(0x03BC)).arg(QChar(0x209B))); + traces.append(QString("%1%2").arg(QChar(0x03BC)).arg(QChar(0x209A))); + traces.append("MAG"); + traces.append("MSG"); + traces.append("Re{Zin}"); + traces.append("Im{Zin}"); + traces.append("Re{Zout}"); + traces.append("Im{Zout}"); + } + + QCombobox_traces->addItems(traces); +} + +// This is the handler that is triggered when the user hits the button to change the color of a given trace +void Qucs_S_SPAR_Viewer::changeTraceColor() +{ + QColor color = QColorDialog::getColor(Qt::white, this, "Select Color"); + if (color.isValid()) { + // Do something with the selected color + // For example, set the background color of the button + QPushButton *button = qobject_cast(sender()); + if (button) { + QPalette palette = button->palette(); + palette.setColor(QPalette::Button, color); + button->setPalette(palette); + button->update(); + + QString ID = button->objectName(); + + int index_to_change_color = -1; + for (int i = 0; i < List_Trace_Color.size(); i++) { + if (List_Trace_Color.at(i)->objectName() == ID) { + index_to_change_color = i; + break; + } + } + + QLabel* label = List_TraceNames.at(index_to_change_color); + QString trace_name = label->text(); + + // Change the color of the series named based on its name + const auto seriesList = chart->series(); + for (QAbstractSeries *s : seriesList) { + QLineSeries *lineSeries = qobject_cast(s); + if (lineSeries && lineSeries->name() == trace_name) { + QPen pen = lineSeries->pen(); + pen.setColor(color); + lineSeries->setPen(pen); + break; + } + } + } + } +} + +// This is the handler that is triggered when the user hits the button to change the line style of a given trace +void Qucs_S_SPAR_Viewer::changeTraceLineStyle() +{ + QComboBox *combo = qobject_cast(sender()); + const auto seriesList = chart->series(); + + QString ID = combo->objectName(); + + int index_to_change_linestyle = -1; + for (int i = 0; i < List_Trace_LineStyle.size(); i++) { + if (List_Trace_LineStyle.at(i)->objectName() == ID) { + index_to_change_linestyle = i; + break; + } + } + + QLabel* label = List_TraceNames.at(index_to_change_linestyle); + QString trace_name = label->text(); + + for (QAbstractSeries *s : seriesList) { + QLineSeries *lineSeries = qobject_cast(s); + if (lineSeries && lineSeries->name() == trace_name) { + QPen pen = lineSeries->pen(); + switch (combo->currentIndex()) { + case 0: // Solid + pen.setStyle(Qt::SolidLine); + break; + case 1: // Dashed + pen.setStyle(Qt::DashLine); + break; + case 2: // Dotted + pen.setStyle(Qt::DotLine); + break; + case 3: // Dash Dot + pen.setStyle(Qt::DashDotLine); + break; + case 4: // Dash Dot Dot Line + pen.setStyle(Qt::DashDotDotLine); + break; + } + lineSeries->setPen(pen); + break; + } + } +} + +// This is the handler that is triggered when the user hits the button to change the line width of a given trace +void Qucs_S_SPAR_Viewer::changeTraceWidth() +{ + QSpinBox *spinbox = qobject_cast(sender()); + const auto seriesList = chart->series(); + + QString ID = spinbox->objectName(); + + int index_to_change_linestyle = -1; + for (int i = 0; i < List_TraceWidth.size(); i++) { + if (List_TraceWidth.at(i)->objectName() == ID) { + index_to_change_linestyle = i; + break; + } + } + + QLabel* label = List_TraceNames.at(index_to_change_linestyle); + QString trace_name = label->text(); + + for (QAbstractSeries *s : seriesList) { + QLineSeries *lineSeries = qobject_cast(s); + if (lineSeries && lineSeries->name() == trace_name) { + QPen pen = lineSeries->pen(); + pen.setWidth(spinbox->value()); + lineSeries->setPen(pen); + break; + } + } +} + +void Qucs_S_SPAR_Viewer::updatePlot() +{ + // Update axes + update_X_axis(); + update_Y_axis(); + + // Trim the traces according to the new settings + updateTraces(); + updateMarkerTable(); + +} + +// This is the handler that updates the x-axis when the x-axis QSpinBoxes change their value +void Qucs_S_SPAR_Viewer::update_X_axis() +{ + // Get the user limits or adjust + adjust_x_axis_div(); + double x_min = QSpinBox_x_axis_min->value(); + double x_max = QSpinBox_x_axis_max->value(); + double x_div = QComboBox_x_axis_div->currentText().toDouble(); + + // Update spinbox limits + disconnect(QSpinBox_x_axis_max, SIGNAL(valueChanged(double)), this, SLOT(updatePlot())); // Needed to avoid duplicating the call to the update function + disconnect(QSpinBox_x_axis_min, SIGNAL(valueChanged(double)), this, SLOT(updatePlot())); // Needed to avoid duplicating the call to the update function + QSpinBox_x_axis_min->setMaximum(x_max); + QSpinBox_x_axis_max->setMinimum(x_min); + connect(QSpinBox_x_axis_min, SIGNAL(valueChanged(double)), SLOT(updatePlot())); + connect(QSpinBox_x_axis_max, SIGNAL(valueChanged(double)), SLOT(updatePlot())); + + // Remove the axis in order to build a new one later + if (xAxis != NULL) { + chart->removeAxis(xAxis); + } + + // x-axis settings + xAxis = new QValueAxis(); + xAxis->setRange(x_min, x_max); // Set the range of the axis + xAxis->setTickInterval(x_div); // Set the interval between ticks + xAxis->setTickCount(floor((x_max-x_min)/x_div)+1); + xAxis->setTitleText("frequency " + QCombobox_x_axis_units->currentText()); + + // Add the axis to the chart + chart->addAxis(xAxis, Qt::AlignBottom); + chart->legend()->hide(); + +} + +// This is the handler that updates the y-axis when the y-axis QSpinBoxes change their value +void Qucs_S_SPAR_Viewer::update_Y_axis() +{ + // y-axis + double y_min = QSpinBox_y_axis_min->value(); + double y_max = QSpinBox_y_axis_max->value(); + double y_div = QComboBox_y_axis_div->currentText().toDouble(); + + // Update spinbox limits + disconnect(QSpinBox_y_axis_max, SIGNAL(valueChanged(double)), this, SLOT(updatePlot())); // Needed to avoid duplicating the call to the update function + disconnect(QSpinBox_y_axis_min, SIGNAL(valueChanged(double)), this, SLOT(updatePlot())); // Needed to avoid duplicating the call to the update function + QSpinBox_y_axis_min->setMaximum(y_max); + QSpinBox_y_axis_max->setMinimum(y_min); + connect(QSpinBox_y_axis_min, SIGNAL(valueChanged(double)), SLOT(updatePlot())); + connect(QSpinBox_y_axis_max, SIGNAL(valueChanged(double)), SLOT(updatePlot())); + + if (yAxis != NULL){ + chart->removeAxis(yAxis); + } + + // y-axis settings + yAxis = new QValueAxis(); + yAxis->setRange(y_min, y_max); // Set the range of the axis + yAxis->setTickInterval(y_div); // Set the interval between ticks + yAxis->setTickCount(floor((y_max-y_min)/y_div)+1); + yAxis->setTitleText("S (dB)"); + + // Add the axis to the chart + chart->addAxis(yAxis, Qt::AlignLeft); +} + +// Each time the x-axis or the y-axis settings change, the traces need to be realigned with respect to +// the new axis. Otherwise, the trace is show as it was with the initial axis settings, without any kind of rescaling +void Qucs_S_SPAR_Viewer::updateTraces() +{ + // Get the series + QList seriesList = chart->series(); + + // Remove series from the chart + for (QAbstractSeries *series : seriesList) { + chart->removeSeries(series); + } + + double freq_scale = getFreqScale(); + + // User settings + double x_axis_min = QSpinBox_x_axis_min->value()/freq_scale; + double x_axis_max = QSpinBox_x_axis_max->value()/freq_scale; + + double y_axis_min = QSpinBox_y_axis_min->value(); + double y_axis_max = QSpinBox_y_axis_max->value(); + + // Remove marker traces + // Iterate through the series list + QList seriesToRemove; + for (QAbstractSeries *series : seriesList) { + //qDebug() << series->name(); + if (series->name().startsWith("Mkr", Qt::CaseInsensitive)) { + seriesToRemove.append(series); + } + } + for (QAbstractSeries *series : seriesToRemove) { + seriesList.removeOne(series); + + // If the series is added to a chart, remove it from the chart as well + if (series->chart()) { + series->chart()->removeSeries(series); + } + + // Delete the series object to free memory + delete series; + } + + // Iterate over all the traces and, if needed: + // 1) Find if the data in the dataset can cover the new frequency span + // 2) If so, trim the trace according to the new limits + // 3) If not, add extra padding + + for (QAbstractSeries *series : seriesList) { + QString trace_name = series->name(); + qreal minX_trace, maxX_trace, minY_trace, maxY_trace; + + QStringList trace_name_parts = trace_name.split('.'); + QString data_file = trace_name_parts[0]; + QString trace_file = trace_name_parts[1]; + + if (trace_file.at(0) == "S"){ + trace_file = trace_file + QString("_dB"); + } + if (trace_file == QString("|%1|").arg(QChar(0x0394))){ + trace_file = "delta"; + } + if (trace_file == QString("%1%2").arg(QChar(0x03BC)).arg(QChar(0x209B))){ + trace_file = "mu"; + } + if (trace_file == QString("%1%2").arg(QChar(0x03BC)).arg(QChar(0x209A))){ + trace_file = "mu_p"; + } + + // Check the limits of the data in the dataset in order to see if the new settings given by the user + // exceed the limits of the available data + getMinMaxValues(data_file, trace_file, minX_trace, maxX_trace, minY_trace, maxY_trace); + + // Find the closest indices to the minimum and the maximum given by the user + int minIndex = findClosestIndex(datasets[data_file]["frequency"], x_axis_min); + int maxIndex = findClosestIndex(datasets[data_file]["frequency"], x_axis_max); + + QList freq_trimmed = datasets[data_file]["frequency"].mid(minIndex, maxIndex - minIndex + 1); + std::transform(freq_trimmed.begin(), freq_trimmed.end(), freq_trimmed.begin(), + [freq_scale](double value) { return value * freq_scale; }); + + QList data_trimmed = datasets[data_file][trace_file].mid(minIndex, maxIndex - minIndex + 1); + + // Get the series data + QXYSeries *xySeries = qobject_cast(series); + xySeries->clear(); // Remove its data + + // Apply clipping if the data exceeds the lower/upper limits + for (int i = 0; i < freq_trimmed.size(); i++) { + double y_value = data_trimmed[i]; + + // Data exceeds the upper limit + if (y_value > y_axis_max){ + y_value = y_axis_max; + } + + // Data exceeds the lower limit + if (y_value < y_axis_min){ + y_value = y_axis_min; + } + + // Add (clipped) data to the series + xySeries->append(QPointF(freq_trimmed[i], y_value)); + } + + } + + // Add marker traces. One per trace + for (int c = 1; ccolumnCount(); c++){//Traces + QScatterSeries *marker_series = new QScatterSeries(); + marker_series->setMarkerShape(QScatterSeries::MarkerShapeCircle); + marker_series->setMarkerSize(10); + marker_series->setColor(Qt::black); + + for (int r = 0; rrowCount(); r++){//Marker + QString y_val = tableMarkers->item(r,c)->text(); + QString text = tableMarkers->item(r,0)->text(); + QStringList parts = text.split(' '); + QString freq = parts[0]; + QString freq_scale = parts[1]; + double x = freq.toDouble()/getFreqScale(freq_scale); + x *= getFreqScale();// Normalize x with respect to the axis scale + double y = y_val.toDouble(); + marker_series->append(x, y); + } + QString trace_name = tableMarkers->horizontalHeaderItem(c)->text(); + QString marker_series_name = QString("Mkr_%1").arg(trace_name); + marker_series->setName(marker_series_name); + seriesList.append(marker_series); + } + + // Add the marker vertical bar + int n_rows = tableMarkers->rowCount(); + int n_cols = tableMarkers->columnCount(); + if (n_cols > 1){ + for (int r = 0; ritem(r,0)->text(); + QStringList parts = text.split(' '); + QString freq = parts[0]; + QString freq_scale = parts[1]; + double x = freq.toDouble()/getFreqScale(freq_scale); + x *= getFreqScale();// Normalize x with respect to the axis scale + QLineSeries *verticalLine = new QLineSeries(); + verticalLine->append(x, y_axis_min); + verticalLine->append(x, y_axis_max); + verticalLine->setPen(QPen(Qt::black, 1, Qt::DashLine)); + + QString verticalLine_name = QString("Mkr_%1").arg(r); + verticalLine->setName(verticalLine_name); + + seriesList.append(verticalLine); + } + } + + // Add series again to the chart. Each series must be linked to an axis + for (QAbstractSeries *series : seriesList) { + chart->addSeries(series); + series->attachAxis(xAxis); + series->attachAxis(yAxis); + } + chart->update(); +} + +// Given a trace, it gives the minimum and the maximum values at both axis. +void Qucs_S_SPAR_Viewer::getMinMaxValues(QString filename, QString tracename, qreal& minX, qreal& maxX, qreal& minY, qreal& maxY) { + // Find the minimum and the maximum in the x-axis + QList freq = datasets[filename]["frequency"]; + minX = freq.first(); + maxX = freq.last(); + + // Find minimum and maximum in the y-axis + QList trace_data = datasets[filename][tracename]; + + auto minIterator = std::min_element(trace_data.begin(), trace_data.end()); + auto maxIterator = std::max_element(trace_data.begin(), trace_data.end()); + + minY = *minIterator; + maxY = *maxIterator; + +} + +int Qucs_S_SPAR_Viewer::findClosestIndex(const QList& list, double value) +{ + return std::min_element(list.begin(), list.end(), + [value](double a, double b) { + return std::abs(a - value) < std::abs(b - value); + }) - list.begin(); +} + +// Ensures that the frequency settings limits does not show numbers like 0.001 or 500000 +void Qucs_S_SPAR_Viewer::checkFreqSettingsLimits(QString filename, double& fmin, double& fmax){ + QList frequency = datasets[filename]["frequency"]; + + while (true) { + fmin = frequency.first(); + fmax = frequency.last(); + + // Check frequency scale setting + double freq_scale = getFreqScale(); + + // Normalize the minimum and maximum frequencies + fmin *= freq_scale; + fmax *= freq_scale; + + // Exit condition + if ((fmax > 1) && (fmax < 3000)){ + break; + } + + if (fmax > 3000){ + // Downscale + int index = QCombobox_x_axis_units->currentIndex(); + if (index < 3) { + disconnect(QCombobox_x_axis_units, SIGNAL(currentIndexChanged(int)), this, SLOT(updatePlot())); // Needed to avoid duplicating the call to the update function + disconnect(QSpinBox_x_axis_max, SIGNAL(valueChanged(int)), this, SLOT(updatePlot())); // Needed to avoid duplicating the call to the update function + disconnect(QSpinBox_x_axis_min, SIGNAL(valueChanged(int)), this, SLOT(updatePlot())); // Needed to avoid duplicating the call to the update function + QCombobox_x_axis_units->setCurrentIndex(index+1); + QSpinBox_x_axis_min->setValue(fmin); + QSpinBox_x_axis_max->setValue(fmax); + connect(QCombobox_x_axis_units, SIGNAL(currentIndexChanged(int)), SLOT(updatePlot())); + connect(QSpinBox_x_axis_min, SIGNAL(valueChanged(int)), SLOT(updatePlot())); + connect(QSpinBox_x_axis_max, SIGNAL(valueChanged(int)), SLOT(updatePlot())); + + } else{ + // It's not possible to downscale more. Break the loop + break; + } + }else{ + // Upscale + int index = QCombobox_x_axis_units->currentIndex(); + if (index > 0) { + disconnect(QCombobox_x_axis_units, SIGNAL(currentIndexChanged(int)), this, SLOT(updatePlot())); // Needed to avoid duplicating the call to the update function + disconnect(QSpinBox_x_axis_max, SIGNAL(valueChanged(int)), this, SLOT(updatePlot())); // Needed to avoid duplicating the call to the update function + disconnect(QSpinBox_x_axis_min, SIGNAL(valueChanged(int)), this, SLOT(updatePlot())); // Needed to avoid duplicating the call to the update function + QCombobox_x_axis_units->setCurrentIndex(index-1); + QSpinBox_x_axis_min->setValue(fmin); + QSpinBox_x_axis_max->setValue(fmax); + connect(QCombobox_x_axis_units, SIGNAL(currentIndexChanged(int)), SLOT(updatePlot())); + connect(QSpinBox_x_axis_min, SIGNAL(valueChanged(int)), SLOT(updatePlot())); + connect(QSpinBox_x_axis_max, SIGNAL(valueChanged(int)), SLOT(updatePlot())); + + } else{ + // It's not possible to upscale more. Break the loop + break; + } + } + } + + return; +} + + +// Automatically adjust the y-axis depending on the y-axis values of the traces displayed +void Qucs_S_SPAR_Viewer::adjust_y_axis_to_trace(QString filename, QString tracename){ + qreal minX, maxX, minY, maxY; + + if (tracename.at(0) == "S"){ + tracename = tracename + QString("_dB"); + } + if (tracename == QString("|%1|").arg(QChar(0x0394))){ + tracename = "delta"; + } + if (tracename == QString("%1%2").arg(QChar(0x03BC)).arg(QChar(0x209B))){ + tracename = "mu"; + } + if (tracename == QString("%1%2").arg(QChar(0x03BC)).arg(QChar(0x209A))){ + tracename = "mu_p"; + } + + + getMinMaxValues(filename, tracename, minX, maxX, minY, maxY); + + if (maxY > this->y_max) { + maxY = 5.0 * std::ceil(maxY / 5.0); + this->y_max = maxY; + } + + if (minY < this->y_min) { + minY = 5.0 * std::floor(minY / 5.0); + this->y_min = minY; + } + + //Adjust the y-axis div depending on the limits + double y_div = QComboBox_y_axis_div->currentText().toDouble(); + + if ((y_div > y_max-y_min) || ((y_max-y_min)/y_div > 10)){ + // No ticks or excesive ticks + int new_index = findClosestIndex(available_y_axis_div, (y_max-y_min)/10); + disconnect(QComboBox_y_axis_div, SIGNAL(currentIndexChanged(int)), this, SLOT(updatePlot())); + QComboBox_y_axis_div->setCurrentIndex(new_index); + connect(QComboBox_y_axis_div, SIGNAL(currentIndexChanged(int)), SLOT(updatePlot())); + } + + disconnect(QSpinBox_y_axis_min, SIGNAL(valueChanged(double)), this, SLOT(updatePlot())); // Needed to avoid duplicating the call to the update function + disconnect(QSpinBox_y_axis_max, SIGNAL(valueChanged(double)), this, SLOT(updatePlot())); + QSpinBox_y_axis_min->setValue(y_min); + QSpinBox_y_axis_max->setValue(y_max); + connect(QSpinBox_y_axis_min, SIGNAL(valueChanged(double)), SLOT(updatePlot())); + connect(QSpinBox_y_axis_max, SIGNAL(valueChanged(double)), SLOT(updatePlot())); + + updatePlot(); +} + + +// Automatically adjust the x-axis depending on the range of the traces displayed +void Qucs_S_SPAR_Viewer::adjust_x_axis_to_file(QString filename){ + QList frequency = datasets[filename]["frequency"]; + + double fmin = frequency.first(); + double fmax = frequency.last(); + + if (fmin < this->f_min) this->f_min = fmin; + if (fmax > this->f_max) this->f_max = fmax; + + + while (true) { + fmin = this->f_min; + fmax = this->f_max; + + // Check frequency scale setting + double freq_scale = getFreqScale(); + + // Normalize the minimum and maximum frequencies + fmin *= freq_scale; + fmax *= freq_scale; + + // Exit condition + if ((fmax > 1) && (fmax < 3000)){ + break; + } + + if (fmax >= 3000){ + // Downscale + int index = QCombobox_x_axis_units->currentIndex(); + if (index < 3) { + disconnect(QCombobox_x_axis_units, SIGNAL(currentIndexChanged(int)), this, SLOT(updatePlot())); // Needed to avoid duplicating the call to the update function + QCombobox_x_axis_units->setCurrentIndex(index+1); + connect(QCombobox_x_axis_units, SIGNAL(currentIndexChanged(int)), SLOT(updatePlot())); + } else{ + // It's not possible to downscale more. Break the loop + break; + } + }else{ + // Upscale + int index = QCombobox_x_axis_units->currentIndex(); + if (index > 0) { + disconnect(QCombobox_x_axis_units, SIGNAL(currentIndexChanged(int)), this, SLOT(updatePlot())); // Needed to avoid duplicating the call to the update function + QCombobox_x_axis_units->setCurrentIndex(index-1); + connect(QCombobox_x_axis_units, SIGNAL(currentIndexChanged(int)), SLOT(updatePlot())); + + + } else{ + // It's not possible to upscale more. Break the loop + break; + } + } + } + + // Disconnect handlers to avoid duplicating the call to the update function + disconnect(QSpinBox_x_axis_min, SIGNAL(valueChanged(double)), this, SLOT(updatePlot())); + disconnect(QSpinBox_x_axis_max, SIGNAL(valueChanged(double)), this, SLOT(updatePlot())); + + // Round to 1 decimal place + fmin = round(fmin * 10.0) / 10.0; + fmax = round(fmax * 10.0) / 10.0; + QSpinBox_x_axis_min->setValue(fmin); + QSpinBox_x_axis_max->setValue(fmax); + QSpinBox_x_axis_max->setMinimum(fmin); // The upper limit cannot be lower than the lower limit + QSpinBox_x_axis_max->setMaximum(fmax); // The lower limit cannot be higher than the higher limit + + // Update x-axis tick + adjust_x_axis_div(); + + // Connect the handlers again + connect(QSpinBox_x_axis_min, SIGNAL(valueChanged(double)), SLOT(updatePlot())); + connect(QSpinBox_x_axis_max, SIGNAL(valueChanged(double)), SLOT(updatePlot())); + + updatePlot(); + +} + + +void Qucs_S_SPAR_Viewer::addMarker(){ + double f1 = QSpinBox_x_axis_min->value(); + double f2 = QSpinBox_x_axis_max->value(); + double f_marker = f1 + 0.5*(f2-f1); + + QString Freq_Marker_Scale = QCombobox_x_axis_units->currentText(); + + int n_markers = List_MarkerNames.size(); + n_markers++; + + QString new_marker_name = QString("Mkr%1").arg(n_markers); + QLabel * new_marker_label = new QLabel(new_marker_name); + new_marker_label->setObjectName(new_marker_name); + List_MarkerNames.append(new_marker_label); + this->MarkersGrid->addWidget(new_marker_label, n_markers, 0); + + QString SpinBox_name = QString("Mkr_SpinBox%1").arg(n_markers); + QDoubleSpinBox * new_marker_Spinbox = new QDoubleSpinBox(); + new_marker_Spinbox->setObjectName(SpinBox_name); + new_marker_Spinbox->setMaximum(QSpinBox_x_axis_max->minimum()); + new_marker_Spinbox->setMaximum(QSpinBox_x_axis_max->maximum()); + new_marker_Spinbox->setValue(f_marker); + connect(new_marker_Spinbox, SIGNAL(valueChanged(double)), SLOT(updateMarkerTable())); + List_MarkerFreq.append(new_marker_Spinbox); + this->MarkersGrid->addWidget(new_marker_Spinbox, n_markers, 1); + + QString Combobox_name = QString("Mkr_ComboBox%1").arg(n_markers); + QComboBox * new_marker_Combo = new QComboBox(); + new_marker_Combo->setObjectName(Combobox_name); + new_marker_Combo->addItem("Hz"); + new_marker_Combo->addItem("kHz"); + new_marker_Combo->addItem("MHz"); + new_marker_Combo->addItem("GHz"); + new_marker_Combo->setCurrentIndex(QCombobox_x_axis_units->currentIndex()); + connect(new_marker_Combo, SIGNAL(currentIndexChanged(int)), SLOT(changeMarkerLimits())); + List_MarkerScale.append(new_marker_Combo); + this->MarkersGrid->addWidget(new_marker_Combo, n_markers, 2); + + // Remove button + QString DeleteButton_name = QString("Mkr_Delete_Btn%1").arg(n_markers); + QToolButton * new_marker_removebutton = new QToolButton(); + new_marker_removebutton->setObjectName(DeleteButton_name); + QIcon icon(":/bitmaps/trash.png"); // Use a resource path or a relative path + new_marker_removebutton->setIcon(icon); + new_marker_removebutton->setStyleSheet(R"( + QToolButton { + background-color: #FF0000; + color: white; + border-radius: 20px; + } + )"); + connect(new_marker_removebutton, SIGNAL(clicked()), SLOT(removeMarker())); + List_Button_DeleteMarker.append(new_marker_removebutton); + this->MarkersGrid->addWidget(new_marker_removebutton, n_markers, 3, Qt::AlignCenter); + + // Add new entry to the table + tableMarkers->setRowCount(n_markers); + QString new_freq = QString("%1 ").arg(QString::number(f_marker, 'f', 2)) + Freq_Marker_Scale; + QTableWidgetItem *newfreq = new QTableWidgetItem(new_freq); + tableMarkers->setItem(n_markers-1, 0, newfreq); + + changeMarkerLimits(Combobox_name); + +} + + +void Qucs_S_SPAR_Viewer::updateMarkerTable(){ + + //If there are no markers, remove the entries and return + int n_markers = List_MarkerNames.size(); + if (n_markers == 0){ + tableMarkers->clear(); + tableMarkers->setColumnCount(0); + tableMarkers->setRowCount(0); + updateTraces(); + return; + } + + //Ensure that the size of the table is correct + QList seriesList = chart->series(); + + // Update marker header + QStringList headers; + headers.clear(); + headers.append("freq"); + for (QAbstractSeries *series : seriesList) { + QString series_name = series->name(); + if (series_name.startsWith("Mkr", Qt::CaseSensitive)){ + //Markers are traces in the QChart, but they cannot be added as markers again! + continue; + } + headers.append(series_name); + } + + tableMarkers->setColumnCount(headers.size());// The first row is for the frequency + tableMarkers->setRowCount(n_markers); + tableMarkers->setHorizontalHeaderLabels(headers); + + QPointF P; + qreal targetX; + QString new_val; + QString freq_marker; + // Update each marker + // Columns are traces. Rows are markers + for (int c = 0; ccolumnCount(); c++){//Traces + for (int r = 0; rrowCount(); r++){//Marker + freq_marker = QString("%1 ").arg(QString::number(List_MarkerFreq[r]->value(), 'f', 1)) + List_MarkerScale[r]->currentText(); + + if (c==0){ + // First column + QTableWidgetItem *new_item = new QTableWidgetItem(freq_marker); + tableMarkers->setItem(r, c, new_item); + continue; + } + targetX = getFreqFromText(freq_marker); + //Normalize with respect to the scale of the x-axis + targetX = targetX*getFreqScale(); + P = findClosestPoint(seriesList[c-1], targetX); + new_val = QString("%1").arg(QString::number(P.y(), 'f', 2)); + QTableWidgetItem *new_item = new QTableWidgetItem(new_val); + tableMarkers->setItem(r, c, new_item); + } + } + + updateTraces();//The markers need to be updated in the chart +} + +// Find the closest x-axis value in a series given a x value (not necesarily in the grid) +QPointF Qucs_S_SPAR_Viewer::findClosestPoint(QAbstractSeries* series, qreal targetX) +{ + // Cast to QXYSeries since we need access to points + QXYSeries* xySeries = qobject_cast(series); + if (!xySeries) { + return QPointF(); // Return invalid point if cast fails + } + + QVector points = xySeries->pointsVector(); + if (points.isEmpty()) { + return QPointF(); // Return invalid point if series is empty + } + + // Initialize with the first point + QPointF closestPoint = points.first(); + qreal minDistance = qAbs(targetX - closestPoint.x()); + + // Iterate through all points to find the closest one + for (const QPointF& point : points) { + qreal distance = qAbs(targetX - point.x()); + if (distance < minDistance) { + minDistance = distance; + closestPoint = point; + } + } + + return closestPoint; +} + + +double Qucs_S_SPAR_Viewer::getFreqFromText(QString freq) +{ + // Remove any whitespace from the string + freq = freq.simplified(); + + // Regular expression to match the number and unit + QRegularExpression re("(\\d+(?:\\.\\d+)?)(\\s*)(Hz|kHz|MHz|GHz)"); + re.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + QRegularExpressionMatch match = re.match(freq); + + if (match.hasMatch()) { + double value = match.captured(1).toDouble(); + QString unit = match.captured(3).toLower(); + + // Convert to Hz based on the unit + if (unit == "khz") { + return value * 1e3; + } else if (unit == "mhz") { + return value * 1e6; + } else if (unit == "ghz") { + return value * 1e9; + } else { + // Assume Hz if no unit or Hz is specified + return value; + } + } + + // Return -1 if the input doesn't match the expected format + return -1; +} + + +// This function is called when the user wants to remove a trace from the plot +void Qucs_S_SPAR_Viewer::removeMarker() +{ + QString ID = qobject_cast(sender())->objectName(); + //qDebug() << "Clicked button:" << ID; + + //Find the index of the button to remove + int index_to_delete = -1; + for (int i = 0; i < List_Button_DeleteMarker.size(); i++) { + if (List_Button_DeleteMarker.at(i)->objectName() == ID) { + index_to_delete = i; + break; + } + } + removeMarker(index_to_delete); +} + + +void Qucs_S_SPAR_Viewer::removeMarker(int index_to_delete) +{ + // Delete the label + QLabel* labelToRemove = List_MarkerNames.at(index_to_delete); + MarkersGrid->removeWidget(labelToRemove); + List_MarkerNames.removeAt(index_to_delete); + delete labelToRemove; + + // Delete the SpinBox + QDoubleSpinBox * SpinBoxToRemove = List_MarkerFreq.at(index_to_delete); + MarkersGrid->removeWidget(SpinBoxToRemove); + List_MarkerFreq.removeAt(index_to_delete); + delete SpinBoxToRemove; + + // Delete the linestyle combo + QComboBox* ComboToRemove = List_MarkerScale.at(index_to_delete); + MarkersGrid->removeWidget(ComboToRemove); + List_MarkerScale.removeAt(index_to_delete); + delete ComboToRemove; + + // Delete the "delete" button + QToolButton* ButtonToRemove = List_Button_DeleteMarker.at(index_to_delete); + MarkersGrid->removeWidget(ButtonToRemove); + List_Button_DeleteMarker.removeAt(index_to_delete); + delete ButtonToRemove; + + updateMarkerTable(); +} + +void Qucs_S_SPAR_Viewer::removeAllMarkers() +{ + int n_markers = List_MarkerNames.size(); + for (int i = 0; i < n_markers; i++) { + removeMarker(n_markers-i-1); + } + + // Remove marker traces +} + +void Qucs_S_SPAR_Viewer::changeFreqUnits() +{ + // Adjust x-axis settings maximum depending on the units combo + double freq_scale = getFreqScale(); + double fmax = this->f_max*freq_scale; + double fmin = this->f_min*freq_scale; + + // Update fmax Spinbox + QSpinBox_x_axis_max->setMaximum(fmax); + QSpinBox_x_axis_max->setValue(fmax); + if (fmax > 1000){ + // Step 1 + QSpinBox_x_axis_max->setSingleStep(1); + } else { + if (fmax > 100){ + // Step 0.1 + QSpinBox_x_axis_max->setSingleStep(0.1); + } else { + // Step 0.01 + QSpinBox_x_axis_max->setSingleStep(0.01); + } + } + + // Update fmin Spinbox + QSpinBox_x_axis_min->setMaximum(fmin); + QSpinBox_x_axis_min->setValue(fmin); + if (fmin > 1000){ + // Step 1 + QSpinBox_x_axis_min->setSingleStep(1); + } else { + if (fmin > 100){ + // Step 0.1 + QSpinBox_x_axis_min->setSingleStep(0.1); + } else { + // Step 0.01 + QSpinBox_x_axis_min->setSingleStep(0.01); + } + } + + // Adjust div + adjust_x_axis_div(); + + updatePlot(); +} + + +void Qucs_S_SPAR_Viewer::adjust_x_axis_div() +{ + double x_min = QSpinBox_x_axis_min->value(); + double x_max = QSpinBox_x_axis_max->value(); + double x_div = QComboBox_x_axis_div->currentText().toDouble(); + + if ((x_div > x_max-x_min) || ((x_max-x_min)/x_div > 15)){ + // No ticks or excesive ticks + int new_index = findClosestIndex(available_x_axis_div, (x_max-x_min)/5); + disconnect(QComboBox_x_axis_div, SIGNAL(currentIndexChanged(int)), this, SLOT(updatePlot())); + QComboBox_x_axis_div->setCurrentIndex(new_index); + connect(QComboBox_x_axis_div, SIGNAL(currentIndexChanged(int)), SLOT(updatePlot())); + } +} + +// If the combobox associated to a marker changes, the limits of the marker must be updated too +void Qucs_S_SPAR_Viewer::changeMarkerLimits() +{ + QString ID = qobject_cast(sender())->objectName(); + //qDebug() << "Clicked button:" << ID; + changeMarkerLimits(ID); + +} + +// If the combobox associated to a marker changes, the limits of the marker must be updated too +void Qucs_S_SPAR_Viewer::changeMarkerLimits(QString ID) +{ + //Find the index of the marker + int index = -1; + for (int i = 0; i < List_MarkerScale.size(); i++) { + if (List_MarkerScale.at(i)->objectName() == ID) { + index = i; + break; + } + } + + // The lower and upper limits are given by the axis settings + double f_upper = QSpinBox_x_axis_max->value(); + double f_lower = QSpinBox_x_axis_min->value(); + double f_scale = getFreqScale(); + + f_upper /=f_scale; + f_lower /=f_scale; + + // Now we have to normalize this with respect to the marker's combo + QString new_scale = List_MarkerScale.at(index)->currentText(); + double f_scale_combo = getFreqScale(new_scale); + f_upper *= f_scale_combo; + f_lower *= f_scale_combo; + + List_MarkerFreq.at(index)->setMinimum(f_lower); + List_MarkerFreq.at(index)->setMaximum(f_upper); + + // Update minimum step + double diff = f_upper - f_lower; + if (diff < 1){ + List_MarkerFreq.at(index)->setSingleStep(0.01); + }else{ + if (diff < 10){ + List_MarkerFreq.at(index)->setSingleStep(0.1); + }else{ + if (diff < 100){ + List_MarkerFreq.at(index)->setSingleStep(1); + }else{ + List_MarkerFreq.at(index)->setSingleStep(10); + } + + } + } + + updateMarkerTable(); +} + + + +void Qucs_S_SPAR_Viewer::dragEnterEvent(QDragEnterEvent *event) +{ + if (event->mimeData()->hasUrls()) { + event->acceptProposedAction(); + } +} + +void Qucs_S_SPAR_Viewer::dropEvent(QDropEvent *event) +{ + QList urls = event->mimeData()->urls(); + QStringList fileList; + + for (const QUrl &url : urls) { + if (url.isLocalFile()) { + fileList << url.toLocalFile(); + } + } + + if (!fileList.isEmpty()) { + addFiles(fileList); + } +} diff --git a/qucs-s-spar-viewer/qucs-s-spar-viewer.h b/qucs-s-spar-viewer/qucs-s-spar-viewer.h new file mode 100644 index 00000000..ebfb67f7 --- /dev/null +++ b/qucs-s-spar-viewer/qucs-s-spar-viewer.h @@ -0,0 +1,177 @@ +#ifndef QUCSSPARVIEWER_H +#define QUCSSPARVIEWER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace QtCharts; + +class QComboBox; +class QTableWidget; +class QLineEdit; +class QIntValidator; +class QDoubleValidator; +class QLabel; +class QPushButton; + +struct tQucsSettings +{ + int x, y; // position of main window + QFont font; + QString LangDir; + QString Language; +}; + +extern struct tQucsSettings QucsSettings; + + +class Qucs_S_SPAR_Viewer : public QMainWindow +{ + Q_OBJECT + public: + Qucs_S_SPAR_Viewer(); + ~Qucs_S_SPAR_Viewer(); + + private slots: + void slotHelpIntro(); + void slotHelpAbout(); + void slotHelpAboutQt(); + void slotQuit(); + + void addFile(); + void addFiles(QStringList); + void removeFile(); + void removeFile(int); + void removeAllFiles(); + + void addTrace(); + void addTrace(QString, QString, QColor); + void removeTrace(); + void removeTrace(int); + void removeTrace(QList); + + void updatePlot(); + void updateTracesCombo(); + + void changeTraceColor(); + void changeTraceLineStyle(); + void changeTraceWidth(); + void changeFreqUnits(); + void changeMarkerLimits(); + void changeMarkerLimits(QString); + + void update_X_axis(); + void update_Y_axis(); + + void addMarker(); + void removeMarker(); + void removeMarker(int); + void removeAllMarkers(); + void updateMarkerTable(); + + protected: + void dragEnterEvent(QDragEnterEvent *event) override; + void dropEvent(QDropEvent *event) override; + + private: + QDockWidget *dockFiles; + QTableWidget * spar_files_Widget; + QPushButton *Button_Add_File, *Delete_All_Files; + + // File list + QList Button_DeleteFile; + + // Filenames and remove buttons + QVBoxLayout *vLayout_Files; + QWidget * FileList_Widget; + QGridLayout * FilesGrid; + QList List_FileNames; + QList List_RemoveButton; + + // Trace list + QDockWidget *dockTracesList; + QWidget * TracesList_Widget; + QGridLayout * TracesGrid; + QList List_TraceNames; + QList List_TraceWidth; + QList List_Trace_Color; + QList List_Trace_LineStyle; + QList List_Trace_Type; + QList List_Button_DeleteTrace; + + // Axis settings widgets + QDockWidget *dockAxisSettings; + QComboBox *QCombobox_x_axis_units;//, *QCombobox_y_axis_units, *QCombobox_y2_axis_units; + QDoubleSpinBox *QSpinBox_x_axis_min, *QSpinBox_x_axis_max; + QList available_x_axis_div; + QComboBox *QComboBox_x_axis_div; + QDoubleSpinBox *QSpinBox_y_axis_min, *QSpinBox_y_axis_max, *QSpinBox_y_axis_div; + QList available_y_axis_div; + QComboBox *QComboBox_y_axis_div; + QDoubleSpinBox *QSpinBox_y2_axis_min, *QSpinBox_y2_axis_max, *QSpinBox_y2_axis_div; + + // Trace management widgets + QComboBox *QCombobox_datasets, *QCombobox_traces; + QPushButton *Button_add_trace; + QTableWidget *Traces_Widget; + + // Datasets + QMap>> datasets; + + /* + KEY | DATA + Filename1.s2p | {"freq", "S11_dB", ..., "S22_ang"} + ... | ... + Filenamek.s3p | {"freq", "S11_dB", ..., "S33_ang"} + */ + + // Trace data + QList trace_list; + QMap> trace_properties; + + // Chart + QChart *chart; + QDockWidget *dockChart; + QValueAxis *xAxis, *yAxis; + double f_min, f_max, y_min, y_max; // Minimum (maximum) values of the display + QList default_colors; + bool removeSeriesByName(QChart*, const QString&); + void updateTraces(); + + // Markers + QDockWidget *dockMarkers; + QWidget *Marker_Widget; + QGridLayout * MarkersGrid; + QTableWidget *tableMarkers; + QPushButton *Button_add_marker, *Button_Remove_All_Markers; + + QList List_MarkerNames; + QList List_MarkerFreq; + QList List_MarkerScale; + QList List_Button_DeleteMarker; + + + // Utilities + void convert_MA_RI_to_dB(double *, double *, double *, double *, QString); + double getFreqScale(); + double getFreqScale(QString); + void getMinMaxValues(QString, QString, qreal&, qreal&, qreal&, qreal&); + void checkFreqSettingsLimits(QString filename, double& fmin, double& fmax); + int findClosestIndex(const QList&, double); + void adjust_x_axis_to_file(QString); + void adjust_y_axis_to_trace(QString, QString); + void adjust_x_axis_div(); + QPointF findClosestPoint(QAbstractSeries*, qreal); + double getFreqFromText(QString); +}; + +#endif diff --git a/qucs-s-spar-viewer/qucs-s-spar-viewer.qrc b/qucs-s-spar-viewer/qucs-s-spar-viewer.qrc new file mode 100644 index 00000000..33f9de92 --- /dev/null +++ b/qucs-s-spar-viewer/qucs-s-spar-viewer.qrc @@ -0,0 +1,6 @@ + + + bitmaps/big.qucs.xpm + bitmaps/trash.png + + diff --git a/qucs-s-spar-viewer/qucsattenuator.cpp b/qucs-s-spar-viewer/qucsattenuator.cpp new file mode 100644 index 00000000..7c17f396 --- /dev/null +++ b/qucs-s-spar-viewer/qucsattenuator.cpp @@ -0,0 +1,857 @@ +/**************************************************************************** +** Qucs Attenuator Synthesis +** qucsattenuator.cpp +** +** +** +** +** +** +** +*****************************************************************************/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "attenuatorfunc.h" +#include "qucsattenuator.h" + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +QucsAttenuator::QucsAttenuator() +{ + + QWidget *centralWidget = new QWidget(this); + setCentralWidget(centralWidget); + + setWindowIcon(QPixmap(":/bitmaps/big.qucs.xpm")); + setWindowTitle("Qucs Attenuator " PACKAGE_VERSION); + + QMenu *fileMenu = new QMenu(tr("&File")); + + QAction *fileQuit = new QAction(tr("&Quit"), this); + fileQuit->setShortcut(QKeySequence::Quit); + connect(fileQuit, SIGNAL(triggered(bool)), SLOT(slotQuit())); + + fileMenu->addAction(fileQuit); + + QMenu *helpMenu = new QMenu(tr("&Help")); + + QAction *helpHelp = new QAction(tr("&Help"), this); + helpHelp->setShortcut(Qt::Key_F1); + helpMenu->addAction(helpHelp); + connect(helpHelp, SIGNAL(triggered(bool)), SLOT(slotHelpIntro())); + + QAction *helpAbout = new QAction(tr("&About"), this); + helpMenu->addAction(helpAbout); + connect(helpAbout, SIGNAL(triggered(bool)), SLOT(slotHelpAbout())); + + + helpMenu->addSeparator(); + + QAction *about = new QAction(tr("About Qt..."), this); + helpMenu->addAction(about); + connect(about, SIGNAL(activated()), SLOT(slotHelpAboutQt())); + + menuBar()->addMenu(fileMenu); + menuBar()->addSeparator(); + menuBar()->addMenu(helpMenu); + + + //==========Left + QVBoxLayout *vboxLeft = new QVBoxLayout(); + + QGroupBox *TopoGroup = new QGroupBox(tr("Topology")); + QGridLayout * topoGrid = new QGridLayout(TopoGroup); + + ComboTopology = new QComboBox();//=================Topology Combobox + ComboTopology->insertItem(1, "Pi"); + ComboTopology->insertItem(2, "Tee"); + ComboTopology->insertItem(3, "Bridged Tee"); + ComboTopology->insertItem(4, "Reflection attenuator"); + ComboTopology->insertItem(5, "Quarter-wave series"); + ComboTopology->insertItem(6, "Quarter-wave shunt"); + ComboTopology->insertItem(7, "L-pad 1st series"); + ComboTopology->insertItem(8, "L-pad 1st shunt"); + ComboTopology->insertItem(9, "Rseries"); + ComboTopology->insertItem(10, "Rshunt"); + connect(ComboTopology, SIGNAL(activated(int)), SLOT(slotTopologyChanged())); + topoGrid->addWidget(ComboTopology, 1,0,1,2); + + pixTopology = new QLabel(TopoGroup);//====================Pixmap for Topology + pixTopology->setPixmap(QPixmap((":/bitmaps/att_pi.png"))); + topoGrid->addWidget(pixTopology,2,0,3,2); + + topoGrid->setSpacing(5); + TopoGroup->setLayout(topoGrid); + + vboxLeft->addWidget(TopoGroup); + + //S-parameter box option + SparBoxCheckbox = new QCheckBox("Add S-parameter simulation"); + SparBoxCheckbox->setChecked(false); + vboxLeft->addWidget(SparBoxCheckbox); + + //==========Right + QVBoxLayout *vboxRight = new QVBoxLayout(); + + QGroupBox * InputGroup = new QGroupBox (tr("Input")); + QGridLayout * inGrid = new QGridLayout(); + inGrid->setSpacing(1); + + DoubleVal = new QDoubleValidator(this); + DoubleVal->setLocale(QLocale::C); + DoubleVal->setBottom(0); + + DoubleValPower = new QDoubleValidator(this); + DoubleValPower->setBottom(-1e9);//The default power unit is dBm, so Pin < 0 is expected + + LabelAtten = new QLabel(tr("Attenuation:"), InputGroup); + inGrid ->addWidget(LabelAtten, 1,0); + QSpinBox_Attvalue = new QDoubleSpinBox(); + QSpinBox_Attvalue->setValue(1); + QSpinBox_Attvalue->setMinimum(0.1); + QSpinBox_Attvalue->setMaximum(1e6); + connect(QSpinBox_Attvalue, SIGNAL(valueChanged(double)), this, + SLOT(slotCalculate()) ); + inGrid->addWidget(QSpinBox_Attvalue, 1,1); + QLabel *Label1 = new QLabel(tr("dB"), InputGroup); + inGrid->addWidget(Label1, 1,2); + + LabelImp1 = new QLabel(tr("Zin:"), InputGroup); + LabelImp1->setWhatsThis("Input impedance"); + inGrid->addWidget(LabelImp1, 2,0); + QSpinBox_Zin = new QDoubleSpinBox(); + QSpinBox_Zin->setValue(50); + QSpinBox_Zin->setMinimum(0); + QSpinBox_Zin->setMaximum(1e6); + connect(QSpinBox_Zin, SIGNAL(valueChanged(double)), this, + SLOT(slotSetText_Zin(double)) ); + + inGrid->addWidget(QSpinBox_Zin, 2,1); + QLabel *Label2 = new QLabel(QChar(0xa9, 0x03), InputGroup); + inGrid->addWidget(Label2, 2,2); + + LabelImp2 = new QLabel(tr("Zout:"), InputGroup); + LabelImp2->setWhatsThis("Output impedance"); + inGrid->addWidget(LabelImp2, 3,0); + QSpinBox_Zout = new QDoubleSpinBox(); + QSpinBox_Zout->setValue(50); + QSpinBox_Zout->setMinimum(0); + QSpinBox_Zout->setMaximum(1e6); + connect(QSpinBox_Zout, SIGNAL(valueChanged(double)), this, + SLOT(slotSetText_Zout(double)) ); + inGrid->addWidget(QSpinBox_Zout, 3,1); + LabelImp2_Ohm = new QLabel(QChar(0xa9, 0x03), InputGroup); + inGrid->addWidget(LabelImp2_Ohm, 3,2); + + Label_Pin = new QLabel(tr("Pin:"), InputGroup); + Label_Pin->setWhatsThis("Input power"); + inGrid->addWidget(Label_Pin, 4,0); + QSpinBox_InputPower = new QDoubleSpinBox(0); + QSpinBox_InputPower->setMinimum(-1e3); + QSpinBox_InputPower->setMaximum(1e5); + connect(QSpinBox_InputPower, SIGNAL(valueChanged(double)), this, SLOT(slotCalculate())); + inGrid->addWidget(QSpinBox_InputPower, 4,1); + QStringList powerunits; + powerunits.append("mW"); + powerunits.append("W"); + powerunits.append("dBm"); + powerunits.append(QString("dB%1V [75%2]").arg(QChar(0xbc, 0x03)).arg(QChar(0xa9, 0x03))); + powerunits.append(QString("dB%1V [50%2]").arg(QChar(0xbc, 0x03)).arg(QChar(0xa9, 0x03))); + powerunits.append(QString("dBmV [75%1]").arg(QChar(0xa9, 0x03))); + powerunits.append(QString("dBmV [50%1]").arg(QChar(0xa9, 0x03))); + Combo_InputPowerUnits = new QComboBox(); + Combo_InputPowerUnits->addItems(powerunits); + Combo_InputPowerUnits->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); + Combo_InputPowerUnits->setCurrentIndex(2);//Input power is mostly given in dBm + connect(Combo_InputPowerUnits, SIGNAL(currentIndexChanged(QString)), this, + SLOT(slot_ComboInputPowerUnits_Changed(const QString&)) ); + inGrid->addWidget(Combo_InputPowerUnits, 4,2); + + //Central frequency + Label_Freq = new QLabel(tr("Freq:"), InputGroup); + Label_Freq->setWhatsThis("Central frequency"); + Label_Freq->hide(); + inGrid->addWidget(Label_Freq, 5,0); + QSpinBox_Freq = new QDoubleSpinBox(0); + QSpinBox_Freq->setMinimum(0.1); + QSpinBox_Freq->setMaximum(1e5); + QSpinBox_Freq->setValue(1500); + QSpinBox_Freq->hide(); + connect(QSpinBox_Freq, SIGNAL(valueChanged(double)), this, SLOT(slotCalculate())); + inGrid->addWidget(QSpinBox_Freq, 5,1); + QStringList frequnits; + frequnits.append("GHz"); + frequnits.append("MHz"); + frequnits.append("kHz"); + frequnits.append("Hz"); + Combo_FreqUnits = new QComboBox(); + Combo_FreqUnits->addItems(frequnits); + Combo_FreqUnits->setCurrentIndex(1);//MHz + Combo_FreqUnits->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); + Combo_FreqUnits->hide(); + inGrid->addWidget(Combo_FreqUnits, 5,2); + + + // R higher or lower than Z0. Only for reflection and QW series/shunt + R_Check = new QCheckBox("Use R > Z0"); + R_Check->hide(); + inGrid->addWidget(R_Check, 6,0); + + //Option for transforming a quarter wavelength transmission line into its lumped element equivalent + Check_QW_CLC = new QCheckBox("Use lumped components"); + Check_QW_CLC->hide(); + connect(Check_QW_CLC, SIGNAL(clicked(bool)), this, SLOT(slotTopologyChanged())); + inGrid->addWidget(Check_QW_CLC, 7,0); + + InputGroup->setLayout(inGrid); + + vboxRight->addWidget(InputGroup); + + Calculate = new QPushButton(tr("Put into Clipboard")); + connect(Calculate, SIGNAL(clicked()), SLOT(slotCalculate())); + + vboxRight->addWidget(Calculate); + + QGroupBox * OutputGroup = new QGroupBox (tr("Output")); + QGridLayout * outGrid = new QGridLayout(OutputGroup); + outGrid->setSpacing(5); + outGrid->setColumnMinimumWidth(3, 20); + + //Power dissipation label + PdissLabel = new QLabel("Pdiss", OutputGroup); + PdissLabel->setAlignment(Qt::AlignCenter); + outGrid->addWidget(PdissLabel, 0,5); + + //R1 value and labels + LabelR1 = new QLabel(tr("R1:"), OutputGroup); + outGrid->addWidget(LabelR1, 1,0); + lineEdit_R1 = new QLineEdit(tr("--"), OutputGroup); + lineEdit_R1->setReadOnly(true); + outGrid->addWidget(lineEdit_R1, 1,1); + QLabel *Label4 = new QLabel(QChar(0xa9, 0x03), OutputGroup); + outGrid->addWidget(Label4, 1,2); + + //R1 power dissipation + lineEdit_R1_Pdiss = new QLineEdit(tr("--"), OutputGroup); + lineEdit_R1_Pdiss->setReadOnly(true); + outGrid->addWidget(lineEdit_R1_Pdiss, 1,5); + ComboR1_PowerUnits = new QComboBox(); + ComboR1_PowerUnits->addItems(powerunits); + ComboR1_PowerUnits->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); + connect(ComboR1_PowerUnits, SIGNAL(currentIndexChanged(QString)), this, + SLOT(slot_ComboR1PowerUnits_Changed(const QString&)) ); + outGrid->addWidget(ComboR1_PowerUnits, 1,6); + + //R2 value and labels + LabelR2 = new QLabel(tr("R2:"), OutputGroup); + outGrid->addWidget(LabelR2, 2,0); + lineEdit_R2 = new QLineEdit(tr("--"), OutputGroup); + lineEdit_R2->setReadOnly(true); + outGrid->addWidget(lineEdit_R2, 2,1); + QLabel *Label5 = new QLabel(QChar(0xa9, 0x03), OutputGroup); + outGrid->addWidget(Label5, 2,2); + + //R2 power dissipation + lineEdit_R2_Pdiss = new QLineEdit(tr("--"), OutputGroup); + lineEdit_R2_Pdiss->setReadOnly(true); + outGrid->addWidget(lineEdit_R2_Pdiss, 2,5); + ComboR2_PowerUnits = new QComboBox(); + ComboR2_PowerUnits->addItems(powerunits); + ComboR2_PowerUnits->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); + connect(ComboR2_PowerUnits, SIGNAL(currentIndexChanged(QString)), this, + SLOT(slot_ComboR2PowerUnits_Changed(const QString&)) ); + outGrid->addWidget(ComboR2_PowerUnits, 2,6); + + //R3 value and labels + LabelR3 = new QLabel(tr("R3:"), OutputGroup); + outGrid->addWidget(LabelR3, 3,0); + lineEdit_R3 = new QLineEdit(tr("--"), OutputGroup); + lineEdit_R3->setReadOnly(true); + outGrid->addWidget(lineEdit_R3, 3,1); + LabelR3_Ohm = new QLabel(QChar(0xa9, 0x03), OutputGroup); + outGrid->addWidget(LabelR3_Ohm, 3,2); + + //R3 power dissipation + lineEdit_R3_Pdiss = new QLineEdit(tr("--"), OutputGroup); + lineEdit_R3_Pdiss->setReadOnly(true); + outGrid->addWidget(lineEdit_R3_Pdiss, 3,5); + ComboR3_PowerUnits = new QComboBox(); + ComboR3_PowerUnits->addItems(powerunits); + ComboR3_PowerUnits->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); + connect(ComboR3_PowerUnits, SIGNAL(currentIndexChanged(QString)), this, + SLOT(slot_ComboR3PowerUnits_Changed(const QString&)) ); + outGrid->addWidget(ComboR3_PowerUnits, 3,6); + + //R4 value and labels + LabelR4 = new QLabel(tr("R4:"), OutputGroup); + outGrid->addWidget(LabelR4, 4,0); + lineEdit_R4 = new QLineEdit(tr("--"), OutputGroup); + lineEdit_R4->setReadOnly(true); + outGrid->addWidget(lineEdit_R4, 4,1); + LabelR4_Ohm = new QLabel(QChar(0xa9, 0x03), OutputGroup); + outGrid->addWidget(LabelR4_Ohm, 4,2); + + //R4 power dissipation + lineEdit_R4_Pdiss = new QLineEdit(tr("--"), OutputGroup); + lineEdit_R4_Pdiss->setReadOnly(true); + outGrid->addWidget(lineEdit_R4_Pdiss, 4,5); + ComboR4_PowerUnits = new QComboBox(); + ComboR4_PowerUnits->addItems(powerunits); + ComboR4_PowerUnits->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); + connect(ComboR4_PowerUnits, SIGNAL(currentIndexChanged(QString)), this, + SLOT(slot_ComboR4PowerUnits_Changed(const QString&)) ); + outGrid->addWidget(ComboR4_PowerUnits, 4,6); + + //Hide R4 widgets. R4 is only used in the Bridge Tee attenuator + LabelR4->hide(); + lineEdit_R4->hide(); + LabelR4_Ohm->hide(); + lineEdit_R4_Pdiss->hide(); + ComboR4_PowerUnits->hide(); + + + // This variable is necessary to provide the power unit conversion when the corresponding power + // comboboxes are changed + LastUnits.append("dBm");//Input power + LastUnits.append("mW");//Power dissipated by R1 + LastUnits.append("mW");//Power dissipated by R2 + LastUnits.append("mW");//Power dissipated by R3 + LastUnits.append("mW");//Power dissipated by R4 + + + vboxRight->addWidget(OutputGroup); + + // put Left and Right together + QHBoxLayout *hbox = new QHBoxLayout(); + hbox->addLayout(vboxLeft); + hbox->addLayout(vboxRight); + + // append the result label + LabelResult = new QLabel(tr(""));//It is not needed to provide a "Success" message. + //The synthesis will fail only for certain attenuation-Z0 ratios + //in Pi or Tee type attenuators + LabelResult->setAlignment(Qt::AlignHCenter); + LabelResult->setStyleSheet("QLabel {color : red; }"); + + QVBoxLayout *vbox = new QVBoxLayout(); + vbox->addLayout(hbox); + vbox->addWidget(LabelResult); + + centralWidget->setLayout(vbox); + slotCalculate(); +} + +QucsAttenuator::~QucsAttenuator() +{ + delete DoubleVal; +} + +void QucsAttenuator::slotHelpIntro() +{ + QMessageBox::about(this, tr("Qucs Attenuator Help"), + tr("QucsAttenuator is an attenuator synthesis program. " + "To create a attenuator, simply enter all " + "the input parameters and press the calculation button. " + "Immediately, the " + "schematic of the attenuator is calculated and " + "put into the clipboard. Now go to Qucs, " + "open an schematic and press " + "CTRL-V (paste from clipboard). The attenuator " + "schematic can now be inserted. " + "Have lots of fun!")); +} + +void QucsAttenuator::slotHelpAboutQt() +{ + QMessageBox::aboutQt(this, tr("About Qt")); +} + +void QucsAttenuator::slotHelpAbout() +{ + QMessageBox::about(this, tr("About..."), + "QucsAttenuator Version " PACKAGE_VERSION+ + tr("\nAttenuator synthesis program\n")+ + tr("Copyright (C) 2006 by")+" Toyoyuki Ishikawa" + "\n"+ + tr("Copyright (C) 2006 by")+" Stefan Jahn" + "\n"+ + tr("Copyright (C) 2024 by")+" Andrés Martínez Mera" + "\n" + "\nThis is free software; see the source for copying conditions." + "\nThere is NO warranty; not even for MERCHANTABILITY or " + "\nFITNESS FOR A PARTICULAR PURPOSE.\n\n"); +} + +void QucsAttenuator::slotQuit() +{ + int tmp; + tmp = x(); + tmp = y(); + tmp = width(); + tmp = height(); + Q_UNUSED(tmp); + + qApp->quit(); +} + +void QucsAttenuator::slotSetText_Zin( double val ) +{ + if((ComboTopology->currentIndex() == BRIDGE_TYPE) || (ComboTopology->currentIndex() == REFLECTION_TYPE)) { + QSpinBox_Zout->blockSignals(true); + QSpinBox_Zout->setValue(val); + QSpinBox_Zout->blockSignals(false); + } + slotCalculate(); +} + +void QucsAttenuator::slotSetText_Zout( double val) +{ + if(ComboTopology->currentIndex() == BRIDGE_TYPE) { + QSpinBox_Zin->blockSignals(true); + QSpinBox_Zin->setValue(val); + QSpinBox_Zin->blockSignals(false); + } + slotCalculate(); +} + +void QucsAttenuator::slotTopologyChanged() +{ + switch(ComboTopology->currentIndex()) + { + case PI_TYPE: + pixTopology->setPixmap(QPixmap((":/bitmaps/att_pi.png"))); + LabelImp1->setText("Zin:"); + LabelImp2->show(); + QSpinBox_Zout->show(); + LabelImp2_Ohm->show(); + LabelR2->setText("R2:"); + LabelR3->show(); + LabelR3->setText("R3:"); + LabelR4->hide(); + lineEdit_R3->show(); + lineEdit_R4->hide(); + LabelR4_Ohm->hide(); + LabelR3_Ohm->show(); + lineEdit_R3_Pdiss->show(); + ComboR3_PowerUnits->show(); + lineEdit_R4_Pdiss->hide(); + ComboR4_PowerUnits->hide(); + R_Check->hide(); + Check_QW_CLC->hide(); + Label_Freq->hide(); + QSpinBox_Freq->hide(); + Combo_FreqUnits->hide(); + lineEdit_R2_Pdiss->show(); + ComboR2_PowerUnits->show(); + break; + case TEE_TYPE: + pixTopology->setPixmap(QPixmap((":/bitmaps/att_tee.png"))); + LabelImp1->setText("Zin:"); + LabelImp2->show(); + QSpinBox_Zout->show(); + LabelImp2_Ohm->show(); + LabelR2->setText("R2:"); + LabelR3->show(); + LabelR3->setText("R3:"); + LabelR4->hide(); + lineEdit_R3->show(); + lineEdit_R4->hide(); + LabelR4_Ohm->hide(); + LabelR3_Ohm->show(); + lineEdit_R3_Pdiss->show(); + ComboR3_PowerUnits->show(); + lineEdit_R4_Pdiss->hide(); + ComboR4_PowerUnits->hide(); + R_Check->hide(); + Check_QW_CLC->hide(); + Label_Freq->hide(); + QSpinBox_Freq->hide(); + Combo_FreqUnits->hide(); + lineEdit_R2_Pdiss->show(); + ComboR2_PowerUnits->show(); + break; + case BRIDGE_TYPE: + pixTopology->setPixmap(QPixmap((":/bitmaps/att_bridge.png"))); + LabelImp1->setText("Z0:"); + LabelImp2->hide(); + QSpinBox_Zout->hide(); + LabelImp2_Ohm->hide(); + LabelR2->setText("R4:"); + LabelR3->show(); + LabelR4->show(); + LabelR3->setText("Z01:"); + LabelR4->setText("Z02:"); + lineEdit_R3->show(); + lineEdit_R4->show(); + LabelR3_Ohm->show(); + LabelR4_Ohm->show(); + lineEdit_R3_Pdiss->show(); + lineEdit_R4_Pdiss->show(); + ComboR3_PowerUnits->show(); + ComboR4_PowerUnits->show(); + QSpinBox_Zout->setValue(QSpinBox_Zin->value()); + R_Check->hide(); + Check_QW_CLC->hide(); + Label_Freq->hide(); + QSpinBox_Freq->hide(); + Combo_FreqUnits->hide(); + lineEdit_R2_Pdiss->show(); + ComboR2_PowerUnits->show(); + break; + case REFLECTION_TYPE: + pixTopology->setPixmap(QPixmap((":/bitmaps/att_reflection.png"))); + LabelImp1->setText("Z0:"); + LabelImp2->hide(); + QSpinBox_Zout->hide(); + LabelImp2_Ohm->hide(); + LabelR2->setText("R2:"); + LabelR3->hide(); + LabelR4->hide(); + lineEdit_R3->hide(); + lineEdit_R4->hide(); + LabelR3_Ohm->hide(); + LabelR4_Ohm->hide(); + lineEdit_R3_Pdiss->hide(); + lineEdit_R4_Pdiss->hide(); + ComboR3_PowerUnits->hide(); + ComboR4_PowerUnits->hide(); + QSpinBox_Zout->setValue(QSpinBox_Zin->value()); + R_Check->show(); + Check_QW_CLC->hide(); + Label_Freq->hide(); + QSpinBox_Freq->hide(); + Combo_FreqUnits->hide(); + lineEdit_R2_Pdiss->show(); + ComboR2_PowerUnits->show(); + break; + case QW_SERIES_TYPE: + if (Check_QW_CLC->isChecked()) pixTopology->setPixmap(QPixmap((":/bitmaps/qw_series_CLC.png"))); + else pixTopology->setPixmap(QPixmap((":/bitmaps/qw_series.png"))); + LabelImp1->setText("Z0:"); + LabelImp2->hide(); + QSpinBox_Zout->hide(); + LabelImp2_Ohm->hide(); + LabelR2->setText("R2:"); + LabelR3->show(); + LabelR3->setText("R3:"); + LabelR4->show(); + LabelR4->setText("Zout"); + lineEdit_R3->show(); + lineEdit_R4->show(); + LabelR3_Ohm->show(); + LabelR4_Ohm->show(); + lineEdit_R3_Pdiss->show(); + lineEdit_R4_Pdiss->hide(); + ComboR3_PowerUnits->show(); + ComboR4_PowerUnits->hide(); + R_Check->hide(); + Check_QW_CLC->show(); + Label_Freq->show(); + QSpinBox_Freq->show(); + Combo_FreqUnits->show(); + lineEdit_R2_Pdiss->show(); + ComboR2_PowerUnits->show(); + break; + case QW_SHUNT_TYPE: + if (Check_QW_CLC->isChecked()) pixTopology->setPixmap(QPixmap((":/bitmaps/qw_shunt_CLC.png"))); + else pixTopology->setPixmap(QPixmap((":/bitmaps/qw_shunt.png"))); + LabelImp1->setText("Z0:"); + LabelImp2->hide(); + QSpinBox_Zout->hide(); + LabelImp2_Ohm->hide(); + LabelR2->setText("R2:"); + LabelR3->show(); + LabelR3->setText("R3:"); + LabelR4->show(); + LabelR4->setText("Zout"); + lineEdit_R3->show(); + lineEdit_R4->show(); + LabelR4_Ohm->show(); + LabelR3_Ohm->show(); + lineEdit_R3_Pdiss->show(); + lineEdit_R4_Pdiss->hide(); + ComboR3_PowerUnits->show(); + ComboR4_PowerUnits->hide(); + R_Check->hide(); + Check_QW_CLC->show(); + Label_Freq->show(); + QSpinBox_Freq->show(); + Combo_FreqUnits->show(); + lineEdit_R2_Pdiss->show(); + ComboR2_PowerUnits->show(); + break; + case L_PAD_1ST_SERIES: + case L_PAD_1ST_SHUNT: + (ComboTopology->currentIndex() == L_PAD_1ST_SERIES) ? pixTopology->setPixmap(QPixmap((":/bitmaps/L_pad_1st_series.png"))) + : pixTopology->setPixmap(QPixmap((":/bitmaps/L_pad_1st_shunt.png"))); + LabelImp1->setText("Z0:"); + LabelImp2->hide(); + QSpinBox_Zout->hide(); + LabelImp2_Ohm->hide(); + LabelR2->setText("R2:"); + LabelR3->show(); + LabelR3->setText("Zout:"); + LabelR4->hide(); + lineEdit_R3->show(); + lineEdit_R4->hide(); + LabelR4_Ohm->hide(); + LabelR3_Ohm->show(); + lineEdit_R3_Pdiss->hide(); + ComboR3_PowerUnits->hide(); + lineEdit_R4_Pdiss->hide(); + ComboR4_PowerUnits->hide(); + R_Check->hide(); + Check_QW_CLC->hide(); + Label_Freq->hide(); + QSpinBox_Freq->hide(); + Combo_FreqUnits->hide(); + lineEdit_R2_Pdiss->show(); + ComboR2_PowerUnits->show(); + break; + + case R_SERIES: + case R_SHUNT: + (ComboTopology->currentIndex() == R_SERIES) ? pixTopology->setPixmap(QPixmap((":/bitmaps/Rseries.png"))) + : pixTopology->setPixmap(QPixmap((":/bitmaps/Rshunt.png"))); + LabelImp1->setText("Zin:"); + LabelImp2->show(); + QSpinBox_Zout->show(); + LabelImp2_Ohm->show(); + LabelR2->setText("Z1:"); + LabelR3->show(); + LabelR3->setText("Z2:"); + LabelR4->hide(); + lineEdit_R3->show(); + lineEdit_R4->hide(); + LabelR4_Ohm->hide(); + LabelR3_Ohm->show(); + lineEdit_R2_Pdiss->hide(); + ComboR2_PowerUnits->hide(); + lineEdit_R3_Pdiss->hide(); + ComboR3_PowerUnits->hide(); + lineEdit_R4_Pdiss->hide(); + ComboR4_PowerUnits->hide(); + R_Check->hide(); + Check_QW_CLC->hide(); + Label_Freq->hide(); + QSpinBox_Freq->hide(); + Combo_FreqUnits->hide(); + break; + + } + adjustSize(); + slotCalculate(); +} + +void QucsAttenuator::slotCalculate() +{ + QUCS_Att qatt; + int result; + QString * s = NULL; + struct tagATT Values; + + Values.Topology = ComboTopology->currentIndex(); + Values.Attenuation = QSpinBox_Attvalue->value(); + Values.Zin = QSpinBox_Zin->value(); + Values.Zout = QSpinBox_Zout->value(); + Values.minR = R_Check->isChecked(); + Values.freq = QSpinBox_Freq->value(); + Values.useLumped = Check_QW_CLC->isChecked(); + + //Frequency scale + if (Combo_FreqUnits->currentText() == "GHz") Values.freq*=1e9; + else if (Combo_FreqUnits->currentText() == "MHz") Values.freq*=1e6; + else if (Combo_FreqUnits->currentText() == "kHz") Values.freq*=1e3; + + //Calculate the input power + Values.Pin = ConvertPowerUnits(QSpinBox_InputPower->value(), Combo_InputPowerUnits->currentText(), "W"); + result = qatt.Calc(&Values); + + if(result != -1) + { + LabelResult->setText(tr("")); + lineEdit_R1->setText(QString::number(Values.R1, 'f', 1)); + lineEdit_R2->setText(QString::number(Values.R2, 'f', 1)); + lineEdit_R3->setText(QString::number(Values.R3, 'f', 1)); + lineEdit_R4->setText(QString::number(Values.R4, 'f', 1)); + + lineEdit_R1_Pdiss->setText(QString::number(ConvertPowerUnits(Values.PR1, QString("W"), ComboR1_PowerUnits->currentText()), 'f', 5)); + lineEdit_R2_Pdiss->setText(QString::number(ConvertPowerUnits(Values.PR2, "W", ComboR2_PowerUnits->currentText()), 'f', 5)); + lineEdit_R3_Pdiss->setText(QString::number(ConvertPowerUnits(Values.PR3, "W", ComboR3_PowerUnits->currentText()), 'f', 5)); + lineEdit_R4_Pdiss->setText(QString::number(ConvertPowerUnits(Values.PR4, "W", ComboR4_PowerUnits->currentText()), 'f', 5)); + + s = qatt.createSchematic(&Values, this->SparBoxCheckbox->isChecked()); + if(!s) return; + + QClipboard *cb = QApplication::clipboard(); + cb->setText(*s); + delete s; + } + else + { + LabelResult->setText(tr("Error: Set Attenuation less than %1 dB").arg(QString::number(Values.MinimumATT, 'f', 3))); + lineEdit_R1->setText("--"); + lineEdit_R2->setText("--"); + lineEdit_R3->setText("--"); + + lineEdit_R1_Pdiss->setText("--"); + lineEdit_R2_Pdiss->setText("--"); + lineEdit_R3_Pdiss->setText("--"); + } + adjustSize(); +} + +//This function is caled when the units of the input power are changed +void QucsAttenuator::slot_ComboInputPowerUnits_Changed(const QString& new_units) +{ + //Convert power + double P = QSpinBox_InputPower->value(); + P =ConvertPowerUnits(P, LastUnits[0], new_units); + QSpinBox_InputPower->setValue(P); + LastUnits[0] = new_units; + + //Change lineedit input policy + if ((new_units == "W") || (new_units == "mW")) + QSpinBox_InputPower->setMinimum(0); + else//dB units + QSpinBox_InputPower->setMinimum(-1e3); +} + +//This function is called when the units of the power dissipated by R1 are changed +void QucsAttenuator::slot_ComboR1PowerUnits_Changed(const QString& new_units) +{ + //Convert power + double P = lineEdit_R1_Pdiss->text().toDouble(); + P =ConvertPowerUnits(P, LastUnits[1], new_units); + lineEdit_R1_Pdiss->setText(QString("%1").arg(P)); + LastUnits[1] = new_units; + + //Change lineedit input policy + if ((new_units == "W") || (new_units == "mW")) + DoubleValPower->setBottom(0); + else//dB units + DoubleValPower->setBottom(-1e9); + lineEdit_R1_Pdiss->setValidator(DoubleValPower); +} + +//This function is caled when the units of the power dissipated by R2 are changed +void QucsAttenuator::slot_ComboR2PowerUnits_Changed(const QString& new_units) +{ + //Convert power + double P = lineEdit_R2_Pdiss->text().toDouble(); + P =ConvertPowerUnits(P, LastUnits[2], new_units); + lineEdit_R2_Pdiss->setText(QString("%1").arg(P)); + LastUnits[2] = new_units; + + //Change lineedit input policy + if ((new_units == "W") || (new_units == "mW")) + DoubleValPower->setBottom(0); + else//dB units + DoubleValPower->setBottom(-1e9); + lineEdit_R2_Pdiss->setValidator(DoubleValPower); +} + +//This function is caled when the units of the power dissipated by R3 are changed +void QucsAttenuator::slot_ComboR3PowerUnits_Changed(const QString& new_units) +{ + //Convert power + double P = lineEdit_R3_Pdiss->text().toDouble(); + P =ConvertPowerUnits(P, LastUnits[3], new_units); + lineEdit_R3_Pdiss->setText(QString("%1").arg(P)); + LastUnits[3] = new_units; + + //Change lineedit input policy + if ((new_units == "W") || (new_units == "mW")) + DoubleValPower->setBottom(0); + else//dB units + DoubleValPower->setBottom(-1e9); + lineEdit_R3_Pdiss->setValidator(DoubleValPower); +} + +//This function is caled when the units of the power dissipated by R4 are changed +void QucsAttenuator::slot_ComboR4PowerUnits_Changed(const QString& new_units) +{ + //Convert power + double P = lineEdit_R4_Pdiss->text().toDouble(); + P =ConvertPowerUnits(P, LastUnits[4], new_units); + lineEdit_R4_Pdiss->setText(QString("%1").arg(P)); + LastUnits[4] = new_units; + + //Change lineedit input policy + if ((new_units == "W") || (new_units == "mW")) + DoubleValPower->setBottom(0); + else//dB units + DoubleValPower->setBottom(-1e9); + lineEdit_R4_Pdiss->setValidator(DoubleValPower); +} + +// This function performs the power units conversion. It receives two arguments: the original units and the +// new units. +double QucsAttenuator::ConvertPowerUnits(double Pin, QString from_units, QString to_units) +{ + //Convert "from_units" to Watts + if (from_units == "W") + ;//Do nothing, this step is not needed + else + if (from_units == "dBm") + Pin = pow(10, 0.1*(Pin-30));//dBm -> W + else + if (from_units == QString("dB%1V [75%2]").arg(QChar(0xbc, 0x03)).arg(QChar(0xa9, 0x03))) + Pin = pow(10, (0.1*Pin-12))/75;//dBuV [75Ohm] -> W + else + if (from_units == QString("dB%1V [50%2]").arg(QChar(0xbc, 0x03)).arg(QChar(0xa9, 0x03))) + Pin = pow(10, (0.1*Pin-12))/50;//dBuV [50Ohm] -> W + else + if (from_units == QString("dBmV [75%2]").arg(QChar(0xa9, 0x03))) + Pin = pow(10, (0.1*Pin-6))/75;//dBmV [75Ohm] -> W + else + if (from_units == QString("dBmV [50%2]").arg(QChar(0xa9, 0x03))) + Pin = pow(10, (0.1*Pin-6))/50;//dBmV [50Ohm] -> W + else + if (from_units == "mW") + Pin = Pin*1e-3;//mW -> W + + //Convert Watts to "to_units" + if (to_units == "W") return Pin;//Already done + if (to_units == "mW") + return Pin*1e3;//W -> mW + + //Convert to dBm. The other units are easily converted from dBm + Pin = 10*log10(Pin)+30;//W->dBm + if (to_units == "dBm") + return Pin;//Already done + else + if (to_units == QString("dB%1V [75%2]").arg(QChar(0xbc, 0x03)).arg(QChar(0xa9, 0x03))) + Pin += 108.7506126339170004686755011380612925566374910126647878220;//W -> dBuV [75Ohm] + else + if (to_units == QString("dB%1V [50%2]").arg(QChar(0xbc, 0x03)).arg(QChar(0xa9, 0x03))) + Pin += 106.9897000433601880478626110527550697323181011853789145868;//W -> dBuV [50Ohm] + else + if (to_units == QString("dBmV [75%2]").arg(QChar(0xa9, 0x03))) + Pin += 48.7506126339170004686755011380612925566374910126647878220;//W -> dBmV [75Ohm] + else + if (to_units == QString("dBmV [50%2]").arg(QChar(0xa9, 0x03))) + Pin += 46.9897000433601880478626110527550697323181011853789145868;//W -> dBmV [50Ohm] + + return Pin; +} diff --git a/qucs/qucs.h b/qucs/qucs.h index 418a8b82..a279b2bc 100644 --- a/qucs/qucs.h +++ b/qucs/qucs.h @@ -355,7 +355,7 @@ public: *distrHor, *distrVert, *selectAll, *callMatch, *changeProps, *addToProj, *editFind, *insEntity, *selectMarker, *createLib, *callConverter, *graph2csv, - *callAtt, *centerHor, *centerVert, *loadModule, *buildModule, *callPwrComb, *callRFLayout; + *callAtt, *centerHor, *centerVert, *loadModule, *buildModule, *callPwrComb, *callRFLayout, *callSPAR_Viewer; QAction *helpQucsIndex; QAction *simSettings; @@ -406,6 +406,7 @@ public slots: void slotCallMatch(); void slotCallAtt(); void slotCallPwrComb(); + void slotCallSPAR_Viewer(); void slotCallRFLayout(); void slotHelpIndex(); // shows a HTML docu: Help Index void slotHelpQucsIndex(); diff --git a/qucs/qucs_actions.cpp b/qucs/qucs_actions.cpp index e8dd8224..1e15fda3 100644 --- a/qucs/qucs_actions.cpp +++ b/qucs/qucs_actions.cpp @@ -867,6 +867,12 @@ void QucsApp::slotCallPwrComb() launchTool(QUCS_NAME "powercombining", "power combining calculation",QStringList()); } +void QucsApp::slotCallSPAR_Viewer() +{ + launchTool(QUCS_NAME "spar-viewer", "s-parameter viewer",QStringList()); +} + + /*! * \brief launch an external application passing arguments * diff --git a/qucs/qucs_init.cpp b/qucs/qucs_init.cpp index e4a5e29c..e7e2ebf7 100644 --- a/qucs/qucs_init.cpp +++ b/qucs/qucs_init.cpp @@ -530,6 +530,11 @@ void QucsApp::initActions() callPwrComb->setWhatsThis(tr("Power combining\n\nStarts power combining calculation program")); connect(callPwrComb, SIGNAL(triggered()), SLOT(slotCallPwrComb())); + callSPAR_Viewer = new QAction(tr("S-parameter Viewer"), this); + callSPAR_Viewer->setStatusTip(tr("Starts S-parameter viewer")); + callSPAR_Viewer->setWhatsThis(tr("S-parameter Viewer\n\nStarts S-parameter viewer")); + connect(callSPAR_Viewer, SIGNAL(triggered()), SLOT(slotCallSPAR_Viewer())); + callConverter = new QAction(tr("Data files converter"), this); callConverter->setShortcut(tr("Ctrl+8")); callConverter->setStatusTip(tr("Convert data file")); @@ -766,6 +771,7 @@ void QucsApp::initMenuBar() toolMenu->addAction(callPwrComb); toolMenu->addAction(callConverter); toolMenu->addAction(callRFLayout); + toolMenu->addAction(callSPAR_Viewer); toolMenu->addSeparator(); cmMenu = new QMenu(tr("Compact modelling"));