Porównaj commity

...

152 Commity
v0.5.2 ... main

Autor SHA1 Wiadomość Data
Holger Müller a04d6d9b39 BIG BINARY BUILD TEST 2024-02-11 12:07:47 +01:00
Holger Müller 00dd59ffc6 library updates 2024-01-10 09:23:35 +01:00
Holger Müller d3216d2ddb
Bugfix/exec crash (#678)
* fixed crash on pop-menu #677
* updated libs
2023-11-14 18:04:18 +01:00
t52ta6ek 96dd23211a
Fix checking for updates (#674)
* Because the application version seems to be a generated value, a different
method needed to be devised to try and determine what the current version
nuber if. We now attempt scan the github release tags for the latest release
version. If a newer version is detected, the application will display the
current version and direct the user to click the link to view the latest
release page.

* Rather, should report error against TAGS_URL and not LATEST_URL at this stage.
2023-08-08 14:30:25 +02:00
Holger Müller 2f8c5346eb
Feature/actions (#672)
* use python3.11
* noconsole for windows binary
* no more 32-Bit versions
2023-08-03 17:01:03 +02:00
t52ta6ek 4257ac152a
Add the nanosaver project icon to resulting Windows application. (#671) 2023-08-03 14:35:03 +02:00
Holger Müller 21e85bdb49
fix swwep update crash #668 (#669) 2023-08-01 12:42:38 +02:00
Holger Müller b4800102d8
Feature/pip updates (#666)
* fix startup script for pyinstaller
* update 3rd party libs
* flatpak setup
* rely on requirements for pyqt6
* xcb fixes for linux binary build
* removed old linux build workflow
2023-07-31 13:31:27 +02:00
Name abb80a5160
Sweep.py: add getters and setters for private fields (#659)
* style, Sweep.py: remove a double negation

* style, NanoVNASaver.py: simplify sweepSource computation

* Sweep.py: add getters and setters for private fields

Beware that this commit removes a lock from
SweepSettings.update_tex_power, and adds one to
DeviceSettings.updatecustomPoint.
Both changse may be incorrect, depending on the role of the lock
(issue #657).

Follows: 6eb24f23 d09b55e1 dbea311a

Since d09b55e1, the Properties.name class attribute is overriden by
each assignment to the properties.name instance attribute.
This is most probably unwanted.

This commit

 * removes @dataclass, which is confusing as some attributes are
   managed because of the lock.
   Because of this, it has to restore __repr__ and __eq__.
 * provides getters and setters for private attributes, and
   protects each update by a thread lock
 * adds a regression test for the bug fixed by d09b55e1 (immutable
   properties).
2023-07-30 09:03:06 +02:00
t52ta6ek 5bed1bc6cc
Control panel width increased a few pixels to allow Markers -> "reference" checkbox to be capitalized like the other checkboxes and radio buttons. Now able to be displayed as "Reference" without cutting off the text of the "Enable Delta Marker" checkbox. (#665)
Added icon_48x48.ico so that pyinstaller/auto-py-to-exe can create a Windows executable with the actual NanoVNA-Saver project icon instead of a generic Python icon. The .ico was created by converting the icon_48x48.png image.

"Files" button in control panel changed to "Files ..." to be consistent with other "..." actions where a user will take additional action after pressing the button.

Minor corrections in the Calibration page "Are you sure?" and "Calibration assistant" pop-ups where text was being run together. e.g. "doso" instead of "do so" etc.

Fix issue: #663
2023-07-30 08:52:49 +02:00
t52ta6ek 20c1e4ec7c
With current firmware, the JNCRadio, SV4401A, SV6301A devices allows 1001 datapoints (#662)
* With current firmware, these JNCRadio_VNA_3G, SV4401A, SV6301A devices allow 1001 datapoints.

Updated datapoints dropdown to show their respective minimums, the 'typical' 101, device default 501 and maximum 1001 points. Users may choose any other values between min and max not seen in the dropdown list using the custom datapoints option.

These devices now use the factory default 501 datapoints as their initial datapoints value.

* cut-paste typo in sweep_points_min (facepalm)
2023-07-26 07:54:31 +02:00
Name 21ba0ef665
Simplify the in-source launcher script (#660)
The nanovna-saver.py script is ignored by setup.cfg, its only purpose
is to test the version in the source directory.

According to https://setuptools.pypa.io/en/latest/history.html,
pkg_resources.py2warn has been removed from setuptools in 2020
2023-07-17 14:21:16 +02:00
t52ta6ek eff83097f8
Added support for devices reporting as SV4401A and SV6301A using Sysj… (#655)
* Added support for devices reporting as SV4401A and SV6301A using Sysjoint's support for SV4401A as model.
2023-07-08 10:41:09 +02:00
Name dbea311a02
Adapt sweep settings imports and tests to source changes (#653)
6eb24f23 from merge request 625
made NamedTuple an ancestor of Properties, adapting the imports and
tests.

d09b55e1 from merge request 628
removed it but forgot to remove the related changes.
2023-07-08 10:39:35 +02:00
t52ta6ek a4a923a649
Jncradiovna3g - support Sysjoint-Tek / CHELEGANCE JNCRadio VNA 3G (#652)
* Added support for Sysjoint-Tek / CHELEGANCE JNCRadio VNA 3G
   Device reported as JNCRadio and Custom points added under Manage tab.
* Minor change to correct spelling or error msg.
2023-07-05 10:18:51 +02:00
Martin ce0c7dd226
Redesign of the About window (#648)
- Info and version check closer together
- More precise wording
- Reflect the connection status of the VNA

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2023-07-05 10:00:47 +02:00
Martin 546d3b188a
use correct URL for NanoVNASaver version check (#647)
Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2023-07-05 10:00:06 +02:00
Martin 1f233819d2
option --auto-connect, connect automatically if one device detected (#645)
* option --auto-connect, connect automatically to the 1st detected device
* autoconnect only if there is exactly one device

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2023-07-05 09:59:26 +02:00
Martin a8ffbc3aee
fix "Could not parse stylesheet of object QGroupBox..." error (#643)
* fix "Could not parse stylesheet of object QGroupBox..." error

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>

* fix https://github.com/NanoVNA-Saver/nanovna-saver/issues/596

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>

---------

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2023-06-21 14:08:19 +02:00
Holger Müller ce8a59d478 fixed typo in calibration code #637 2023-05-13 16:49:34 +02:00
Crispin Tschirky aab2a15f69
Build of MacOS app bundle NanoVNASaver.app (#634)
* add icon for MacOS based on icon_48x48.png
* add macos build script to build NanoVNASaver.app for current architecture
* add releas app action to build: NanoVNASaver.app-x86_64.tar.gz action
2023-05-05 10:40:54 +02:00
Sascha Silbe 9b4575e307
nanovna-saver.py: fix execution from outside source directory (#632)
nanovna-saver.py can be called from outside the source directory. The
module import path needs to be resolved relative to the source
directory, not relative to the current working directory of the
process.

Fixes: b0110002 ("moved to pyscaffold directory structure")
2023-05-02 11:12:21 +02:00
Henk Vergonet 8f86722c1e
fix: PyQt6 fixes (#629)
More fixes for PyQt6 should now be usable again with >python3.7 & PyQt6

Calibration - tested
Display Configuration - tested
2023-04-18 12:14:21 +02:00
Henk Vergonet d09b55e1ae
fix: sweep settings and hw version update (#628)
* Remove privacy breaches from the HTML documentation

Icons and screenshots should not inform distant websites each time an
user reads readme.html.

The suggestions in this draft are independent and will probably be
discussed separately, but they affect a single file so for a first
review a single commit is convenient.

* fix: sweep settings and hw version update

NamedTuples are inmutable either use _replace or use the class itself.

* Revert "Remove privacy breaches from the HTML documentation"

---------

Co-authored-by: Nicolas Boulenguez <nicolas@debian.org>
2023-04-15 19:23:28 +02:00
Name 6eb24f2315
Trivial style changes (#625)
* Style: update type annotations

* Style: simplify extraction of version from metadata

* Style: replace some handwritten classes with namedtuples or dataclasses

* RIZ.py: remove unused import

* Style: remove some redundant lambda constructs

* Marker/Values: remove __init__ parameters

Mutable default values imply some complexity. In this case, the
constructor is always called without arguments.
2023-03-22 15:56:59 +01:00
Name d89c9f9d94
tests/data/s2p: remove DOS end of line characters (#624)
Git sometimes replaces CRLF with a single characters, so the tar.gz
archives generated by github from tags differ from the tagged commit.

The tests also pass with a single line terminator.
2023-03-20 09:28:20 +01:00
Name f34f3d1f67
Add an UNIX-style manual page (#622)
Some redistributors want a manual page for each executable in path.

The installation path may differ accross systems, so the manual page
is not installed by default.
2023-03-20 09:27:06 +01:00
Name 1cd5c052db
__main__.py: remove /bin/env shebang (#623)
The file is not intended for direct execution,
not installed in the path,
and not marked as executable.
Its extension is sufficient for editors to trigger syntax highlighting.

The shebang seems to only trigger warnings on systems wher /bin/env is
unavailable (it may be in /usr/bin for example).
2023-03-20 09:25:55 +01:00
Holger Müller 52cdac4f52 back to python 3.9 for windows 2023-03-15 14:19:30 +01:00
Holger Müller fafe0b2536 setup fixes 2023-03-15 14:07:32 +01:00
Holger Müller c5e00666aa pip update 2023-03-15 13:52:29 +01:00
Holger Müller 8dec23296e updated workflows 2023-03-15 13:14:14 +01:00
Holger Müller d1592ac1a3 github actions fetch-depth 2023-03-15 13:02:49 +01:00
Holger Müller 4e06fc53cf generate version for binary builds 2023-03-15 12:55:56 +01:00
Holger Müller 45c2338196 Merge branch 'main' of github.com:NanoVNA-Saver/nanovna-saver 2023-03-15 12:14:10 +01:00
Holger Müller 2bab4d4b0d try to get version in about 2023-03-15 12:06:13 +01:00
Holger Müller b3a9f6d8cb
Delete _version.py 2023-03-14 21:18:02 +01:00
Holger Müller 3c752a9731 build fixes 2023-03-14 21:00:56 +01:00
Holger Müller b2c2598d3c requierements and workflow 2023-03-14 20:10:27 +01:00
dependabot[bot] c18a6c226f
Bump setuptools from 65.3.0 to 65.5.1 (#617)
Bumps [setuptools](https://github.com/pypa/setuptools) from 65.3.0 to 65.5.1.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/CHANGES.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v65.3.0...v65.5.1)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-14 19:24:46 +01:00
Holger Müller dd2f5b8a5d
Feature/tinysa (#616)
* TinySA and PyQt6 fixes
2023-03-14 19:22:46 +01:00
Holger Müller b322d3dc09 Updated changes in README.rst 2023-03-13 12:13:44 +01:00
Holger Müller 5b21315a11 PyQt6 updates 2023-03-13 12:13:44 +01:00
Holger Müller 9ace7d8cd4 PyQt6 fixes 2023-03-13 12:13:44 +01:00
Holger Müller b768a8e01b pyqt6 2023-03-13 12:13:44 +01:00
Holger Müller 2c58b2ba8f
Merge pull request #614 from MarcFontaine/main
restore options.entry_points
2023-03-10 19:02:58 +01:00
MarcFontaine a45baea9e2
restore options.entry_points 2023-03-10 18:46:50 +01:00
Holger Müller db5cd98e03
Merge pull request #612 from NanoVNA-Saver/feature/v0.6.0
Feature/v0.6.0
2023-03-08 13:57:16 +01:00
Holger Müller 74792b3192
Merge pull request #611 from zarath/updates
used black for reformatting/lintig
2023-03-08 09:52:38 +01:00
Holger Müller 50b540a832 used black for reformatting/lintig 2023-03-08 09:40:39 +01:00
Holger Müller 094b0185e7
Merge pull request #609 from zarath/updates
moved to pyscaffold directory structure
2023-03-07 21:18:17 +01:00
Holger Müller b0110002ec moved to pyscaffold directory structure
and fixed resonances analysys crash #608
2023-03-07 21:11:12 +01:00
Holger Müller c0e177bf1a linting 2023-03-05 13:33:05 +01:00
Holger Müller 185a64b5ae removed unused imports 2023-02-28 21:18:00 +01:00
Holger Müller f7d72d4320 fixed another python3.11 float int crash 2023-02-28 20:32:19 +01:00
Holger Müller 82e582b9c0 Release v0.5.5 2023-02-28 19:52:51 +01:00
Holger Müller 59e7e1809a library version updates 2023-02-28 19:39:46 +01:00
Holger Müller 0b82754350 Updated installation instructions 2023-02-28 19:07:44 +01:00
Holger Müller 6f6f6c65e1 Move cal data parsing to CalDataSet class 2023-02-28 18:50:18 +01:00
Holger Müller 92a8a0e39d removed duplicate code for scrollareas 2023-02-27 21:01:35 +01:00
Holger Müller b47e665575
Merge pull request #606 from zarath/feature/fixes_for_release
Feature/fixes for release
2023-02-26 19:48:52 +01:00
Holger Müller 7f920249b1 linting 2023-02-26 19:46:38 +01:00
Holger Müller 5860b04ce6 generate cal data file content from CalDataSet 2023-02-26 19:46:23 +01:00
Holger Müller 9231737b70
Merge pull request #605 from zarath/bugfix/#603_calibration
Fix calibration data loading
2023-02-26 10:14:55 +01:00
Holger Müller 29518eef00 Fix calibration data loading 2023-02-26 10:12:05 +01:00
Holger Müller 8e9976a540
Merge pull request #604 from NanoVNA-Saver/feature/release_0.5.5
Feature/release 0.5.5
2023-02-24 20:08:13 +01:00
Holger Müller 0fbb301435 fix for sol data load 2023-02-24 20:03:41 +01:00
Holger Müller 93ee51d236 Workaround for V2 serial int crash 2023-02-24 19:31:58 +01:00
Holger Müller 925cf6d4e1 Merge remote-tracking branch 'upstream/main' into feature/release_0.5.5 2023-02-24 19:31:32 +01:00
Holger Müller 09246b6a34 Linting 2023-02-19 09:08:14 +01:00
Roel Jordans dc8874c1c9 Stop sweeping with stop button in continous mode 2023-02-19 09:08:14 +01:00
Roel Jordans c4623ddd90 Added brief documentation of the new S11 mu option to README.md
Signed-off-by: Roel Jordans <r.jordans@tue.nl>
2023-02-19 09:08:14 +01:00
Roel Jordans 02371bc56b Clarify calculation and origin of np.conj() 2023-02-19 09:08:14 +01:00
Roel Jordans 0ffe0eaf72 Moved core dimension fields to toplevel menu
also added copy method for new settings
2023-02-19 09:08:14 +01:00
Roel Jordans ee3467e5ec Calculating mu of a core based on given dimensions 2023-02-19 09:08:14 +01:00
Roel Jordans 3d3e31e176 Re-organize RI chart to have RIZ specialization
- separate impedance plotting specific bits
- preparation new chart to plot mu' and mu'' for given core dimensions
2023-02-19 09:08:14 +01:00
Martin f377c999fa fix #592 (crash on mouse click in TDR window) (#593)
* fix #592 (crash on mouse click in TDR window)

AttributeError: 'list' object has no attribute 'size'

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>

* Apply suggestions from code review

Looks better, I was too much focused on the "size" and it was all about the emptiness.

Co-authored-by: Holger Müller <zarath@gmx.de>

---------

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
Co-authored-by: Holger Müller <zarath@gmx.de>
2023-02-19 09:08:14 +01:00
Holger Müller 4cebe94b87 Linting 2023-02-19 09:08:14 +01:00
Holger Müller e4bd720160 Updated github workflows and library versions 2023-02-19 09:08:14 +01:00
Martin a437029fcd Proposal for #485 - make all windows scrollable (#591)
Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2023-02-19 09:08:14 +01:00
Holger Müller d7867b7535 Linting 2023-02-19 09:06:23 +01:00
Holger Müller 09d8b2b866 Merge branch 'feature/release_0.5.5' of github.com:NanoVNA-Saver/nanovna-saver into feature/release_0.5.5 2023-02-19 08:55:26 +01:00
Roel Jordans 69f5089c1f Stop sweeping with stop button in continous mode 2023-02-19 08:54:49 +01:00
Roel Jordans 044c1c885e Added brief documentation of the new S11 mu option to README.md
Signed-off-by: Roel Jordans <r.jordans@tue.nl>
2023-02-19 08:54:49 +01:00
Roel Jordans 0c3f179303 Clarify calculation and origin of np.conj() 2023-02-19 08:54:49 +01:00
Roel Jordans 9b199b53a9 Moved core dimension fields to toplevel menu
also added copy method for new settings
2023-02-19 08:54:49 +01:00
Roel Jordans dc44d33786 Calculating mu of a core based on given dimensions 2023-02-19 08:54:49 +01:00
Roel Jordans 3265d0368b Re-organize RI chart to have RIZ specialization
- separate impedance plotting specific bits
- preparation new chart to plot mu' and mu'' for given core dimensions
2023-02-19 08:54:49 +01:00
Holger Müller a9d0e02e4d Merge remote-tracking branch 'upstream/main' into feature/release_0.5.5 2023-02-14 08:44:25 +01:00
ikatkov 2c868d818f getYPosition fix 2023-02-13 17:26:45 +01:00
Martin f996ee9ceb
fix #592 (crash on mouse click in TDR window) (#593)
* fix #592 (crash on mouse click in TDR window)

AttributeError: 'list' object has no attribute 'size'

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>

* Apply suggestions from code review

Looks better, I was too much focused on the "size" and it was all about the emptiness.

Co-authored-by: Holger Müller <zarath@gmx.de>

---------

Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
Co-authored-by: Holger Müller <zarath@gmx.de>
2023-02-01 18:12:43 +01:00
Holger Müller d313911840 Linting 2023-02-01 11:38:34 +01:00
Holger Müller 7c86009b3e Updated github workflows and library versions 2023-02-01 09:04:09 +01:00
Martin c536de6dc8
Proposal for #485 - make all windows scrollable (#591)
Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2023-02-01 08:28:23 +01:00
Holger Müller d6b2f8119b
fix numpy related crash in tdr modules (#587) 2023-01-25 09:29:33 +01:00
Holger Müller d654ea0441
Removed Changelog.md from setup.py 2023-01-03 16:38:43 +01:00
Jaroslav Škarvada 4d21d6dfdc
desktop file: drop exec and fix it according to the spec (#582)
Fixes #580, fixes #581

Signed-off-by: Jaroslav Škarvada <jskarvad@redhat.com>

Signed-off-by: Jaroslav Škarvada <jskarvad@redhat.com>
2023-01-03 16:06:18 +01:00
Holger Müller ed362a0c4b
Merge pull request #577 from NanoVNA-Saver/feature/release_0.5.4
release 0.5.4
2023-01-01 16:37:13 +01:00
Holger Müller 2a9a4101f0 release 0.5.4 2023-01-01 16:31:38 +01:00
Holger Müller 74d3ac7d07
Feature/python3 11 (#576)
* updated requirements
* use python3.11 in release actions
* fixed np.complex deprecation
2023-01-01 16:06:43 +01:00
Holger Müller f6e1868a95
Merge pull request #572 from zarath/feature/python3_11
dataclass fix for python3.11 compatability
2022-12-18 11:41:11 +01:00
Holger Müller fa03e7d753 dataclass fix for python3.11 compatability 2022-12-08 17:26:17 +01:00
Holger Müller fb50f4a01b
New SI prefixes added - Ronna, Quetta - Yeah! (#570) 2022-11-24 16:42:52 +01:00
Attilio Panniello 9c5b1e01ea
Support of NanoVNA V2 Plus5 on Windows (#566)
(FW 20220814)

Co-authored-by: Attilio Panniello <63241631+apanniello@users.noreply.github.com>
2022-10-16 18:52:35 +02:00
Holger Müller 4d94bbec92 moved a static method to simple function 2022-10-16 08:14:55 +02:00
Holger Müller 1951388c71 suppress pylint import complaints 2022-10-16 08:14:55 +02:00
Martin ad14650fc5 experimental rpm build (works on a debian system)
Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2022-10-16 08:12:49 +02:00
Holger Müller 10d786e787 fixed packagename for purge in Makefile 2022-10-15 21:33:37 +02:00
Holger Müller 239edc1cd0 kept most setup info in setup.cfg
TODO: switch to pyproject.toml in future
2022-10-15 21:33:37 +02:00
Martin 0485e2c8c2 Feat: Improve deb build, add README, icon, desktop file to the deb package
Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2022-10-15 21:33:37 +02:00
Martin c5bee7f3e3
add a Makefile to build a simple debian package (#560)
Signed-off-by: Martin <Ho-Ro@users.noreply.github.com>
2022-10-13 11:57:41 +02:00
Holger Müller 533a543a1b
Feature/documentation (#558)
* documentation updates
* github workflow files
2022-10-09 19:02:05 +02:00
Holger Müller c5a23fcd46
Merge pull request #557 from zarath/bugfix/calibration_crash
use math instead of table for log step calculation
2022-10-07 21:49:01 +02:00
Holger Müller d57ae78efa use math instead of table for log step calculation 2022-10-07 21:42:58 +02:00
Holger Müller 7b9dd5ab0a
Feature/refactoring (#556)
* github workflow naming for ubuntu 22.04
* refactored Hardware.py
* use contextlib instead of try
* use dataclass instead of userdict
* simplyfied sweep worker
* fixed calibration data loading
* explicit import of scipy functions - may fix #555
2022-10-06 18:15:59 +02:00
Holger Müller 114b815c72
Merge pull request #554 from zarath/bugfix/python3.8
bugfix for python3.8 compatability
2022-09-29 12:48:46 +02:00
Holger Müller 35686319cd bugfix for python3.8 compatability
fixes #522, #511
2022-09-29 12:47:02 +02:00
Holger Müller 8e6ab89189 added ubuntu 22.04 release 2022-09-25 18:12:32 +02:00
Holger Müller 62b5c5a1b2
Merge pull request #553 from zarath/bugfix/analysis_import_fix
fixed import pass for analyis module
2022-09-24 20:49:27 +02:00
Holger Müller 193711dc6a fixed import pass for analyis module 2022-09-24 20:47:17 +02:00
Holger Müller 0f19d5aa3c
Merge pull request #550 from zarath/feature/refactoring
Feature/refactoring
2022-09-21 22:37:11 +02:00
Holger Müller 8f224e0e37 refactored ResonanceAnalysis 2022-09-21 18:39:26 +02:00
Holger Müller 400ed54f9a Split history NanoVNASaver/Analysis/ResonanceAnalysis.py to NanoVNASaver/Analysis/EFHWAnalysis.py 2022-09-21 17:46:27 +02:00
Holger Müller ec23d1b3c8 Split history NanoVNASaver/Analysis/ResonanceAnalysis.py to NanoVNASaver/Analysis/EFHWAnalysis.py 2022-09-21 17:46:27 +02:00
Holger Müller 8f016399bb Split history NanoVNASaver/Analysis/ResonanceAnalysis.py to NanoVNASaver/Analysis/EFHWAnalysis.py 2022-09-21 17:46:27 +02:00
Holger Müller d33924511d Split history NanoVNASaver/Analysis/ResonanceAnalysis.py to NanoVNASaver/Analysis/EFHWAnalysis.py 2022-09-21 17:46:27 +02:00
Holger Müller 44e38515bc refactoring analytics 2022-09-21 17:45:08 +02:00
Holger Müller 0b1b73cfc1
fixed refactored attribute 2022-09-20 20:04:27 +02:00
Holger Müller d1ea20f989
Simplified analysis 2022-09-20 19:52:34 +02:00
Holger Müller 01eb028f9f
updated libraries 2022-09-20 07:26:50 +02:00
Holger Müller 24a4ca0ffa
refactored BandStopAnalysis 2022-09-19 19:21:22 +02:00
Holger Müller a73028e2c3
derive BandStop from BandPass 2022-09-18 20:09:56 +02:00
Holger Müller a732aea84b
formatting 2022-09-18 20:09:19 +02:00
Holger Müller a6c3ccc0d3
moved cable data to variable 2022-09-18 20:06:45 +02:00
Holger Müller 8e73456668
added test for printing NaN (Not a Number) 2022-09-18 18:32:35 +02:00
Holger Müller 879d5ddea3
Feature/v0.5.4 pre (#548)
* Simplified VSWR analysis
* Split history VSWRAnalysis.py to ResonanceAnalysis.py
* simplified BandPassAnalysis
2022-09-18 18:03:52 +02:00
Holger Müller 6630568ed9
Merge pull request #544 from zarath/bugfix/541_crash_in_ri
More int float issues fixed.
2022-09-16 08:20:55 +02:00
Holger Müller d163143356
Version bump 2022-09-16 08:15:16 +02:00
Holger Müller 79a577ffe3
pycodestyle fixes 2022-09-15 21:59:55 +02:00
Holger Müller cabb8a4351
pycodestyle fixes 2022-09-15 21:05:07 +02:00
Holger Müller ef6a3c2d0a
Another float glitched trough.. 2022-09-15 20:32:44 +02:00
Holger Müller 1609295bd9
pycodestyle changes 2022-09-15 20:08:23 +02:00
Holger Müller 3f8151aad7
ensure int type in draw coordinates 2022-09-15 17:37:41 +02:00
Holger Müller 36bff6a09d
fixed default for SITools::Value 2022-09-15 08:03:26 +02:00
Holger Müller 8bc452d48f
Linting Charts 2022-09-15 07:53:08 +02:00
Holger Müller d86cbec7c4
Cast varible type to int 2022-09-15 07:51:28 +02:00
Holger Müller 92af43ae22
Merge pull request #547 from fk0815/main
Fix test_restore_dataclass
2022-09-14 21:06:57 +02:00
Frank Kunz c792c1bd69 Fix test_restore_dataclass
Signed-off-by: Frank Kunz <mailinglists@kunz-im-inter.net>
2022-09-14 20:58:27 +02:00
Holger Müller 4159c70558
Imports and linebreaks adjusted 2022-09-14 19:00:25 +02:00
Holger Müller 2f69f5c154
More int float issues fixed. 2022-09-13 18:46:02 +02:00
Holger Müller 05f7b9bbf0
updated documentation 2022-09-11 20:10:44 +02:00
Holger Müller f0e51639b9
fixed typo in workflow 2022-09-11 19:16:25 +02:00
195 zmienionych plików z 15693 dodań i 12368 usunięć

Wyświetl plik

@ -1,20 +1,9 @@
# .coveragerc to control coverage.py
[run]
# ignore GUI code atm.
omit =
NanoVNASaver/About.py
NanoVNASaver/Analysis/*.py
NanoVNASaver/Calibration.py
NanoVNASaver/Charts/*.py
NanoVNASaver/Controls/*.py
NanoVNASaver/Hardware/*.py
NanoVNASaver/Inputs.py
NanoVNASaver/Marker/*.py
NanoVNASaver/NanoVNASaver.py
NanoVNASaver/Settings/Bands.py
NanoVNASaver/SweepWorker.py
NanoVNASaver/Windows/*.py
**/__init__.py
NanoVNASaver/__main__.py
branch = True
source = tests
#omit = src/
[report]
fail_under = 90.0
show_missing = True

Wyświetl plik

@ -0,0 +1,37 @@
---
name: Bug Report
about: Create a report to help NanoVNA-Saver to improve
title: "bug: "
labels: "bug"
assignees: ""
---
# Bug Report
**NanoVNA-Saver version:**
<!-- Please specify commit or tag version. -->
**Current behavior:**
<!-- Describe how the bug manifests. -->
**Expected behavior:**
<!-- Describe what you expect the behavior to be without the bug. -->
**Steps to reproduce:**
<!-- Explain the steps required to duplicate the issue, especially if you are able to provide a sample application. -->
**Related code:**
<!-- If you are able to illustrate the bug or feature request with an example, please provide it here. -->
```
insert short code snippets here
```
**Other information:**
<!-- List any other information that is relevant to your issue. Related issues, suggestions on how to fix, Stack Overflow links, forum links, etc. -->

Wyświetl plik

@ -0,0 +1,35 @@
---
name: Feature Request
about: Suggest an idea for this project
title: "feat: "
labels: "enhancement"
assignees: ""
---
# Feature Request
**Describe the Feature Request**
<!-- A clear and concise description of what the feature request is. Please include if your feature request is related to a problem. -->
**Describe Preferred Solution**
<!-- A clear and concise description of what you want to happen. -->
**Describe Alternatives**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Related Code**
<!-- If you are able to illustrate the bug or feature request with an example, please provide it here. -->
**Additional Context**
<!-- List any other information that is relevant to your issue. Stack traces, related issues, suggestions on how to add, use case, Stack Overflow links, forum links, screenshots, OS if applicable, etc. -->
**If the feature request is approved, would you be willing to submit a PR?**
_(Help can be provided if you need assistance submitting a PR)_
- [ ] Yes
- [ ] No

Wyświetl plik

@ -0,0 +1,7 @@
---
name: Codebase improvement
about: Provide your feedback for the existing codebase. Suggest a better solution for algorithms, development tools, etc.
title: "dev: "
labels: "enhancement"
assignees: ""
---

Wyświetl plik

@ -1,5 +1,6 @@
---
blank_issues_enabled: false
contact_links:
- name: nanovna-users groups.io group
url: https://groups.io/g/nanovna-users/
about: Please ask any questions about using the NanoVNA or NanoVNA-Saver on this mailing list.
- name: NanoVNA-Saver Community Support
url: https://github.com/zarath@gmx.de/nanovna-saver/discussions
about: Please ask and answer questions here.

Wyświetl plik

@ -0,0 +1,40 @@
<!--- Please provide a general summary of your changes in the title above -->
## Pull Request type
<!-- Please try to limit your pull request to one type; submit multiple pull requests if needed. -->
Please check the type of change your PR introduces:
- [] Bugfix
- [] Feature
- [] Code style update (formatting, renaming)
- [] Refactoring (no functional changes, no API changes)
- [] Build-related changes
- [] Documentation content changes
- [] Other (please describe):
## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
Issue Number: N/A
## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by this PR. -->
-
-
-
## Does this introduce a breaking change?
- [] Yes
- [] No
<!-- If this does introduce a breaking change, please describe the impact and migration path for existing applications below. -->
## Other information
<!-- Any other information that is important to this PR, such as screenshots of how the component looks before and after the change. -->

Wyświetl plik

@ -13,26 +13,25 @@ jobs:
strategy:
matrix:
# os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-latest, ]
os: [ubuntu-latest]
# python-version: [3.7, 3.8]
python-version: [3.9, ]
python-version: [3.8, 3.9]
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint with pylint
run: |
pip install pylint
pylint --exit-zero NanoVNASaver
- name: Unittests / Coverage
run: |
pip install pytest-cov
pytest --cov=NanoVNASaver
- uses: actions/checkout@v3
- name: Set up Python 3
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint with pylint
run: |
pip install pylint
pylint --exit-zero NanoVNASaver
- name: Unittests / Coverage
run: |
pip install pytest-cov
pytest --cov=NanoVNASaver

Wyświetl plik

@ -1,4 +1,4 @@
name: Linux Release
name: Modern Linux Release
on:
push:
@ -8,30 +8,41 @@ on:
jobs:
release:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install python
run: |
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt-get update
sudo apt install -y python3.9 python3-pip python3.9-venv \
python3.9-dev \
python3-pyqt5
sudo apt install -y python3.11 python3-pip python3.11-venv \
python3.11-dev \
'^libxcb.*-dev' libx11-xcb-dev \
libglu1-mesa-dev libxrender-dev libxi-dev \
libxkbcommon-dev libxkbcommon-x11-dev
- name: Install dependencies and pyinstall
run: |
python3.9 -m venv build
python3.11 -m venv build
. build/bin/activate
python -m pip install pip==22.2.2 setuptools==65.3.0
python -m pip install pip==23.3.2 setuptools==69.0.3
pip install -r requirements.txt
pip install PyInstaller==5.3
pip install PyInstaller==6.3.0
- name: Build binary
run: |
. build/bin/activate
pyinstaller --onefile -n nanovna-saver nanovna-saver.py
python setup.py -V
pyinstaller --onefile \
-p src \
--add-data "build/lib/python3.11/site-packages/PyQt6/sip.*.so:PyQt6/sip.so" \
--add-data "build/lib/python3.11/site-packages/PyQt6/Qt6:PyQt6/Qt6"
-n nanovna-saver \
nanovna-saver.py
- name: Archive production artifacts
uses: actions/upload-artifact@v1
with:
name: NanoVNASaver.linux
name: NanoVNASaver.linux_modern
path: dist/nanovna-saver

Wyświetl plik

@ -11,19 +11,22 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
- uses: actions/checkout@v3
with:
python-version: 3.9
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: Install dependencies and pyinstall
run: |
python -m pip install pip==22.2.2 setuptools==65.3.0
python -m pip install pip==23.3.2 setuptools==69.0.3
pip install -r requirements.txt
pip install PyInstaller=5.3
pip install PyInstaller==6.3.0
- name: Build binary
run: |
pyinstaller --onefile -n nanovna-saver nanovna-saver.py
python setup.py -V
pyinstaller --onefile -p src -n nanovna-saver nanovna-saver.py
- name: Archive production artifacts
uses: actions/upload-artifact@v1

Wyświetl plik

@ -0,0 +1,43 @@
name: Mac Release App
on:
push:
tags:
- v*
workflow_dispatch:
jobs:
release:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: Get Target Environment
id: targetenv
run: |
echo "arch=`uname -m`" >> "$GITHUB_ENV"
- name: Install dependencies and pyinstall
run: |
python -m pip install pip==23.3.2 setuptools==69.0.3
pip install -r requirements.txt
pip install PyInstaller==6.3.0
- name: Build binary
run: |
python setup.py -V
pyinstaller --onedir -p src -n NanoVNASaver nanovna-saver.py --window --clean -y -i icon_48x48.icns
tar -C dist -zcf ./dist/NanoVNASaver.app-${{ env.arch }}.tar.gz NanoVNASaver.app
echo "Created: NanoVNASaver.app-${{ env.arch }}.tar.gz"
- name: Archive production artifacts
uses: actions/upload-artifact@v1
with:
name: NanoVNASaver.app-${{ env.arch }}.tar.gz
path: dist/NanoVNASaver.app-${{ env.arch }}.tar.gz

Wyświetl plik

@ -11,24 +11,32 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
arch: [x64, x86]
arch: [x64, ]
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
- uses: actions/checkout@v3
with:
python-version: 3.9
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.11
architecture: ${{ matrix.arch }}
- name: Install dependencies and pyinstall
run: |
python -m pip install pip==22.2.2 setuptools==65.3.0
pip install -r requirements.txt
pip install PyInstaller==5.3
python3 -m venv venv
.\venv\Scripts\activate
python3 -m pip install pip==23.3.2
python3 -m pip install -U setuptools setuptools-scm
python3 -m pip install -r requirements.txt
python3 -m pip install PyInstaller==6.3.0
python3 -m pip uninstall -y PyQt6-sip
python3 -m pip install PyQt6-sip==13.6.0
- name: Build binary
run: |
pyinstaller --onefile -n nanovna-saver.exe nanovna-saver.py
.\venv\Scripts\activate
python3 setup.py -V
pyinstaller --onefile --noconsole -i icon_48x48.ico -p src -n nanovna-saver.exe nanovna-saver.py
- name: Archive production artifacts
uses: actions/upload-artifact@v1
with:

44
.github/workflows/stale.yml vendored 100644
Wyświetl plik

@ -0,0 +1,44 @@
---
name: Stale
on:
schedule:
- cron: "0 8 * * *"
workflow_dispatch:
jobs:
stale:
name: 🧹 Clean up stale issues and PRs
runs-on: ubuntu-latest
steps:
- name: 🚀 Run stale
uses: actions/stale@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90
days-before-close: 30
remove-stale-when-updated: true
stale-issue-label: "stale"
exempt-issue-labels: "no-stale,help-wanted"
stale-issue-message: >
There hasn't been any activity on this issue recently, and in order
to prioritize active issues, it will be marked as stale.
Please make sure to update to the latest version and
check if that solves the issue. Let us know if that works for you
by leaving a 👍
Because this issue is marked as stale, it will be closed and locked
in 7 days if no further activity occurs.
Thank you for your contributions!
stale-pr-label: "stale"
exempt-pr-labels: "no-stale"
stale-pr-message: >
There hasn't been any activity on this pull request recently, and in
order to prioritize active work, it has been marked as stale.
This PR will be closed and locked in 7 days if no further activity
occurs.
Thank you for your contributions!

69
.gitignore vendored
Wyświetl plik

@ -1,15 +1,56 @@
/venv/
/env/
.idea/
.vscode/
/build/
/dist/
/nanovna-saver.spec
*.egg-info/
*.pyc
*.cal
settings.json
.gitignore
# Temporary and binary files
*~
*.py[cod]
*.so
*.cfg
!.isort.cfg
!setup.cfg
*.orig
*.log
*.pot
__pycache__/*
.cache/*
.*.swp
*/.ipynb_checkpoints/*
.DS_Store
# Project files
.ropeproject
.project
.pydevproject
.settings
.idea
.vscode
tags
# Package files
*.egg
*.eggs/
.installed.cfg
*.egg-info
# Unittest and coverage
htmlcov/*
.coverage
.flatpak-builder
/nanovna-saver.exe.spec
.coverage.*
.tox
junit*.xml
coverage.xml
.pytest_cache/
# Build and docs folder/files
build/*
dist/*
sdist/*
docs/api/*
docs/_rst/*
docs/_build/*
cover/*
MANIFEST
**/_version.py
.flatpak-builder/*
# Per-project virtualenvs
.venv*/
.conda*/
.python-version

Wyświetl plik

@ -12,4 +12,4 @@ disable=W0614,C0410,C0321,C0111,I0011,C0103
# allow ls for list
good-names=_,a,b,c,dt,db,e,f,fn,fd,i,j,k,v,kv,kw,l,m,n,ls,t,t0,t1,t2,t3,w,h,x,y,z,it,op
[MASTER]
extension-pkg-whitelist=PyQt5
extension-pkg-allow-list=PyQt6.QtWidgets,PyQt6.QtGui,PyQt6.QtCore

27
.readthedocs.yml 100644
Wyświetl plik

@ -0,0 +1,27 @@
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# Build documentation with MkDocs
#mkdocs:
# configuration: mkdocs.yml
# Optionally build your docs in additional formats such as PDF
formats:
- pdf
build:
os: ubuntu-22.04
tools:
python: "3.11"
python:
install:
- requirements: docs/requirements.txt
- {path: ., method: pip}

43
AUTHORS.rst 100644
Wyświetl plik

@ -0,0 +1,43 @@
============
Contributors
============
* Attilio Panniello <attilio.panniello@gmail.com>
* bicycleGuy <michaelrunyan@Michaels-iMac.home>
* Carl Tremblay <cinosh07@hotmail.com>
* cinosh07 <cinosh07@hotmail.com>
* Dan Halbert <halbert@halwitz.org>
* Daniel Lingvay <dlingvay@grubhub.com>
* Davide Gerhard <rainbow@irh.it>
* Denis Bondar <bondar.den@gmail.com>
* dhunt1342 <dhunt1342@users.noreply.github.com>
* DiSlord <dislord@mail.ru>
* Frank Kunz <mailinglists@kunz-im-inter.net>
* Galileo <galileo@pkm-inc.com>
* Holger Mueller <zarath@gmx.de>
* ikatkov <ikatkov@gmail.com>
* Ishmael Samuel <ishmaelsamuel79@gmail.com>
* James Limbouris <james@digitalmatter.com>
* Jaroslav Škarvada <jskarvad@redhat.com>
* Kevin Zembower <kevin@zembower.org>
* Mark Zachmann <Mark.Zachmann@snug.dog>
* Martin <Ho-Ro@users.noreply.github.com>
* Mauro Gaioni <m.gaioni@asst-valcamonica.it>
* Mauro <mauro@lenny.station>
* mihtjel <mihtjel@gmail.com>
* Mike4U <9957897+Mike4U@users.noreply.github.com>
* mss <marcspeck@gmail.com>
* Neil Katin <github2@askneil.com>
* Ohan Smit <psynosaur@gmail.com>
* Olgierd Pilarczyk <opilarczyk@egnyte.com>
* Oscilllator <harry.dudleybestow@gmail.com>
* Patrick Coleman <blinken@gmail.com>
* Peter B Marks <peter.marks@pobox.com>
* Psynosaur <psynosaur@gmail.com>
* RandMental <RandMental@users.noreply.github.com>
* Roel Jordans <r.jordans@tue.nl>
* Rune B. Broberg <mihtjel@gmail.com>
* Sascha Silbe <sascha-pgp@silbe.org>
* sysjoint-tek <63992872+sysjoint-tek@users.noreply.github.com>
* Thomas de Lellis <24543390+t52ta6ek@users.noreply.github.com>
* zstadler <zeev.stadler@gmail.com>

Wyświetl plik

@ -1,128 +0,0 @@
Changelog
=========
0.5.0
-----
- Fix crash on open in use serial device
- Use a Defaults module for all settings -
ignores old .ini settings
- Refactoring and unifying Chart classes
- No more automatic update checks (more privacy)
- Corrected error handling in NanaVNA\_V2 code
0.4.0
-----
- PA0JOZ Enhanced Response Correction
- Fix linux binary build
- Many bugfixes
v0.3.10
------
- Default Band ranges for 5 and 9cm
- Layout should fit on smaller screens
- Fixed fixed axis settings
- Show VNA type in port selector
- Recognise tinySA (screenshot only)
- Some more cables in TDR
- Reference plane applied after calibration
- Calibration fixes by DiSlord
v0.3.9
------
- TX Power on V2
- New analysis
- Magnitude Z Chart
- VSWR Chart improvements
v0.3.8
------
- Allow editing of bands above 2.4GHz
- Restore column layout on start
- Support for Nanovna-F V2
- Fixes a crash with S21 hack
v0.3.7
------
- Added a delta marker
- Segments can now have exponential different step widths
(see logarithmic sweeping)
- More different data points selectable
(shorter are useful on logarithmic sweeping)
- Scrollable marker column
- Markers initialize on start, middle, end
- Frequency input is now more "lazy"
10m, 50K and 1g are now valid for 10MHz, 50kHz and 1GHz
- Added a wavelength field to Markers
- 32 bit windows binaries build in actions
- Stability improvements due to better exception handling
- Workaround for wrong first S21mag value on V2 devices
v0.3.6
------
- Implemented bandwidth setting in device management
v0.3.5
------
- Sweep worker now initializes full dataset on setting changes.
Therefore no resize of charts when doing multi segment sweep
- Changing datapoints in DeviceSettings are reflected in SweepSettings widget step size
- Simplified calibration code by just using scipy.interp1d with fill\_value
- Established Interface class to ease locking and allow non usb connections in future
- Cleaned up VNA code. Added some pause statements to get more robust readings
- Added MagLoopAnalysis
- Touchstone class can now generate interpolated Datapoints for a given frequency
Will be usefull in future analysis code
- Fixed a bug in Version comparison
v0.3.4
------
- Refactored Analysis
- Add Antenna Analysis
- Fixed bug in Through Calibration
- Fixed bug in s2p saving
- Fixed crash when clicking connect with no device connected
- Fixed module error with source installation if
pkg\_resources missing
v0.3.3
------
- Fixed data acquisition with S-A-A-2 / NanoVNA V2
- Refactored calibration code
- Calibration data between known datapoints in now
interpolated by spline interpolation
- Fixed through calibration
v0.3.2
------
- fixed crash with averaging sweeps
also averaging now discards reading by geometrical distance
v0.3.1
------
- fixed crash with calibration assistant
v0.3.0
------
- Support for S-A-A-2 / NanoVNA V2
- Support for 202 Datapoints/scan with NanoVNA-H
- Support for attenuator at S11
- Massive code separation to easy additon of
Hardware, Charts, Analysis ...
Known Issues
------------
- -H / -H4 supports depends on Firmware

322
CONTRIBUTING.rst 100644
Wyświetl plik

@ -0,0 +1,322 @@
============
Contributing
============
Welcome to ``nanovna-saver`` contributor's guide.
This document focuses on getting any potential contributor familiarized
with the development processes, but `other kinds of contributions`_ are also
appreciated.
If you are new to using git_ or have never collaborated in a project previously,
please have a look at `contribution-guide.org`_. Other resources are also
listed in the excellent `guide created by FreeCodeCamp`_ [#contrib1]_.
Please notice, all users and contributors are expected to be **open,
considerate, reasonable, and respectful**. When in doubt, `Python Software
Foundation's Code of Conduct`_ is a good reference in terms of behavior
guidelines.
Issue Reports
=============
If you experience bugs or general issues with ``nanovna-saver``, please have a look
on the `issue tracker`_. If you don't see anything useful there, please feel
free to fire an issue report.
.. tip::
Please don't forget to include the closed issues in your search.
Sometimes a solution was already reported, and the problem is considered
**solved**.
New issue reports should include information about your programming environment
(e.g., operating system, Python version) and steps to reproduce the problem.
Please try also to simplify the reproduction steps to a very minimal example
that still illustrates the problem you are facing. By removing other factors,
you help us to identify the root cause of the issue.
Documentation Improvements
==========================
You can help improve ``nanovna-saver`` docs by making them more readable and coherent, or
by adding missing information and correcting mistakes.
``nanovna-saver`` documentation should use Sphinx_ as its main documentation compiler.
This means that the docs are kept in the same repository as the project code, and
that any documentation update is done in the same way was a code contribution.
.. tip::
Please notice that the `GitHub web interface`_ provides a quick way of
propose changes in ``nanovna-saver``'s files. While this mechanism can
be tricky for normal code contributions, it works perfectly fine for
contributing to the docs, and can be quite handy.
If you are interested in trying this method out, please navigate to
the ``docs`` folder in the source repository_, find which file you
would like to propose changes and click in the little pencil icon at the
top, to open `GitHub's code editor`_. Once you finish editing the file,
please write a message in the form at the bottom of the page describing
which changes have you made and what are the motivations behind them and
submit your proposal.
When working on documentation changes in your local machine, you can
compile them using |tox|_::
tox -e docs
and use Python's built-in web server for a preview in your web browser
(``http://localhost:8000``)::
python3 -m http.server --directory 'docs/_build/html'
Code Contributions
==================
.. todo:: Please include a reference or explanation about the internals of the project.
An architecture description, design principles or at least a summary of the
main concepts will make it easy for potential contributors to get started
quickly.
Submit an issue
---------------
Before you work on any non-trivial code contribution it's best to first create
a report in the `issue tracker`_ to start a discussion on the subject.
This often provides additional considerations and avoids unnecessary work.
Create an environment
---------------------
Before you start coding, we recommend creating an isolated `virtual
environment`_ to avoid any problems with your installed Python packages.
This can easily be done via either |virtualenv|_::
virtualenv <PATH TO VENV>
source <PATH TO VENV>/bin/activate
or Miniconda_::
conda create -n nanovna-saver python=3 six virtualenv pytest pytest-cov
conda activate nanovna-saver
Clone the repository
--------------------
#. Create an user account on |the repository service| if you do not already have one.
#. Fork the project repository_: click on the *Fork* button near the top of the
page. This creates a copy of the code under your account on |the repository service|.
#. Clone this copy to your local disk::
git clone git@github.com:YourLogin/nanovna-saver.git
cd nanovna-saver
#. You should run::
pip install -U pip setuptools -e .
to be able to import the package under development in the Python REPL.
.. todo:: if you are not using pre-commit, please remove the following item:
#. Install |pre-commit|_::
pip install pre-commit
pre-commit install
``nanovna-saver`` comes with a lot of hooks configured to automatically help the
developer to check the code being written.
Implement your changes
----------------------
#. Create a branch to hold your changes::
git checkout -b my-feature
and start making changes. Never work on the main branch!
#. Start your work on this branch. Don't forget to add docstrings_ to new
functions, modules and classes, especially if they are part of public APIs.
#. Add yourself to the list of contributors in ``AUTHORS.rst``.
#. When youre done editing, do::
git add <MODIFIED FILES>
git commit
to record your changes in git_.
.. todo:: if you are not using pre-commit, please remove the following item:
Please make sure to see the validation messages from |pre-commit|_ and fix
any eventual issues.
This should automatically use flake8_/black_ to check/fix the code style
in a way that is compatible with the project.
.. important:: Don't forget to add unit tests and documentation in case your
contribution adds an additional feature and is not just a bugfix.
Moreover, writing a `descriptive commit message`_ is highly recommended.
In case of doubt, you can check the commit history with::
git log --graph --decorate --pretty=oneline --abbrev-commit --all
to look for recurring communication patterns.
#. Please check that your changes don't break any unit tests with::
tox
(after having installed |tox|_ with ``pip install tox`` or ``pipx``).
You can also use |tox|_ to run several other pre-configured tasks in the
repository. Try ``tox -av`` to see a list of the available checks.
Submit your contribution
------------------------
#. If everything works fine, push your local branch to |the repository service| with::
git push -u origin my-feature
#. Go to the web page of your fork and click |contribute button|
to send your changes for review.
.. todo:: if you are using GitHub, you can uncomment the following paragraph
Find more detailed information in `creating a PR`_. You might also want to open
the PR as a draft first and mark it as ready for review after the feedbacks
from the continuous integration (CI) system or any required fixes.
Troubleshooting
---------------
The following tips can be used when facing problems to build or test the
package:
#. Make sure to fetch all the tags from the upstream repository_.
The command ``git describe --abbrev=0 --tags`` should return the version you
are expecting. If you are trying to run CI scripts in a fork repository,
make sure to push all the tags.
You can also try to remove all the egg files or the complete egg folder, i.e.,
``.eggs``, as well as the ``*.egg-info`` folders in the ``src`` folder or
potentially in the root of your project.
#. Sometimes |tox|_ misses out when new dependencies are added, especially to
``setup.cfg`` and ``docs/requirements.txt``. If you find any problems with
missing dependencies when running a command with |tox|_, try to recreate the
``tox`` environment using the ``-r`` flag. For example, instead of::
tox -e docs
Try running::
tox -r -e docs
#. Make sure to have a reliable |tox|_ installation that uses the correct
Python version (e.g., 3.7+). When in doubt you can run::
tox --version
# OR
which tox
If you have trouble and are seeing weird errors upon running |tox|_, you can
also try to create a dedicated `virtual environment`_ with a |tox|_ binary
freshly installed. For example::
virtualenv .venv
source .venv/bin/activate
.venv/bin/pip install tox
.venv/bin/tox -e all
#. `Pytest can drop you`_ in an interactive session in the case an error occurs.
In order to do that you need to pass a ``--pdb`` option (for example by
running ``tox -- -k <NAME OF THE FALLING TEST> --pdb``).
You can also setup breakpoints manually instead of using the ``--pdb`` option.
Maintainer tasks
================
Releases
--------
.. todo:: This section assumes you are using PyPI to publicly release your package.
If instead you are using a different/private package index, please update
the instructions accordingly.
If you are part of the group of maintainers and have correct user permissions
on PyPI_, the following steps can be used to release a new version for
``nanovna-saver``:
#. Make sure all unit tests are successful.
#. Tag the current commit on the main branch with a release tag, e.g., ``v1.2.3``.
#. Push the new tag to the upstream repository_, e.g., ``git push upstream v1.2.3``
#. Clean up the ``dist`` and ``build`` folders with ``tox -e clean``
(or ``rm -rf dist build``)
to avoid confusion with old builds and Sphinx docs.
#. Run ``tox -e build`` and check that the files in ``dist`` have
the correct version (no ``.dirty`` or git_ hash) according to the git_ tag.
Also check the sizes of the distributions, if they are too big (e.g., >
500KB), unwanted clutter may have been accidentally included.
#. Run ``tox -e publish -- --repository pypi`` and check that everything was
uploaded to PyPI_ correctly.
.. [#contrib1] Even though, these resources focus on open source projects and
communities, the general ideas behind collaborating with other developers
to collectively create software are general and can be applied to all sorts
of environments, including private companies and proprietary code bases.
.. <-- start -->
.. todo:: Please review and change the following definitions:
.. |the repository service| replace:: GitHub
.. |contribute button| replace:: "Create pull request"
.. _repository: https://github.com/<USERNAME>/nanovna-saver
.. _issue tracker: https://github.com/<USERNAME>/nanovna-saver/issues
.. <-- end -->
.. |virtualenv| replace:: ``virtualenv``
.. |pre-commit| replace:: ``pre-commit``
.. |tox| replace:: ``tox``
.. _black: https://pypi.org/project/black/
.. _CommonMark: https://commonmark.org/
.. _contribution-guide.org: https://www.contribution-guide.org/
.. _creating a PR: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request
.. _descriptive commit message: https://chris.beams.io/posts/git-commit
.. _docstrings: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html
.. _first-contributions tutorial: https://github.com/firstcontributions/first-contributions
.. _flake8: https://flake8.pycqa.org/en/stable/
.. _git: https://git-scm.com
.. _GitHub's fork and pull request workflow: https://guides.github.com/activities/forking/
.. _guide created by FreeCodeCamp: https://github.com/FreeCodeCamp/how-to-contribute-to-open-source
.. _Miniconda: https://docs.conda.io/en/latest/miniconda.html
.. _MyST: https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html
.. _other kinds of contributions: https://opensource.guide/how-to-contribute
.. _pre-commit: https://pre-commit.com/
.. _PyPI: https://pypi.org/
.. _PyScaffold's contributor's guide: https://pyscaffold.org/en/stable/contributing.html
.. _Pytest can drop you: https://docs.pytest.org/en/stable/how-to/failures.html#using-python-library-pdb-with-pytest
.. _Python Software Foundation's Code of Conduct: https://www.python.org/psf/conduct/
.. _reStructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/
.. _Sphinx: https://www.sphinx-doc.org/en/master/
.. _tox: https://tox.wiki/en/stable/
.. _virtual environment: https://realpython.com/python-virtual-environments-a-primer/
.. _virtualenv: https://virtualenv.pypa.io/en/stable/
.. _GitHub web interface: https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files
.. _GitHub's code editor: https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files

60
Makefile 100644
Wyświetl plik

@ -0,0 +1,60 @@
.PHONY: info
info:
@echo "- type 'make deb' to build a debian package"
@echo "- type 'make rpm' to build an (experimental) rpm package"
@echo "- you need the debian packages"
@echo " fakeroot python3-setuptools python3-stdeb dh-python"
@echo
# build a new debian package and create a link in the current directory
.PHONY: deb
deb: clean
@# build the deb package
PYBUILD_DISABLE=test python3 setup.py \
--command-packages=stdeb.command \
sdist_dsc --compat 10 --package3 nanovnasaver --section electronics \
bdist_deb
@# create a link in the main directory
-@ rm nanovnasaver_*_all.deb
-@ln `ls deb_dist/nanovnasaver_*.deb | tail -1` .
@# and show the result
@ls -l nanovnasaver_*.deb
# build a new rpm package and create a link in the current directory
.PHONY: rpm
rpm: clean
@# build the rpm package
PYBUILD_DISABLE=test python3 setup.py bdist_rpm
@# create a link in the main directory
-@ rm NanoVNASaver-*.noarch.rpm
@ln `ls dist/NanoVNASaver-*.noarch.rpm | tail -1` .
@# and show the result
@ls -l NanoVNASaver-*.noarch.rpm
# remove all package build artifacts (keep the *.deb)
.PHONY: clean
clean:
python setup.py clean
-rm -rf build deb_dist dist *.tar.gz *.egg*
# remove all package build artefacts
.PHONY: distclean
distclean: clean
-rm -f *.deb *.rpm
# build and install a new debian package
.PHONY: debinstall
debinstall: deb
sudo apt install ./nanovnasaver_*.deb
# uninstall this debian package
.PHONY: debuninstall
debuninstall:
sudo apt purge nanovnasaver

Wyświetl plik

@ -0,0 +1,18 @@
[Desktop Entry]
Categories=Electronics;Education;
Comment[de_DE]=Programm das Daten vom NanoVNA liest, anzeigt und speichert
Comment=Tool for reading, displaying and saving data from the NanoVNA
Exec=NanoVNASaver
GenericName[de_DE]=
GenericName=
Icon=NanoVNASaver_48x48
MimeType=
Name[de_DE]=NanoVNASaver
Name=NanoVNASaver
StartupNotify=true
Terminal=false
Type=Application
X-DBUS-ServiceName=
X-DBUS-StartupType=
X-KDE-SubstituteUID=false
X-KDE-Username=

Wyświetl plik

@ -1,150 +0,0 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import math
import numpy as np
from PyQt5 import QtWidgets
from scipy import signal
logger = logging.getLogger(__name__)
class Analysis:
_widget = None
@classmethod
def find_crossing_zero(cls, data):
"""
Find values crossing zero
return list of tuples (before, crossing, after)
indicating the index of data list
crossing is where data == 0
or data nearest 0
at maximum 1 value == 0
data must not start or end with 0
:param cls:
:param data: list of values
"""
my_data = np.array(data)
zeroes = np.where(my_data == 0)[0]
if 0 in zeroes:
raise ValueError("Data must non start with 0")
if len(data) - 1 in zeroes:
raise ValueError("Data must non end with 0")
crossing = [(n - 1, n, n + 1) for n in zeroes]
for n in np.where((my_data[:-1] * my_data[1:]) < 0)[0]:
if abs(data[n]) <= abs(data[n + 1]):
crossing.append((n, n, n + 1))
else:
crossing.append((n, n + 1, n + 1))
return crossing
@classmethod
def find_minimums(cls, data, threshold):
"""
Find values above threshold
return list of tuples (start, lowest, end)
indicating the index of data list
:param cls:
:param data: list of values
:param threshold:
"""
minimums = []
min_start = -1
min_idx = -1
min_val = threshold
for i, d in enumerate(data):
if d < threshold and i < len(data) - 1:
if d < min_val:
min_val = d
min_idx = i
if min_start == -1:
min_start = i
elif min_start != -1:
# We are above the threshold, and were in a section that was
# below
minimums.append((min_start, min_idx, i - 1))
min_start = -1
min_idx = -1
min_val = threshold
return minimums
@classmethod
def find_maximums(cls, data, threshold=None):
"""
Find peacs
:param cls:
:param data: list of values
:param threshold:
"""
peaks, _ = signal.find_peaks(
data, width=2, distance=3, prominence=1)
# my_data = np.array(data)
# maximums = argrelextrema(my_data, np.greater)[0]
if threshold is None:
return peaks
return [k for k in peaks if data[k] > threshold]
def __init__(self, app: QtWidgets.QWidget):
self.app = app
def widget(self) -> QtWidgets.QWidget:
return self._widget
def runAnalysis(self):
pass
def reset(self):
pass
def calculateRolloff(self, location1, location2):
if location1 == location2:
return 0, 0
frequency1 = self.app.data.s21[location1].freq
frequency2 = self.app.data.s21[location2].freq
gain1 = self.app.data.s21[location1].gain
gain2 = self.app.data.s21[location2].gain
frequency_factor = frequency2 / frequency1
if frequency_factor < 1:
frequency_factor = 1 / frequency_factor
attenuation = abs(gain1 - gain2)
logger.debug("Measured points: %d Hz and %d Hz",
frequency1, frequency2)
logger.debug("%f dB over %f factor", attenuation, frequency_factor)
octave_attenuation = attenuation / \
(math.log10(frequency_factor) / math.log10(2))
decade_attenuation = attenuation / math.log10(frequency_factor)
return octave_attenuation, decade_attenuation

Wyświetl plik

@ -1,362 +0,0 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import math
from PyQt5 import QtWidgets
from NanoVNASaver.Formatting import format_frequency
from NanoVNASaver.Analysis import Analysis
logger = logging.getLogger(__name__)
class BandPassAnalysis(Analysis):
def __init__(self, app):
super().__init__(app)
self._widget = QtWidgets.QWidget()
layout = QtWidgets.QFormLayout()
self._widget.setLayout(layout)
layout.addRow(QtWidgets.QLabel("Band pass filter analysis"))
layout.addRow(
QtWidgets.QLabel(
f"Please place {self.app.markers[0].name} in the filter passband."))
self.result_label = QtWidgets.QLabel()
self.lower_cutoff_label = QtWidgets.QLabel()
self.lower_six_db_label = QtWidgets.QLabel()
self.lower_sixty_db_label = QtWidgets.QLabel()
self.lower_db_per_octave_label = QtWidgets.QLabel()
self.lower_db_per_decade_label = QtWidgets.QLabel()
self.upper_cutoff_label = QtWidgets.QLabel()
self.upper_six_db_label = QtWidgets.QLabel()
self.upper_sixty_db_label = QtWidgets.QLabel()
self.upper_db_per_octave_label = QtWidgets.QLabel()
self.upper_db_per_decade_label = QtWidgets.QLabel()
layout.addRow("Result:", self.result_label)
layout.addRow(QtWidgets.QLabel(""))
self.center_frequency_label = QtWidgets.QLabel()
self.span_label = QtWidgets.QLabel()
self.six_db_span_label = QtWidgets.QLabel()
self.quality_label = QtWidgets.QLabel()
layout.addRow("Center frequency:", self.center_frequency_label)
layout.addRow("Bandwidth (-3 dB):", self.span_label)
layout.addRow("Quality factor:", self.quality_label)
layout.addRow("Bandwidth (-6 dB):", self.six_db_span_label)
layout.addRow(QtWidgets.QLabel(""))
layout.addRow(QtWidgets.QLabel("Lower side:"))
layout.addRow("Cutoff frequency:", self.lower_cutoff_label)
layout.addRow("-6 dB point:", self.lower_six_db_label)
layout.addRow("-60 dB point:", self.lower_sixty_db_label)
layout.addRow("Roll-off:", self.lower_db_per_octave_label)
layout.addRow("Roll-off:", self.lower_db_per_decade_label)
layout.addRow(QtWidgets.QLabel(""))
layout.addRow(QtWidgets.QLabel("Upper side:"))
layout.addRow("Cutoff frequency:", self.upper_cutoff_label)
layout.addRow("-6 dB point:", self.upper_six_db_label)
layout.addRow("-60 dB point:", self.upper_sixty_db_label)
layout.addRow("Roll-off:", self.upper_db_per_octave_label)
layout.addRow("Roll-off:", self.upper_db_per_decade_label)
def reset(self):
self.result_label.clear()
self.center_frequency_label.clear()
self.span_label.clear()
self.quality_label.clear()
self.six_db_span_label.clear()
self.upper_cutoff_label.clear()
self.upper_six_db_label.clear()
self.upper_sixty_db_label.clear()
self.upper_db_per_octave_label.clear()
self.upper_db_per_decade_label.clear()
self.lower_cutoff_label.clear()
self.lower_six_db_label.clear()
self.lower_sixty_db_label.clear()
self.lower_db_per_octave_label.clear()
self.lower_db_per_decade_label.clear()
def runAnalysis(self):
self.reset()
pass_band_location = self.app.markers[0].location
logger.debug("Pass band location: %d", pass_band_location)
if len(self.app.data.s21) == 0:
logger.debug("No data to analyse")
self.result_label.setText("No data to analyse.")
return
if pass_band_location < 0:
logger.debug("No location for %s", self.app.markers[0].name)
self.result_label.setText(
f"Please place {self.app.markers[0].name} in the passband.")
return
pass_band_db = self.app.data.s21[pass_band_location].gain
logger.debug("Initial passband gain: %d", pass_band_db)
initial_lower_cutoff_location = -1
for i in range(pass_band_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 3:
# We found a cutoff location
initial_lower_cutoff_location = i
break
if initial_lower_cutoff_location < 0:
self.result_label.setText("Lower cutoff location not found.")
return
initial_lower_cutoff_frequency = self.app.data.s21[initial_lower_cutoff_location].freq
logger.debug("Found initial lower cutoff frequency at %d", initial_lower_cutoff_frequency)
initial_upper_cutoff_location = -1
for i in range(pass_band_location, len(self.app.data.s21), 1):
if (pass_band_db - self.app.data.s21[i].gain) > 3:
# We found a cutoff location
initial_upper_cutoff_location = i
break
if initial_upper_cutoff_location < 0:
self.result_label.setText("Upper cutoff location not found.")
return
initial_upper_cutoff_frequency = self.app.data.s21[initial_upper_cutoff_location].freq
logger.debug("Found initial upper cutoff frequency at %d", initial_upper_cutoff_frequency)
peak_location = -1
peak_db = self.app.data.s21[initial_lower_cutoff_location].gain
for i in range(initial_lower_cutoff_location, initial_upper_cutoff_location, 1):
db = self.app.data.s21[i].gain
if db > peak_db:
peak_db = db
peak_location = i
logger.debug("Found peak of %f at %d", peak_db, self.app.data.s11[peak_location].freq)
lower_cutoff_location = -1
pass_band_db = peak_db
for i in range(peak_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 3:
# We found the cutoff location
lower_cutoff_location = i
break
lower_cutoff_frequency = self.app.data.s21[lower_cutoff_location].freq
lower_cutoff_gain = self.app.data.s21[lower_cutoff_location].gain - pass_band_db
if lower_cutoff_gain < -4:
logger.debug("Lower cutoff frequency found at %f dB"
" - insufficient data points for true -3 dB point.",
lower_cutoff_gain)
logger.debug("Found true lower cutoff frequency at %d", lower_cutoff_frequency)
self.lower_cutoff_label.setText(
f"{format_frequency(lower_cutoff_frequency)}"
f" ({round(lower_cutoff_gain, 1)} dB)")
self.app.markers[1].setFrequency(str(lower_cutoff_frequency))
self.app.markers[1].frequencyInput.setText(str(lower_cutoff_frequency))
upper_cutoff_location = -1
pass_band_db = peak_db
for i in range(peak_location, len(self.app.data.s21), 1):
if (pass_band_db - self.app.data.s21[i].gain) > 3:
# We found the cutoff location
upper_cutoff_location = i
break
upper_cutoff_frequency = self.app.data.s21[upper_cutoff_location].freq
upper_cutoff_gain = self.app.data.s21[upper_cutoff_location].gain - pass_band_db
if upper_cutoff_gain < -4:
logger.debug("Upper cutoff frequency found at %f dB"
" - insufficient data points for true -3 dB point.",
upper_cutoff_gain)
logger.debug("Found true upper cutoff frequency at %d", upper_cutoff_frequency)
self.upper_cutoff_label.setText(
f"{format_frequency(upper_cutoff_frequency)}"
f" ({round(upper_cutoff_gain, 1)} dB)")
self.app.markers[2].setFrequency(str(upper_cutoff_frequency))
self.app.markers[2].frequencyInput.setText(str(upper_cutoff_frequency))
span = upper_cutoff_frequency - lower_cutoff_frequency
center_frequency = math.sqrt(
lower_cutoff_frequency * upper_cutoff_frequency)
q = center_frequency / span
self.span_label.setText(format_frequency(span))
self.center_frequency_label.setText(
format_frequency(center_frequency))
self.quality_label.setText(str(round(q, 2)))
self.app.markers[0].setFrequency(str(round(center_frequency)))
self.app.markers[0].frequencyInput.setText(str(round(center_frequency)))
# Lower roll-off
lower_six_db_location = -1
for i in range(lower_cutoff_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 6:
# We found 6dB location
lower_six_db_location = i
break
if lower_six_db_location < 0:
self.result_label.setText("Lower 6 dB location not found.")
return
lower_six_db_cutoff_frequency = self.app.data.s21[lower_six_db_location].freq
self.lower_six_db_label.setText(
format_frequency(lower_six_db_cutoff_frequency))
ten_db_location = -1
for i in range(lower_cutoff_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(lower_cutoff_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(lower_six_db_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
break
if sixty_db_location > 0:
if sixty_db_location > 0:
sixty_db_cutoff_frequency = self.app.data.s21[sixty_db_location].freq
self.lower_sixty_db_label.setText(
format_frequency(sixty_db_cutoff_frequency))
elif ten_db_location != -1 and twenty_db_location != -1:
ten = self.app.data.s21[ten_db_location].freq
twenty = self.app.data.s21[twenty_db_location].freq
sixty_db_frequency = ten * \
10 ** (5 * (math.log10(twenty) - math.log10(ten)))
self.lower_sixty_db_label.setText(
f"{format_frequency(sixty_db_frequency)} (derived)")
else:
self.lower_sixty_db_label.setText("Not calculated")
if ten_db_location > 0 and twenty_db_location > 0 and ten_db_location != twenty_db_location:
octave_attenuation, decade_attenuation = self.calculateRolloff(
ten_db_location, twenty_db_location)
self.lower_db_per_octave_label.setText(
str(round(octave_attenuation, 3)) + " dB / octave")
self.lower_db_per_decade_label.setText(
str(round(decade_attenuation, 3)) + " dB / decade")
else:
self.lower_db_per_octave_label.setText("Not calculated")
self.lower_db_per_decade_label.setText("Not calculated")
# Upper roll-off
upper_six_db_location = -1
for i in range(upper_cutoff_location, len(self.app.data.s21), 1):
if (pass_band_db - self.app.data.s21[i].gain) > 6:
# We found 6dB location
upper_six_db_location = i
break
if upper_six_db_location < 0:
self.result_label.setText("Upper 6 dB location not found.")
return
upper_six_db_cutoff_frequency = self.app.data.s21[upper_six_db_location].freq
self.upper_six_db_label.setText(
format_frequency(upper_six_db_cutoff_frequency))
six_db_span = upper_six_db_cutoff_frequency - lower_six_db_cutoff_frequency
self.six_db_span_label.setText(
format_frequency(six_db_span))
ten_db_location = -1
for i in range(upper_cutoff_location, len(self.app.data.s21), 1):
if (pass_band_db - self.app.data.s21[i].gain) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(upper_cutoff_location, len(self.app.data.s21), 1):
if (pass_band_db - self.app.data.s21[i].gain) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(upper_six_db_location, len(self.app.data.s21), 1):
if (pass_band_db - self.app.data.s21[i].gain) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
break
if sixty_db_location > 0:
sixty_db_cutoff_frequency = self.app.data.s21[sixty_db_location].freq
self.upper_sixty_db_label.setText(
format_frequency(sixty_db_cutoff_frequency))
elif ten_db_location != -1 and twenty_db_location != -1:
ten = self.app.data.s21[ten_db_location].freq
twenty = self.app.data.s21[twenty_db_location].freq
sixty_db_frequency = ten * \
10 ** (5 * (math.log10(twenty) - math.log10(ten)))
self.upper_sixty_db_label.setText(
f"{format_frequency(sixty_db_frequency)} (derived)")
else:
self.upper_sixty_db_label.setText("Not calculated")
if ten_db_location > 0 and twenty_db_location > 0 and ten_db_location != twenty_db_location:
octave_attenuation, decade_attenuation = self.calculateRolloff(
ten_db_location, twenty_db_location)
self.upper_db_per_octave_label.setText(
f"{round(octave_attenuation, 3)} dB / octave")
self.upper_db_per_decade_label.setText(
f"{round(decade_attenuation, 3)} dB / decade")
else:
self.upper_db_per_octave_label.setText("Not calculated")
self.upper_db_per_decade_label.setText("Not calculated")
if upper_cutoff_gain < -4 or lower_cutoff_gain < -4:
self.result_label.setText(
f"Analysis complete ({len(self.app.data.s11)} points)\n"
f"Insufficient data for analysis. Increase segment count.")
else:
self.result_label.setText(
f"Analysis complete ({len(self.app.data.s11)} points)")

Wyświetl plik

@ -1,316 +0,0 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import math
from PyQt5 import QtWidgets
from NanoVNASaver.Analysis import Analysis
from NanoVNASaver.Formatting import format_frequency
logger = logging.getLogger(__name__)
class BandStopAnalysis(Analysis):
def __init__(self, app):
super().__init__(app)
self._widget = QtWidgets.QWidget()
layout = QtWidgets.QFormLayout()
self._widget.setLayout(layout)
layout.addRow(QtWidgets.QLabel("Band stop filter analysis"))
self.result_label = QtWidgets.QLabel()
self.lower_cutoff_label = QtWidgets.QLabel()
self.lower_six_db_label = QtWidgets.QLabel()
self.lower_sixty_db_label = QtWidgets.QLabel()
self.lower_db_per_octave_label = QtWidgets.QLabel()
self.lower_db_per_decade_label = QtWidgets.QLabel()
self.upper_cutoff_label = QtWidgets.QLabel()
self.upper_six_db_label = QtWidgets.QLabel()
self.upper_sixty_db_label = QtWidgets.QLabel()
self.upper_db_per_octave_label = QtWidgets.QLabel()
self.upper_db_per_decade_label = QtWidgets.QLabel()
layout.addRow("Result:", self.result_label)
layout.addRow(QtWidgets.QLabel(""))
self.center_frequency_label = QtWidgets.QLabel()
self.span_label = QtWidgets.QLabel()
self.six_db_span_label = QtWidgets.QLabel()
self.quality_label = QtWidgets.QLabel()
layout.addRow("Center frequency:", self.center_frequency_label)
layout.addRow("Bandwidth (-3 dB):", self.span_label)
layout.addRow("Quality factor:", self.quality_label)
layout.addRow("Bandwidth (-6 dB):", self.six_db_span_label)
layout.addRow(QtWidgets.QLabel(""))
layout.addRow(QtWidgets.QLabel("Lower side:"))
layout.addRow("Cutoff frequency:", self.lower_cutoff_label)
layout.addRow("-6 dB point:", self.lower_six_db_label)
layout.addRow("-60 dB point:", self.lower_sixty_db_label)
layout.addRow("Roll-off:", self.lower_db_per_octave_label)
layout.addRow("Roll-off:", self.lower_db_per_decade_label)
layout.addRow(QtWidgets.QLabel(""))
layout.addRow(QtWidgets.QLabel("Upper side:"))
layout.addRow("Cutoff frequency:", self.upper_cutoff_label)
layout.addRow("-6 dB point:", self.upper_six_db_label)
layout.addRow("-60 dB point:", self.upper_sixty_db_label)
layout.addRow("Roll-off:", self.upper_db_per_octave_label)
layout.addRow("Roll-off:", self.upper_db_per_decade_label)
def reset(self):
self.result_label.clear()
self.span_label.clear()
self.quality_label.clear()
self.six_db_span_label.clear()
self.upper_cutoff_label.clear()
self.upper_six_db_label.clear()
self.upper_sixty_db_label.clear()
self.upper_db_per_octave_label.clear()
self.upper_db_per_decade_label.clear()
self.lower_cutoff_label.clear()
self.lower_six_db_label.clear()
self.lower_sixty_db_label.clear()
self.lower_db_per_octave_label.clear()
self.lower_db_per_decade_label.clear()
def runAnalysis(self):
self.reset()
if len(self.app.data.s21) == 0:
logger.debug("No data to analyse")
self.result_label.setText("No data to analyse.")
return
peak_location = -1
peak_db = self.app.data.s21[0].gain
for i in range(len(self.app.data.s21)):
db = self.app.data.s21[i].gain
if db > peak_db:
peak_db = db
peak_location = i
logger.debug("Found peak of %f at %d", peak_db, self.app.data.s11[peak_location].freq)
lower_cutoff_location = -1
pass_band_db = peak_db
for i in range(len(self.app.data.s21)):
if (pass_band_db - self.app.data.s21[i].gain) > 3:
# We found the cutoff location
lower_cutoff_location = i
break
lower_cutoff_frequency = self.app.data.s21[lower_cutoff_location].freq
lower_cutoff_gain = self.app.data.s21[lower_cutoff_location].gain - pass_band_db
if lower_cutoff_gain < -4:
logger.debug("Lower cutoff frequency found at %f dB"
" - insufficient data points for true -3 dB point.",
lower_cutoff_gain)
logger.debug("Found true lower cutoff frequency at %d", lower_cutoff_frequency)
self.lower_cutoff_label.setText(
f"{format_frequency(lower_cutoff_frequency)}"
f" ({round(lower_cutoff_gain, 1)} dB)")
self.app.markers[1].setFrequency(str(lower_cutoff_frequency))
self.app.markers[1].frequencyInput.setText(str(lower_cutoff_frequency))
upper_cutoff_location = -1
for i in range(len(self.app.data.s21)-1, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 3:
# We found the cutoff location
upper_cutoff_location = i
break
upper_cutoff_frequency = self.app.data.s21[upper_cutoff_location].freq
upper_cutoff_gain = self.app.data.s21[upper_cutoff_location].gain - pass_band_db
if upper_cutoff_gain < -4:
logger.debug("Upper cutoff frequency found at %f dB"
" - insufficient data points for true -3 dB point.",
upper_cutoff_gain)
logger.debug("Found true upper cutoff frequency at %d", upper_cutoff_frequency)
self.upper_cutoff_label.setText(
f"{format_frequency(upper_cutoff_frequency)}"
f" ({round(upper_cutoff_gain, 1)} dB)")
self.app.markers[2].setFrequency(str(upper_cutoff_frequency))
self.app.markers[2].frequencyInput.setText(str(upper_cutoff_frequency))
span = upper_cutoff_frequency - lower_cutoff_frequency
center_frequency = math.sqrt(lower_cutoff_frequency * upper_cutoff_frequency)
q = center_frequency / span
self.span_label.setText(format_frequency(span))
self.center_frequency_label.setText(
format_frequency(center_frequency))
self.quality_label.setText(str(round(q, 2)))
self.app.markers[0].setFrequency(str(round(center_frequency)))
self.app.markers[0].frequencyInput.setText(str(round(center_frequency)))
# Lower roll-off
lower_six_db_location = -1
for i in range(lower_cutoff_location, len(self.app.data.s21)):
if (pass_band_db - self.app.data.s21[i].gain) > 6:
# We found 6dB location
lower_six_db_location = i
break
if lower_six_db_location < 0:
self.result_label.setText("Lower 6 dB location not found.")
return
lower_six_db_cutoff_frequency = self.app.data.s21[lower_six_db_location].freq
self.lower_six_db_label.setText(
format_frequency(lower_six_db_cutoff_frequency))
ten_db_location = -1
for i in range(lower_cutoff_location, len(self.app.data.s21)):
if (pass_band_db - self.app.data.s21[i].gain) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(lower_cutoff_location, len(self.app.data.s21)):
if (pass_band_db - self.app.data.s21[i].gain) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(lower_six_db_location, len(self.app.data.s21)):
if (pass_band_db - self.app.data.s21[i].gain) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
break
if sixty_db_location > 0:
sixty_db_cutoff_frequency = self.app.data.s21[sixty_db_location].freq
self.lower_sixty_db_label.setText(
format_frequency(sixty_db_cutoff_frequency))
elif ten_db_location != -1 and twenty_db_location != -1:
ten = self.app.data.s21[ten_db_location].freq
twenty = self.app.data.s21[twenty_db_location].freq
sixty_db_frequency = ten * 10 ** (5 * (math.log10(twenty) - math.log10(ten)))
self.lower_sixty_db_label.setText(
f"{format_frequency(sixty_db_frequency)} (derived)")
else:
self.lower_sixty_db_label.setText("Not calculated")
if (ten_db_location > 0 and
twenty_db_location > 0 and
ten_db_location != twenty_db_location):
octave_attenuation, decade_attenuation = self.calculateRolloff(
ten_db_location, twenty_db_location)
self.lower_db_per_octave_label.setText(
f"{round(octave_attenuation, 3)} dB / octave")
self.lower_db_per_decade_label.setText(
f"{round(decade_attenuation, 3)} dB / decade")
else:
self.lower_db_per_octave_label.setText("Not calculated")
self.lower_db_per_decade_label.setText("Not calculated")
# Upper roll-off
upper_six_db_location = -1
for i in range(upper_cutoff_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 6:
# We found 6dB location
upper_six_db_location = i
break
if upper_six_db_location < 0:
self.result_label.setText("Upper 6 dB location not found.")
return
upper_six_db_cutoff_frequency = self.app.data.s21[upper_six_db_location].freq
self.upper_six_db_label.setText(
format_frequency(upper_six_db_cutoff_frequency))
six_db_span = upper_six_db_cutoff_frequency - lower_six_db_cutoff_frequency
self.six_db_span_label.setText(
format_frequency(six_db_span))
ten_db_location = -1
for i in range(upper_cutoff_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(upper_cutoff_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(upper_six_db_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
break
if sixty_db_location > 0:
sixty_db_cutoff_frequency = self.app.data.s21[sixty_db_location].freq
self.upper_sixty_db_label.setText(
format_frequency(sixty_db_cutoff_frequency))
elif ten_db_location != -1 and twenty_db_location != -1:
ten = self.app.data.s21[ten_db_location].freq
twenty = self.app.data.s21[twenty_db_location].freq
sixty_db_frequency = ten * 10 ** (
5 * (math.log10(twenty) - math.log10(ten)))
self.upper_sixty_db_label.setText(
f"{format_frequency(sixty_db_frequency)} (derived)")
else:
self.upper_sixty_db_label.setText("Not calculated")
if (ten_db_location > 0 and
twenty_db_location > 0 and
ten_db_location != twenty_db_location):
octave_attenuation, decade_attenuation = self.calculateRolloff(
ten_db_location, twenty_db_location)
self.upper_db_per_octave_label.setText(
f"{round(octave_attenuation, 3)} dB / octave")
self.upper_db_per_decade_label.setText(
f"{round(decade_attenuation, 3)} dB / decade")
else:
self.upper_db_per_octave_label.setText("Not calculated")
self.upper_db_per_decade_label.setText("Not calculated")
if upper_cutoff_gain < -4 or lower_cutoff_gain < -4:
self.result_label.setText(
f"Analysis complete ({len(self.app.data.s11)} points)\n"
f"Insufficient data for analysis. Increase segment count.")
else:
self.result_label.setText(
f"Analysis complete ({len(self.app.data.s11)} points)")

Wyświetl plik

@ -1,190 +0,0 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import math
from PyQt5 import QtWidgets
from NanoVNASaver.Analysis import Analysis
from NanoVNASaver.Formatting import format_frequency
logger = logging.getLogger(__name__)
class HighPassAnalysis(Analysis):
def __init__(self, app):
super().__init__(app)
self._widget = QtWidgets.QWidget()
layout = QtWidgets.QFormLayout()
self._widget.setLayout(layout)
layout.addRow(QtWidgets.QLabel("High pass filter analysis"))
layout.addRow(QtWidgets.QLabel(
f"Please place {self.app.markers[0].name} in the filter passband."))
self.result_label = QtWidgets.QLabel()
self.cutoff_label = QtWidgets.QLabel()
self.six_db_label = QtWidgets.QLabel()
self.sixty_db_label = QtWidgets.QLabel()
self.db_per_octave_label = QtWidgets.QLabel()
self.db_per_decade_label = QtWidgets.QLabel()
layout.addRow("Result:", self.result_label)
layout.addRow("Cutoff frequency:", self.cutoff_label)
layout.addRow("-6 dB point:", self.six_db_label)
layout.addRow("-60 dB point:", self.sixty_db_label)
layout.addRow("Roll-off:", self.db_per_octave_label)
layout.addRow("Roll-off:", self.db_per_decade_label)
def reset(self):
self.result_label.clear()
self.cutoff_label.clear()
self.six_db_label.clear()
self.sixty_db_label.clear()
self.db_per_octave_label.clear()
self.db_per_decade_label.clear()
def runAnalysis(self):
self.reset()
pass_band_location = self.app.markers[0].location
logger.debug("Pass band location: %d", pass_band_location)
if len(self.app.data.s21) == 0:
logger.debug("No data to analyse")
self.result_label.setText("No data to analyse.")
return
if pass_band_location < 0:
logger.debug("No location for %s", self.app.markers[0].name)
self.result_label.setText(
f"Please place {self.app.markers[0].name } in the passband.")
return
pass_band_db = self.app.data.s21[pass_band_location].gain
logger.debug("Initial passband gain: %d", pass_band_db)
initial_cutoff_location = -1
for i in range(pass_band_location, -1, -1):
db = self.app.data.s21[i].gain
if (pass_band_db - db) > 3:
# We found a cutoff location
initial_cutoff_location = i
break
if initial_cutoff_location < 0:
self.result_label.setText("Cutoff location not found.")
return
initial_cutoff_frequency = self.app.data.s21[initial_cutoff_location].freq
logger.debug("Found initial cutoff frequency at %d", initial_cutoff_frequency)
peak_location = -1
peak_db = self.app.data.s21[initial_cutoff_location].gain
for i in range(len(self.app.data.s21) - 1, initial_cutoff_location - 1, -1):
if self.app.data.s21[i].gain > peak_db:
peak_db = db
peak_location = i
logger.debug("Found peak of %f at %d", peak_db, self.app.data.s11[peak_location].freq)
self.app.markers[0].setFrequency(str(self.app.data.s21[peak_location].freq))
self.app.markers[0].frequencyInput.setText(str(self.app.data.s21[peak_location].freq))
cutoff_location = -1
pass_band_db = peak_db
for i in range(peak_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 3:
# We found the cutoff location
cutoff_location = i
break
cutoff_frequency = self.app.data.s21[cutoff_location].freq
cutoff_gain = self.app.data.s21[cutoff_location].gain - pass_band_db
if cutoff_gain < -4:
logger.debug("Cutoff frequency found at %f dB"
" - insufficient data points for true -3 dB point.",
cutoff_gain)
logger.debug("Found true cutoff frequency at %d", cutoff_frequency)
self.cutoff_label.setText(
f"{format_frequency(cutoff_frequency)}"
f" {round(cutoff_gain, 1)} dB)")
self.app.markers[1].setFrequency(str(cutoff_frequency))
self.app.markers[1].frequencyInput.setText(str(cutoff_frequency))
six_db_location = -1
for i in range(cutoff_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 6:
# We found 6dB location
six_db_location = i
break
if six_db_location < 0:
self.result_label.setText("6 dB location not found.")
return
six_db_cutoff_frequency = self.app.data.s21[six_db_location].freq
self.six_db_label.setText(
format_frequency(six_db_cutoff_frequency))
ten_db_location = -1
for i in range(cutoff_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(cutoff_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(six_db_location, -1, -1):
if (pass_band_db - self.app.data.s21[i].gain) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
break
if sixty_db_location > 0:
if sixty_db_location > 0:
sixty_db_cutoff_frequency = self.app.data.s21[sixty_db_location].freq
self.sixty_db_label.setText(
format_frequency(sixty_db_cutoff_frequency))
elif ten_db_location != -1 and twenty_db_location != -1:
ten = self.app.data.s21[ten_db_location].freq
twenty = self.app.data.s21[twenty_db_location].freq
sixty_db_frequency = ten * 10 ** (5 * (math.log10(twenty) - math.log10(ten)))
self.sixty_db_label.setText(
f"{format_frequency(sixty_db_frequency)} (derived)")
else:
self.sixty_db_label.setText("Not calculated")
if ten_db_location > 0 and twenty_db_location > 0 and ten_db_location != twenty_db_location:
octave_attenuation, decade_attenuation = self.calculateRolloff(
ten_db_location, twenty_db_location)
self.db_per_octave_label.setText(str(round(octave_attenuation, 3)) + " dB / octave")
self.db_per_decade_label.setText(str(round(decade_attenuation, 3)) + " dB / decade")
else:
self.db_per_octave_label.setText("Not calculated")
self.db_per_decade_label.setText("Not calculated")
self.result_label.setText(f"Analysis complete ({len(self.app.data.s11)}) points)")

Wyświetl plik

@ -1,205 +0,0 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import math
from PyQt5 import QtWidgets
from NanoVNASaver.Analysis import Analysis
from NanoVNASaver.Formatting import format_frequency
logger = logging.getLogger(__name__)
class LowPassAnalysis(Analysis):
def __init__(self, app):
super().__init__(app)
self._widget = QtWidgets.QWidget()
layout = QtWidgets.QFormLayout()
self._widget.setLayout(layout)
layout.addRow(QtWidgets.QLabel("Low pass filter analysis"))
layout.addRow(
QtWidgets.QLabel(
f"Please place {self.app.markers[0].name}"
f" in the filter passband."))
self.result_label = QtWidgets.QLabel()
self.cutoff_label = QtWidgets.QLabel()
self.six_db_label = QtWidgets.QLabel()
self.sixty_db_label = QtWidgets.QLabel()
self.db_per_octave_label = QtWidgets.QLabel()
self.db_per_decade_label = QtWidgets.QLabel()
layout.addRow("Result:", self.result_label)
layout.addRow("Cutoff frequency:", self.cutoff_label)
layout.addRow("-6 dB point:", self.six_db_label)
layout.addRow("-60 dB point:", self.sixty_db_label)
layout.addRow("Roll-off:", self.db_per_octave_label)
layout.addRow("Roll-off:", self.db_per_decade_label)
def reset(self):
self.result_label.clear()
self.cutoff_label.clear()
self.six_db_label.clear()
self.sixty_db_label.clear()
self.db_per_octave_label.clear()
self.db_per_decade_label.clear()
def runAnalysis(self):
self.reset()
pass_band_location = self.app.markers[0].location
logger.debug("Pass band location: %d", pass_band_location)
if len(self.app.data.s21) == 0:
logger.debug("No data to analyse")
self.result_label.setText("No data to analyse.")
return
if pass_band_location < 0:
logger.debug("No location for %s",
self.app.markers[0].name)
self.result_label.setText(
f"Please place {self.app.markers[0].name} in the passband.")
return
pass_band_db = self.app.data.s21[pass_band_location].gain
logger.debug("Initial passband gain: %d", pass_band_db)
initial_cutoff_location = -1
for i in range(pass_band_location, len(self.app.data.s21)):
db = self.app.data.s21[i].gain
if (pass_band_db - db) > 3:
# We found a cutoff location
initial_cutoff_location = i
break
if initial_cutoff_location < 0:
self.result_label.setText("Cutoff location not found.")
return
initial_cutoff_frequency = self.app.data.s21[initial_cutoff_location].freq
logger.debug("Found initial cutoff frequency at %d", initial_cutoff_frequency)
peak_location = -1
peak_db = self.app.data.s21[initial_cutoff_location].gain
for i in range(0, initial_cutoff_location):
db = self.app.data.s21[i].gain
if db > peak_db:
peak_db = db
peak_location = i
logger.debug("Found peak of %f at %d", peak_db, self.app.data.s11[peak_location].freq)
self.app.markers[0].setFrequency(str(self.app.data.s21[peak_location].freq))
self.app.markers[0].frequencyInput.setText(str(self.app.data.s21[peak_location].freq))
cutoff_location = -1
pass_band_db = peak_db
for i in range(peak_location, len(self.app.data.s21)):
db = self.app.data.s21[i].gain
if (pass_band_db - db) > 3:
# We found the cutoff location
cutoff_location = i
break
cutoff_frequency = self.app.data.s21[cutoff_location].freq
cutoff_gain = self.app.data.s21[cutoff_location].gain - pass_band_db
if cutoff_gain < -4:
logger.debug(
"Cutoff frequency found at %f dB"
" - insufficient data points for true -3 dB point.",
cutoff_gain)
logger.debug("Found true cutoff frequency at %d", cutoff_frequency)
self.cutoff_label.setText(
f"{format_frequency(cutoff_frequency)}"
f" ({round(cutoff_gain, 1)} dB)")
self.app.markers[1].setFrequency(str(cutoff_frequency))
self.app.markers[1].frequencyInput.setText(str(cutoff_frequency))
six_db_location = -1
for i in range(cutoff_location, len(self.app.data.s21)):
db = self.app.data.s21[i].gain
if (pass_band_db - db) > 6:
# We found 6dB location
six_db_location = i
break
if six_db_location < 0:
self.result_label.setText("6 dB location not found.")
return
six_db_cutoff_frequency = self.app.data.s21[six_db_location].freq
self.six_db_label.setText(
format_frequency(six_db_cutoff_frequency))
ten_db_location = -1
for i in range(cutoff_location, len(self.app.data.s21)):
db = self.app.data.s21[i].gain
if (pass_band_db - db) > 10:
# We found 6dB location
ten_db_location = i
break
twenty_db_location = -1
for i in range(cutoff_location, len(self.app.data.s21)):
db = self.app.data.s21[i].gain
if (pass_band_db - db) > 20:
# We found 6dB location
twenty_db_location = i
break
sixty_db_location = -1
for i in range(six_db_location, len(self.app.data.s21)):
db = self.app.data.s21[i].gain
if (pass_band_db - db) > 60:
# We found 60dB location! Wow.
sixty_db_location = i
break
if sixty_db_location > 0:
sixty_db_cutoff_frequency = self.app.data.s21[sixty_db_location].freq
self.sixty_db_label.setText(
format_frequency(sixty_db_cutoff_frequency))
elif ten_db_location != -1 and twenty_db_location != -1:
ten = self.app.data.s21[ten_db_location].freq
twenty = self.app.data.s21[twenty_db_location].freq
sixty_db_frequency = ten * \
10 ** (5 * (math.log10(twenty) - math.log10(ten)))
self.sixty_db_label.setText(
f"{format_frequency(sixty_db_frequency)} (derived)")
else:
self.sixty_db_label.setText("Not calculated")
if (ten_db_location > 0 and
twenty_db_location > 0 and
ten_db_location != twenty_db_location):
octave_attenuation, decade_attenuation = self.calculateRolloff(
ten_db_location, twenty_db_location)
self.db_per_octave_label.setText(
str(round(octave_attenuation, 3)) + " dB / octave")
self.db_per_decade_label.setText(
str(round(decade_attenuation, 3)) + " dB / decade")
else:
self.db_per_octave_label.setText("Not calculated")
self.db_per_decade_label.setText("Not calculated")
self.result_label.setText(
"Analysis complete (" + str(len(self.app.data.s11)) + " points)")

Wyświetl plik

@ -1,185 +0,0 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
from PyQt5 import QtWidgets
from scipy import signal
import numpy as np
from NanoVNASaver.Analysis import Analysis
from NanoVNASaver.Formatting import format_vswr
from NanoVNASaver.Formatting import format_gain
from NanoVNASaver.Formatting import format_resistance
from NanoVNASaver.Formatting import format_frequency_short
logger = logging.getLogger(__name__)
class PeakSearchAnalysis(Analysis):
class QHLine(QtWidgets.QFrame):
def __init__(self):
super().__init__()
self.setFrameShape(QtWidgets.QFrame.HLine)
def __init__(self, app):
super().__init__(app)
self._widget = QtWidgets.QWidget()
self.layout = QtWidgets.QFormLayout()
self._widget.setLayout(self.layout)
self.rbtn_data_group = QtWidgets.QButtonGroup()
self.rbtn_data_vswr = QtWidgets.QRadioButton("VSWR")
self.rbtn_data_resistance = QtWidgets.QRadioButton("Resistance")
self.rbtn_data_reactance = QtWidgets.QRadioButton("Reactance")
self.rbtn_data_s21_gain = QtWidgets.QRadioButton("S21 Gain")
self.rbtn_data_group.addButton(self.rbtn_data_vswr)
self.rbtn_data_group.addButton(self.rbtn_data_resistance)
self.rbtn_data_group.addButton(self.rbtn_data_reactance)
self.rbtn_data_group.addButton(self.rbtn_data_s21_gain)
self.rbtn_data_vswr.setChecked(True)
self.rbtn_peak_group = QtWidgets.QButtonGroup()
self.rbtn_peak_positive = QtWidgets.QRadioButton("Positive")
self.rbtn_peak_negative = QtWidgets.QRadioButton("Negative")
self.rbtn_peak_both = QtWidgets.QRadioButton("Both")
self.rbtn_peak_group.addButton(self.rbtn_peak_positive)
self.rbtn_peak_group.addButton(self.rbtn_peak_negative)
self.rbtn_peak_group.addButton(self.rbtn_peak_both)
self.rbtn_peak_positive.setChecked(True)
self.input_number_of_peaks = QtWidgets.QSpinBox()
self.input_number_of_peaks.setValue(1)
self.input_number_of_peaks.setMinimum(1)
self.input_number_of_peaks.setMaximum(10)
self.checkbox_move_markers = QtWidgets.QCheckBox()
self.layout.addRow(QtWidgets.QLabel("<b>Settings</b>"))
self.layout.addRow("Data source", self.rbtn_data_vswr)
self.layout.addRow("", self.rbtn_data_resistance)
self.layout.addRow("", self.rbtn_data_reactance)
self.layout.addRow("", self.rbtn_data_s21_gain)
self.layout.addRow(PeakSearchAnalysis.QHLine())
self.layout.addRow("Peak type", self.rbtn_peak_positive)
self.layout.addRow("", self.rbtn_peak_negative)
# outer_layout.addRow("", self.rbtn_peak_both)
self.layout.addRow(PeakSearchAnalysis.QHLine())
self.layout.addRow("Max number of peaks", self.input_number_of_peaks)
self.layout.addRow("Move markers", self.checkbox_move_markers)
self.layout.addRow(PeakSearchAnalysis.QHLine())
self.layout.addRow(QtWidgets.QLabel("<b>Results</b>"))
self.results_header = self.layout.rowCount()
def runAnalysis(self):
self.reset()
data = []
sign = 1
count = self.input_number_of_peaks.value()
if self.rbtn_data_vswr.isChecked():
fn = format_vswr
for d in self.app.data.s11:
data.append(d.vswr)
elif self.rbtn_data_s21_gain.isChecked():
fn = format_gain
for d in self.app.data.s21:
data.append(d.gain)
elif self.rbtn_data_resistance.isChecked():
fn = format_resistance
for d in self.app.data.s11:
data.append(d.impedance().real)
elif self.rbtn_data_reactance.isChecked():
fn = str
for d in self.app.data.s11:
data.append(d.impedance().imag)
else:
logger.warning("Searching for peaks on unknown data")
return
if self.rbtn_peak_positive.isChecked():
peaks, _ = signal.find_peaks(
data, width=3, distance=3, prominence=1)
elif self.rbtn_peak_negative.isChecked():
sign = -1
data = [x * sign for x in data]
peaks, _ = signal.find_peaks(
data, width=3, distance=3, prominence=1)
# elif self.rbtn_peak_both.isChecked():
# peaks_max, _ = signal.find_peaks(data, width=3, distance=3, prominence=1)
# peaks_min, _ = signal.find_peaks(np.array(data)*-1, width=3, distance=3, prominence=1)
# peaks = np.concatenate((peaks_max, peaks_min))
else:
# Both is not yet in
logger.warning(
"Searching for peaks,"
" but neither looking at positive nor negative?")
return
# Having found the peaks, get the prominence data
for i, p in np.ndenumerate(peaks):
logger.debug("Peak %i at %d", i, p)
prominences = signal.peak_prominences(data, peaks)[0]
logger.debug("%d prominences", len(prominences))
# Find the peaks with the most extreme values
# Alternately, allow the user to select "most prominent"?
indices = np.argpartition(prominences, -count)[-count:]
logger.debug("%d indices", len(indices))
for i in indices:
logger.debug("Index %d", i)
logger.debug("Prominence %f", prominences[i])
logger.debug("Index in sweep %d", peaks[i])
logger.debug("Frequency %d", self.app.data.s11[peaks[i]].freq)
logger.debug("Value %f", sign * data[peaks[i]])
self.layout.addRow(
f"Freq {format_frequency_short(self.app.data.s11[peaks[i]].freq)}",
QtWidgets.QLabel(f" value {fn(sign * data[peaks[i]])}"
))
if self.checkbox_move_markers.isChecked():
if count > len(self.app.markers):
logger.warning("More peaks found than there are markers")
for i in range(min(count, len(self.app.markers))):
self.app.markers[i].setFrequency(
str(self.app.data.s11[peaks[indices[i]]].freq))
self.app.markers[i].frequencyInput.setText(
str(self.app.data.s11[peaks[indices[i]]].freq))
max_val = -10**10
max_idx = -1
for p in peaks:
if data[p] > max_val:
max_val = data[p]
max_idx = p
logger.debug("Max peak at %d, value %f", max_idx, max_val)
def reset(self):
logger.debug("Reset analysis")
logger.debug("Results start at %d, out of %d",
self.results_header, self.layout.rowCount())
for _ in range(self.results_header, self.layout.rowCount()):
logger.debug("deleting %s", self.layout.rowCount())
self.layout.removeRow(self.layout.rowCount() - 1)

Wyświetl plik

@ -1,126 +0,0 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
from PyQt5 import QtWidgets
import numpy as np
from NanoVNASaver.Analysis import Analysis, PeakSearchAnalysis
from NanoVNASaver.Formatting import format_frequency
logger = logging.getLogger(__name__)
class SimplePeakSearchAnalysis(Analysis):
def __init__(self, app):
super().__init__(app)
self._widget = QtWidgets.QWidget()
outer_layout = QtWidgets.QFormLayout()
self._widget.setLayout(outer_layout)
self.rbtn_data_group = QtWidgets.QButtonGroup()
self.rbtn_data_vswr = QtWidgets.QRadioButton("VSWR")
self.rbtn_data_resistance = QtWidgets.QRadioButton("Resistance")
self.rbtn_data_reactance = QtWidgets.QRadioButton("Reactance")
self.rbtn_data_s21_gain = QtWidgets.QRadioButton("S21 Gain")
self.rbtn_data_group.addButton(self.rbtn_data_vswr)
self.rbtn_data_group.addButton(self.rbtn_data_resistance)
self.rbtn_data_group.addButton(self.rbtn_data_reactance)
self.rbtn_data_group.addButton(self.rbtn_data_s21_gain)
self.rbtn_data_s21_gain.setChecked(True)
self.rbtn_peak_group = QtWidgets.QButtonGroup()
self.rbtn_peak_positive = QtWidgets.QRadioButton("Highest value")
self.rbtn_peak_negative = QtWidgets.QRadioButton("Lowest value")
self.rbtn_peak_group.addButton(self.rbtn_peak_positive)
self.rbtn_peak_group.addButton(self.rbtn_peak_negative)
self.rbtn_peak_positive.setChecked(True)
self.checkbox_move_marker = QtWidgets.QCheckBox()
outer_layout.addRow(QtWidgets.QLabel("<b>Settings</b>"))
outer_layout.addRow("Data source", self.rbtn_data_vswr)
outer_layout.addRow("", self.rbtn_data_resistance)
outer_layout.addRow("", self.rbtn_data_reactance)
outer_layout.addRow("", self.rbtn_data_s21_gain)
outer_layout.addRow(PeakSearchAnalysis.QHLine())
outer_layout.addRow("Peak type", self.rbtn_peak_positive)
outer_layout.addRow("", self.rbtn_peak_negative)
outer_layout.addRow(PeakSearchAnalysis.QHLine())
outer_layout.addRow("Move marker to peak", self.checkbox_move_marker)
outer_layout.addRow(PeakSearchAnalysis.QHLine())
outer_layout.addRow(QtWidgets.QLabel("<b>Results</b>"))
self.peak_frequency = QtWidgets.QLabel()
self.peak_value = QtWidgets.QLabel()
outer_layout.addRow("Peak frequency:", self.peak_frequency)
outer_layout.addRow("Peak value:", self.peak_value)
def runAnalysis(self):
if self.rbtn_data_vswr.isChecked():
suffix = ""
data = []
for d in self.app.data.s11:
data.append(d.vswr)
elif self.rbtn_data_resistance.isChecked():
suffix = " \N{OHM SIGN}"
data = []
for d in self.app.data.s11:
data.append(d.impedance().real)
elif self.rbtn_data_reactance.isChecked():
suffix = " \N{OHM SIGN}"
data = []
for d in self.app.data.s11:
data.append(d.impedance().imag)
elif self.rbtn_data_s21_gain.isChecked():
suffix = " dB"
data = []
for d in self.app.data.s21:
data.append(d.gain)
else:
logger.warning("Searching for peaks on unknown data")
return
if len(data) == 0:
return
if self.rbtn_peak_positive.isChecked():
idx_peak = np.argmax(data)
elif self.rbtn_peak_negative.isChecked():
idx_peak = np.argmin(data)
else:
# Both is not yet in
logger.warning(
"Searching for peaks,"
" but neither looking at positive nor negative?")
return
self.peak_frequency.setText(
format_frequency(self.app.data.s11[idx_peak].freq))
self.peak_value.setText(str(round(data[idx_peak], 3)) + suffix)
if self.checkbox_move_marker.isChecked() and len(self.app.markers) >= 1:
self.app.markers[0].setFrequency(str(self.app.data.s11[idx_peak].freq))
self.app.markers[0].frequencyInput.setText(
format_frequency(self.app.data.s11[idx_peak].freq))

Wyświetl plik

@ -1,471 +0,0 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import os
import csv
import logging
from collections import OrderedDict
import numpy as np
from PyQt5 import QtWidgets
from NanoVNASaver.Analysis import Analysis, PeakSearchAnalysis
from NanoVNASaver.Formatting import (
format_frequency, format_complex_imp,
format_frequency_short, format_resistance)
from NanoVNASaver.RFTools import reflection_coefficient
logger = logging.getLogger(__name__)
def round_2(x):
return round(x, 2)
def format_resistence_neg(x):
return format_resistance(x, allow_negative=True)
class VSWRAnalysis(Analysis):
max_dips_shown = 3
vswr_limit_value = 1.5
class QHLine(QtWidgets.QFrame):
def __init__(self):
super().__init__()
self.setFrameShape(QtWidgets.QFrame.HLine)
def __init__(self, app):
super().__init__(app)
self._widget = QtWidgets.QWidget()
self.layout = QtWidgets.QFormLayout()
self._widget.setLayout(self.layout)
self.input_vswr_limit = QtWidgets.QDoubleSpinBox()
self.input_vswr_limit.setValue(self.vswr_limit_value)
self.input_vswr_limit.setSingleStep(0.1)
self.input_vswr_limit.setMinimum(1)
self.input_vswr_limit.setMaximum(25)
self.input_vswr_limit.setDecimals(2)
self.checkbox_move_marker = QtWidgets.QCheckBox()
self.layout.addRow(QtWidgets.QLabel("<b>Settings</b>"))
self.layout.addRow("VSWR limit", self.input_vswr_limit)
self.layout.addRow(VSWRAnalysis.QHLine())
self.results_label = QtWidgets.QLabel("<b>Results</b>")
self.layout.addRow(self.results_label)
def runAnalysis(self):
max_dips_shown = self.max_dips_shown
data = [d.vswr for d in self.app.data.s11]
# min_idx = np.argmin(data)
#
# logger.debug("Minimum at %d", min_idx)
# logger.debug("Value at minimum: %f", data[min_idx])
# logger.debug("Frequency: %d", self.app.data.s11[min_idx].freq)
#
# if self.checkbox_move_marker.isChecked():
# self.app.markers[0].setFrequency(str(self.app.data.s11[min_idx].freq))
# self.app.markers[0].frequencyInput.setText(str(self.app.data.s11[min_idx].freq))
threshold = self.input_vswr_limit.value()
minimums = self.find_minimums(data, threshold)
logger.debug("Found %d sections under %f threshold",
len(minimums), threshold)
results_header = self.layout.indexOf(self.results_label)
logger.debug("Results start at %d, out of %d",
results_header, self.layout.rowCount())
for _ in range(results_header, self.layout.rowCount()):
self.layout.removeRow(self.layout.rowCount() - 1)
if len(minimums) > max_dips_shown:
self.layout.addRow(QtWidgets.QLabel("<b>More than " + str(max_dips_shown) +
" dips found. Lowest shown.</b>"))
dips = []
for m in minimums:
start, lowest, end = m
dips.append(data[lowest])
best_dips = []
for _ in range(max_dips_shown):
min_idx = np.argmin(dips)
best_dips.append(minimums[min_idx])
dips.remove(dips[min_idx])
minimums.remove(minimums[min_idx])
minimums = best_dips
self.minimums = minimums
if len(minimums) > 0:
for m in minimums:
start, lowest, end = m
if start != end:
logger.debug(
"Section from %d to %d, lowest at %d", start, end, lowest)
self.layout.addRow("Start", QtWidgets.QLabel(
format_frequency(self.app.data.s11[start].freq)))
self.layout.addRow(
"Minimum",
QtWidgets.QLabel(
f"{format_frequency(self.app.data.s11[lowest].freq)}"
f" ({round(data[lowest], 2)})"))
self.layout.addRow("End", QtWidgets.QLabel(
format_frequency(self.app.data.s11[end].freq)))
self.layout.addRow(
"Span",
QtWidgets.QLabel(
format_frequency(self.app.data.s11[end].freq -
self.app.data.s11[start].freq)))
else:
self.layout.addRow("Low spot", QtWidgets.QLabel(
format_frequency(self.app.data.s11[lowest].freq)))
self.layout.addWidget(PeakSearchAnalysis.QHLine())
# Remove the final separator line
self.layout.removeRow(self.layout.rowCount() - 1)
else:
self.layout.addRow(QtWidgets.QLabel(
"No areas found with VSWR below " + str(round(threshold, 2)) + "."))
class ResonanceAnalysis(Analysis):
# max_dips_shown = 3
@classmethod
def vswr_transformed(cls, z, ratio=49) -> float:
refl = reflection_coefficient(z / ratio)
mag = abs(refl)
if mag == 1:
return 1
return (1 + mag) / (1 - mag)
class QHLine(QtWidgets.QFrame):
def __init__(self):
super().__init__()
self.setFrameShape(QtWidgets.QFrame.HLine)
def __init__(self, app):
super().__init__(app)
self._widget = QtWidgets.QWidget()
self.layout = QtWidgets.QFormLayout()
self._widget.setLayout(self.layout)
self.input_description = QtWidgets.QLineEdit("")
self.checkbox_move_marker = QtWidgets.QCheckBox()
self.layout.addRow(QtWidgets.QLabel("<b>Settings</b>"))
self.layout.addRow("Description", self.input_description)
self.layout.addRow(VSWRAnalysis.QHLine())
self.layout.addRow(VSWRAnalysis.QHLine())
self.results_label = QtWidgets.QLabel("<b>Results</b>")
self.layout.addRow(self.results_label)
def _get_data(self, index):
my_data = {"freq": self.app.data.s11[index].freq,
"s11": self.app.data.s11[index].z,
"lambda": self.app.data.s11[index].wavelength,
"impedance": self.app.data.s11[index].impedance(),
"vswr": self.app.data.s11[index].vswr,
}
my_data["vswr_49"] = self.vswr_transformed(
my_data["impedance"], 49)
my_data["vswr_4"] = self.vswr_transformed(
my_data["impedance"], 4)
my_data["r"] = my_data["impedance"].real
my_data["x"] = my_data["impedance"].imag
return my_data
def _get_crossing(self):
data = [d.phase for d in self.app.data.s11]
return sorted(self.find_crossing_zero(data))
def runAnalysis(self):
self.reset()
# self.results_label = QtWidgets.QLabel("<b>Results</b>")
# max_dips_shown = self.max_dips_shown
description = self.input_description.text()
if description:
filename = os.path.join("/tmp/", "{}.csv".format(description))
else:
filename = None
crossing = self._get_crossing()
logger.debug("Found %d sections ",
len(crossing))
results_header = self.layout.indexOf(self.results_label)
logger.debug("Results start at %d, out of %d",
results_header, self.layout.rowCount())
for _ in range(results_header, self.layout.rowCount()):
self.layout.removeRow(self.layout.rowCount() - 1)
# if len(crossing) > max_dips_shown:
# self.layout.addRow(QtWidgets.QLabel("<b>More than " + str(max_dips_shown) +
# " dips found. Lowest shown.</b>"))
# self.crossing = crossing[:max_dips_shown]
if len(crossing) > 0:
extended_data = []
for m in crossing:
start, lowest, end = m
my_data = self._get_data(lowest)
extended_data.append(my_data)
if start != end:
logger.debug(
"Section from %d to %d, lowest at %d", start, end, lowest)
self.layout.addRow(
"Resonance",
QtWidgets.QLabel(
f"{format_frequency(self.app.data.s11[lowest].freq)}"
f" ({format_complex_imp(self.app.data.s11[lowest].impedance())})"))
else:
self.layout.addRow("Resonance", QtWidgets.QLabel(
format_frequency(self.app.data.s11[lowest].freq)))
self.layout.addWidget(PeakSearchAnalysis.QHLine())
# Remove the final separator line
self.layout.removeRow(self.layout.rowCount() - 1)
if filename and extended_data:
with open(filename, 'w', newline='') as csvfile:
fieldnames = extended_data[0].keys()
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for row in extended_data:
writer.writerow(row)
else:
self.layout.addRow(QtWidgets.QLabel(
"No resonance found"))
class EFHWAnalysis(ResonanceAnalysis):
"""
find only resonance when HI impedance
"""
old_data = []
def reset(self):
logger.debug("reset")
def runAnalysis(self):
self.reset()
# self.results_label = QtWidgets.QLabel("<b>Results</b>")
# max_dips_shown = self.max_dips_shown
description = self.input_description.text()
if description:
filename = os.path.join("/tmp/", "{}.csv".format(description))
else:
filename = None
crossing = self._get_crossing()
data = []
for d in self.app.data.s11:
data.append(d.impedance().real)
maximums = sorted(self.find_maximums(data, threshold=500))
results_header = self.layout.indexOf(self.results_label)
logger.debug("Results start at %d, out of %d",
results_header, self.layout.rowCount())
for i in range(results_header, self.layout.rowCount()):
self.layout.removeRow(self.layout.rowCount() - 1)
extended_data = OrderedDict()
# both = np.intersect1d([i[1] for i in crossing], maximums)
both = []
tolerance = 2
for i in maximums:
for l, _, h in crossing:
if l - tolerance <= i <= h + tolerance:
both.append(i)
continue
if l > i:
continue
if both:
logger.info("%i crossing HW", len(both))
logger.info(crossing)
logger.info(maximums)
logger.info(both)
for m in both:
my_data = self._get_data(m)
if m in extended_data:
extended_data[m].update(my_data)
else:
extended_data[m] = my_data
for i in range(min(len(both), len(self.app.markers))):
# self.app.markers[i].label = {}
# for l in TYPES:
# self.app.markers[i][l.label_id] = MarkerLabel(l.name)
# self.app.markers[i].label['actualfreq'].setMinimumWidth(
# 100)
# self.app.markers[i].label['returnloss'].setMinimumWidth(80)
self.app.markers[i].setFrequency(
str(self.app.data.s11[both[i]].freq))
self.app.markers[i].frequencyInput.setText(
str(self.app.data.s11[both[i]].freq))
else:
logger.info("TO DO: find near data")
for _, lowest, _ in crossing:
my_data = self._get_data(lowest)
if lowest in extended_data:
extended_data[lowest].update(my_data)
else:
extended_data[lowest] = my_data
logger.debug("maximumx %s of type %s", maximums, type(maximums))
for m in maximums:
logger.debug("m %s of type %s", m, type(m))
my_data = self._get_data(m)
if m in extended_data:
extended_data[m].update(my_data)
else:
extended_data[m] = my_data
# saving and comparing
fields = [("freq", format_frequency_short),
("r", format_resistence_neg),
("lambda", round_2),
]
if self.old_data:
diff = self.compare(
self.old_data[-1], extended_data, fields=fields)
else:
diff = self.compare({}, extended_data, fields=fields)
self.old_data.append(extended_data)
for i, index in enumerate(sorted(extended_data.keys())):
self.layout.addRow(
f"{format_frequency_short(self.app.data.s11[index].freq)}",
QtWidgets.QLabel(f" ({diff[i]['freq']})"
f" {format_complex_imp(self.app.data.s11[index].impedance())}"
f" ({diff[i]['r']})"
f" {diff[i]['lambda']} m"))
# Remove the final separator line
# self.layout.removeRow(self.layout.rowCount() - 1)
if filename and extended_data:
with open(filename, 'w', newline='') as csvfile:
fieldnames = extended_data[sorted(
extended_data.keys())[0]].keys()
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for index in sorted(extended_data.keys()):
row = extended_data[index]
writer.writerow(row)
def compare(self, old, new, fields=None):
"""
Compare data to help changes
NB
must be same sweep
( same index must be same frequence )
:param old:
:param new:
"""
fields = fields or [("freq", str), ]
def no_compare():
return {k: "-" for k, _ in fields}
old_idx = sorted(old.keys())
# 'odict_keys' object is not subscriptable
new_idx = sorted(new.keys())
diff = {}
i_max = min(len(old_idx), len(new_idx))
i_tot = max(len(old_idx), len(new_idx))
if i_max == i_tot:
logger.debug("may be the same antenna ... analyzing")
else:
logger.warning("resonances changed from %s to %s",
len(old_idx), len(new_idx))
logger.debug("Trying to compare only first %s resonances", i_max)
split = 0
max_delta_f = 1000000 # 1M
for i, k in enumerate(new_idx):
my_diff = {}
logger.info("Risonance %s at %s", i,
format_frequency(new[k]["freq"]))
if len(old_idx) <= i + split:
diff[i] = no_compare()
continue
delta_f = new[k]["freq"] - old[old_idx[i + split]]["freq"]
if abs(delta_f) < max_delta_f:
logger.debug("can compare")
else:
logger.debug("can't compare, %s is too much ",
format_frequency(delta_f))
if delta_f > 0:
logger.debug("possible missing band, ")
if len(old_idx) > (i + split + 1):
if abs(new[k]["freq"] - old[old_idx[i + split + 1]]["freq"]) < max_delta_f:
logger.debug("new is missing band, compare next ")
split += 1
# FIXME: manage 2 or more band missing ?!?
else:
logger.debug("new band, non compare ")
diff[i] = no_compare()
continue
else:
logger.debug("new band, non compare ")
diff[i] = no_compare()
split -= 1
continue
for d, fn in fields:
my_diff[d] = fn(new[k][d] - old[old_idx[i + split]][d])
logger.info("Delta %s = %s", d,
my_diff[d])
diff[i] = my_diff
for i in range(i_max, i_tot):
# add missing in old ... if any
diff[i] = no_compare()
return diff

Wyświetl plik

@ -1,9 +0,0 @@
from .Analysis import Analysis
from .BandPassAnalysis import BandPassAnalysis
from .BandStopAnalysis import BandStopAnalysis
from .HighPassAnalysis import HighPassAnalysis
from .LowPassAnalysis import LowPassAnalysis
from .PeakSearchAnalysis import PeakSearchAnalysis
from .SimplePeakSearchAnalysis import SimplePeakSearchAnalysis
from .VSWRAnalysis import VSWRAnalysis
from .AntennaAnalysis import MagLoopAnalysis

Wyświetl plik

@ -1,405 +0,0 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import cmath
import math
import os
import re
from collections import defaultdict, UserDict
from typing import List
from scipy.interpolate import interp1d
from NanoVNASaver.RFTools import Datapoint
RXP_CAL_LINE = re.compile(r"""^\s*
(?P<freq>\d+) \s+
(?P<shortr>[-0-9Ee.]+) \s+ (?P<shorti>[-0-9Ee.]+) \s+
(?P<openr>[-0-9Ee.]+) \s+ (?P<openi>[-0-9Ee.]+) \s+
(?P<loadr>[-0-9Ee.]+) \s+ (?P<loadi>[-0-9Ee.]+)(?: \s
(?P<throughr>[-0-9Ee.]+) \s+ (?P<throughi>[-0-9Ee.]+) \s+
(?P<thrureflr>[-0-9Ee.]+) \s+ (?P<thrurefli>[-0-9Ee.]+) \s+
(?P<isolationr>[-0-9Ee.]+) \s+ (?P<isolationi>[-0-9Ee.]+)
)?
""", re.VERBOSE)
logger = logging.getLogger(__name__)
def correct_delay(d: Datapoint, delay: float, reflect: bool = False):
mult = 2 if reflect else 1
corr_data = d.z * cmath.exp(
complex(0, 1) * 2 * math.pi * d.freq * delay * -1 * mult)
return Datapoint(d.freq, corr_data.real, corr_data.imag)
class CalData(UserDict):
def __init__(self):
data = {
"short": None,
"open": None,
"load": None,
"through": None,
"thrurefl": None,
"isolation": None,
# the frequence
"freq": 0,
# 1 Port
"e00": 0.0, # Directivity
"e11": 0.0, # Port1 match
"delta_e": 0.0, # Tracking
"e10e01": 0.0, # Forward Reflection Tracking
# 2 port
"e30": 0.0, # Forward isolation
"e22": 0.0, # Port2 match
"e10e32": 0.0, # Forward transmission
}
super().__init__(data)
def __str__(self):
d = self.data
s = (f'{d["freq"]}'
f' {d["short"].re} {d["short"].im}'
f' {d["open"].re} {d["open"].im}'
f' {d["load"].re} {d["load"].im}')
if d["through"] is not None:
s += (f' {d["through"].re} {d["through"].im}'
f' {d["thrurefl"].re} {d["thrurefl"].im}'
f' {d["isolation"].re} {d["isolation"].im}')
return s
class CalDataSet:
def __init__(self):
self.data = defaultdict(CalData)
def insert(self, name: str, dp: Datapoint):
if name not in self.data[dp.freq]:
raise KeyError(name)
self.data[dp.freq]["freq"] = dp.freq
self.data[dp.freq][name] = dp
def frequencies(self) -> List[int]:
return sorted(self.data.keys())
def get(self, freq: int) -> CalData:
return self.data[freq]
def items(self):
yield from self.data.items()
def values(self):
for freq in self.frequencies():
yield self.get(freq)
def size_of(self, name: str) -> int:
return len([v for v in self.data.values() if v[name] is not None])
def complete1port(self) -> bool:
for val in self.data.values():
for name in ("short", "open", "load"):
if val[name] is None:
return False
return any(self.data)
def complete2port(self) -> bool:
for val in self.data.values():
for name in ("short", "open", "load", "through", "thrurefl", "isolation"):
if val[name] is None:
return False
return any(self.data)
class Calibration:
CAL_NAMES = ("short", "open", "load", "through", "thrurefl", "isolation",)
IDEAL_SHORT = complex(-1, 0)
IDEAL_OPEN = complex(1, 0)
IDEAL_LOAD = complex(0, 0)
def __init__(self):
self.notes = []
self.dataset = CalDataSet()
self.interp = {}
self.useIdealShort = True
self.shortL0 = 5.7 * 10E-12
self.shortL1 = -8960 * 10E-24
self.shortL2 = -1100 * 10E-33
self.shortL3 = -41200 * 10E-42
self.shortLength = -34.2 # Picoseconfrequenciesds
# These numbers look very large, considering what Keysight
# suggests their numbers are.
self.useIdealOpen = True
# Subtract 50fF for the nanoVNA calibration if nanoVNA is
# calibrated?
self.openC0 = 2.1 * 10E-14
self.openC1 = 5.67 * 10E-23
self.openC2 = -2.39 * 10E-31
self.openC3 = 2.0 * 10E-40
self.openLength = 0
self.useIdealLoad = True
self.loadR = 25
self.loadL = 0
self.loadC = 0
self.loadLength = 0
self.useIdealThrough = True
self.throughLength = 0
self.isCalculated = False
self.source = "Manual"
def insert(self, name: str, data: List[Datapoint]):
for dp in data:
self.dataset.insert(name, dp)
def size(self) -> int:
return len(self.dataset.frequencies())
def data_size(self, name) -> int:
return self.dataset.size_of(name)
def isValid1Port(self) -> bool:
return self.dataset.complete1port()
def isValid2Port(self) -> bool:
return self.dataset.complete2port()
def _calc_port_1(self, freq: int, cal: CalData):
g1 = self.gamma_short(freq)
g2 = self.gamma_open(freq)
g3 = self.gamma_load(freq)
gm1 = cal["short"].z
gm2 = cal["open"].z
gm3 = cal["load"].z
denominator = (g1 * (g2 - g3) * gm1 +
g2 * g3 * gm2 - g2 * g3 * gm3 -
(g2 * gm2 - g3 * gm3) * g1)
cal["e00"] = - ((g2 * gm3 - g3 * gm3) * g1 * gm2 -
(g2 * g3 * gm2 - g2 * g3 * gm3 -
(g3 * gm2 - g2 * gm3) * g1) * gm1
) / denominator
cal["e11"] = ((g2 - g3) * gm1 - g1 * (gm2 - gm3) +
g3 * gm2 - g2 * gm3) / denominator
cal["delta_e"] = - ((g1 * (gm2 - gm3) - g2 * gm2 + g3 *
gm3) * gm1 + (g2 * gm3 - g3 * gm3) *
gm2) / denominator
def _calc_port_2(self, freq: int, cal: CalData):
gt = self.gamma_through(freq)
gm4 = cal["through"].z
gm5 = cal["thrurefl"].z
gm6 = cal["isolation"].z
gm7 = gm5 - cal["e00"]
cal["e30"] = cal["isolation"].z
cal["e10e01"] = cal["e00"] * cal["e11"] - cal["delta_e"]
cal["e22"] = gm7 / (
gm7 * cal["e11"] * gt ** 2 + cal["e10e01"] * gt ** 2)
cal["e10e32"] = (gm4 - gm6) * (
1 - cal["e11"] * cal["e22"] * gt ** 2) / gt
def calc_corrections(self):
if not self.isValid1Port():
logger.warning(
"Tried to calibrate from insufficient data.")
raise ValueError(
"All of short, open and load calibration steps"
"must be completed for calibration to be applied.")
logger.debug("Calculating calibration for %d points.", self.size())
for freq, caldata in self.dataset.items():
try:
self._calc_port_1(freq, caldata)
if self.isValid2Port():
self._calc_port_2(freq, caldata)
except ZeroDivisionError as exc:
self.isCalculated = False
logger.error(
"Division error - did you use the same measurement"
" for two of short, open and load?")
raise ValueError(
f"Two of short, open and load returned the same"
f" values at frequency {freq}Hz.") from exc
self.gen_interpolation()
self.isCalculated = True
logger.debug("Calibration correctly calculated.")
def gamma_short(self, freq: int) -> complex:
g = Calibration.IDEAL_SHORT
if not self.useIdealShort:
logger.debug("Using short calibration set values.")
Zsp = complex(0, 2 * math.pi * freq * (
self.shortL0 + self.shortL1 * freq +
self.shortL2 * freq ** 2 + self.shortL3 * freq ** 3))
# Referencing https://arxiv.org/pdf/1606.02446.pdf (18) - (21)
g = (Zsp / 50 - 1) / (Zsp / 50 + 1) * cmath.exp(
complex(0, 2 * math.pi * 2 * freq * self.shortLength * -1))
return g
def gamma_open(self, freq: int) -> complex:
g = Calibration.IDEAL_OPEN
if not self.useIdealOpen:
logger.debug("Using open calibration set values.")
Zop = complex(0, 2 * math.pi * freq * (
self.openC0 + self.openC1 * freq +
self.openC2 * freq ** 2 + self.openC3 * freq ** 3))
g = ((1 - 50 * Zop) / (1 + 50 * Zop)) * cmath.exp(
complex(0, 2 * math.pi * 2 * freq * self.openLength * -1))
return g
def gamma_load(self, freq: int) -> complex:
g = Calibration.IDEAL_LOAD
if not self.useIdealLoad:
logger.debug("Using load calibration set values.")
Zl = complex(self.loadR, 0)
if self.loadC > 0:
Zl = self.loadR / complex(1, 2 * self.loadR * math.pi * freq * self.loadC)
if self.loadL > 0:
Zl = Zl + complex(0, 2 * math.pi * freq * self.loadL)
g = (Zl / 50 - 1) / (Zl / 50 + 1) * cmath.exp(
complex(0, 2 * math.pi * 2 * freq * self.loadLength * -1))
return g
def gamma_through(self, freq: int) -> complex:
g = complex(1, 0)
if not self.useIdealThrough:
logger.debug("Using through calibration set values.")
g = cmath.exp(complex(0, 1) * 2 * math.pi *
self.throughLength * freq * -1)
return g
def gen_interpolation(self):
freq = []
e00 = []
e11 = []
delta_e = []
e10e01 = []
e30 = []
e22 = []
e10e32 = []
for caldata in self.dataset.values():
freq.append(caldata["freq"])
e00.append(caldata["e00"])
e11.append(caldata["e11"])
delta_e.append(caldata["delta_e"])
e10e01.append(caldata["e10e01"])
e30.append(caldata["e30"])
e22.append(caldata["e22"])
e10e32.append(caldata["e10e32"])
self.interp = {
"e00": interp1d(freq, e00,
kind="slinear", bounds_error=False,
fill_value=(e00[0], e00[-1])),
"e11": interp1d(freq, e11,
kind="slinear", bounds_error=False,
fill_value=(e11[0], e11[-1])),
"delta_e": interp1d(freq, delta_e,
kind="slinear", bounds_error=False,
fill_value=(delta_e[0], delta_e[-1])),
"e10e01": interp1d(freq, e10e01,
kind="slinear", bounds_error=False,
fill_value=(e10e01[0], e10e01[-1])),
"e30": interp1d(freq, e30,
kind="slinear", bounds_error=False,
fill_value=(e30[0], e30[-1])),
"e22": interp1d(freq, e22,
kind="slinear", bounds_error=False,
fill_value=(e22[0], e22[-1])),
"e10e32": interp1d(freq, e10e32,
kind="slinear", bounds_error=False,
fill_value=(e10e32[0], e10e32[-1])),
}
def correct11(self, dp: Datapoint):
i = self.interp
s11 = (dp.z - i["e00"](dp.freq)) / (
(dp.z * i["e11"](dp.freq)) - i["delta_e"](dp.freq))
return Datapoint(dp.freq, s11.real, s11.imag)
def correct21(self, dp: Datapoint, dp11: Datapoint):
i = self.interp
s21 = (dp.z - i["e30"](dp.freq)) / i["e10e32"](dp.freq)
s21 = s21 * (i["e10e01"](dp.freq) / (i["e11"](dp.freq) * dp11.z - i["delta_e"](dp.freq)))
return Datapoint(dp.freq, s21.real, s21.imag)
# TODO: implement tests
def save(self, filename: str):
# Save the calibration data to file
if not self.isValid1Port():
raise ValueError("Not a valid 1-Port calibration")
with open(filename, mode="w", encoding='utf-8') as calfile:
calfile.write("# Calibration data for NanoVNA-Saver\n")
for note in self.notes:
calfile.write(f"! {note}\n")
calfile.write(
"# Hz ShortR ShortI OpenR OpenI LoadR LoadI"
" ThroughR ThroughI ThrureflR ThrureflI IsolationR IsolationI\n")
for freq in self.dataset.frequencies():
calfile.write(f"{self.dataset.get(freq)}\n")
# TODO: implement tests
# TODO: Exception should be catched by caller
def load(self, filename):
self.source = os.path.basename(filename)
self.dataset = CalDataSet()
self.notes = []
parsed_header = False
with open(filename, encoding='utf-8') as calfile:
for i, line in enumerate(calfile):
line = line.strip()
if line.startswith("!"):
note = line[2:]
self.notes.append(note)
continue
if line.startswith("#"):
if not parsed_header and line == (
"# Hz ShortR ShortI OpenR OpenI LoadR LoadI"
" ThroughR ThroughI ThrureflR ThrureflI IsolationR IsolationI"):
parsed_header = True
continue
if not parsed_header:
logger.warning(
"Warning: Read line without having read header: %s",
line)
continue
m = RXP_CAL_LINE.search(line)
if not m:
logger.warning("Illegal data in cal file. Line %i", i)
cal = m.groupdict()
nr_cals = 6 if cal["throughr"] else 3
for name in Calibration.CAL_NAMES[:nr_cals]:
self.dataset.insert(
name,
Datapoint(int(cal["freq"]),
float(cal[f"{name}r"]),
float(cal[f"{name}i"])))

Wyświetl plik

@ -1,102 +0,0 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020ff NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
from PyQt5 import QtGui, QtCore
from NanoVNASaver.Charts.Chart import Chart
from NanoVNASaver.Charts.Square import SquareChart
logger = logging.getLogger(__name__)
class SmithChart(SquareChart):
def drawChart(self, qp: QtGui.QPainter) -> None:
center_x = self.width() // 2
center_y = self.height() // 2
width_2 = self.dim.width // 2
height_2 = self.dim.height // 2
qp.setPen(QtGui.QPen(Chart.color.text))
qp.drawText(3, 15, self.name)
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawEllipse(QtCore.QPoint(center_x, center_y), width_2, height_2)
qp.drawLine(center_x - width_2, center_y,
center_x + width_2, center_y)
qp.drawEllipse(
QtCore.QPoint(center_x + int(self.dim.width/4), center_y),
self.dim.width // 4, self.dim.height // 4) # Re(Z) = 1
qp.drawEllipse(
QtCore.QPoint(center_x + self.dim.width // 3, center_y),
self.dim.width // 6, self.dim.height // 6) # Re(Z) = 2
qp.drawEllipse(
QtCore.QPoint(center_x + 3 * self.dim.width // 8, center_y),
self.dim.width // 8, self.dim.height // 8) # Re(Z) = 3
qp.drawEllipse(
QtCore.QPoint(center_x + 5 * self.dim.width // 12, center_y),
self.dim.width // 12, self.dim.height // 12) # Re(Z) = 5
qp.drawEllipse(
QtCore.QPoint(center_x + self.dim.width // 6, center_y),
self.dim.width // 3, self.dim.height // 3) # Re(Z) = 0.5
qp.drawEllipse(
QtCore.QPoint(center_x + self.dim.width // 12, center_y),
5 * self.dim.width // 12, 5 * self.dim.height // 12) # Re(Z) = 0.2
qp.drawArc(center_x + 3 * self.dim.width // 8, center_y,
self.dim.width // 4, self.dim.width // 4,
90 * 16, 152 * 16) # Im(Z) = -5
qp.drawArc(center_x + 3 * self.dim.width // 8, center_y,
self.dim.width // 4, -self.dim.width // 4,
-90 * 16, -152 * 16) # Im(Z) = 5
qp.drawArc(center_x + self.dim.width // 4, center_y,
width_2, height_2,
90 * 16, 127 * 16) # Im(Z) = -2
qp.drawArc(center_x + self.dim.width // 4, center_y,
width_2, -height_2,
-90 * 16, -127 * 16) # Im(Z) = 2
qp.drawArc(center_x, center_y,
self.dim.width, self.dim.height,
90*16, 90*16) # Im(Z) = -1
qp.drawArc(center_x, center_y,
self.dim.width, - self.dim.height,
-90 * 16, -90 * 16) # Im(Z) = 1
qp.drawArc(center_x - width_2, center_y,
self.dim.width * 2, self.dim.height * 2,
int(99.5*16), int(43.5*16)) # Im(Z) = -0.5
qp.drawArc(center_x - width_2, center_y,
self.dim.width * 2, -self.dim.height * 2,
int(-99.5 * 16), int(-43.5 * 16)) # Im(Z) = 0.5
qp.drawArc(center_x - self.dim.width * 2, center_y,
self.dim.width * 5, self.dim.height * 5,
int(93.85 * 16), int(18.85 * 16)) # Im(Z) = -0.2
qp.drawArc(center_x - self.dim.width * 2, center_y,
self.dim.width * 5, -self.dim.height * 5,
int(-93.85 * 16), int(-18.85 * 16)) # Im(Z) = 0.2
self.drawTitle(qp)
qp.setPen(Chart.color.swr)
for swr in self.swrMarkers:
if swr <= 1:
continue
gamma = (swr - 1)/(swr + 1)
r = gamma * self.dim.width // 2
qp.drawEllipse(QtCore.QPoint(center_x, center_y), r, r)
qp.drawText(
QtCore.QRect(center_x - 50, center_y - 4 + r, 100, 20),
QtCore.Qt.AlignCenter, f"{swr}")

Wyświetl plik

@ -1,485 +0,0 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import math
import logging
import numpy as np
from PyQt5 import QtWidgets, QtGui, QtCore
from NanoVNASaver.Charts.Chart import Chart
logger = logging.getLogger(__name__)
class TDRChart(Chart):
maxDisplayLength = 50
minDisplayLength = 0
fixedSpan = False
minImpedance = 0
maxImpedance = 1000
fixedValues = False
markerLocation = -1
def __init__(self, name):
super().__init__(name)
self.tdrWindow = None
self.bottomMargin = 25
self.topMargin = 20
self.setMinimumSize(300, 300)
self.setSizePolicy(
QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding))
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, Chart.color.background)
self.setPalette(pal)
self.setAutoFillBackground(True)
self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
self.menu = QtWidgets.QMenu()
self.reset = QtWidgets.QAction("Reset")
self.reset.triggered.connect(self.resetDisplayLimits)
self.menu.addAction(self.reset)
self.x_menu = QtWidgets.QMenu("Length axis")
self.mode_group = QtWidgets.QActionGroup(self.x_menu)
self.action_automatic = QtWidgets.QAction("Automatic")
self.action_automatic.setCheckable(True)
self.action_automatic.setChecked(True)
self.action_automatic.changed.connect(
lambda: self.setFixedSpan(self.action_fixed_span.isChecked()))
self.action_fixed_span = QtWidgets.QAction("Fixed span")
self.action_fixed_span.setCheckable(True)
self.action_fixed_span.changed.connect(
lambda: self.setFixedSpan(self.action_fixed_span.isChecked()))
self.mode_group.addAction(self.action_automatic)
self.mode_group.addAction(self.action_fixed_span)
self.x_menu.addAction(self.action_automatic)
self.x_menu.addAction(self.action_fixed_span)
self.x_menu.addSeparator()
self.action_set_fixed_start = QtWidgets.QAction(
f"Start ({self.minDisplayLength})")
self.action_set_fixed_start.triggered.connect(self.setMinimumLength)
self.action_set_fixed_stop = QtWidgets.QAction(
f"Stop ({self.maxDisplayLength})")
self.action_set_fixed_stop.triggered.connect(self.setMaximumLength)
self.x_menu.addAction(self.action_set_fixed_start)
self.x_menu.addAction(self.action_set_fixed_stop)
self.y_menu = QtWidgets.QMenu("Impedance axis")
self.y_mode_group = QtWidgets.QActionGroup(self.y_menu)
self.y_action_automatic = QtWidgets.QAction("Automatic")
self.y_action_automatic.setCheckable(True)
self.y_action_automatic.setChecked(True)
self.y_action_automatic.changed.connect(
lambda: self.setFixedValues(self.y_action_fixed.isChecked()))
self.y_action_fixed = QtWidgets.QAction("Fixed")
self.y_action_fixed.setCheckable(True)
self.y_action_fixed.changed.connect(
lambda: self.setFixedValues(self.y_action_fixed.isChecked()))
self.y_mode_group.addAction(self.y_action_automatic)
self.y_mode_group.addAction(self.y_action_fixed)
self.y_menu.addAction(self.y_action_automatic)
self.y_menu.addAction(self.y_action_fixed)
self.y_menu.addSeparator()
self.y_action_set_fixed_maximum = QtWidgets.QAction(
f"Maximum ({self.maxImpedance})")
self.y_action_set_fixed_maximum.triggered.connect(
self.setMaximumImpedance)
self.y_action_set_fixed_minimum = QtWidgets.QAction(
f"Minimum ({self.minImpedance})")
self.y_action_set_fixed_minimum.triggered.connect(
self.setMinimumImpedance)
self.y_menu.addAction(self.y_action_set_fixed_maximum)
self.y_menu.addAction(self.y_action_set_fixed_minimum)
self.menu.addMenu(self.x_menu)
self.menu.addMenu(self.y_menu)
self.menu.addSeparator()
self.menu.addAction(self.action_save_screenshot)
self.action_popout = QtWidgets.QAction("Popout chart")
self.action_popout.triggered.connect(
lambda: self.popoutRequested.emit(self))
self.menu.addAction(self.action_popout)
self.dim.width = self.width() - self.leftMargin - self.rightMargin
self.dim.height = self.height() - self.bottomMargin - self.topMargin
def contextMenuEvent(self, event):
self.action_set_fixed_start.setText(
f"Start ({self.minDisplayLength})")
self.action_set_fixed_stop.setText(
f"Stop ({self.maxDisplayLength})")
self.y_action_set_fixed_minimum.setText(
f"Minimum ({self.minImpedance})")
self.y_action_set_fixed_maximum.setText(
f"Maximum ({self.maxImpedance})")
self.menu.exec_(event.globalPos())
def isPlotable(self, x, y):
return self.leftMargin <= x <= self.width() - self.rightMargin and \
self.topMargin <= y <= self.height() - self.bottomMargin
def resetDisplayLimits(self):
self.fixedSpan = False
self.minDisplayLength = 0
self.maxDisplayLength = 100
self.fixedValues = False
self.minImpedance = 0
self.maxImpedance = 1000
self.update()
def setFixedSpan(self, fixed_span):
self.fixedSpan = fixed_span
self.update()
def setMinimumLength(self):
min_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Start length (m)",
"Set start length (m)", value=self.minDisplayLength,
min=0, decimals=1)
if not selected:
return
if not (self.fixedSpan and min_val >= self.maxDisplayLength):
self.minDisplayLength = min_val
if self.fixedSpan:
self.update()
def setMaximumLength(self):
max_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Stop length (m)",
"Set stop length (m)", value=self.minDisplayLength,
min=0.1, decimals=1)
if not selected:
return
if not (self.fixedSpan and max_val <= self.minDisplayLength):
self.maxDisplayLength = max_val
if self.fixedSpan:
self.update()
def setFixedValues(self, fixed_values):
self.fixedValues = fixed_values
self.update()
def setMinimumImpedance(self):
min_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Minimum impedance (\N{OHM SIGN})",
"Set minimum impedance (\N{OHM SIGN})",
value=self.minDisplayLength,
min=0, decimals=1)
if not selected:
return
if not (self.fixedValues and min_val >= self.maxImpedance):
self.minImpedance = min_val
if self.fixedValues:
self.update()
def setMaximumImpedance(self):
max_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Maximum impedance (\N{OHM SIGN})",
"Set maximum impedance (\N{OHM SIGN})",
value=self.minDisplayLength,
min=0.1, decimals=1)
if not selected:
return
if not (self.fixedValues and max_val <= self.minImpedance):
self.maxImpedance = max_val
if self.fixedValues:
self.update()
def copy(self):
new_chart: TDRChart = super().copy()
new_chart.tdrWindow = self.tdrWindow
new_chart.minDisplayLength = self.minDisplayLength
new_chart.maxDisplayLength = self.maxDisplayLength
new_chart.fixedSpan = self.fixedSpan
new_chart.minImpedance = self.minImpedance
new_chart.maxImpedance = self.maxImpedance
new_chart.fixedValues = self.fixedValues
self.tdrWindow.updated.connect(new_chart.update)
return new_chart
def mouseMoveEvent(self, a0: QtGui.QMouseEvent) -> None:
if a0.buttons() == QtCore.Qt.RightButton:
a0.ignore()
return
if a0.buttons() == QtCore.Qt.MiddleButton:
# Drag the display
a0.accept()
if self.dragbox.move_x != -1 and self.dragbox.move_y != -1:
dx = self.dragbox.move_x - a0.x()
dy = self.dragbox.move_y - a0.y()
self.zoomTo(self.leftMargin + dx, self.topMargin + dy,
self.leftMargin + self.dim.width + dx,
self.topMargin + self.dim.height + dy)
self.dragbox.move_x = a0.x()
self.dragbox.move_y = a0.y()
return
if a0.modifiers() == QtCore.Qt.ControlModifier:
# Dragging a box
if not self.dragbox.state:
self.dragbox.pos_start = (a0.x(), a0.y())
self.dragbox.pos = (a0.x(), a0.y())
self.update()
a0.accept()
return
x = a0.x()
absx = x - self.leftMargin
if absx < 0 or absx > self.width() - self.rightMargin:
a0.ignore()
return
a0.accept()
width = self.width() - self.leftMargin - self.rightMargin
if self.tdrWindow.td.size:
if self.fixedSpan:
max_index = np.searchsorted(
self.tdrWindow.distance_axis, self.maxDisplayLength * 2)
min_index = np.searchsorted(
self.tdrWindow.distance_axis, self.minDisplayLength * 2)
x_step = (max_index - min_index) / width
else:
max_index = math.ceil(len(self.tdrWindow.distance_axis) / 2)
x_step = max_index / width
self.markerLocation = int(round(absx * x_step))
self.update()
return
def paintEvent(self, _: QtGui.QPaintEvent) -> None:
qp = QtGui.QPainter(self)
qp.setPen(QtGui.QPen(Chart.color.text))
qp.drawText(3, 15, self.name)
width = self.width() - self.leftMargin - self.rightMargin
height = self.height() - self.bottomMargin - self.topMargin
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin - 5,
self.height() - self.bottomMargin,
self.width() - self.rightMargin,
self.height() - self.bottomMargin)
qp.drawLine(self.leftMargin,
self.topMargin - 5,
self.leftMargin,
self.height() - self.bottomMargin + 5)
# Number of ticks does not include the origin
ticks = (self.width() - self.leftMargin) // 100
self.drawTitle(qp)
if self.tdrWindow.td.size:
if self.fixedSpan:
max_length = max(0.1, self.maxDisplayLength)
max_index = np.searchsorted(
self.tdrWindow.distance_axis, max_length * 2)
min_index = np.searchsorted(
self.tdrWindow.distance_axis, self.minDisplayLength * 2)
if max_index == min_index:
if max_index < len(self.tdrWindow.distance_axis) - 1:
max_index += 1
else:
min_index -= 1
x_step = (max_index - min_index) / width
else:
min_index = 0
max_index = math.ceil(len(self.tdrWindow.distance_axis) / 2)
x_step = max_index / width
if self.fixedValues:
min_impedance = max(0, self.minImpedance)
max_impedance = max(0.1, self.maxImpedance)
else:
# TODO: Limit the search to the selected span?
min_impedance = max(
0,
np.min(self.tdrWindow.step_response_Z) / 1.05)
max_impedance = min(
1000,
np.max(self.tdrWindow.step_response_Z) * 1.05)
y_step = np.max(self.tdrWindow.td) * 1.1 / height
y_impedance_step = (max_impedance - min_impedance) / height
for i in range(ticks):
x = self.leftMargin + round((i + 1) * width / ticks)
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(x, self.topMargin, x, self.topMargin + height)
qp.setPen(QtGui.QPen(Chart.color.text))
qp.drawText(
x - 15,
self.topMargin + height + 15,
str(round(
self.tdrWindow.distance_axis[
min_index +
int((x - self.leftMargin) * x_step) - 1] / 2,
1)) + "m")
qp.setPen(QtGui.QPen(Chart.color.text))
qp.drawText(
self.leftMargin - 10,
self.topMargin + height + 15,
str(round(self.tdrWindow.distance_axis[min_index] / 2,
1)) + "m")
y_ticks = math.floor(height / 60)
y_tick_step = height / y_ticks
for i in range(y_ticks):
y = self.bottomMargin + int(i * y_tick_step)
qp.setPen(Chart.color.foreground)
qp.drawLine(self.leftMargin, y, self.leftMargin + width, y)
y_val = max_impedance - y_impedance_step * i * y_tick_step
qp.setPen(Chart.color.text)
qp.drawText(3, y + 3, str(round(y_val, 1)))
qp.drawText(3, self.topMargin + height + 3,
str(round(min_impedance, 1)))
pen = QtGui.QPen(Chart.color.sweep)
pen.setWidth(self.dim.point)
qp.setPen(pen)
for i in range(min_index, max_index):
if i < min_index or i > max_index:
continue
x = self.leftMargin + int((i - min_index) / x_step)
y = (self.topMargin + height) - \
int(self.tdrWindow.td[i] / y_step)
if self.isPlotable(x, y):
pen.setColor(Chart.color.sweep)
qp.setPen(pen)
qp.drawPoint(x, y)
x = self.leftMargin + int((i - min_index) / x_step)
y = (self.topMargin + height) - int(
(self.tdrWindow.step_response_Z[i] - min_impedance) /
y_impedance_step)
if self.isPlotable(x, y):
pen.setColor(Chart.color.sweep_secondary)
qp.setPen(pen)
qp.drawPoint(x, y)
id_max = np.argmax(self.tdrWindow.td)
max_point = QtCore.QPoint(
self.leftMargin + int((id_max - min_index) / x_step),
(self.topMargin + height) - int(self.tdrWindow.td[id_max] / y_step))
qp.setPen(self.markers[0].color)
qp.drawEllipse(max_point, 2, 2)
qp.setPen(Chart.color.text)
qp.drawText(max_point.x() - 10, max_point.y() - 5,
str(round(self.tdrWindow.distance_axis[id_max] / 2,
2)) + "m")
if self.markerLocation != -1:
marker_point = QtCore.QPoint(
self.leftMargin +
int((self.markerLocation - min_index) / x_step),
(self.topMargin + height) -
int(self.tdrWindow.td[self.markerLocation] / y_step))
qp.setPen(Chart.color.text)
qp.drawEllipse(marker_point, 2, 2)
qp.drawText(
marker_point.x() - 10,
marker_point.y() - 5,
str(round(self.tdrWindow.distance_axis[self.markerLocation] / 2,
2)) + "m")
if self.dragbox.state and self.dragbox.pos[0] != -1:
dashed_pen = QtGui.QPen(
Chart.color.foreground, 1, QtCore.Qt.DashLine)
qp.setPen(dashed_pen)
qp.drawRect(
QtCore.QRect(
QtCore.QPoint(*self.dragbox.pos_start),
QtCore.QPoint(*self.dragbox.pos)
)
)
qp.end()
def valueAtPosition(self, y):
if self.tdrWindow.td.size:
height = self.height() - self.topMargin - self.bottomMargin
absy = (self.height() - y) - self.bottomMargin
if self.fixedValues:
min_impedance = self.minImpedance
max_impedance = self.maxImpedance
else:
min_impedance = max(
0,
np.min(self.tdrWindow.step_response_Z) / 1.05)
max_impedance = min(
1000,
np.max(self.tdrWindow.step_response_Z) * 1.05)
y_step = (max_impedance - min_impedance) / height
return y_step * absy + min_impedance
return 0
def lengthAtPosition(self, x, limit=True):
if not self.tdrWindow.td.size:
return 0
width = self.width() - self.leftMargin - self.rightMargin
absx = x - self.leftMargin
min_length = self.minDisplayLength if self.fixedSpan else 0
max_length = self.maxDisplayLength if self.fixedSpan else (
self.tdrWindow.distance_axis[
math.ceil(len(self.tdrWindow.distance_axis) / 2)
] / 2)
x_step = (max_length - min_length) / width
if limit and absx < 0:
return min_length
return max_length if limit and absx > width else absx * x_step + min_length
def zoomTo(self, x1, y1, x2, y2):
logger.debug(
"Zoom to (x,y) by (x,y): (%d, %d) by (%d, %d)", x1, y1, x2, y2)
val1 = self.valueAtPosition(y1)
val2 = self.valueAtPosition(y2)
if val1 != val2:
self.minImpedance = round(min(val1, val2), 3)
self.maxImpedance = round(max(val1, val2), 3)
self.setFixedValues(True)
len1 = max(0, self.lengthAtPosition(x1, limit=False))
len2 = max(0, self.lengthAtPosition(x2, limit=False))
if len1 >= 0 and len2 >= 0 and len1 != len2:
self.minDisplayLength = min(len1, len2)
self.maxDisplayLength = max(len1, len2)
self.setFixedSpan(True)
self.update()
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
super().resizeEvent(a0)
self.dim.width = self.width() - self.leftMargin - self.rightMargin
self.dim.height = self.height() - self.bottomMargin - self.topMargin

Wyświetl plik

@ -1,3 +0,0 @@
from .MarkerControl import MarkerControl
from .SweepControl import SweepControl
from .SerialControl import SerialControl

Wyświetl plik

@ -1,3 +0,0 @@
from .Widget import Marker
from .Delta import DeltaMarker
from .Values import Value, default_label_ids

Wyświetl plik

@ -1,117 +0,0 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
from enum import Enum
from math import log
from threading import Lock
from typing import Iterator, Tuple
logger = logging.getLogger(__name__)
class SweepMode(Enum):
SINGLE = 0
CONTINOUS = 1
AVERAGE = 2
class Properties:
def __init__(self, name: str = "",
mode: 'SweepMode' = SweepMode.SINGLE,
averages: Tuple[int, int] = (3, 0),
logarithmic: bool = False):
self.name = name
self.mode = mode
self.averages = averages
self.logarithmic = logarithmic
def __repr__(self):
return (
f"Properties('{self.name}', {self.mode}, {self.averages},"
f" {self.logarithmic})")
class Sweep:
def __init__(self, start: int = 3600000, end: int = 30000000,
points: int = 101, segments: int = 1,
properties: 'Properties' = Properties()):
self.start = start
self.end = end
self.points = points
self.segments = segments
self.properties = properties
self.lock = Lock()
self.check()
logger.debug("%s", self)
def __repr__(self) -> str:
return (
f"Sweep({self.start}, {self.end}, {self.points}, {self.segments},"
f" {self.properties})")
def __eq__(self, other) -> bool:
return (self.start == other.start and
self.end == other.end and
self.points == other.points and
self.segments == other.segments and
self.properties == other.properties)
def copy(self) -> 'Sweep':
return Sweep(self.start, self.end, self.points, self.segments,
self.properties)
@property
def span(self) -> int:
return self.end - self.start
@property
def stepsize(self) -> int:
return round(self.span / (self.points * self.segments - 1))
def check(self):
if (
self.segments <= 0
or self.points <= 0
or self.start <= 0
or self.end <= 0
or self.stepsize < 1
):
raise ValueError(f"Illegal sweep settings: {self}")
def _exp_factor(self, index: int) -> float:
return 1 - log(self.segments + 1 - index) / log(self.segments + 1)
def get_index_range(self, index: int) -> Tuple[int, int]:
if not self.properties.logarithmic:
start = self.start + index * self.points * self.stepsize
end = start + (self.points - 1) * self.stepsize
else:
start = round(self.start + self.span * self._exp_factor(index))
end = round(self.start + self.span * self._exp_factor(index + 1))
logger.debug("get_index_range(%s) -> (%s, %s)", index, start, end)
return start, end
def get_frequencies(self) -> Iterator[int]:
for i in range(self.segments):
start, stop = self.get_index_range(i)
step = (stop - start) / self.points
freq = start
for _ in range(self.points):
yield round(freq)
freq += step

Wyświetl plik

@ -1,2 +0,0 @@
from .Bands import BandsModel
from .Sweep import Sweep

Wyświetl plik

@ -1,88 +0,0 @@
# NanoVNASaver
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import re
logger = logging.getLogger(__name__)
class Version:
RXP = re.compile(r"""^
\D*
(?P<major>\d+)\.
(?P<minor>\d+)\.?
(?P<revision>\d+)?
(?P<note>.*)
$""", re.VERBOSE)
def __init__(self, vstring: str = "0.0.0"):
self.data = {
"major": 0,
"minor": 0,
"revision": 0,
"note": "",
}
try:
self.data = Version.RXP.search(vstring).groupdict()
for name in ("major", "minor", "revision"):
self.data[name] = int(self.data[name])
except TypeError:
self.data["revision"] = 0
except AttributeError:
logger.error("Unable to parse version: %s", vstring)
def __gt__(self, other: "Version") -> bool:
l, r = self.data, other.data
for name in ("major", "minor", "revision"):
if l[name] > r[name]:
return True
if l[name] < r[name]:
return False
return False
def __lt__(self, other: "Version") -> bool:
return other.__gt__(self)
def __ge__(self, other: "Version") -> bool:
return self.__gt__(other) or self.__eq__(other)
def __le__(self, other: "Version") -> bool:
return other.__gt__(self) or self.__eq__(other)
def __eq__(self, other: "Version") -> bool:
return self.data == other.data
def __str__(self) -> str:
return (f'{self.data["major"]}.{self.data["minor"]}'
f'.{self.data["revision"]}{self.data["note"]}')
@property
def major(self) -> int:
return self.data["major"]
@property
def minor(self) -> int:
return self.data["minor"]
@property
def revision(self) -> int:
return self.data["revision"]
@property
def note(self) -> str:
return self.data["note"]

Wyświetl plik

@ -1,159 +0,0 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import contextlib
import logging
from time import strftime, localtime
from urllib import request, error
from PyQt5 import QtWidgets, QtCore
from NanoVNASaver.About import VERSION_URL, INFO_URL
from NanoVNASaver.Version import Version
logger = logging.getLogger(__name__)
class AboutWindow(QtWidgets.QWidget):
def __init__(self, app: QtWidgets.QWidget):
super().__init__()
self.app = app
self.setWindowTitle("About NanoVNASaver")
self.setWindowIcon(self.app.icon)
top_layout = QtWidgets.QHBoxLayout()
self.setLayout(top_layout)
QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
icon_layout = QtWidgets.QVBoxLayout()
top_layout.addLayout(icon_layout)
icon = QtWidgets.QLabel()
icon.setPixmap(self.app.icon.pixmap(128, 128))
icon_layout.addWidget(icon)
icon_layout.addStretch()
layout = QtWidgets.QVBoxLayout()
top_layout.addLayout(layout)
layout.addWidget(QtWidgets.QLabel(
f"NanoVNASaver version {self.app.version}"))
layout.addWidget(QtWidgets.QLabel(""))
layout.addWidget(QtWidgets.QLabel(
"\N{COPYRIGHT SIGN} Copyright 2019, 2020 Rune B. Broberg\n"
"\N{COPYRIGHT SIGN} Copyright 2020ff NanoVNA-Saver Authors"
))
layout.addWidget(QtWidgets.QLabel(
"This program comes with ABSOLUTELY NO WARRANTY"))
layout.addWidget(QtWidgets.QLabel(
"This program is licensed under the"
" GNU General Public License version 3"))
layout.addWidget(QtWidgets.QLabel(""))
link_label = QtWidgets.QLabel(
f'For further details, see: <a href="{INFO_URL}">'
f"{INFO_URL}")
link_label.setOpenExternalLinks(True)
layout.addWidget(link_label)
layout.addWidget(QtWidgets.QLabel(""))
self.versionLabel = QtWidgets.QLabel(
"NanoVNA Firmware Version: Not connected.")
layout.addWidget(self.versionLabel)
layout.addStretch()
btn_check_version = QtWidgets.QPushButton("Check for updates")
btn_check_version.clicked.connect(self.findUpdates)
self.updateLabel = QtWidgets.QLabel("Last checked: ")
update_hbox = QtWidgets.QHBoxLayout()
update_hbox.addWidget(btn_check_version)
update_form = QtWidgets.QFormLayout()
update_hbox.addLayout(update_form)
update_hbox.addStretch()
update_form.addRow(self.updateLabel)
layout.addLayout(update_hbox)
layout.addStretch()
btn_ok = QtWidgets.QPushButton("Ok")
btn_ok.clicked.connect(lambda: self.close()) # noqa
layout.addWidget(btn_ok)
def show(self):
super().show()
self.updateLabels()
def updateLabels(self):
with contextlib.suppress(IOError, AttributeError):
self.versionLabel.setText(
f"NanoVNA Firmware Version: {self.app.vna.name} "
f"v{self.app.vna.version}")
def findUpdates(self, automatic=False):
latest_version = Version()
latest_url = ""
try:
req = request.Request(VERSION_URL)
req.add_header('User-Agent', f'NanoVNA-Saver/{self.app.version}')
for line in request.urlopen(req, timeout=3):
line = line.decode("utf-8")
if line.startswith("VERSION ="):
latest_version = Version(line[8:].strip(" \"'"))
if line.startswith("RELEASE_URL ="):
latest_url = line[13:].strip(" \"'")
except error.HTTPError as e:
logger.exception("Checking for updates produced an HTTP exception: %s", e)
self.updateLabel.setText("Connection error.")
return
except TypeError as e:
logger.exception("Checking for updates provided an unparseable file: %s", e)
self.updateLabel.setText("Data error reading versions.")
return
except error.URLError as e:
logger.exception("Checking for updates produced a URL exception: %s", e)
self.updateLabel.setText("Connection error.")
return
logger.info("Latest version is %s", latest_version)
this_version = Version(self.app.version)
logger.info("This is %s", this_version)
if latest_version > this_version:
logger.info("New update available: %s!", latest_version)
if automatic:
QtWidgets.QMessageBox.information(
self,
"Updates available",
f"There is a new update for NanoVNA-Saver available!\n"
f"Version {latest_version}\n\n"
f'Press "About" to find the update.')
else:
QtWidgets.QMessageBox.information(
self, "Updates available",
"There is a new update for NanoVNA-Saver available!")
self.updateLabel.setText(
f'<a href="{latest_url}">New version available</a>.')
self.updateLabel.setOpenExternalLinks(True)
else:
# Probably don't show a message box, just update the screen?
# Maybe consider showing it if not an automatic update.
#
self.updateLabel.setText(
f"Last checked: "
f"{strftime('%Y-%m-%d %H:%M:%S', localtime())}")
return

Wyświetl plik

@ -1,176 +0,0 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020, 2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import math
import numpy as np
from scipy import signal
from PyQt5 import QtWidgets, QtCore
logger = logging.getLogger(__name__)
class TDRWindow(QtWidgets.QWidget):
updated = QtCore.pyqtSignal()
def __init__(self, app: QtWidgets.QWidget):
super().__init__()
self.app = app
self.td = np.array([])
self.distance_axis = []
self.step_response = []
self.step_response_Z = []
self.setWindowTitle("TDR")
self.setWindowIcon(self.app.icon)
QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
layout = QtWidgets.QFormLayout()
self.setLayout(layout)
self.tdr_velocity_dropdown = QtWidgets.QComboBox()
self.tdr_velocity_dropdown.addItem("Jelly filled (0.64)", 0.64)
self.tdr_velocity_dropdown.addItem("Polyethylene (0.66)", 0.66)
self.tdr_velocity_dropdown.addItem("PTFE (Teflon) (0.70)", 0.70)
self.tdr_velocity_dropdown.addItem("Pulp Insulation (0.72)", 0.72)
self.tdr_velocity_dropdown.addItem("Foam or Cellular PE (0.78)", 0.78)
self.tdr_velocity_dropdown.addItem("Semi-solid PE (SSPE) (0.84)", 0.84)
self.tdr_velocity_dropdown.addItem(
"Air (Helical spacers) (0.94)", 0.94)
self.tdr_velocity_dropdown.insertSeparator(
self.tdr_velocity_dropdown.count())
# Lots of cable types added by Larry Goga, AE5CZ
self.tdr_velocity_dropdown.addItem(
"RG-6/U PE 75\N{OHM SIGN} (Belden 8215) (0.66)", 0.66)
self.tdr_velocity_dropdown.addItem(
"RG-6/U Foam 75\N{OHM SIGN} (Belden 9290) (0.81)", 0.81)
self.tdr_velocity_dropdown.addItem(
"RG-8/U PE 50\N{OHM SIGN} (Belden 8237) (0.66)", 0.66)
self.tdr_velocity_dropdown.addItem(
"RG-8/U Foam (Belden 8214) (0.78)", 0.78)
self.tdr_velocity_dropdown.addItem("RG-8/U (Belden 9913) (0.84)", 0.84)
# Next one added by EKZ, KC3KZ, from measurement of actual cable
self.tdr_velocity_dropdown.addItem(
"RG-8/U (Shireen RFC®400 Low Loss) (0.86)", 0.86)
self.tdr_velocity_dropdown.addItem("RG-8X (Belden 9258) (0.82)", 0.82)
# Next three added by EKZ, KC3KZ, from measurement of actual cable
self.tdr_velocity_dropdown.addItem(
"RG-8X (Wireman \"Super 8\" CQ106) (0.81)", 0.81)
self.tdr_velocity_dropdown.addItem(
"RG-8X (Wireman \"MINI-8 Lo-Loss\" CQ118) (0.82)", 0.82)
self.tdr_velocity_dropdown.addItem(
"RG-58 (Wireman \"CQ 58 Lo-Loss Flex\" CQ129FF) (0.79)", 0.79)
self.tdr_velocity_dropdown.addItem(
"RG-11/U 75\N{OHM SIGN} Foam HDPE (Belden 9292) (0.84)", 0.84)
self.tdr_velocity_dropdown.addItem(
"RG-58/U 52\N{OHM SIGN} PE (Belden 9201) (0.66)", 0.66)
self.tdr_velocity_dropdown.addItem(
"RG-58A/U 54\N{OHM SIGN} Foam (Belden 8219) (0.73)", 0.73)
self.tdr_velocity_dropdown.addItem(
"RG-59A/U PE 75\N{OHM SIGN} (Belden 8241) (0.66)", 0.66)
self.tdr_velocity_dropdown.addItem(
"RG-59A/U Foam 75\N{OHM SIGN} (Belden 8241F) (0.78)", 0.78)
self.tdr_velocity_dropdown.addItem(
"RG-174 PE (Belden 8216)(0.66)", 0.66)
self.tdr_velocity_dropdown.addItem(
"RG-174 Foam (Belden 7805R) (0.735)", 0.735)
self.tdr_velocity_dropdown.addItem(
"RG-213/U PE (Belden 8267) (0.66)", 0.66)
self.tdr_velocity_dropdown.addItem("RG316 (0.695)", 0.695)
self.tdr_velocity_dropdown.addItem("RG402 (0.695)", 0.695)
self.tdr_velocity_dropdown.addItem("LMR-240 (0.84)", 0.84)
self.tdr_velocity_dropdown.addItem("LMR-240UF (0.80)", 0.80)
self.tdr_velocity_dropdown.addItem("LMR-400 (0.85)", 0.85)
self.tdr_velocity_dropdown.addItem("LMR400UF (0.83)", 0.83)
self.tdr_velocity_dropdown.addItem("Davis Bury-FLEX (0.82)", 0.82)
self.tdr_velocity_dropdown.insertSeparator(
self.tdr_velocity_dropdown.count())
self.tdr_velocity_dropdown.addItem("Custom", -1)
self.tdr_velocity_dropdown.setCurrentIndex(1) # Default to PE (0.66)
self.tdr_velocity_dropdown.currentIndexChanged.connect(self.updateTDR)
layout.addRow(self.tdr_velocity_dropdown)
self.tdr_velocity_input = QtWidgets.QLineEdit()
self.tdr_velocity_input.setDisabled(True)
self.tdr_velocity_input.setText("0.66")
self.tdr_velocity_input.textChanged.connect(self.app.dataUpdated)
layout.addRow("Velocity factor", self.tdr_velocity_input)
self.tdr_result_label = QtWidgets.QLabel()
layout.addRow("Estimated cable length:", self.tdr_result_label)
layout.addRow(self.app.tdr_chart)
def updateTDR(self):
c = 299792458
# TODO: Let the user select whether to use high or low resolution TDR?
FFT_POINTS = 2**14
if len(self.app.data.s11) < 2:
return
if self.tdr_velocity_dropdown.currentData() == -1:
self.tdr_velocity_input.setDisabled(False)
else:
self.tdr_velocity_input.setDisabled(True)
self.tdr_velocity_input.setText(
str(self.tdr_velocity_dropdown.currentData()))
try:
v = float(self.tdr_velocity_input.text())
except ValueError:
return
step_size = self.app.data.s11[1].freq - self.app.data.s11[0].freq
if step_size == 0:
self.tdr_result_label.setText("")
logger.info("Cannot compute cable length at 0 span")
return
s11 = [np.complex(d.re, d.im) for d in self.app.data.s11]
window = np.blackman(len(self.app.data.s11))
windowed_s11 = window * s11
self.td = np.abs(np.fft.ifft(windowed_s11, FFT_POINTS))
step = np.ones(FFT_POINTS)
self.step_response = signal.convolve(self.td, step)
self.step_response_Z = 50 * (
1 + self.step_response) / (1 - self.step_response)
time_axis = np.linspace(0, 1 / step_size, FFT_POINTS)
self.distance_axis = time_axis * v * c
# peak = np.max(td)
# We should check that this is an actual *peak*, and not just a vague maximum
index_peak = np.argmax(self.td)
cable_len = round(self.distance_axis[index_peak] / 2, 3)
feet = math.floor(cable_len / 0.3048)
inches = round(((cable_len / 0.3048) - feet) * 12, 1)
self.tdr_result_label.setText(f"{cable_len}m ({feet}ft {inches}in)")
self.app.tdr_result_label.setText(f"{cable_len}m")
self.updated.emit()

Wyświetl plik

@ -0,0 +1 @@
icon_48x48.png

14
Pipfile
Wyświetl plik

@ -1,14 +0,0 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
pyserial = "*"
pyqt5 = "*"
numpy = "*"
[requires]
python_version = "3.7"

294
README.md
Wyświetl plik

@ -1,294 +0,0 @@
[![Latest Release](https://img.shields.io/github/v/release/NanoVNA-Saver/nanovna-saver.svg)](https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest)
[![License](https://img.shields.io/github/license/NanoVNA-Saver/nanovna-saver.svg)](https://github.com/NanoVNA-Saver/nanovna-saver/blob/master/LICENSE)
[![Downloads](https://img.shields.io/github/downloads/NanoVNA-Saver/nanovna-saver/total.svg)](https://github.com/NanoVNA-Saver/nanovna-saver/releases/)
[![GitHub Releases](https://img.shields.io/github/downloads/NanoVNA-Saver/nanovna-saver/latest/total)](https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest)
[![Donate](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=T8KTGVDQF5K6E&item_name=NanoVNASaver+Development&currency_code=EUR&source=url)
NanoVNASaver
============
A multiplatform tool to save Touchstone files from the NanoVNA,
sweep frequency spans in segments to gain more than 101 data
points, and generally display and analyze the resulting data.
- Copyright 2019, 2020 Rune B. Broberg
- Copyright 2020ff NanoVNA-Saver Authors
Latest Changes
--------------
### Changes in 0.5.0
- Fix crash on open in use serial device
- Use a Defaults module for all settings -
ignores old .ini settings
- Refactoring and unifying Chart classes
- No more automatic update checks (more privacy)
- Corrected error handling in NanaVNA\_V2 code
### Changes in 0.4.0
- PA0JOZ Enhanced Response Correction
This is the reason vor minor version increase as older callibration data shouldn't
be use.
- Fix linux binary build
- Many bugfixes
Introduction
------------
This software connects to a NanoVNA and extracts the data for
display on a computer and allows saving the sweep data to Touchstone files.
Current features:
- Reading data from a NanoVNA -- Compatible devices: NanoVNA, NanoVNA-H,
NanoVNA-H4, NanoVNA-F, AVNA via Teensy
- Splitting a frequency range into multiple segments to increase resolution
(tried up to >10k points)
- Averaging data for better results particularly at higher frequencies
- Displaying data on multiple chart types, such as Smith, LogMag, Phase and
VSWR-charts, for both S11 and S21
- Displaying markers, and the impedance, VSWR, Q, equivalent
capacitance/inductance etc. at these locations
- Displaying customizable frequency bands as reference, for example amateur
radio bands
- Exporting and importing 1-port and 2-port Touchstone files
- TDR function (measurement of cable length) - including impedance display
- Filter analysis functions for low-pass, high-pass, band-pass and band-stop
filters
- Display of both an active and a reference trace
- Live updates of data from the NanoVNA, including for multi-segment sweeps
- In-application calibration, including compensation for non-ideal calibration
standards
- Customizable display options, including "dark mode"
- Exporting images of plotted values
0.1.4:
![Screenshot of version 0.1.4](https://i.imgur.com/ZoFsV2V.png)
Running the application
-----------------------
The software was written in Python on Windows, using Pycharm, and the modules
PyQT5, numpy, scipy and pyserial.
Main development is currently done on Linux (Mint 21 "Vanessa" Cinnamon)
### Binary releases
You can find 64bit binary releases for Windows, Linux and MacOS under
<https://github.com/NanoVNA-Saver/nanovna-saver/releases/>
Versions older than Windows 7 are not known to work.
#### Windows 7
It requires Service Pack 1 and [Microsoft VC++ Redistributable](
https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads).
For most users, this would already be installed.
#### Windows 10
The downloadable executable runs directly, and requires no installation.
### Installation and Use with pip
1. Clone repo and cd into the directory
git clone https://github.com/NanoVNA-Saver/nanovna-saver
cd nanovna-saver
2. Run the pip installation
pip3 install .
3. Once completed run with the following command
NanoVNASaver
#### Ubuntu 20.04
1. Install python3.8 and pip
sudo apt install python3.8 python3-pip
python3 -m venv ~/.venv_nano
. ~/.venv_nano/bin/activate
pip install -U pip
2. Clone repo and cd into the directory
git clone https://github.com/NanoVNA-Saver/nanovna-saver
cd nanovna-saver
3. Update pip and run the pip installation
python3 -m pip install .
(You may need to install the additional packages python3-distutils,
python3-setuptools and python3-wheel for this command to work on some
distributions.)
4. Once completed run with the following command
. ~/.venv_nano/bin/activate
python3 nanovna-saver.py
#### MacPorts
Via a MacPorts distribution maintained by @ra1nb0w.
1. Install MacPorts following the [install guide](https://www.macports.org/install.php)
2. Install NanoVNASaver :
sudo port install NanoVNASaver
3. Now you can run the software from shell `NanoVNASaver` or run as app
`/Applications/MacPorts/NanoVNASaver.app`
#### Homebrew
1. Install Homebrew from <https://brew.sh/> (This will ask for your password)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
2. Python :
brew install python
3. Pip :<br/>
Download the get-pip.py file and run it to install pip
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python3 get-pip.py
4. NanoVNASaver Installation : <br/>
clone the source code to the nanovna-saver folder
git clone https://github.com/NanoVNA-Saver/nanovna-saver
cd nanovna-saver
5. Install local pip packages
python3 -m pip install .
6. Run nanovna-saver in the nanovna-saver folder by:
python3 nanovna-saver.py
Using the software
------------------
Connect your NanoVNA to a serial port, and enter this serial port in the serial
port box. If the NanoVNA is connected before the application starts, it should
be automatically detected. Otherwise, click "Rescan". Click "Connect to device"
to connect.
The app can collect multiple segments to get more accurate measurements. Enter
the number of segments to be done in the "Segments" box. Each segment is 101
data points, and takes about 1.5 seconds to complete.
Frequencies are entered in Hz, or suffixed with k or M. Scientific notation
(6.5e6 for 6.5MHz) also works.
Markers can be manually entered, or controlled using the mouse. For mouse
control, select the active marker using the radio buttons, or hold "shift"
while clicking to drag the nearest marker. The marker readout boxes show the
actual frequency where values are measured. Marker readouts can be hidden
using the "hide data" button when not needed.
Display settings are available under "Display setup". These allow changing the
chart colours, the application font size and which graphs are displayed. The
settings are saved between program starts.
### Calibration
_Before using NanoVNA-Saver, please ensure that the device itself is in a
reasonable calibration state._
A calibration of both ports across the entire frequency span, saved to save
slot 0, is sufficient. If the NanoVNA is completely uncalibrated, its readings
may be outside the range accepted by the application.
In-application calibration is available, either assuming ideal standards or
with relevant standard correction. To manually calibrate, sweep each standard
in turn and press the relevant button in the calibration window.
For assisted calibration, press the "Calibration Assistant" button. If desired,
enter a note in the provided field describing the conditions under which the
calibration was performed.
Calibration results may be saved and loaded using the provided buttons at the
bottom of the window. Notes are saved and loaded along with the calibration
data.
![Screenshot of Calibration Window](https://i.imgur.com/p94cxOX.png)
Users of known characterized calibration standard sets can enter the data for
these, and save the sets.
After pressing _Apply_, the calibration is immediately applied to the latest
sweep data.
\! _Currently, load capacitance is unsupported_ \!
### TDR
To get accurate TDR measurements, calibrate the device, and attach the cable to
be measured at the calibration plane - i.e. at the same position where the
calibration load would be attached. Open the "Time Domain Reflectometry"
window, and select the correct cable type, or manually enter a propagation
factor.
### Frequency bands
Open the "Display setup" window to configure the display of frequency bands. By
clicking "show bands", predefined frequency bands will be shown on the
frequency-based charts. Click manage bands to change which bands are shown,
and the frequency limits of each. Bands default and reset to European amateur
radio band frequencies.
License
-------
This software is licensed under version 3 of the GNU General Public License. It
comes with NO WARRANTY.
You can use it, commercially as well. You may make changes to the code, but I
(and the license) ask that you give these changes back to the community.
Links
-----
- Ohan Smit wrote an introduction to using the application:
[https://zs1sci.com/blog/nanovnasaver/]
- HexAndFlex wrote a 3-part (thus far) series on Getting Started with the NanoVNA:
[https://hexandflex.com/2019/08/31/getting-started-with-the-nanovna-part-1/] - Part 3 is dedicated to NanoVNASaver:
[https://hexandflex.com/2019/09/15/getting-started-with-the-nanovna-part-3-pc-software/]
- Gunthard Kraus did documentation in English and German:
[http://www.gunthard-kraus.de/fertig_NanoVNA/English/]
[http://www.gunthard-kraus.de/fertig_NanoVNA/Deutsch/]
Credits
-------
Original application by Rune B. Broberg (5Q5R)
Contributions and changes by Holger Müller, David Hunt and others.
TDR inspiration shamelessly stolen from the work of Salil (VU2CWA) at
<https://nuclearrambo.com/wordpress/accurately-measuring-cable-length-with-nanovna/>
TDR cable types by Larry Goga.
Bugfixes and Python installation work by Ohan Smit.
Thanks to everyone who have tested, commented and inspired. Particular thanks
go to the alpha testing crew who suffer the early instability of new versions.
This software is available free of charge. If you read all this way, and you
*still* want to support it, you may donate to the developer using the button
below:
[![Paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=T8KTGVDQF5K6E&item_name=NanoVNASaver+Development&currency_code=EUR&source=url)

271
README.rst 100644
Wyświetl plik

@ -0,0 +1,271 @@
.. role:: raw-html-m2r(raw)
:format: html
.. image:: https://img.shields.io/github/v/release/NanoVNA-Saver/nanovna-saver.svg
:target: https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest
:alt: Latest Release
.. image:: https://img.shields.io/github/license/NanoVNA-Saver/nanovna-saver.svg
:target: https://github.com/NanoVNA-Saver/nanovna-saver/blob/master/LICENSE.txt
:alt: License
.. image:: https://img.shields.io/github/downloads/NanoVNA-Saver/nanovna-saver/total.svg
:target: https://github.com/NanoVNA-Saver/nanovna-saver/releases/
:alt: Downloads
.. image:: https://img.shields.io/github/downloads/NanoVNA-Saver/nanovna-saver/latest/total
:target: https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest
:alt: GitHub Releases
.. image:: https://img.shields.io/badge/paypal-donate-yellow.svg
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=T8KTGVDQF5K6E&item_name=NanoVNASaver+Development&currency_code=EUR&source=url
:alt: Donate
NanoVNASaver
============
A multiplatform tool to save Touchstone files from the NanoVNA,
sweep frequency spans in segments to gain more than 101 data
points, and generally display and analyze the resulting data.
* Copyright 2019, 2020 Rune B. Broberg
* Copyright 2020ff NanoVNA-Saver Authors
It's developed in **Python 3 (>=3.8)** using **PyQt6**, **numpy** and
**scipy**.
Introduction
------------
This software connects to a NanoVNA and extracts the data for
display on a computer and allows saving the sweep data to Touchstone files.
:raw-html-m2r:`<a href="#current-features"></a>`
Current features
^^^^^^^^^^^^^^^^
* Reading data from a NanoVNA -- Compatible devices: NanoVNA, NanoVNA-H,
NanoVNA-H4, NanoVNA-F, AVNA via Teensy
* Reading data from a TinySA
* Splitting a frequency range into multiple segments to increase resolution
(tried up to >10k points)
* Averaging data for better results particularly at higher frequencies
* Displaying data on multiple chart types, such as Smith, LogMag, Phase and
VSWR-charts, for both S11 and S21
* Displaying markers, and the impedance, VSWR, Q, equivalent
capacitance/inductance etc. at these locations
* Displaying customizable frequency bands as reference, for example amateur
radio bands
* Exporting and importing 1-port and 2-port Touchstone files
* TDR function (measurement of cable length) - including impedance display
* Filter analysis functions for low-pass, high-pass, band-pass and band-stop
filters
* Display of both an active and a reference trace
* Live updates of data from the NanoVNA, including for multi-segment sweeps
* In-application calibration, including compensation for non-ideal calibration
standards
* Customizable display options, including "dark mode"
* Exporting images of plotted values
Screenshot
^^^^^^^^^^
.. image:: https://i.imgur.com/ZoFsV2V.png
:target: https://i.imgur.com/ZoFsV2V.png
:alt: Screenshot of version 0.1.4
Running the application
-----------------------
Main development is currently done on Linux (Mint 21 "Vanessa" Cinnamon)
Installation
------------
Binary releases
^^^^^^^^^^^^^^^
You can find current binary releases for Windows, Linux and MacOS under
https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest
The 32bit Windows binaries are somewhat smaller and seems to be a
little bit more stable.
`Detailed installation instructions <docs/INSTALLATION.md>`_
Using the software
------------------
Connect your NanoVNA to a serial port, and enter this serial port in the serial
port box. If the NanoVNA is connected before the application starts, it should
be automatically detected. Otherwise, click "Rescan". Click "Connect to device"
to connect.
The app can collect multiple segments to get more accurate measurements. Enter
the number of segments to be done in the "Segments" box. Each segment is 101
data points, and takes about 1.5 seconds to complete.
Frequencies are entered in Hz, or suffixed with k or M. Scientific notation
(6.5e6 for 6.5MHz) also works.
Markers can be manually entered, or controlled using the mouse. For mouse
control, select the active marker using the radio buttons, or hold "shift"
while clicking to drag the nearest marker. The marker readout boxes show the
actual frequency where values are measured. Marker readouts can be hidden
using the "hide data" button when not needed.
Display settings are available under "Display setup". These allow changing the
chart colours, the application font size and which graphs are displayed. The
settings are saved between program starts.
Calibration
^^^^^^^^^^^
*Before using NanoVNA-Saver, please ensure that the device itself is in a
reasonable calibration state.*
A calibration of both ports across the entire frequency span, saved to save
slot 0, is sufficient. If the NanoVNA is completely uncalibrated, its readings
may be outside the range accepted by the application.
In-application calibration is available, either assuming ideal standards or
with relevant standard correction. To manually calibrate, sweep each standard
in turn and press the relevant button in the calibration window.
For assisted calibration, press the "Calibration Assistant" button. If desired,
enter a note in the provided field describing the conditions under which the
calibration was performed.
Calibration results may be saved and loaded using the provided buttons at the
bottom of the window. Notes are saved and loaded along with the calibration
data.
.. image:: https://i.imgur.com/p94cxOX.png
:target: https://i.imgur.com/p94cxOX.png
:alt: Screenshot of Calibration Window
Users of known characterized calibration standard sets can enter the data for
these, and save the sets.
After pressing *Apply*\ , the calibration is immediately applied to the latest
sweep data.
! *Currently, load capacitance is unsupported* !
TDR
^^^
To get accurate TDR measurements, calibrate the device, and attach the cable to
be measured at the calibration plane - i.e. at the same position where the
calibration load would be attached. Open the "Time Domain Reflectometry"
window, and select the correct cable type, or manually enter a propagation
factor.
Measuring inductor core permeability
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The permeability (mu) of cores can be measured using a one-port measurement.
Put one or more windings on a core of known dimensions and use the "S11 mu"
plot from the "Display Setup". The core dimensions (cross section area in mm2,
effective length in mm) and number of windings can be set in the context menu
for the plot (right click on the plot).
Latest Changes
^^^^^^^^^^^^^^
* Using PyQt6
* Moved to PyScaffold project structure
* Fixed crash in resonance analysis
* Added TinySA readout and screenshot
Changes in 0.5.5
^^^^^^^^^^^^^^^^
* Measuring inductor core permeability
* Bugfixes for calibration data loading and saving
* Let V2 Devices more time for usb-serial setup
* Make some windows scrollable
Changes in 0.5.4
^^^^^^^^^^^^^^^^
* Bugfixes for Python3.11 compatability
* Bugfix for Python3.8 compatability
* use math instead of table for log step calculation
* Support of NanoVNA V2 Plus5 on Windows
* New SI prefixes added - Ronna, Quetta
* addes a Makefile to build a packages
* Simplyfied sweep worker
* Fixed calibration data loading
* Explicit import of scipy functions - #555
* Refactoring of Analysis modules
Contributing
------------
First off, thanks for taking the time to contribute! Contributions are what
make the open-source community such an amazing place to learn, inspire, and
create. Any contributions you make will benefit everybody else and are
**greatly appreciated**.
Please read `our contribution guidelines <docs/CONTRIBUTING.md>`_\ , and thank
you for being involved!
License
-------
This software is licensed under version 3 of the GNU General Public License. It
comes with NO WARRANTY.
You can use it, commercially as well. You may make changes to the code, but I
(and the license) ask that you give these changes back to the community.
References
----------
* Ohan Smit wrote an introduction to using the application:
[https://zs1sci.com/blog/nanovnasaver/]
* HexAndFlex wrote a 3-part (thus far) series on Getting Started with the
NanoVNA:
[https://hexandflex.com/2019/08/31/getting-started-with-the-nanovna-part-1/]
- Part 3 is dedicated to NanoVNASaver:
[https://hexandflex.com/2019/09/15/getting-started-with-the-nanovna-part-3-pc-software/]
* Gunthard Kraus did documentation in English and German:
[http://www.gunthard-kraus.de/fertig_NanoVNA/English/]
[http://www.gunthard-kraus.de/fertig_NanoVNA/Deutsch/]
Acknowledgements
----------------
Original application by Rune B. Broberg (5Q5R)
Contributions and changes by Holger Müller (DG5DBH), David Hunt and others.
TDR inspiration shamelessly stolen from the work of Salil (VU2CWA) at
https://nuclearrambo.com/wordpress/accurately-measuring-cable-length-with-nanovna/
TDR cable types by Larry Goga.
Bugfixes and Python installation work by Ohan Smit.
Thanks to everyone who have tested, commented and inspired. Particular thanks
go to the alpha testing crew who suffer the early instability of new versions.
This software is available free of charge. If you read all this way, and you
*still* want to support it, you may donate to the developer using the button
below:
.. image:: https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=T8KTGVDQF5K6E&item_name=NanoVNASaver+Development&currency_code=EUR&source=url
:alt: Paypal

21
build-macos-app.sh 100755
Wyświetl plik

@ -0,0 +1,21 @@
# Builds a NanoVNASaver.app on MacOS
# ensure you have pyqt >=6.4 installed (brew install pyqt)
#
export VENV_DIR=macbuildenv
# setup build venv
python3 -m venv ${VENV_DIR}
. ./${VENV_DIR}/bin/activate
# install required dependencies (pyqt libs must be installed on the system)
python3 -m pip install pip==23.0.1 setuptools==67.6.0
pip install -r requirements.txt
pip install PyInstaller==5.9.0
python3 setup.py -V
pyinstaller --onedir -p src -n NanoVNASaver nanovna-saver.py --window --clean -y -i icon_48x48.icns
tar -C dist -zcf ./dist/NanoVNASaver.app-`uname -m`.tar.gz NanoVNASaver.app
deactivate
rm -rf ${VENV_DIR}

Wyświetl plik

@ -1,2 +1,3 @@
#!/bin/sh
export PYTHONPATH="src"
exec python -m debugpy --listen 5678 --wait-for-client $@

Wyświetl plik

@ -0,0 +1,83 @@
Contributor Covenant Code of Conduct
====================================
Our Pledge
----------
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to make participation in our project and
our community a harassment-free experience for everyone, regardless of age,
body size, disability, ethnicity, sex characteristics, gender identity and
expression, level of experience, education, socio-economic status, nationality,
personal appearance, race, religion, or sexual identity and orientation.
Our Standards
-------------
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances Trolling, insulting/derogatory comments, and personal or political
attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission Other conduct which could reasonably be
considered inappropriate in a professional setting
Our Responsibilities
--------------------
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
Scope
-----
This Code of Conduct applies within all project spaces, and it also applies
when an individual is representing the project or its community in public
spaces. Examples of representing a project or community include using an
official project email address, posting via an official social media account,
or acting as an appointed representative at an online or offline event.
Representation of a project may be further defined and clarified by project
maintainers.
Enforcement
-----------
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project maintainer using any of the [private contact
addresses](https://github.com/Nanovna-Saver/nanovna-saver#support). All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an
incident. Further details of specific enforcement policies may be posted
separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor
Covenant](https://www.contributor-covenant.org), version 1.4, available at
<https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>
For answers to common questions about this code of conduct, see
<https://www.contributor-covenant.org/faq>

Wyświetl plik

@ -0,0 +1,51 @@
Contributing
============
When contributing to this repository, please first discuss the change you wish
to make via issue, email, or any other method with the owners of this
repository before making a change.
Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it
in all your interactions with the project.
Development environment setup
------------------------------
1. Clone the repo
```sh
git clone https://github.com/NanoVNA-Saver/nanovna-saver
```
2. TODO
## Issues and feature requests
You've found a bug in the source code, a mistake in the documentation or maybe
you'd like a new feature?Take a look at [GitHub
Discussions](https://github.com/NanoVNA-Saver/nanovna-saver/discussions) to see
if it's already being discussed. You can help us by [submitting an issue on
GitHub](https://github.com/NanoVNA-Saver/nanovna-saver/issues). Before you
create an issue, make sure to search the issue archive -- your issue may have
already been addressed!
Please try to create bug reports that are:
- _Reproducible._ Include steps to reproduce the problem.
- _Specific._ Include as much detail as possible: which version, what environment, etc.
- _Unique._ Do not duplicate existing opened issues.
- _Scoped to a Single Bug._ One bug per report.
**Even better: Submit a pull request with a fix or new feature!**
### How to submit a Pull Request
1. Search our repository for open or closed
[Pull Requests](https://github.com/NanoVNA-Saver/nanovna-saver/pulls)
that relate to your submission. You don't want to duplicate effort.
2. Fork the project
3. Create your feature branch (`git checkout -b feat/amazing_feature`)
4. Commit your changes (`git commit -m 'feat: add amazing_feature'`)
NanoVNA-Saver uses [conventional commits](https://www.conventionalcommits.org),
so please follow the specification in your commit messages. 5. Push to the
branch (`git push origin feat/amazing_feature`)
6. [Open a Pull Request](https://github.com/NanoVNA-Saver/nanovna-saver/compare?expand=1)

Wyświetl plik

@ -0,0 +1,129 @@
# Installation Instructions
## Installation and Use with pip
Copy the link of the tgz from latest relaese and install it with pip install. e.g.:
pip3 install https://github.com/NanoVNA-Saver/nanovna-saver/archive/refs/tags/v0.5.5.tar.gz
Once completed run with the following command: `NanoVNASaver`
The instructions omit the easiest way to get the program running under Linux - no installation - just start it in the git directory. This makes it difficult for pure users, e.g. hams, who therefore even try to run the Windows exe version under Wine.
Proposal - Add these sections below to the top README.md, e.g. between "Detailed installation instructions" and "Using the software" (Please review and add e.g. more necessary debian packages):
## Running on Linux without installation
The program simply works from the source directory without having to install it.
Simple step-by-step instruction, open a terminal window and type:
sudo apt install git python3-pyqt5 python3-numpy python3-scipy
git clone https://github.com/NanoVNA-Saver/nanovna-saver
cd nanovna-saver
Perhaps your system needs a few additional python modules:
- Run with `python nanovna-saver.py` and look at the response of (e.g. missing modules).
- Install the missing modules, preferably via `sudo apt install ...`
until `nanovna-saver.py` starts up.
Now the program can be used from the `nanovna-saver` directory.
## Installing via DEB for Debian (and Ubuntu)
The installation has the benefit that it allows you to run the program from anywhere, because the
main program is found via the regular `$PATH` and the modules are located in the Python module path.
If you're using a debian based distro you should consider to build your own `*.deb` package.
This has the advantage that NanoVNASaver can be installed and uninstalled cleanly in the system.
For this you need to install `python3-stdeb` - the module for converting Python code and modules into a Debian package:
apt install python3-stdeb
Then you can build the package via:
make deb
This package can be installed the usual way with
sudo dpkg -i nanovnasaver....deb
or
sudo apt install ./nanovnasaver....deb
### Installing via RPM (experimental)
`make rpm` builds an (untested) rpm package that can be installed on your system the usual way.
## Ubuntu 20.04 / 22.04
1. Install python3 and pip
sudo apt install python3 python3-pip
python3 -m venv ~/.venv_nano
. ~/.venv_nano/bin/activate
pip install -U pip
2. Clone repo and cd into the directory
git clone https://github.com/NanoVNA-Saver/nanovna-saver
cd nanovna-saver
3. Update pip and run the pip installation
python3 -m pip install .
(You may need to install the additional packages python3-distutils,
python3-setuptools and python3-wheel for this command to work on some
distributions.)
4. Once completed run with the following command
. ~/.venv_nano/bin/activate
python3 nanovna-saver.py
## MacPorts
Via a MacPorts distribution maintained by @ra1nb0w.
1. Install MacPorts following the [install guide](https://www.macports.org/install.php)
2. Install NanoVNASaver :
sudo port install NanoVNASaver
3. Now you can run the software from shell `NanoVNASaver` or run as app
`/Applications/MacPorts/NanoVNASaver.app`
## Homebrew
1. Install Homebrew from <https://brew.sh/> (This will ask for your password)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
2. Python :
brew install python
3. Pip :<br/>
Download the get-pip.py file and run it to install pip
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python3 get-pip.py
4. NanoVNASaver Installation : <br/>
clone the source code to the nanovna-saver folder
git clone https://github.com/NanoVNA-Saver/nanovna-saver
cd nanovna-saver
5. Install local pip packages
python3 -m pip install .
6. Run nanovna-saver in the nanovna-saver folder by:
python3 nanovna-saver.py

29
docs/Makefile 100644
Wyświetl plik

@ -0,0 +1,29 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
AUTODOCDIR = api
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $?), 1)
$(error "The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://sphinx-doc.org/")
endif
.PHONY: help clean Makefile
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
clean:
rm -rf $(BUILDDIR)/* $(AUTODOCDIR)
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

1
docs/_static/.gitignore vendored 100644
Wyświetl plik

@ -0,0 +1 @@
# Empty directory

2
docs/authors.rst 100644
Wyświetl plik

@ -0,0 +1,2 @@
.. _authors:
.. include:: ../AUTHORS.rst

286
docs/conf.py 100644
Wyświetl plik

@ -0,0 +1,286 @@
# This file is execfile()d with the current directory set to its containing dir.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import os
import sys
import shutil
# -- Path setup --------------------------------------------------------------
__location__ = os.path.dirname(__file__)
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.join(__location__, "../src"))
# -- Run sphinx-apidoc -------------------------------------------------------
# This hack is necessary since RTD does not issue `sphinx-apidoc` before running
# `sphinx-build -b html . _build/html`. See Issue:
# https://github.com/readthedocs/readthedocs.org/issues/1139
# DON'T FORGET: Check the box "Install your project inside a virtualenv using
# setup.py install" in the RTD Advanced Settings.
# Additionally it helps us to avoid running apidoc manually
try: # for Sphinx >= 1.7
from sphinx.ext import apidoc
except ImportError:
from sphinx import apidoc
output_dir = os.path.join(__location__, "api")
module_dir = os.path.join(__location__, "../src/NanoVNASaver")
try:
shutil.rmtree(output_dir)
except FileNotFoundError:
pass
try:
import sphinx
cmd_line = f"sphinx-apidoc --implicit-namespaces -f -o {output_dir} {module_dir}"
args = cmd_line.split(" ")
if tuple(sphinx.__version__.split(".")) >= ("1", "7"):
# This is a rudimentary parse_version to avoid external dependencies
args = args[1:]
apidoc.main(args)
except Exception as e:
print("Running `sphinx-apidoc` failed!\n{}".format(e))
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.todo",
"sphinx.ext.autosummary",
"sphinx.ext.viewcode",
"sphinx.ext.coverage",
"sphinx.ext.doctest",
"sphinx.ext.ifconfig",
"sphinx.ext.mathjax",
"sphinx.ext.napoleon",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# The suffix of source filenames.
source_suffix = ".rst"
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = "index"
# General information about the project.
project = "nanovna-saver"
copyright = "2023, Holger Mueller"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# version: The short X.Y version.
# release: The full version, including alpha/beta/rc tags.
# If you dont need the separation provided between version and release,
# just set them both to the same value.
try:
from NanoVNASaver import __version__ as version
except ImportError:
version = ""
if not version or version.lower() == "unknown":
version = os.getenv("READTHEDOCS_VERSION", "unknown") # automatically set by RTD
release = version
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
# today = ''
# Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", ".venv"]
# The reST default role (used for this markup: `text`) to use for all documents.
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
# If this is True, todo emits a warning for each TODO entries. The default is False.
todo_emit_warnings = True
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = "alabaster"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {
"sidebar_width": "300px",
"page_width": "1200px"
}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
# html_logo = ""
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
# html_additional_pages = {}
# If false, no module index is generated.
# html_domain_indices = True
# If false, no index is generated.
# html_use_index = True
# If true, the index is split into individual pages for each letter.
# html_split_index = False
# If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = "nanovna-saver-doc"
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
# The paper size ("letterpaper" or "a4paper").
# "papersize": "letterpaper",
# The font size ("10pt", "11pt" or "12pt").
# "pointsize": "10pt",
# Additional stuff for the LaTeX preamble.
# "preamble": "",
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
("index", "user_guide.tex", "nanovna-saver Documentation", "Holger Mueller", "manual")
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
# latex_logo = ""
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False
# If true, show page references after internal links.
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
# latex_appendices = []
# If false, no module index is generated.
# latex_domain_indices = True
# -- External mapping --------------------------------------------------------
python_version = ".".join(map(str, sys.version_info[0:2]))
intersphinx_mapping = {
"sphinx": ("https://www.sphinx-doc.org/en/master", None),
"python": ("https://docs.python.org/" + python_version, None),
"matplotlib": ("https://matplotlib.org", None),
"numpy": ("https://numpy.org/doc/stable", None),
"sklearn": ("https://scikit-learn.org/stable", None),
"pandas": ("https://pandas.pydata.org/pandas-docs/stable", None),
"scipy": ("https://docs.scipy.org/doc/scipy/reference", None),
"setuptools": ("https://setuptools.pypa.io/en/stable/", None),
"pyscaffold": ("https://pyscaffold.org/en/stable", None),
}
print(f"loading configurations for {project} {version} ...", file=sys.stderr)

Wyświetl plik

@ -0,0 +1 @@
.. include:: ../CONTRIBUTING.rst

60
docs/index.rst 100644
Wyświetl plik

@ -0,0 +1,60 @@
=============
nanovna-saver
=============
This is the documentation of **nanovna-saver**.
.. note::
This is the main page of your project's `Sphinx`_ documentation.
It is formatted in `reStructuredText`_. Add additional pages
by creating rst-files in ``docs`` and adding them to the `toctree`_ below.
Use then `references`_ in order to link them from this page, e.g.
:ref:`authors` and :ref:`changes`.
It is also possible to refer to the documentation of other Python packages
with the `Python domain syntax`_. By default you can reference the
documentation of `Sphinx`_, `Python`_, `NumPy`_, `SciPy`_, `matplotlib`_,
`Pandas`_, `Scikit-Learn`_. You can add more by extending the
``intersphinx_mapping`` in your Sphinx's ``conf.py``.
The pretty useful extension `autodoc`_ is activated by default and lets
you include documentation from docstrings. Docstrings can be written in
`Google style`_ (recommended!), `NumPy style`_ and `classical style`_.
Contents
========
.. toctree::
:maxdepth: 2
Overview <readme>
Contributions & Help <contributing>
License <license>
Authors <authors>
Module Reference <api/modules>
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
.. _toctree: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html
.. _reStructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html
.. _references: https://www.sphinx-doc.org/en/stable/markup/inline.html
.. _Python domain syntax: https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#the-python-domain
.. _Sphinx: https://www.sphinx-doc.org/
.. _Python: https://docs.python.org/
.. _Numpy: https://numpy.org/doc/stable
.. _SciPy: https://docs.scipy.org/doc/scipy/reference/
.. _matplotlib: https://matplotlib.org/contents.html#
.. _Pandas: https://pandas.pydata.org/pandas-docs/stable
.. _Scikit-Learn: https://scikit-learn.org/stable
.. _autodoc: https://www.sphinx-doc.org/en/master/ext/autodoc.html
.. _Google style: https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings
.. _NumPy style: https://numpydoc.readthedocs.io/en/latest/format.html
.. _classical style: https://www.sphinx-doc.org/en/master/domains.html#info-field-lists

7
docs/license.rst 100644
Wyświetl plik

@ -0,0 +1,7 @@
.. _license:
=======
License
=======
.. include:: ../LICENSE.txt

Wyświetl plik

@ -0,0 +1,66 @@
.\" English manual page for nanovna-saver
.\"
.\" Copyright (C) 2023-2023 Nicolas Boulenguez <nicolas@debian.org>
.\"
.\" This program is free software: you can redistribute it and/or
.\" modify it under the terms of the GNU General Public License as
.\" published by the Free Software Foundation, either version 3 of the
.\" License, or (at your option) any later version.
.\" This program is distributed in the hope that it will be useful, but
.\" WITHOUT ANY WARRANTY; without even the implied warranty of
.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
.\" General Public License for more details.
.\" You should have received a copy of the GNU General Public License
.\" along with this program. If not, see <http://www.gnu.org/licenses/>.
.\"
.TH NANOVNASAVER 1 "2023-03-19"
.\"----------------------------------------------------------------------
.SH NAME
NANOVNASAVER \- save Touchstone files from the NanoVNA device
.\"----------------------------------------------------------------------
.SH SYNOPSIS
.B NanoVNASaver
.RB [\| \-h \|]
.RB [\| \-d \|]
.RB [\| \-D
.IR DEBUG_FILE \|]
.RB [\| \-f
.IR FILE \|]
.RB [\| \-r
.IR REF_FILE \|]
.RB [\| \-\-version \|]
.\"----------------------------------------------------------------------
.SH DESCRCIPTION
The NanoVNASaver graphical tool saves Touchstone files from the
NanoVNA, sweeps frequency spans in segments to gain more data points,
and generally displays and analyzes the resulting data.
.PP
The authors expect most users to use a graphical launcher instead of
the command line interface.
.\"----------------------------------------------------------------------
.SH OPTIONS
.TP
\fB\-h\fR, \fB\-\-help\fR
Show a summary of options and exit.
.TP
\fB\-d\fR, \fB\-\-debug\fR
Set loglevel to debug.
.TP
\fB\-D \fIDEBUG_FILE\fR, \fB\-\-debug\-file \fIDEBUG_FILE\fR
File to write debug logging output to.
.TP
\fB\-f \fIFILE\fR, \fB\-\-file \fIFILE\fR
Touchstone file to load as sweep for off device usage.
.TP
\fB\-r \fIREF_FILE\fR, \fB\-\-ref\-file \fIREF_FILE\fR
Touchstone file to load as reference for off device usage.
.TP
\fB\-\-version\fR
Show program's version number and exit.
.\"----------------------------------------------------------------------
.SH SEE ALSO
The documentation is installed at
.BR /usr/share/doc/nanovna-saver/ .
.\"----------------------------------------------------------------------
.SH HISTORY
This page has been written for Debian but may be reused by others.

2
docs/readme.rst 100644
Wyświetl plik

@ -0,0 +1,2 @@
.. _readme:
.. include:: ../README.rst

Wyświetl plik

@ -0,0 +1,5 @@
# Requirements file for ReadTheDocs, check .readthedocs.yml.
# To build the module reference correctly, make sure every external package
# under `install_requires` in `setup.cfg` is also listed here!
sphinx>=3.2.1
# sphinx_rtd_theme

Wyświetl plik

@ -1,6 +1,6 @@
app-id: io.github.zarath.nanovna-saver
runtime: org.kde.Platform
runtime-version: '5.15-21.08'
runtime-version: '6.5'
sdk: org.kde.Sdk
command: /app/bin/NanoVNASaver
build-options:
@ -10,7 +10,7 @@ modules:
- name: nanonva-saver
buildsystem: simple
build-commands:
- pip3 install --prefix=/app wheel
- pip3 install --prefix=/app wheel setuptools setuptools-scm
- pip3 install --prefix=/app git+https://github.com/NanoVNA-Saver/nanovna-saver.git
finish-args:
# X11 + XShm access

BIN
icon_48x48.icns 100644

Plik binarny nie jest wyświetlany.

BIN
icon_48x48.ico 100644

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 109 KiB

Wyświetl plik

@ -1,6 +1,7 @@
#! /usr/bin/env python3
# NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019. Rune B. Broberg
# NanoVNASaver - a python program to view and export Touchstone data from a
# NanoVNA
# Copyright (C) 2019. Rune B. Broberg
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -15,15 +16,22 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from contextlib import suppress
# This launcher is ignored by setuptools. Its only purpose is direct
# execution from a source tree.
# noinspection PyUnresolvedReferences
with suppress(ImportError):
# pylint: disable=no-name-in-module,import-error,unused-import
# pyright: reportMissingImports=false
import pkg_resources.py2_warn
import os.path
import sys
from NanoVNASaver.__main__ import main
# Ignore the current working directory.
src = os.path.join(os.path.dirname(__file__), "src")
if __name__ == '__main__':
main()
if os.path.exists(src):
sys.path.insert(0, src)
# pylint: disable-next=wrong-import-position
import NanoVNASaver.__main__
# The traditional test does not make sense here.
assert __name__ == "__main__"
NanoVNASaver.__main__.main()

19
pyproject.toml 100644
Wyświetl plik

@ -0,0 +1,19 @@
[build-system]
# AVOID CHANGING REQUIRES: IT WILL BE UPDATED BY PYSCAFFOLD!
requires = ["setuptools>=46.1.0", "setuptools_scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"
[NanoVNASaver]
dynamic = ["version"]
[tool.setuptools_scm]
# For smarter version schemes and other configuration options,
# check out https://github.com/pypa/setuptools_scm
root="."
version_scheme = "no-guess-dev"
write_to = "src/NanoVNASaver/_version.py"
[tool.pytest.ini_options]
pythonpath = [
".", "src",
]

Wyświetl plik

@ -1,5 +1,9 @@
pyserial==3.5
PyQt5==5.15.7
numpy==1.23.2
scipy==1.9.1
Cython==0.29.32
PyQt6==6.5.2
PyQt6-sip==13.6.0
sip==6.8.1
numpy==1.26.3
scipy==1.12.0
Cython==3.0.8
setuptools==69.0.3
setuptools-scm==8.0.4

Wyświetl plik

@ -1,23 +1,104 @@
# This file is used to configure your project.
# Read more about the various options under:
# https://setuptools.pypa.io/en/latest/userguide/declarative_config.html
# https://setuptools.pypa.io/en/latest/references/keywords.html
[metadata]
name = NanoVNASaver
author = Rune B. Broberg
author_email= NanoVNA-Saver@users.noreply.github.com
license = GNU GPL V3
license_file = LICENSE
description = A Qt GUI for the NanoVNA and derivates
long_description = file: README.md
license_files = LICENSE,
description = GUI for the NanoVNA and derivates
long_description = file: README.rst
url = https://github.com/NanoVNA-Saver/nanovna-saver
version = attr: NanoVNASaver.About.VERSION
version = attr: NanoVNASaver.About.version
platforms= all
[options]
zip_safe = False
packages = find_namespace:
install_requires=
include_package_data = True
package_dir =
=src
# Require a min/specific Python version (comma-separated conditions)
python_requires = >=3.8, <4
# Add here dependencies of your project (line-separated), e.g. requests>=2.2,<3.0.
# Version specifiers like >=2.2,<3.0 avoid problems due to API changes in
# new major versions. This works if the required packages follow Semantic Versioning.
# For more information, check out https://semver.org/.
install_requires =
pyserial>=3.5
PyQt5>=5.15.0
PyQt6>=5.15.0
numpy>=1.21.1
scipy>=1.7.1
Cython>=0.29.24
python_requires = >=3.8, <4
setuptools-scm
[options.packages.find]
where = src
exclude =
tests
[options.extras_require]
# Add here additional requirements for extra features, to install with:
# `pip install nanovna-saver[PDF]` like:
# PDF = ReportLab; RXP
# Add here test requirements (semicolon/line-separated)
testing =
setuptools
pytest
pytest-cov
[options.entry_points]
console_scripts =
NanoVNASaver = NanoVNASaver.__main__:main
[tool:pytest]
# Specify command line options as you would do when invoking pytest directly.
# e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml
# in order to write a coverage file that can be read by Jenkins.
# CAUTION: --cov flags may prohibit setting breakpoints while debugging.
# Comment those flags to avoid this pytest issue.
addopts =
--cov NanoVNASaver --cov-report term-missing
--verbose
norecursedirs =
dist
build
.tox
testpaths = tests
# Use pytest markers to select/deselect specific tests
# markers =
# slow: mark tests as slow (deselect with '-m "not slow"')
# system: mark end-to-end system tests
[devpi:upload]
# Options for the devpi: PyPI server and packaging tool
# VCS export must be deactivated since we are using setuptools-scm
no_vcs = 1
formats = bdist_wheel
[flake8]
# Some sane defaults for the code style checker flake8
max_line_length = 88
extend_ignore = E203, W503
# ^ Black-compatible
# E203 and W503 have edge cases handled by black
exclude =
.tox
build
dist
.eggs
docs/conf.py
[pyscaffold]
# PyScaffold's parameters when the project was created.
# This will be used when updating. Do not change!
version = 4.4
package = NanoVNASaver
extensions =
no_skeleton

Wyświetl plik

@ -1,21 +1,21 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
Setup file for nanovna-saver.
Use setup.cfg to configure your project.
This file was generated with PyScaffold 4.4.
PyScaffold helps you to put up the scaffold of your new Python project.
Learn more under: https://pyscaffold.org/
"""
from setuptools import setup
setup()
if __name__ == "__main__":
try:
setup(use_scm_version={"version_scheme": "no-guess-dev"})
except: # noqa
print(
"\n\nAn error occurred while building the project, "
"please ensure you have the most updated version of setuptools, "
"setuptools_scm and wheel with:\n"
" pip install -U setuptools setuptools_scm wheel\n\n"
)
raise

Wyświetl plik

@ -17,13 +17,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
VERSION = "0.5.2"
VERSION_URL = (
"https://raw.githubusercontent.com/"
"NanoVNA-Saver/nanovna-saver/master/NanoVNASaver/About.py")
from setuptools_scm import get_version
try:
version = get_version(root='..', relative_to=__file__)
except LookupError:
from NanoVNASaver._version import version
INFO_URL = "https://github.com/NanoVNA-Saver/nanovna-saver"
INFO = f"""NanoVNASaver {VERSION}
INFO = f"""NanoVNASaver {version}
Copyright (C) 2019, 2020 Rune B. Broberg
Copyright (C) 2020ff NanoVNA-Saver Authors
@ -34,4 +35,7 @@ This program is licensed under the GNU General Public License version 3
See {INFO_URL} for further details.
"""
RELEASE_URL = "https://github.com/NanoVNA-Saver/nanovna-saver"
TAGS_URL = "https://github.com/NanoVNA-Saver/nanovna-saver/tags"
TAGS_KEY = "/NanoVNA-Saver/nanovna-saver/releases/tag/v"
LATEST_URL = "https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest"

Wyświetl plik

@ -1,7 +1,8 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020ff NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -16,16 +17,11 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from PyQt5.Qt import QTimer
'''
Created on May 30th 2020
@author: mauro
'''
import logging
from time import sleep
from PyQt5 import QtWidgets
from PyQt6 import QtWidgets
from NanoVNASaver.Analysis.VSWRAnalysis import VSWRAnalysis
@ -39,6 +35,7 @@ class MagLoopAnalysis(VSWRAnalysis):
Useful for tuning magloop.
"""
max_dips_shown = 1
vswr_bandwith_value = 2.56 # -3 dB ?!?
@ -60,13 +57,19 @@ class MagLoopAnalysis(VSWRAnalysis):
if self.min_freq is None:
self.min_freq = new_start
self.max_freq = new_end
logger.debug("setting hard limits to %s - %s",
self.min_freq, self.max_freq)
logger.debug(
"setting hard limits to %s - %s", self.min_freq, self.max_freq
)
if len(self.minimums) > 1:
self.layout.addRow("", QtWidgets.QLabel(
"Multiple minimums, not magloop or try to lower VSWR limit"))
self.layout.addRow(
"",
QtWidgets.QLabel(
"Multiple minimums, not magloop or try to lower VSWR limit"
),
)
return
if len(self.minimums) == 1:
m = self.minimums[0]
start, lowest, end = m
@ -76,21 +79,25 @@ class MagLoopAnalysis(VSWRAnalysis):
logger.debug(" Zoom to %s-%s", new_start, new_end)
elif self.vswr_limit_value == self.vswr_bandwith_value:
Q = self.app.data.s11[lowest].freq / \
(self.app.data.s11[end].freq -
self.app.data.s11[start].freq)
Q = self.app.data.s11[lowest].freq / (
self.app.data.s11[end].freq - self.app.data.s11[start].freq
)
self.layout.addRow("Q", QtWidgets.QLabel(f"{int(Q)}"))
new_start = self.app.data.s11[start].freq - self.bandwith
new_end = self.app.data.s11[end].freq + self.bandwith
logger.debug("Single Spot, new scan on %s-%s",
new_start, new_end)
logger.debug(
"Single Spot, new scan on %s-%s", new_start, new_end
)
if self.vswr_limit_value > self.vswr_bandwith_value:
self.vswr_limit_value = max(
self.vswr_bandwith_value, self.vswr_limit_value - 1)
self.vswr_bandwith_value, self.vswr_limit_value - 1
)
self.input_vswr_limit.setValue(self.vswr_limit_value)
logger.debug(
"found higher minimum, lowering vswr search to %s", self.vswr_limit_value)
"found higher minimum, lowering vswr search to %s",
self.vswr_limit_value,
)
else:
new_start = new_start - 5 * self.bandwith
new_end = new_end + 5 * self.bandwith
@ -101,26 +108,24 @@ class MagLoopAnalysis(VSWRAnalysis):
self.vswr_limit_value += 2
self.input_vswr_limit.setValue(self.vswr_limit_value)
logger.debug(
"no minimum found, looking for higher value %s", self.vswr_limit_value)
"no minimum found, looking for higher value %s",
self.vswr_limit_value,
)
new_start = max(self.min_freq, new_start)
new_end = min(self.max_freq, new_end)
logger.debug("next search will be %s - %s for vswr %s",
new_start,
new_end,
self.vswr_limit_value)
logger.debug(
"next search will be %s - %s for vswr %s",
new_start,
new_end,
self.vswr_limit_value,
)
self.app.sweep_control.set_start(new_start)
self.app.sweep_control.set_end(new_end)
# set timer to let finish all stuff before new sweep
QTimer.singleShot(2000, self._safe_sweep)
def _safe_sweep(self):
"""
sweep only if button enabled
to prevent multiple/concurrent sweep
"""
# TODO: get info if sweep is running instead of just sleeping
# a guessed time
sleep(2.0)
if self.app.sweep_control.btn_start.isEnabled():
self.app.sweep_start()
else:
logger.error("sweep alredy running")

Wyświetl plik

@ -0,0 +1,219 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020ff NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import math
from PyQt6 import QtWidgets
import NanoVNASaver.AnalyticTools as at
from NanoVNASaver.Analysis.Base import Analysis, CUTOFF_VALS
from NanoVNASaver.Formatting import format_frequency
logger = logging.getLogger(__name__)
class BandPassAnalysis(Analysis):
def __init__(self, app):
super().__init__(app)
for label in (
"octave_l",
"octave_r",
"decade_l",
"decade_r",
"freq_center",
"span_3.0dB",
"span_6.0dB",
"q_factor",
):
self.label[label] = QtWidgets.QLabel()
for attn in CUTOFF_VALS:
self.label[f"{attn:.1f}dB_l"] = QtWidgets.QLabel()
self.label[f"{attn:.1f}dB_r"] = QtWidgets.QLabel()
layout = self.layout
layout.addRow(self.label["titel"])
layout.addRow(
QtWidgets.QLabel(
f"Please place {self.app.markers[0].name}"
f" in the filter passband."
)
)
layout.addRow("Result:", self.label["result"])
layout.addRow(QtWidgets.QLabel(""))
layout.addRow("Center frequency:", self.label["freq_center"])
layout.addRow("Bandwidth (-3 dB):", self.label["span_3.0dB"])
layout.addRow("Quality factor:", self.label["q_factor"])
layout.addRow("Bandwidth (-6 dB):", self.label["span_6.0dB"])
layout.addRow(QtWidgets.QLabel(""))
layout.addRow(QtWidgets.QLabel("Lower side:"))
layout.addRow("Cutoff frequency:", self.label["3.0dB_l"])
layout.addRow("-6 dB point:", self.label["6.0dB_l"])
layout.addRow("-60 dB point:", self.label["60.0dB_l"])
layout.addRow("Roll-off:", self.label["octave_l"])
layout.addRow("Roll-off:", self.label["decade_l"])
layout.addRow(QtWidgets.QLabel(""))
layout.addRow(QtWidgets.QLabel("Upper side:"))
layout.addRow("Cutoff frequency:", self.label["3.0dB_r"])
layout.addRow("-6 dB point:", self.label["6.0dB_r"])
layout.addRow("-60 dB point:", self.label["60.0dB_r"])
layout.addRow("Roll-off:", self.label["octave_r"])
layout.addRow("Roll-off:", self.label["decade_r"])
self.set_titel("Band pass filter analysis")
def runAnalysis(self):
if not self.app.data.s21:
logger.debug("No data to analyse")
self.set_result("No data to analyse.")
return
self.reset()
s21 = self.app.data.s21
gains = [d.gain for d in s21]
if (peak := self.find_center(gains)) < 0:
return
peak_db = gains[peak]
logger.debug("Filter center pos: %d(%fdB)", peak, peak_db)
# find passband bounderies
cutoff_pos = self.find_bounderies(gains, peak, peak_db)
cutoff_freq = {
att: s21[val].freq if val >= 0 else math.nan
for att, val in cutoff_pos.items()
}
cutoff_gain = {
att: gains[val] if val >= 0 else math.nan
for att, val in cutoff_pos.items()
}
logger.debug("Cuttoff frequencies: %s", cutoff_freq)
logger.debug("Cuttoff gains: %s", cutoff_gain)
self.derive_60dB(cutoff_pos, cutoff_freq)
result = {
"span_3.0dB": cutoff_freq["3.0dB_r"] - cutoff_freq["3.0dB_l"],
"span_6.0dB": cutoff_freq["6.0dB_r"] - cutoff_freq["6.0dB_l"],
"freq_center": math.sqrt(
cutoff_freq["3.0dB_l"] * cutoff_freq["3.0dB_r"]
),
}
result["q_factor"] = result["freq_center"] / result["span_3.0dB"]
result["octave_l"], result["decade_l"] = at.calculate_rolloff(
s21, cutoff_pos["10.0dB_l"], cutoff_pos["20.0dB_l"]
)
result["octave_r"], result["decade_r"] = at.calculate_rolloff(
s21, cutoff_pos["10.0dB_r"], cutoff_pos["20.0dB_r"]
)
for label, val in cutoff_freq.items():
self.label[label].setText(
f"{format_frequency(val)}" f" ({cutoff_gain[label]:.1f} dB)"
)
for label in ("freq_center", "span_3.0dB", "span_6.0dB"):
self.label[label].setText(format_frequency(result[label]))
self.label["q_factor"].setText(f"{result['q_factor']:.2f}")
for label in ("octave_l", "decade_l", "octave_r", "decade_r"):
self.label[label].setText(f"{result[label]:.3f}dB/{label[:-2]}")
self.app.markers[0].setFrequency(f"{result['freq_center']}")
self.app.markers[1].setFrequency(f"{cutoff_freq['3.0dB_l']}")
self.app.markers[2].setFrequency(f"{cutoff_freq['3.0dB_r']}")
if cutoff_gain["3.0dB_l"] < -4 or cutoff_gain["3.0dB_r"] < -4:
logger.warning(
"Data points insufficient for true -3 dB points."
"Cutoff gains: %fdB, %fdB",
cutoff_gain["3.0dB_l"],
cutoff_gain["3.0dB_r"],
)
self.set_result(
f"Analysis complete ({len(s21)} points)\n"
f"Insufficient data for analysis. Increase segment count."
)
return
self.set_result(f"Analysis complete ({len(s21)} points)")
def derive_60dB(
self, cutoff_pos: dict[str, int], cutoff_freq: dict[str, float]
):
"""derive 60dB cutoff if needed an possible
Args:
cutoff_pos (dict[str, int])
cutoff_freq (dict[str, float])
"""
if (
math.isnan(cutoff_freq["60.0dB_l"])
and cutoff_pos["20.0dB_l"] != -1
and cutoff_pos["10.0dB_l"] != -1
):
cutoff_freq["60.0dB_l"] = cutoff_freq["10.0dB_l"] * 10 ** (
5
* (
math.log10(cutoff_pos["20.0dB_l"])
- math.log10(cutoff_pos["10.0dB_l"])
)
)
if (
math.isnan(cutoff_freq["60.0dB_r"])
and cutoff_pos["20.0dB_r"] != -1
and cutoff_pos["10.0dB_r"] != -1
):
cutoff_freq["60.0dB_r"] = cutoff_freq["10.0dB_r"] * 10 ** (
5
* (
math.log10(cutoff_pos["20.0dB_r"])
- math.log10(cutoff_pos["10.0dB_r"])
)
)
def find_center(self, gains: list[float]) -> int:
marker = self.app.markers[0]
if marker.location <= 0 or marker.location >= len(gains) - 1:
logger.debug(
"No valid location for %s (%s)", marker.name, marker.location
)
self.set_result(f"Please place {marker.name} in the passband.")
return -1
# find center of passband based on marker pos
if (peak := at.center_from_idx(gains, marker.location)) < 0:
self.set_result("Bandpass center not found")
return -1
return peak
def find_bounderies(
self, gains: list[float], peak: int, peak_db: float
) -> dict[str, int]:
cutoff_pos = {}
for attn in CUTOFF_VALS:
cutoff_pos[f"{attn:.1f}dB_l"] = at.cut_off_left(
gains, peak, peak_db, attn
)
cutoff_pos[f"{attn:.1f}dB_r"] = at.cut_off_right(
gains, peak, peak_db, attn
)
return cutoff_pos

Wyświetl plik

@ -0,0 +1,45 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import NanoVNASaver.AnalyticTools as at
from NanoVNASaver.Analysis.Base import CUTOFF_VALS
from NanoVNASaver.Analysis.BandPassAnalysis import BandPassAnalysis
logger = logging.getLogger(__name__)
class BandStopAnalysis(BandPassAnalysis):
def __init__(self, app):
super().__init__(app)
self.set_titel("Band stop filter analysis")
def find_center(self, gains: list[float]) -> int:
return max(enumerate(gains), key=lambda i: i[1])[0]
def find_bounderies(
self, gains: list[float], _: int, peak_db: float
) -> dict[str, int]:
cutoff_pos = {}
for attn in CUTOFF_VALS:
(
cutoff_pos[f"{attn:.1f}dB_l"],
cutoff_pos[f"{attn:.1f}dB_r"],
) = at.dip_cut_offs(gains, peak_db, attn)
return cutoff_pos

Wyświetl plik

@ -0,0 +1,58 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020ff NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
from PyQt6 import QtWidgets
logger = logging.getLogger(__name__)
CUTOFF_VALS = (3.0, 6.0, 10.0, 20.0, 60.0)
class QHLine(QtWidgets.QFrame):
def __init__(self):
super().__init__()
self.setFrameShape(QtWidgets.QFrame.Shape.HLine)
class Analysis:
def __init__(self, app: QtWidgets.QWidget):
self.app = app
self.label: dict[str, QtWidgets.QLabel] = {
"titel": QtWidgets.QLabel(),
"result": QtWidgets.QLabel(),
}
self.layout = QtWidgets.QFormLayout()
self._widget = QtWidgets.QWidget()
self._widget.setLayout(self.layout)
def widget(self) -> QtWidgets.QWidget:
return self._widget
def runAnalysis(self):
pass
def reset(self):
for label in self.label.values():
label.clear()
def set_result(self, text):
self.label["result"].setText(text)
def set_titel(self, text):
self.label["titel"].setText(text)

Wyświetl plik

@ -0,0 +1,177 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020ff NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import csv
import logging
from PyQt6 import QtWidgets
import NanoVNASaver.AnalyticTools as at
from NanoVNASaver.Analysis.ResonanceAnalysis import (
ResonanceAnalysis,
format_resistence_neg,
)
from NanoVNASaver.Formatting import (
format_frequency,
format_complex_imp,
format_frequency_short,
)
logger = logging.getLogger(__name__)
class EFHWAnalysis(ResonanceAnalysis):
"""
find only resonance when HI impedance
"""
def __init__(self, app):
super().__init__(app)
self.old_data = []
def do_resonance_analysis(self):
s11 = self.app.data.s11
maximums = sorted(
at.maxima([d.impedance().real for d in s11], threshold=500)
)
extended_data = {}
logger.info("TO DO: find near data")
for lowest in self.crossings:
my_data = self._get_data(lowest)
if lowest in extended_data:
extended_data[lowest].update(my_data)
else:
extended_data[lowest] = my_data
logger.debug("maximumx %s of type %s", maximums, type(maximums))
for m in maximums:
logger.debug("m %s of type %s", m, type(m))
my_data = self._get_data(m)
if m in extended_data:
extended_data[m].update(my_data)
else:
extended_data[m] = my_data
fields = [
("freq", format_frequency_short),
("r", format_resistence_neg),
("lambda", lambda x: round(x, 2)),
]
if self.old_data:
diff = self.compare(self.old_data[-1], extended_data, fields=fields)
else:
diff = self.compare({}, extended_data, fields=fields)
self.old_data.append(extended_data)
for i, idx in enumerate(sorted(extended_data.keys())):
self.layout.addRow(
f"{format_frequency_short(s11[idx].freq)}",
QtWidgets.QLabel(
f" ({diff[i]['freq']})"
f" {format_complex_imp(s11[idx].impedance())}"
f" ({diff[i]['r']}) {diff[i]['lambda']} m"
),
)
if self.filename and extended_data:
with open(
self.filename, "w", newline="", encoding="utf-8"
) as csvfile:
fieldnames = extended_data[
sorted(extended_data.keys())[0]
].keys()
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for idx in sorted(extended_data.keys()):
writer.writerow(extended_data[idx])
def compare(self, old, new, fields=None):
"""
Compare data to help changes
NB
must be same sweep
( same index must be same frequence )
:param old:
:param new:
"""
fields = fields or [
("freq", str),
]
def no_compare():
return {k: "-" for k, _ in fields}
old_idx = sorted(old.keys())
# 'odict_keys' object is not subscriptable
new_idx = sorted(new.keys())
diff = {}
i_max = min(len(old_idx), len(new_idx))
i_tot = max(len(old_idx), len(new_idx))
if i_max != i_tot:
logger.warning(
"resonances changed from %s to %s", len(old_idx), len(new_idx)
)
split = 0
max_delta_f = 1_000_000
for i, k in enumerate(new_idx):
if len(old_idx) <= i + split:
diff[i] = no_compare()
continue
logger.info("Resonance %s at %s", i, new[k]["freq"])
delta_f = new[k]["freq"] - old[old_idx[i + split]]["freq"]
if abs(delta_f) < max_delta_f:
logger.debug("can compare")
diff[i] = {
desc: fnc(new[k][desc] - old[old_idx[i + split]][desc])
for desc, fnc in fields
}
logger.debug("Deltas %s", diff[i])
continue
logger.debug(
"can't compare, %s is too much ", format_frequency(delta_f)
)
if delta_f > 0:
logger.debug("possible missing band, ")
if len(old_idx) > (i + split + 1):
if (
abs(
new[k]["freq"] - old[old_idx[i + split + 1]]["freq"]
)
< max_delta_f
):
logger.debug("new is missing band, compare next ")
split += 1
# FIXME: manage 2 or more band missing ?!?
continue
logger.debug("new band, non compare ")
diff[i] = no_compare()
continue
logger.debug("new band, non compare ")
diff[i] = no_compare()
split -= 1
for i in range(i_max, i_tot):
# add missing in old ... if any
diff[i] = no_compare()
return diff

Wyświetl plik

@ -0,0 +1,125 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import math
from PyQt6 import QtWidgets
import NanoVNASaver.AnalyticTools as at
from NanoVNASaver.Analysis.Base import Analysis, CUTOFF_VALS
from NanoVNASaver.Formatting import format_frequency
logger = logging.getLogger(__name__)
class HighPassAnalysis(Analysis):
def __init__(self, app):
super().__init__(app)
self.label["octave"] = QtWidgets.QLabel()
self.label["decade"] = QtWidgets.QLabel()
for attn in CUTOFF_VALS:
self.label[f"{attn:.1f}dB"] = QtWidgets.QLabel()
self.label[f"{attn:.1f}dB"] = QtWidgets.QLabel()
layout = self.layout
layout.addRow(self.label["titel"])
layout.addRow(
QtWidgets.QLabel(
f"Please place {self.app.markers[0].name}"
f" in the filter passband."
)
)
layout.addRow("Result:", self.label["result"])
layout.addRow("Cutoff frequency:", self.label["3.0dB"])
layout.addRow("-6 dB point:", self.label["6.0dB"])
layout.addRow("-60 dB point:", self.label["60.0dB"])
layout.addRow("Roll-off:", self.label["octave"])
layout.addRow("Roll-off:", self.label["decade"])
self.set_titel("Highpass analysis")
def runAnalysis(self):
if not self.app.data.s21:
logger.debug("No data to analyse")
self.set_result("No data to analyse.")
return
self.reset()
s21 = self.app.data.s21
gains = [d.gain for d in s21]
if (peak := self.find_level(gains)) < 0:
return
peak_db = gains[peak]
logger.debug("Passband position: %d(%fdB)", peak, peak_db)
cutoff_pos = self.find_cutoffs(gains, peak, peak_db)
cutoff_freq = {
att: s21[val].freq if val >= 0 else math.nan
for att, val in cutoff_pos.items()
}
cutoff_gain = {
att: gains[val] if val >= 0 else math.nan
for att, val in cutoff_pos.items()
}
logger.debug("Cuttoff frequencies: %s", cutoff_freq)
logger.debug("Cuttoff gains: %s", cutoff_gain)
octave, decade = at.calculate_rolloff(
s21, cutoff_pos["10.0dB"], cutoff_pos["20.0dB"]
)
if cutoff_gain["3.0dB"] < -4:
logger.debug(
"Cutoff frequency found at %f dB"
" - insufficient data points for true -3 dB point.",
cutoff_gain,
)
logger.debug("Found true cutoff frequency at %d", cutoff_freq["3.0dB"])
for label, val in cutoff_freq.items():
self.label[label].setText(
f"{format_frequency(val)}" f" ({cutoff_gain[label]:.1f} dB)"
)
self.label["octave"].setText(f"{octave:.3f}dB/octave")
self.label["decade"].setText(f"{decade:.3f}dB/decade")
self.app.markers[0].setFrequency(str(s21[peak].freq))
self.app.markers[1].setFrequency(str(cutoff_freq["3.0dB"]))
self.app.markers[2].setFrequency(str(cutoff_freq["6.0dB"]))
self.set_result(f"Analysis complete ({len(s21)}) points)")
def find_level(self, gains: list[float]) -> int:
marker = self.app.markers[0]
logger.debug("Pass band location: %d", marker.location)
if marker.location < 0:
self.set_result(f"Please place {marker.name} in the passband.")
return -1
return at.center_from_idx(gains, marker.location)
def find_cutoffs(
self, gains: list[float], peak: int, peak_db: float
) -> dict[str, int]:
return {
f"{attn:.1f}dB": at.cut_off_left(gains, peak, peak_db, attn)
for attn in CUTOFF_VALS
}

Wyświetl plik

@ -16,3 +16,25 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import NanoVNASaver.AnalyticTools as at
from NanoVNASaver.Analysis.Base import CUTOFF_VALS
from NanoVNASaver.Analysis.HighPassAnalysis import HighPassAnalysis
logger = logging.getLogger(__name__)
class LowPassAnalysis(HighPassAnalysis):
def __init__(self, app):
super().__init__(app)
self.set_titel("Lowpass filter analysis")
def find_cutoffs(
self, gains: list[float], peak: int, peak_db: float
) -> dict[str, int]:
return {
f"{attn:.1f}dB": at.cut_off_right(gains, peak, peak_db, attn)
for attn in CUTOFF_VALS
}

Wyświetl plik

@ -0,0 +1,113 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
from PyQt6 import QtWidgets
import numpy as np
# pylint: disable=import-error, no-name-in-module
from scipy.signal import find_peaks, peak_prominences
from NanoVNASaver.Analysis.Base import QHLine
from NanoVNASaver.Analysis.SimplePeakSearchAnalysis import (
SimplePeakSearchAnalysis,
)
from NanoVNASaver.Formatting import format_frequency_short
logger = logging.getLogger(__name__)
class PeakSearchAnalysis(SimplePeakSearchAnalysis):
def __init__(self, app):
super().__init__(app)
self.peak_cnt = QtWidgets.QSpinBox()
self.peak_cnt.setValue(1)
self.peak_cnt.setMinimum(1)
self.peak_cnt.setMaximum(10)
self.layout.addRow("Max number of peaks", self.peak_cnt)
self.layout.addRow(QHLine())
self.layout.addRow(QtWidgets.QLabel("<b>Results</b>"))
self.results_header = self.layout.rowCount()
self.set_titel("Peak search")
def runAnalysis(self):
if not self.app.data.s11:
return
self.reset()
s11 = self.app.data.s11
data, fmt_fnc = self.data_and_format()
inverted = False
if self.button["peak_l"].isChecked():
inverted = True
peaks, _ = find_peaks(
-np.array(data), width=3, distance=3, prominence=1
)
else:
self.button["peak_h"].setChecked(True)
peaks, _ = find_peaks(data, width=3, distance=3, prominence=1)
# Having found the peaks, get the prominence data
for i, p in np.ndenumerate(peaks):
logger.debug("Peak %s at %s", i, p)
prominences = peak_prominences(data, peaks)[0]
logger.debug("%d prominences", len(prominences))
# Find the peaks with the most extreme values
# Alternately, allow the user to select "most prominent"?
count = self.peak_cnt.value()
if count > len(prominences):
count = len(prominences)
self.peak_cnt.setValue(count)
indices = np.argpartition(prominences, -count)[-count:]
logger.debug("%d indices", len(indices))
for i in indices:
pos = peaks[i]
self.layout.addRow(
f"Freq: {format_frequency_short(s11[pos].freq)}",
QtWidgets.QLabel(
f" Value: {fmt_fnc(-data[pos] if inverted else data[pos])}"
),
)
if self.button["move_marker"].isChecked():
if count > len(self.app.markers):
logger.warning("More peaks found than there are markers")
for i in range(min(count, len(self.app.markers))):
self.app.markers[i].setFrequency(
str(s11[peaks[indices[i]]].freq)
)
def reset(self):
super().reset()
logger.debug(
"Results start at %d, out of %d",
self.results_header,
self.layout.rowCount(),
)
for _ in range(self.results_header, self.layout.rowCount()):
logger.debug("deleting %s", self.layout.rowCount())
self.layout.removeRow(self.layout.rowCount() - 1)

Wyświetl plik

@ -0,0 +1,127 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020ff NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import os
import csv
import logging
from PyQt6 import QtWidgets
import NanoVNASaver.AnalyticTools as at
from NanoVNASaver.Analysis.Base import Analysis, QHLine
from NanoVNASaver.Formatting import format_frequency, format_resistance
from NanoVNASaver.RFTools import reflection_coefficient
logger = logging.getLogger(__name__)
def format_resistence_neg(x):
return format_resistance(x, allow_negative=True)
def vswr_transformed(z, ratio=49) -> float:
refl = reflection_coefficient(z / ratio)
mag = abs(refl)
return 1 if mag == 1 else (1 + mag) / (1 - mag)
class ResonanceAnalysis(Analysis):
def __init__(self, app):
super().__init__(app)
self.crossings: list[int] = []
self.filename = ""
self._widget = QtWidgets.QWidget()
self.layout = QtWidgets.QFormLayout()
self._widget.setLayout(self.layout)
self.input_description = QtWidgets.QLineEdit("")
self.checkbox_move_marker = QtWidgets.QCheckBox()
self.layout.addRow(QtWidgets.QLabel("<b>Settings</b>"))
self.layout.addRow("Description", self.input_description)
self.layout.addRow(QHLine())
self.layout.addRow(QHLine())
self.results_label = QtWidgets.QLabel("<b>Results</b>")
self.layout.addRow(self.results_label)
def _get_data(self, index):
s11 = self.app.data.s11
my_data = {
"freq": s11[index].freq,
"s11": s11[index].z,
"lambda": s11[index].wavelength,
"impedance": s11[index].impedance(),
"vswr": s11[index].vswr,
}
my_data["vswr_49"] = vswr_transformed(my_data["impedance"], 49)
my_data["vswr_4"] = vswr_transformed(my_data["impedance"], 4)
my_data["r"] = my_data["impedance"].real
my_data["x"] = my_data["impedance"].imag
return my_data
def runAnalysis(self):
self.reset()
self.filename = (
os.path.join("/tmp/", f"{self.input_description.text()}.csv")
if self.input_description.text()
else ""
)
results_header = self.layout.indexOf(self.results_label)
logger.debug(
"Results start at %d, out of %d",
results_header,
self.layout.rowCount(),
)
for _ in range(results_header, self.layout.rowCount()):
self.layout.removeRow(self.layout.rowCount() - 1)
self.crossings = sorted(
set(at.zero_crossings([d.phase for d in self.app.data.s11]))
)
logger.debug("Found %d sections ", len(self.crossings))
if not self.crossings:
self.layout.addRow(QtWidgets.QLabel("No resonance found"))
return
self.do_resonance_analysis()
def do_resonance_analysis(self):
extended_data = []
for crossing in self.crossings:
extended_data.append(self._get_data(crossing))
self.layout.addRow(
"Resonance",
QtWidgets.QLabel(
format_frequency(self.app.data.s11[crossing].freq)
),
)
self.layout.addWidget(QHLine())
# Remove the final separator line
self.layout.removeRow(self.layout.rowCount() - 1)
if self.filename and extended_data:
with open(
self.filename, "w", encoding="utf-8", newline=""
) as csvfile:
fieldnames = extended_data[0].keys()
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for row in extended_data:
writer.writerow(row)

Wyświetl plik

@ -0,0 +1,123 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
from typing import Callable
from PyQt6 import QtWidgets
import numpy as np
from NanoVNASaver.Analysis.Base import Analysis, QHLine
from NanoVNASaver.Formatting import (
format_frequency,
format_gain,
format_resistance,
format_vswr,
)
logger = logging.getLogger(__name__)
class SimplePeakSearchAnalysis(Analysis):
def __init__(self, app):
super().__init__(app)
self.label["peak_freq"] = QtWidgets.QLabel()
self.label["peak_db"] = QtWidgets.QLabel()
self.button = {
"vswr": QtWidgets.QRadioButton("VSWR"),
"resistance": QtWidgets.QRadioButton("Resistance"),
"reactance": QtWidgets.QRadioButton("Reactance"),
"gain": QtWidgets.QRadioButton("S21 Gain"),
"peak_h": QtWidgets.QRadioButton("Highest value"),
"peak_l": QtWidgets.QRadioButton("Lowest value"),
"move_marker": QtWidgets.QCheckBox(),
}
self.button["gain"].setChecked(True)
self.button["peak_h"].setChecked(True)
self.btn_group = {
"data": QtWidgets.QButtonGroup(),
"peak": QtWidgets.QButtonGroup(),
}
for btn in ("vswr", "resistance", "reactance", "gain"):
self.btn_group["data"].addButton(self.button[btn])
self.btn_group["peak"].addButton(self.button["peak_h"])
self.btn_group["peak"].addButton(self.button["peak_l"])
layout = self.layout
layout.addRow(self.label["titel"])
layout.addRow(QHLine())
layout.addRow(QtWidgets.QLabel("<b>Settings</b>"))
layout.addRow("Data source", self.button["vswr"])
layout.addRow("", self.button["resistance"])
layout.addRow("", self.button["reactance"])
layout.addRow("", self.button["gain"])
layout.addRow(QHLine())
layout.addRow("Peak type", self.button["peak_h"])
layout.addRow("", self.button["peak_l"])
layout.addRow(QHLine())
layout.addRow("Move marker to peak", self.button["move_marker"])
layout.addRow(QHLine())
layout.addRow(self.label["result"])
layout.addRow("Peak frequency:", self.label["peak_freq"])
layout.addRow("Peak value:", self.label["peak_db"])
self.set_titel("Simple peak search")
def runAnalysis(self):
if not self.app.data.s11:
return
s11 = self.app.data.s11
data, fmt_fnc = self.data_and_format()
if self.button["peak_l"].isChecked():
idx_peak = np.argmin(data)
else:
self.button["peak_h"].setChecked(True)
idx_peak = np.argmax(data)
self.label["peak_freq"].setText(format_frequency(s11[idx_peak].freq))
self.label["peak_db"].setText(fmt_fnc(data[idx_peak]))
if self.button["move_marker"].isChecked() and self.app.markers:
self.app.markers[0].setFrequency(f"{s11[idx_peak].freq}")
def data_and_format(self) -> tuple[list[float], Callable]:
s11 = self.app.data.s11
s21 = self.app.data.s21
if not s21:
self.button["gain"].setEnabled(False)
if self.button["gain"].isChecked():
self.button["vswr"].setChecked(True)
else:
self.button["gain"].setEnabled(True)
if self.button["gain"].isChecked():
return ([d.gain for d in s21], format_gain)
if self.button["resistance"].isChecked():
return ([d.impedance().real for d in s11], format_resistance)
if self.button["reactance"].isChecked():
return ([d.impedance().imag for d in s11], format_resistance)
# default
return ([d.vswr for d in s11], format_vswr)

Wyświetl plik

@ -0,0 +1,112 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
from PyQt6 import QtWidgets
import NanoVNASaver.AnalyticTools as at
from NanoVNASaver.Analysis.Base import Analysis, QHLine
from NanoVNASaver.Formatting import format_frequency, format_vswr
logger = logging.getLogger(__name__)
class VSWRAnalysis(Analysis):
max_dips_shown = 3
vswr_limit_value = 1.5
def __init__(self, app):
super().__init__(app)
self._widget = QtWidgets.QWidget()
self.layout = QtWidgets.QFormLayout()
self._widget.setLayout(self.layout)
self.input_vswr_limit = QtWidgets.QDoubleSpinBox()
self.input_vswr_limit.setValue(VSWRAnalysis.vswr_limit_value)
self.input_vswr_limit.setSingleStep(0.1)
self.input_vswr_limit.setMinimum(1)
self.input_vswr_limit.setMaximum(25)
self.input_vswr_limit.setDecimals(2)
self.checkbox_move_marker = QtWidgets.QCheckBox()
self.layout.addRow(QtWidgets.QLabel("<b>Settings</b>"))
self.layout.addRow("VSWR limit", self.input_vswr_limit)
self.layout.addRow(QHLine())
self.results_label = QtWidgets.QLabel("<b>Results</b>")
self.layout.addRow(self.results_label)
self.minimums: list[int] = []
def runAnalysis(self):
if not self.app.data.s11:
return
s11 = self.app.data.s11
data = [d.vswr for d in s11]
threshold = self.input_vswr_limit.value()
minima = sorted(at.minima(data, threshold), key=lambda i: data[i])[
: VSWRAnalysis.max_dips_shown
]
self.minimums = minima
results_header = self.layout.indexOf(self.results_label)
logger.debug(
"Results start at %d, out of %d",
results_header,
self.layout.rowCount(),
)
for _ in range(results_header, self.layout.rowCount()):
self.layout.removeRow(self.layout.rowCount() - 1)
if not minima:
self.layout.addRow(
QtWidgets.QLabel(
f"No areas found with VSWR below {format_vswr(threshold)}."
)
)
return
for idx in minima:
rng = at.take_from_idx(data, idx, lambda i: i[1] < threshold)
begin, end = rng[0], rng[-1]
self.layout.addRow(
"Start", QtWidgets.QLabel(format_frequency(s11[begin].freq))
)
self.layout.addRow(
"Minimum",
QtWidgets.QLabel(
f"{format_frequency(s11[idx].freq)}"
f" ({round(s11[idx].vswr, 2)})"
),
)
self.layout.addRow(
"End", QtWidgets.QLabel(format_frequency(s11[end].freq))
)
self.layout.addRow(
"Span",
QtWidgets.QLabel(
format_frequency((s11[end].freq - s11[begin].freq))
),
)
self.layout.addWidget(QHLine())
self.layout.removeRow(self.layout.rowCount() - 1)

Wyświetl plik

@ -0,0 +1,191 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020ff NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import itertools as it
import math
from typing import Callable
import numpy as np
# pylint: disable=import-error, no-name-in-module
from scipy.signal import find_peaks
from NanoVNASaver.RFTools import Datapoint
def zero_crossings(data: list[float]) -> list[int]:
"""find zero crossings
Args:
data (list[float]): data list execute
Returns:
list[int]: sorted indices of zero crossing points
"""
if not data:
return []
np_data = np.array(data)
# start with real zeros (ignore first and last element)
real_zeros = [
n for n in np.where(np_data == 0.0)[0] if n not in {0, np_data.size - 1}
]
# now multipy elements to find change in signess
crossings = [
n if abs(np_data[n]) < abs(np_data[n + 1]) else n + 1
for n in np.where((np_data[:-1] * np_data[1:]) < 0.0)[0]
]
return sorted(real_zeros + crossings)
def maxima(data: list[float], threshold: float = 0.0) -> list[int]:
"""maxima
Args:
data (list[float]): data list to execute
Returns:
list[int]: indices of maxima
"""
peaks = find_peaks(data, width=2, distance=3, prominence=1)[0].tolist()
return [i for i in peaks if data[i] > threshold] if threshold else peaks
def minima(data: list[float], threshold: float = 0.0) -> list[int]:
"""minima
Args:
data (list[float]): data list to execute
Returns:
list[int]: indices of minima
"""
bottoms = find_peaks(-np.array(data), width=2, distance=3, prominence=1)[
0
].tolist()
return [i for i in bottoms if data[i] < threshold] if threshold else bottoms
def take_from_idx(
data: list[float], idx: int, predicate: Callable
) -> list[int]:
"""take_from_center
Args:
data (list[float]): data list to execute
idx (int): index of a start position
predicate (Callable): predicate on which elements to take
from center. (e.g. lambda i: i[1] < threshold)
Returns:
list[int]: indices of element matching predicate left
and right from index
"""
lower = list(
reversed(
[
i
for i, _ in it.takewhile(
predicate, reversed(list(enumerate(data[:idx])))
)
]
)
)
upper = [i for i, _ in it.takewhile(predicate, enumerate(data[idx:], idx))]
return lower + upper
def center_from_idx(gains: list[float], idx: int, delta: float = 3.0) -> int:
"""find maximum from index postion of gains in a attn dB gain span
Args:
gains (list[float]): gain values
idx (int): start position to search from
delta (float, optional): max gain delta from start. Defaults to 3.0.
Returns:
int: position of highest gain from start in range (-1 if no data)
"""
peak_db = gains[idx]
rng = take_from_idx(gains, idx, lambda i: abs(peak_db - i[1]) < delta)
return max(rng, key=lambda i: gains[i]) if rng else -1
def cut_off_left(
gains: list[float], idx: int, peak_gain: float, attn: float = 3.0
) -> int:
"""find first position in list where gain in attn lower then peak
left from index
Args:
gains (list[float]): gain values
idx (int): start position to search from
peak_gain (float): reference gain value
attn (float, optional): attenuation to search position for.
Defaults to 3.0.
Returns:
int: position of attenuation point. (-1 if no data)
"""
return next(
(i for i in range(idx, -1, -1) if (peak_gain - gains[i]) > attn), -1
)
def cut_off_right(
gains: list[float], idx: int, peak_gain: float, attn: float = 3.0
) -> int:
"""find first position in list where gain in attn lower then peak
right from index
Args:
gains (list[float]): gain values
idx (int): start position to search from
peak_gain (float): reference gain value
attn (float, optional): attenuation to search position for.
Defaults to 3.0.
Returns:
int: position of attenuation point. (-1 if no data)
"""
return next(
(i for i in range(idx, len(gains)) if (peak_gain - gains[i]) > attn), -1
)
def dip_cut_offs(
gains: list[float], peak_gain: float, attn: float = 3.0
) -> tuple[int, int]:
rng = np.where(np.array(gains) < (peak_gain - attn))[0].tolist()
return (rng[0], rng[-1]) if rng else (math.nan, math.nan)
def calculate_rolloff(
s21: list[Datapoint], idx_1: int, idx_2: int
) -> tuple[float, float]:
if idx_1 == idx_2:
return (math.nan, math.nan)
freq_1, freq_2 = s21[idx_1].freq, s21[idx_2].freq
gain_1, gain_2 = s21[idx_1].gain, s21[idx_2].gain
factor = freq_1 / freq_2 if freq_1 > freq_2 else freq_2 / freq_1
attn = abs(gain_1 - gain_2)
decade_attn = attn / math.log10(factor)
octave_attn = decade_attn * math.log10(2)
return (octave_attn, decade_attn)

Wyświetl plik

@ -0,0 +1,543 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import cmath
import math
import os
import re
from collections import defaultdict, UserDict
from dataclasses import dataclass
from scipy.interpolate import interp1d
from NanoVNASaver.RFTools import Datapoint
IDEAL_SHORT = complex(-1, 0)
IDEAL_OPEN = complex(1, 0)
IDEAL_LOAD = complex(0, 0)
IDEAL_THROUGH = complex(1, 0)
RXP_CAL_HEADER = re.compile(
r"""
^ \# \s+ Hz \s+
ShortR \s+ ShortI \s+ OpenR \s+ OpenI \s+
LoadR \s+ LoadI
(?P<through> \s+ ThroughR \s+ ThroughI)?
(?P<thrurefl> \s+ ThrureflR \s+ ThrureflI)?
(?P<isolation> \s+ IsolationR \s+ IsolationI)?
\s* $
""",
re.VERBOSE | re.IGNORECASE,
)
RXP_CAL_LINE = re.compile(
r"""
^ \s*
(?P<freq>\d+) \s+
(?P<shortr>[-0-9Ee.]+) \s+ (?P<shorti>[-0-9Ee.]+) \s+
(?P<openr>[-0-9Ee.]+) \s+ (?P<openi>[-0-9Ee.]+) \s+
(?P<loadr>[-0-9Ee.]+) \s+ (?P<loadi>[-0-9Ee.]+)
( \s+ (?P<throughr>[-0-9Ee.]+) \s+ (?P<throughi>[-0-9Ee.]+))?
( \s+ (?P<thrureflr>[-0-9Ee.]+) \s+ (?P<thrurefli>[-0-9Ee.]+))?
( \s+ (?P<isolationr>[-0-9Ee.]+) \s+ (?P<isolationi>[-0-9Ee.]+))?
\s* $
""",
re.VERBOSE,
)
logger = logging.getLogger(__name__)
def correct_delay(d: Datapoint, delay: float, reflect: bool = False):
mult = 2 if reflect else 1
corr_data = d.z * cmath.exp(
complex(0, 1) * 2 * math.pi * d.freq * delay * -1 * mult
)
return Datapoint(d.freq, corr_data.real, corr_data.imag)
@dataclass
class CalData:
# pylint: disable=too-many-instance-attributes
short: complex = complex(0.0, 0.0)
open: complex = complex(0.0, 0.0)
load: complex = complex(0.0, 0.0)
through: complex = complex(0.0, 0.0)
thrurefl: complex = complex(0.0, 0.0)
isolation: complex = complex(0.0, 0.0)
freq: int = 0
e00: float = 0.0 # Directivity
e11: float = 0.0 # Port1 match
delta_e: float = 0.0 # Tracking
e10e01: float = 0.0 # Forward Reflection Tracking
# 2 port
e30: float = 0.0 # Forward isolation
e22: float = 0.0 # Port2 match
e10e32: float = 0.0 # Forward transmission
def __str__(self):
return (
f"{self.freq}"
f" {self.short.real} {self.short.imag}"
f" {self.open.real} {self.open.imag}"
f" {self.load.real} {self.load.imag}"
+ (
f" {self.through.real} {self.through.imag}"
f" {self.thrurefl.real} {self.thrurefl.imag}"
f" {self.isolation.real} {self.isolation.imag}"
if self.through
else ""
)
)
@dataclass
class CalElement:
# pylint: disable=too-many-instance-attributes
short_is_ideal: bool = True
short_l0: float = 5.7e-12
short_l1: float = -8.96e-20
short_l2: float = -1.1e-29
short_l3: float = -4.12e-37
short_length: float = -34.2 # ps
open_is_ideal: bool = True
open_c0: float = 2.1e-14
open_c1: float = 5.67e-23
open_c2: float = -2.39e-31
open_c3: float = 2.0e-40
open_length: float = 0.0
load_is_ideal: bool = True
load_r: float = 50.0
load_l: float = 0.0
load_c: float = 0.0
load_length: float = 0.0
through_is_ideal: bool = True
through_length: float = 0.0
class CalDataSet(UserDict):
def __init__(self):
super().__init__()
self.notes = ""
self.data: defaultdict[int, CalData] = defaultdict(CalData)
def __str__(self):
return (
(
"# Calibration data for NanoVNA-Saver\n"
+ "\n".join([f"! {note}" for note in self.notes.splitlines()])
+ "\n"
+ "# Hz ShortR ShortI OpenR OpenI LoadR LoadI"
+ (
" ThroughR ThroughI ThrureflR"
" ThrureflI IsolationR IsolationI\n"
if self.complete2port()
else "\n"
)
+ "\n".join(
[f"{self.data.get(freq)}" for freq in self.frequencies()]
)
+ "\n"
)
if self.complete1port()
else ""
)
def _append_match(
self, m: re.Match, header: str, line_nr: int, line: str
) -> None:
cal = m.groupdict()
columns = {col[:-1] for col in cal.keys() if cal[col] and col != "freq"}
if "through" in columns and header == "sol":
logger.warning(
"Through data with sol header. %i: %s", line_nr, line
)
# fix short data (without thrurefl)
if "thrurefl" in columns and "isolation" not in columns:
cal["isolationr"] = cal["thrureflr"]
cal["isolationi"] = cal["thrurefli"]
cal["thrureflr"], cal["thrurefli"] = None, None
for name in columns:
self.insert(
name,
Datapoint(
int(cal["freq"]),
float(cal[f"{name}r"]),
float(cal[f"{name}i"]),
),
)
def from_str(self, text: str) -> "CalDataSet":
# reset data
self.notes = ""
self.data = defaultdict(CalData)
header = ""
# parse text
for i, line in enumerate(text.splitlines(), 1):
line = line.strip()
if line.startswith("!"):
self.notes += f"{line[2:]}\n"
continue
if m := RXP_CAL_HEADER.search(line):
if header:
logger.warning(
"Duplicate header in cal data. %i: %s", i, line
)
header = "through" if m.group("through") else "sol"
continue
if not line or line.startswith("#"):
continue
m = RXP_CAL_LINE.search(line)
if not m:
logger.warning("Illegal caldata. Line %i: %s", i, line)
continue
if not header:
logger.warning(
"Caldata without having read header: %i: %s", i, line
)
self._append_match(m, header, line, i)
return self
def insert(self, name: str, dp: Datapoint):
if name not in {
"short",
"open",
"load",
"through",
"thrurefl",
"isolation",
}:
raise KeyError(name)
freq = dp.freq
setattr(self.data[freq], name, (dp.z))
self.data[freq].freq = freq
def frequencies(self) -> list[int]:
return sorted(self.data.keys())
def get(self, key: int, default: CalData = None) -> CalData:
return self.data.get(key, default)
def items(self):
yield from self.data.items()
def values(self):
for freq in self.frequencies():
yield self.get(freq)
def size_of(self, name: str) -> int:
return len([True for val in self.data.values() if getattr(val, name)])
def complete1port(self) -> bool:
for val in self.data.values():
if not all((val.short, val.open, val.load)):
return False
return any(self.data)
def complete2port(self) -> bool:
if not self.complete1port():
return False
for val in self.data.values():
if not all((val.through, val.thrurefl, val.isolation)):
return False
return any(self.data)
class Calibration:
def __init__(self):
self.notes = []
self.dataset = CalDataSet()
self.cal_element = CalElement()
self.interp = {}
self.isCalculated = False
self.source = "Manual"
def insert(self, name: str, data: list[Datapoint]):
for dp in data:
self.dataset.insert(name, dp)
def size(self) -> int:
return len(self.dataset.frequencies())
def data_size(self, name) -> int:
return self.dataset.size_of(name)
def isValid1Port(self) -> bool:
return self.dataset.complete1port()
def isValid2Port(self) -> bool:
return self.dataset.complete2port()
def _calc_port_1(self, freq: int, cal: CalData):
g1 = self.gamma_short(freq)
g2 = self.gamma_open(freq)
g3 = self.gamma_load(freq)
gm1 = cal.short
gm2 = cal.open
gm3 = cal.load
denominator = (
g1 * (g2 - g3) * gm1
+ g2 * g3 * gm2
- g2 * g3 * gm3
- (g2 * gm2 - g3 * gm3) * g1
)
cal.e00 = (
-(
(g2 * gm3 - g3 * gm3) * g1 * gm2
- (g2 * g3 * gm2 - g2 * g3 * gm3 - (g3 * gm2 - g2 * gm3) * g1)
* gm1
)
/ denominator
)
cal.e11 = (
(g2 - g3) * gm1 - g1 * (gm2 - gm3) + g3 * gm2 - g2 * gm3
) / denominator
cal.delta_e = (
-(
(g1 * (gm2 - gm3) - g2 * gm2 + g3 * gm3) * gm1
+ (g2 * gm3 - g3 * gm3) * gm2
)
/ denominator
)
def _calc_port_2(self, freq: int, cal: CalData):
gt = self.gamma_through(freq)
gm4 = cal.through
gm5 = cal.thrurefl
gm6 = cal.isolation
gm7 = gm5 - cal.e00
cal.e30 = cal.isolation
cal.e10e01 = cal.e00 * cal.e11 - cal.delta_e
cal.e22 = gm7 / (gm7 * cal.e11 * gt**2 + cal.e10e01 * gt**2)
cal.e10e32 = (gm4 - gm6) * (1 - cal.e11 * cal.e22 * gt**2) / gt
def calc_corrections(self):
if not self.isValid1Port():
logger.warning("Tried to calibrate from insufficient data.")
raise ValueError(
"All of short, open and load calibration steps"
"must be completed for calibration to be applied."
)
logger.debug("Calculating calibration for %d points.", self.size())
for freq, caldata in self.dataset.items():
try:
self._calc_port_1(freq, caldata)
if self.isValid2Port():
self._calc_port_2(freq, caldata)
except ZeroDivisionError as exc:
self.isCalculated = False
logger.error(
"Division error - did you use the same measurement"
" for two of short, open and load?"
)
raise ValueError(
f"Two of short, open and load returned the same"
f" values at frequency {freq}Hz."
) from exc
self.gen_interpolation()
self.isCalculated = True
logger.debug("Calibration correctly calculated.")
def gamma_short(self, freq: int) -> complex:
if self.cal_element.short_is_ideal:
return IDEAL_SHORT
logger.debug("Using short calibration set values.")
cal_element = self.cal_element
Zsp = complex(
0.0,
2.0
* math.pi
* freq
* (
cal_element.short_l0
+ cal_element.short_l1 * freq
+ cal_element.short_l2 * freq**2
+ cal_element.short_l3 * freq**3
),
)
# Referencing https://arxiv.org/pdf/1606.02446.pdf (18) - (21)
return (
(Zsp / 50.0 - 1.0)
/ (Zsp / 50.0 + 1.0)
* cmath.exp(
complex(0.0, -4.0 * math.pi * freq * cal_element.short_length)
)
)
def gamma_open(self, freq: int) -> complex:
if self.cal_element.open_is_ideal:
return IDEAL_OPEN
logger.debug("Using open calibration set values.")
cal_element = self.cal_element
Zop = complex(
0.0,
2.0
* math.pi
* freq
* (
cal_element.open_c0
+ cal_element.open_c1 * freq
+ cal_element.open_c2 * freq**2
+ cal_element.open_c3 * freq**3
),
)
return ((1.0 - 50.0 * Zop) / (1.0 + 50.0 * Zop)) * cmath.exp(
complex(0.0, -4.0 * math.pi * freq * cal_element.open_length)
)
def gamma_load(self, freq: int) -> complex:
if self.cal_element.load_is_ideal:
return IDEAL_LOAD
logger.debug("Using load calibration set values.")
cal_element = self.cal_element
Zl = complex(cal_element.load_r, 0.0)
if cal_element.load_c > 0.0:
Zl = cal_element.load_r / complex(
1.0,
2.0 * cal_element.load_r * math.pi * freq * cal_element.load_c,
)
if cal_element.load_l > 0.0:
Zl = Zl + complex(0.0, 2 * math.pi * freq * cal_element.load_l)
return (
(Zl / 50.0 - 1.0)
/ (Zl / 50.0 + 1.0)
* cmath.exp(
complex(0.0, -4 * math.pi * freq * cal_element.load_length)
)
)
def gamma_through(self, freq: int) -> complex:
if self.cal_element.through_is_ideal:
return IDEAL_THROUGH
logger.debug("Using through calibration set values.")
cal_element = self.cal_element
return cmath.exp(
complex(0.0, -2.0 * math.pi * cal_element.through_length * freq)
)
def gen_interpolation(self):
(freq, e00, e11, delta_e, e10e01, e30, e22, e10e32) = zip(
*[
(
c.freq,
c.e00,
c.e11,
c.delta_e,
c.e10e01,
c.e30,
c.e22,
c.e10e32,
)
for c in self.dataset.values()
]
)
self.interp = {
"e00": interp1d(
freq,
e00,
kind="slinear",
bounds_error=False,
fill_value=(e00[0], e00[-1]),
),
"e11": interp1d(
freq,
e11,
kind="slinear",
bounds_error=False,
fill_value=(e11[0], e11[-1]),
),
"delta_e": interp1d(
freq,
delta_e,
kind="slinear",
bounds_error=False,
fill_value=(delta_e[0], delta_e[-1]),
),
"e10e01": interp1d(
freq,
e10e01,
kind="slinear",
bounds_error=False,
fill_value=(e10e01[0], e10e01[-1]),
),
"e30": interp1d(
freq,
e30,
kind="slinear",
bounds_error=False,
fill_value=(e30[0], e30[-1]),
),
"e22": interp1d(
freq,
e22,
kind="slinear",
bounds_error=False,
fill_value=(e22[0], e22[-1]),
),
"e10e32": interp1d(
freq,
e10e32,
kind="slinear",
bounds_error=False,
fill_value=(e10e32[0], e10e32[-1]),
),
}
def correct11(self, dp: Datapoint):
i = self.interp
s11 = (dp.z - i["e00"](dp.freq)) / (
(dp.z * i["e11"](dp.freq)) - i["delta_e"](dp.freq)
)
return Datapoint(dp.freq, s11.real, s11.imag)
def correct21(self, dp: Datapoint, dp11: Datapoint):
i = self.interp
s21 = (dp.z - i["e30"](dp.freq)) / i["e10e32"](dp.freq)
s21 = s21 * (
i["e10e01"](dp.freq)
/ (i["e11"](dp.freq) * dp11.z - i["delta_e"](dp.freq))
)
return Datapoint(dp.freq, s21.real, s21.imag)
def save(self, filename: str):
self.dataset.notes = "\n".join(self.notes)
if not self.isValid1Port():
raise ValueError("Not a valid calibration")
with open(filename, mode="w", encoding="utf-8") as calfile:
calfile.write(str(self.dataset))
def load(self, filename):
self.source = os.path.basename(filename)
with open(filename, encoding="utf-8") as calfile:
self.dataset = CalDataSet().from_str(calfile.read())
self.notes = self.dataset.notes.splitlines()

Wyświetl plik

@ -18,9 +18,8 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import math
import logging
from typing import List
from PyQt5 import QtGui
from PyQt6 import QtGui
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.Charts.Chart import Chart
@ -33,11 +32,11 @@ class CombinedLogMagChart(LogMagChart):
def __init__(self, name=""):
super().__init__(name)
self.data11: List[Datapoint] = []
self.data21: List[Datapoint] = []
self.data11: list[Datapoint] = []
self.data21: list[Datapoint] = []
self.reference11: List[Datapoint] = []
self.reference21: List[Datapoint] = []
self.reference11: list[Datapoint] = []
self.reference21: list[Datapoint] = []
def setCombinedData(self, data11, data21):
self.data11 = data11
@ -61,19 +60,24 @@ class CombinedLogMagChart(LogMagChart):
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(Chart.color.text))
qp.drawText(int(round(self.dim.width / 2)) - 20, 15,
f"{self.name} {self.name_unit}")
qp.drawText(
int(self.dim.width // 2) - 20, 15, f"{self.name} {self.name_unit}"
)
qp.drawText(10, 15, "S11")
qp.drawText(self.leftMargin + self.dim.width - 8, 15, "S21")
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin,
self.topMargin - 5,
self.leftMargin,
self.topMargin + self.dim.height + 5)
qp.drawLine(self.leftMargin - 5,
self.topMargin + self.dim.height,
self.leftMargin + self.dim.width,
self.topMargin + self.dim.height)
qp.drawLine(
self.leftMargin,
self.topMargin - 5,
self.leftMargin,
self.topMargin + self.dim.height + 5,
)
qp.drawLine(
self.leftMargin - 5,
self.topMargin + self.dim.height,
self.leftMargin + self.dim.width,
self.topMargin + self.dim.height,
)
def drawValues(self, qp: QtGui.QPainter):
if len(self.data11) == 0 and len(self.reference11) == 0:
@ -116,8 +120,12 @@ class CombinedLogMagChart(LogMagChart):
pen = QtGui.QPen(c)
pen.setWidth(2)
qp.setPen(pen)
qp.drawLine(self.leftMargin + self.dim.width - 20, 9,
self.leftMargin + self.dim.width - 15, 9)
qp.drawLine(
self.leftMargin + self.dim.width - 20,
9,
self.leftMargin + self.dim.width - 15,
9,
)
if self.reference11:
c = QtGui.QColor(Chart.color.reference)
@ -131,8 +139,12 @@ class CombinedLogMagChart(LogMagChart):
pen = QtGui.QPen(c)
pen.setWidth(2)
qp.setPen(pen)
qp.drawLine(self.leftMargin + self.dim.width - 20, 14,
self.leftMargin + self.dim.width - 15, 14)
qp.drawLine(
self.leftMargin + self.dim.width - 20,
14,
self.leftMargin + self.dim.width - 15,
14,
)
self.drawData(qp, self.data11, Chart.color.sweep)
self.drawData(qp, self.data21, Chart.color.sweep_secondary)

Wyświetl plik

@ -18,30 +18,42 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
from dataclasses import dataclass, replace
from typing import List, Set, Tuple, ClassVar, Any
from dataclasses import dataclass, field, replace
from typing import ClassVar, Any
from PyQt5 import QtWidgets, QtGui, QtCore
from PyQt5.QtCore import pyqtSignal
from PyQt6 import QtWidgets, QtGui, QtCore
from PyQt6.QtCore import pyqtSignal, Qt
from PyQt6.QtGui import QColor, QColorConstants, QAction
from NanoVNASaver import Defaults
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.Marker import Marker
from NanoVNASaver.Marker.Widget import Marker
logger = logging.getLogger(__name__)
@dataclass
class ChartColors: # pylint: disable=too-many-instance-attributes
background: QtGui.QColor = QtGui.QColor(QtCore.Qt.white)
foreground: QtGui.QColor = QtGui.QColor(QtCore.Qt.lightGray)
reference: QtGui.QColor = QtGui.QColor(0, 0, 255, 64)
reference_secondary: QtGui.QColor = QtGui.QColor(0, 0, 192, 48)
sweep: QtGui.QColor = QtGui.QColor(QtCore.Qt.darkYellow)
sweep_secondary: QtGui.QColor = QtGui.QColor(QtCore.Qt.darkMagenta)
swr: QtGui.QColor = QtGui.QColor(255, 0, 0, 128)
text: QtGui.QColor = QtGui.QColor(QtCore.Qt.black)
bands: QtGui.QColor = QtGui.QColor(128, 128, 128, 48)
background: QColor = field(
default_factory=lambda: QColor(QColorConstants.White)
)
foreground: QColor = field(
default_factory=lambda: QColor(QColorConstants.LightGray)
)
reference: QColor = field(default_factory=lambda: QColor(0, 0, 255, 64))
reference_secondary: QColor = field(
default_factory=lambda: QColor(0, 0, 192, 48)
)
sweep: QColor = field(
default_factory=lambda: QColor(QColorConstants.DarkYellow)
)
sweep_secondary: QColor = field(
default_factory=lambda: QColor(QColorConstants.DarkMagenta)
)
swr: QColor = field(default_factory=lambda: QColor(255, 0, 0, 128))
text: QColor = field(default_factory=lambda: QColor(QColorConstants.Black))
bands: QColor = field(default_factory=lambda: QColor(128, 128, 128, 48))
@dataclass
class ChartDimensions:
@ -52,14 +64,16 @@ class ChartDimensions:
line: int = 1
point: int = 2
@dataclass
class ChartDragBox:
pos: Tuple[int] = (-1, -1)
pos_start: Tuple[int] = (0, 0)
pos: tuple[int] = (-1, -1)
pos_start: tuple[int] = (0, 0)
state: bool = False
move_x: int = -1
move_y: int = -1
@dataclass
class ChartFlags:
draw_lines: bool = False
@ -72,7 +86,7 @@ class ChartMarker(QtWidgets.QWidget):
self.qp = qp
def draw(self, x: int, y: int, color: QtGui.QColor, text: str = ""):
offset = Defaults.cfg.chart.marker_size // 2
offset = int(Defaults.cfg.chart.marker_size // 2)
if Defaults.cfg.chart.marker_at_tip:
y -= offset
pen = QtGui.QPen(color)
@ -90,7 +104,7 @@ class ChartMarker(QtWidgets.QWidget):
if text and Defaults.cfg.chart.marker_label:
text_width = self.qp.fontMetrics().horizontalAdvance(text)
self.qp.drawText(x - text_width // 2, y - 3 - offset, text)
self.qp.drawText(x - int(text_width // 2), y - 3 - offset, text)
class Chart(QtWidgets.QWidget):
@ -101,7 +115,7 @@ class Chart(QtWidgets.QWidget):
def __init__(self, name):
super().__init__()
self.name = name
self.sweepTitle = ''
self.sweepTitle = ""
self.leftMargin = 30
self.rightMargin = 20
@ -114,21 +128,23 @@ class Chart(QtWidgets.QWidget):
self.draggedMarker = None
self.data: List[Datapoint] = []
self.reference: List[Datapoint] = []
self.data: list[Datapoint] = []
self.reference: list[Datapoint] = []
self.markers: List[Marker] = []
self.swrMarkers: Set[float] = set()
self.markers: list[Marker] = []
self.swrMarkers: set[float] = set()
self.action_popout = QtWidgets.QAction("Popout chart")
self.action_popout.triggered.connect(lambda: self.popoutRequested.emit(self))
self.action_popout = QAction("Popout chart")
self.action_popout.triggered.connect(
lambda: self.popoutRequested.emit(self)
)
self.addAction(self.action_popout)
self.action_save_screenshot = QtWidgets.QAction("Save image")
self.action_save_screenshot = QAction("Save image")
self.action_save_screenshot.triggered.connect(self.saveScreenshot)
self.addAction(self.action_save_screenshot)
self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
def setReference(self, data):
self.reference = data
@ -176,8 +192,8 @@ class Chart(QtWidgets.QWidget):
None,
)
def getNearestMarker(self, x, y) -> Marker:
if len(self.data) == 0:
def getNearestMarker(self, x, y) -> Marker | None:
if not self.data:
return None
shortest = 10**6
nearest = None
@ -189,7 +205,7 @@ class Chart(QtWidgets.QWidget):
nearest = m
return nearest
def getPosition(self, d: Datapoint) -> Tuple[int, int]:
def getPosition(self, d: Datapoint) -> tuple[int, int]:
return self.getXPosition(d), self.getYPosition(d)
def setDrawLines(self, draw_lines):
@ -197,34 +213,43 @@ class Chart(QtWidgets.QWidget):
self.update()
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
if event.buttons() == QtCore.Qt.RightButton:
if event.buttons() == Qt.MouseButton.RightButton:
event.ignore()
return
if event.buttons() == QtCore.Qt.MiddleButton:
if event.buttons() == Qt.MouseButton.MiddleButton:
# Drag event
event.accept()
self.dragbox.move_x = event.x()
self.dragbox.move_y = event.y()
self.dragbox.move_x = event.position().x()
self.dragbox.move_y = event.position().y()
return
if event.modifiers() == QtCore.Qt.ControlModifier:
if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
event.accept()
self.dragbox.state = True
self.dragbox.pos_start = (event.x(), event.y())
self.dragbox.pos_start = (
event.position().x(),
event.position().y(),
)
return
if event.modifiers() == QtCore.Qt.ShiftModifier:
self.draggedMarker = self.getNearestMarker(event.x(), event.y())
if event.modifiers() == Qt.KeyboardModifier.ShiftModifier:
self.draggedMarker = self.getNearestMarker(
event.position().x(), event.position().y()
)
self.mouseMoveEvent(event)
def mouseReleaseEvent(self, a0: QtGui.QMouseEvent):
self.draggedMarker = None
if self.dragbox.state:
self.zoomTo(self.dragbox.pos_start[0], self.dragbox.pos_start[1], a0.x(), a0.y())
self.zoomTo(
self.dragbox.pos_start[0],
self.dragbox.pos_start[1],
a0.position().x(),
a0.position().y(),
)
self.dragbox.state = False
self.dragbox.pos = (-1, -1)
self.dragbox.pos_start = (0, 0)
self.update()
def wheelEvent(self, a0: QtGui.QWheelEvent) -> None:
delta = a0.angleDelta().y()
if not delta or (not self.data and not self.reference):
@ -232,8 +257,8 @@ class Chart(QtWidgets.QWidget):
return
modifiers = a0.modifiers()
zoom_x = modifiers != QtCore.Qt.ShiftModifier
zoom_y = modifiers != QtCore.Qt.ControlModifier
zoom_x = modifiers != Qt.KeyboardModifier.ShiftModifier
zoom_y = modifiers != Qt.KeyboardModifier.ControlModifier
rate = -delta / 120
# zooming in 10% increments and 9% complementary
divisor = 10 if delta > 0 else 9
@ -241,8 +266,8 @@ class Chart(QtWidgets.QWidget):
factor_x = rate * self.dim.width / divisor if zoom_x else 0
factor_y = rate * self.dim.height / divisor if zoom_y else 0
abs_x = max(0, a0.x() - self.leftMargin)
abs_y = max(0, a0.y() - self.topMargin)
abs_x = max(0, a0.position().x() - self.leftMargin)
abs_y = max(0, a0.position().y() - self.topMargin)
ratio_x = abs_x / self.dim.width
ratio_y = abs_y / self.dim.height
@ -251,7 +276,7 @@ class Chart(QtWidgets.QWidget):
int(self.leftMargin + ratio_x * factor_x),
int(self.topMargin + ratio_y * factor_y),
int(self.leftMargin + self.dim.width - (1 - ratio_x) * factor_x),
int(self.topMargin + self.dim.height - (1 - ratio_y) * factor_y)
int(self.topMargin + self.dim.height - (1 - ratio_y) * factor_y),
)
a0.accept()
@ -261,8 +286,10 @@ class Chart(QtWidgets.QWidget):
def saveScreenshot(self):
logger.info("Saving %s to file...", self.name)
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
parent=self, caption="Save image",
filter="PNG (*.png);;All files (*.*)")
parent=self,
caption="Save image",
filter="PNG (*.png);;All files (*.*)",
)
logger.debug("Filename: %s", filename)
if not filename:
@ -303,9 +330,9 @@ class Chart(QtWidgets.QWidget):
self.update()
@staticmethod
def drawMarker(x: int, y: int,
qp: QtGui.QPainter, color: QtGui.QColor,
number: int=0):
def drawMarker(
x: int, y: int, qp: QtGui.QPainter, color: QtGui.QColor, number: int = 0
):
cmarker = ChartMarker(qp)
cmarker.draw(x, y, color, f"{number}")
@ -314,11 +341,11 @@ class Chart(QtWidgets.QWidget):
if position is None:
qf = QtGui.QFontMetricsF(self.font())
width = qf.boundingRect(self.sweepTitle).width()
position = QtCore.QPointF(self.width()/2 - width/2, 15)
position = QtCore.QPointF(self.width() / 2 - width / 2, 15)
qp.drawText(position, self.sweepTitle)
def update(self):
pal = self.palette()
pal.setColor(QtGui.QPalette.Background, Chart.color.background)
pal.setColor(QtGui.QPalette.ColorRole.Window, Chart.color.background)
self.setPalette(pal)
super().update()

Wyświetl plik

@ -18,16 +18,19 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import math
import logging
from typing import List, Tuple
import numpy as np
from PyQt5 import QtWidgets, QtGui, QtCore
from PyQt6 import QtWidgets, QtGui, QtCore
from PyQt6.QtCore import Qt
from NanoVNASaver.Charts.Chart import Chart
from NanoVNASaver.Formatting import (
parse_frequency, parse_value,
format_frequency_chart, format_frequency_chart_2,
format_y_axis)
parse_frequency,
parse_value,
format_frequency_chart,
format_frequency_chart_2,
format_y_axis,
)
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.SITools import Format, Value
@ -35,7 +38,6 @@ logger = logging.getLogger(__name__)
class FrequencyChart(Chart):
def __init__(self, name):
super().__init__(name)
self.maxFrequency = 100000000
@ -66,80 +68,90 @@ class FrequencyChart(Chart):
self.maxValue = 1
self.span = 1
self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
mode_group = QtWidgets.QActionGroup(self)
self.setContextMenuPolicy(Qt.ContextMenuPolicy.DefaultContextMenu)
mode_group = QtGui.QActionGroup(self)
self.menu = QtWidgets.QMenu()
self.reset = QtWidgets.QAction("Reset")
self.reset = QtGui.QAction("Reset")
self.reset.triggered.connect(self.resetDisplayLimits)
self.menu.addAction(self.reset)
self.x_menu = QtWidgets.QMenu("Frequency axis")
self.action_automatic = QtWidgets.QAction("Automatic")
self.action_automatic = QtGui.QAction("Automatic")
self.action_automatic.setCheckable(True)
self.action_automatic.setChecked(True)
self.action_automatic.changed.connect(
lambda: self.setFixedSpan(self.action_fixed_span.isChecked()))
self.action_fixed_span = QtWidgets.QAction("Fixed span")
lambda: self.setFixedSpan(self.action_fixed_span.isChecked())
)
self.action_fixed_span = QtGui.QAction("Fixed span")
self.action_fixed_span.setCheckable(True)
self.action_fixed_span.changed.connect(
lambda: self.setFixedSpan(self.action_fixed_span.isChecked()))
lambda: self.setFixedSpan(self.action_fixed_span.isChecked())
)
mode_group.addAction(self.action_automatic)
mode_group.addAction(self.action_fixed_span)
self.x_menu.addAction(self.action_automatic)
self.x_menu.addAction(self.action_fixed_span)
self.x_menu.addSeparator()
self.action_set_fixed_start = QtWidgets.QAction(
f"Start ({format_frequency_chart(self.minFrequency)})")
self.action_set_fixed_start = QtGui.QAction(
f"Start ({format_frequency_chart(self.minFrequency)})"
)
self.action_set_fixed_start.triggered.connect(self.setMinimumFrequency)
self.action_set_fixed_stop = QtWidgets.QAction(
f"Stop ({format_frequency_chart(self.maxFrequency)})")
self.action_set_fixed_stop = QtGui.QAction(
f"Stop ({format_frequency_chart(self.maxFrequency)})"
)
self.action_set_fixed_stop.triggered.connect(self.setMaximumFrequency)
self.x_menu.addAction(self.action_set_fixed_start)
self.x_menu.addAction(self.action_set_fixed_stop)
self.x_menu.addSeparator()
frequency_mode_group = QtWidgets.QActionGroup(self.x_menu)
self.action_set_linear_x = QtWidgets.QAction("Linear")
frequency_mode_group = QtGui.QActionGroup(self.x_menu)
self.action_set_linear_x = QtGui.QAction("Linear")
self.action_set_linear_x.setCheckable(True)
self.action_set_logarithmic_x = QtWidgets.QAction("Logarithmic")
self.action_set_logarithmic_x = QtGui.QAction("Logarithmic")
self.action_set_logarithmic_x.setCheckable(True)
frequency_mode_group.addAction(self.action_set_linear_x)
frequency_mode_group.addAction(self.action_set_logarithmic_x)
self.action_set_linear_x.triggered.connect(
lambda: self.setLogarithmicX(False))
lambda: self.setLogarithmicX(False)
)
self.action_set_logarithmic_x.triggered.connect(
lambda: self.setLogarithmicX(True))
lambda: self.setLogarithmicX(True)
)
self.action_set_linear_x.setChecked(True)
self.x_menu.addAction(self.action_set_linear_x)
self.x_menu.addAction(self.action_set_logarithmic_x)
self.y_menu = QtWidgets.QMenu("Data axis")
self.y_action_automatic = QtWidgets.QAction("Automatic")
self.y_action_automatic = QtGui.QAction("Automatic")
self.y_action_automatic.setCheckable(True)
self.y_action_automatic.setChecked(True)
self.y_action_automatic.changed.connect(
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked()))
self.y_action_fixed_span = QtWidgets.QAction("Fixed span")
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked())
)
self.y_action_fixed_span = QtGui.QAction("Fixed span")
self.y_action_fixed_span.setCheckable(True)
self.y_action_fixed_span.changed.connect(
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked()))
mode_group = QtWidgets.QActionGroup(self)
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked())
)
mode_group = QtGui.QActionGroup(self)
mode_group.addAction(self.y_action_automatic)
mode_group.addAction(self.y_action_fixed_span)
self.y_menu.addAction(self.y_action_automatic)
self.y_menu.addAction(self.y_action_fixed_span)
self.y_menu.addSeparator()
self.action_set_fixed_minimum = QtWidgets.QAction(
f"Minimum ({self.minDisplayValue})")
self.action_set_fixed_minimum = QtGui.QAction(
f"Minimum ({self.minDisplayValue})"
)
self.action_set_fixed_minimum.triggered.connect(self.setMinimumValue)
self.action_set_fixed_maximum = QtWidgets.QAction(
f"Maximum ({self.maxDisplayValue})")
self.action_set_fixed_maximum = QtGui.QAction(
f"Maximum ({self.maxDisplayValue})"
)
self.action_set_fixed_maximum.triggered.connect(self.setMaximumValue)
self.y_menu.addAction(self.action_set_fixed_maximum)
@ -147,17 +159,19 @@ class FrequencyChart(Chart):
if self.logarithmicYAllowed(): # This only works for some plot types
self.y_menu.addSeparator()
vertical_mode_group = QtWidgets.QActionGroup(self.y_menu)
self.action_set_linear_y = QtWidgets.QAction("Linear")
vertical_mode_group = QtGui.QActionGroup(self.y_menu)
self.action_set_linear_y = QtGui.QAction("Linear")
self.action_set_linear_y.setCheckable(True)
self.action_set_logarithmic_y = QtWidgets.QAction("Logarithmic")
self.action_set_logarithmic_y = QtGui.QAction("Logarithmic")
self.action_set_logarithmic_y.setCheckable(True)
vertical_mode_group.addAction(self.action_set_linear_y)
vertical_mode_group.addAction(self.action_set_logarithmic_y)
self.action_set_linear_y.triggered.connect(
lambda: self.setLogarithmicY(False))
lambda: self.setLogarithmicY(False)
)
self.action_set_logarithmic_y.triggered.connect(
lambda: self.setLogarithmicY(True))
lambda: self.setLogarithmicY(True)
)
self.action_set_linear_y.setChecked(True)
self.y_menu.addAction(self.action_set_linear_y)
self.y_menu.addAction(self.action_set_logarithmic_y)
@ -166,19 +180,25 @@ class FrequencyChart(Chart):
self.menu.addMenu(self.y_menu)
self.menu.addSeparator()
self.menu.addAction(self.action_save_screenshot)
self.action_popout = QtWidgets.QAction("Popout chart")
self.action_popout = QtGui.QAction("Popout chart")
self.action_popout.triggered.connect(
lambda: self.popoutRequested.emit(self))
lambda: self.popoutRequested.emit(self)
)
self.menu.addAction(self.action_popout)
self.setFocusPolicy(QtCore.Qt.ClickFocus)
self.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
self.setMinimumSize(self.dim.width + self.rightMargin + self.leftMargin,
self.dim.height + self.topMargin + self.bottomMargin)
self.setMinimumSize(
self.dim.width + self.rightMargin + self.leftMargin,
self.dim.height + self.topMargin + self.bottomMargin,
)
self.setSizePolicy(
QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding))
QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.MinimumExpanding,
QtWidgets.QSizePolicy.Policy.MinimumExpanding,
)
)
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, Chart.color.background)
pal.setColor(QtGui.QPalette.ColorRole.Window, Chart.color.background)
self.setPalette(pal)
self.setAutoFillBackground(True)
@ -196,13 +216,17 @@ class FrequencyChart(Chart):
def contextMenuEvent(self, event):
self.action_set_fixed_start.setText(
f"Start ({format_frequency_chart(self.minFrequency)})")
f"Start ({format_frequency_chart(self.minFrequency)})"
)
self.action_set_fixed_stop.setText(
f"Stop ({format_frequency_chart(self.maxFrequency)})")
f"Stop ({format_frequency_chart(self.maxFrequency)})"
)
self.action_set_fixed_minimum.setText(
f"Minimum ({self.minDisplayValue})")
f"Minimum ({self.minDisplayValue})"
)
self.action_set_fixed_maximum.setText(
f"Maximum ({self.maxDisplayValue})")
f"Maximum ({self.maxDisplayValue})"
)
if self.fixedSpan:
self.action_fixed_span.setChecked(True)
@ -214,7 +238,7 @@ class FrequencyChart(Chart):
else:
self.y_action_automatic.setChecked(True)
self.menu.exec_(event.globalPos())
self.menu.exec(event.globalPos())
def setFixedSpan(self, fixed_span: bool):
self.fixedSpan = fixed_span
@ -236,14 +260,16 @@ class FrequencyChart(Chart):
self.logarithmicY = logarithmic and self.logarithmicYAllowed()
self.update()
@staticmethod
def logarithmicYAllowed() -> bool:
def logarithmicYAllowed(self) -> bool:
return False
def setMinimumFrequency(self):
min_freq_str, selected = QtWidgets.QInputDialog.getText(
self, "Start frequency",
"Set start frequency", text=str(self.minFrequency))
self,
"Start frequency",
"Set start frequency",
text=str(self.minFrequency),
)
if not selected:
return
span = abs(self.maxFrequency - self.minFrequency)
@ -258,8 +284,11 @@ class FrequencyChart(Chart):
def setMaximumFrequency(self):
max_freq_str, selected = QtWidgets.QInputDialog.getText(
self, "Stop frequency",
"Set stop frequency", text=str(self.maxFrequency))
self,
"Stop frequency",
"Set stop frequency",
text=str(self.maxFrequency),
)
if not selected:
return
span = abs(self.maxFrequency - self.minFrequency)
@ -274,27 +303,33 @@ class FrequencyChart(Chart):
def setMinimumValue(self):
text, selected = QtWidgets.QInputDialog.getText(
self, "Minimum value",
self,
"Minimum value",
"Set minimum value",
text=format_y_axis(self.minDisplayValue, self.name_unit))
text=format_y_axis(self.minDisplayValue, self.name_unit),
)
if not selected:
return
text = text.replace("dB", "")
min_val = parse_value(text)
yspan = abs(self.maxDisplayValue - self.minDisplayValue)
self.minDisplayValue = min_val
if self.minDisplayValue >= self.maxDisplayValue:
self.maxDisplayValue = self.minDisplayValue + yspan
# TODO: negativ logarythmical scale
if self.logarithmicY and min_val <= 0:
self.minDisplayValue = 0.01
# if self.logarithmicY and min_val <= 0:
# self.minDisplayValue = 0.01
self.fixedValues = True
self.update()
def setMaximumValue(self):
text, selected = QtWidgets.QInputDialog.getText(
self, "Maximum value",
self,
"Maximum value",
"Set maximum value",
text=format_y_axis(self.maxDisplayValue, self.name_unit))
text=format_y_axis(self.maxDisplayValue, self.name_unit),
)
text = text.replace("dB", "")
if not selected:
return
max_val = parse_value(text)
@ -323,18 +358,22 @@ class FrequencyChart(Chart):
if self.logarithmicX:
span = math.log(self.fstop) - math.log(self.fstart)
return self.leftMargin + round(
self.dim.width * (math.log(d.freq) -
math.log(self.fstart)) / span)
self.dim.width
* (math.log(d.freq) - math.log(self.fstart))
/ span
)
return self.leftMargin + round(
self.dim.width * (d.freq - self.fstart) / span)
self.dim.width * (d.freq - self.fstart) / span
)
return math.floor(self.width() / 2)
def getYPosition(self, d: Datapoint) -> int:
try:
return (
self.topMargin +
round((self.maxValue - self.value_function(d) /
self.span * self.dim.height)))
return self.topMargin + round(
(self.maxValue - self.value_function(d))
/ self.span
* self.dim.height
)
except ValueError:
return self.topMargin
@ -365,7 +404,7 @@ class FrequencyChart(Chart):
step = span / self.dim.width
return round(self.fstart + absx * step)
def valueAtPosition(self, y) -> List[float]:
def valueAtPosition(self, y) -> list[float]:
"""
Returns the chart-specific value(s) at the specified Y-position
:param y: The Y position to calculate for.
@ -400,31 +439,34 @@ class FrequencyChart(Chart):
self.update()
def mouseMoveEvent(self, a0: QtGui.QMouseEvent):
if a0.buttons() == QtCore.Qt.RightButton:
if a0.buttons() == Qt.MouseButton.RightButton:
a0.ignore()
return
if a0.buttons() == QtCore.Qt.MiddleButton:
if a0.buttons() == Qt.MouseButton.MiddleButton:
# Drag the display
a0.accept()
if self.dragbox.move_x != -1 and self.dragbox.move_y != -1:
dx = self.dragbox.move_x - a0.x()
dy = self.dragbox.move_y - a0.y()
self.zoomTo(self.leftMargin + dx, self.topMargin + dy,
self.leftMargin + self.dim.width + dx,
self.topMargin + self.dim.height + dy)
dx = self.dragbox.move_x - a0.position().x()
dy = self.dragbox.move_y - a0.position().y()
self.zoomTo(
self.leftMargin + dx,
self.topMargin + dy,
self.leftMargin + self.dim.width + dx,
self.topMargin + self.dim.height + dy,
)
self.dragbox.move_x = a0.x()
self.dragbox.move_y = a0.y()
self.dragbox.move_x = a0.position().x()
self.dragbox.move_y = a0.position().y()
return
if a0.modifiers() == QtCore.Qt.ControlModifier:
if a0.modifiers() == Qt.KeyboardModifier.ControlModifier:
# Dragging a box
if not self.dragbox.state:
self.dragbox.pos_start = (a0.x(), a0.y())
self.dragbox.pos = (a0.x(), a0.y())
self.dragbox.pos_start = (a0.position().x(), a0.position().y())
self.dragbox.pos = (a0.position().x(), a0.position().y())
self.update()
a0.accept()
return
x = a0.x()
x = a0.position().x()
f = self.frequencyAtPosition(x)
if x == -1:
a0.ignore()
@ -433,11 +475,12 @@ class FrequencyChart(Chart):
m = self.getActiveMarker()
if m is not None:
m.setFrequency(str(f))
m.frequencyInput.setText(str(f))
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
self.dim.width = a0.size().width() - self.rightMargin - self.leftMargin
self.dim.height = a0.size().height() - self.bottomMargin - self.topMargin
self.dim.height = (
a0.size().height() - self.bottomMargin - self.topMargin
)
self.update()
def paintEvent(self, _: QtGui.QPaintEvent) -> None:
@ -450,24 +493,30 @@ class FrequencyChart(Chart):
qp.end()
def _data_oob(self, data: list[Datapoint]) -> bool:
return (data[0].freq > self.fstop or self.data[-1].freq < self.fstart)
return data[0].freq > self.fstop or self.data[-1].freq < self.fstart
def _check_frequency_boundaries(self, qp: QtGui.QPainter):
if (self.data and self._data_oob(self.data) and
(not self.reference or self._data_oob(self.reference))):
if (
self.data
and self._data_oob(self.data)
and (not self.reference or self._data_oob(self.reference))
):
# Data outside frequency range
qp.setBackgroundMode(QtCore.Qt.OpaqueMode)
qp.setBackgroundMode(Qt.BGMode.OpaqueMode)
qp.setBackground(Chart.color.background)
qp.setPen(Chart.color.text)
qp.drawText(self.leftMargin + self.dim.width // 2 - 70,
self.topMargin + self.dim.height // 2 - 20,
"Data outside frequency span")
qp.drawText(
self.leftMargin + int(self.dim.width // 2) - 70,
self.topMargin + int(self.dim.height // 2) - 20,
"Data outside frequency span",
)
def drawDragbog(self, qp: QtGui.QPainter):
dashed_pen = QtGui.QPen(Chart.color.foreground, 1, QtCore.Qt.DashLine)
dashed_pen = QtGui.QPen(Chart.color.foreground, 1, Qt.PenStyle.DashLine)
qp.setPen(dashed_pen)
top_left = QtCore.QPoint(
self.dragbox.pos_start[0], self.dragbox.pos_start[1])
self.dragbox.pos_start[0], self.dragbox.pos_start[1]
)
bottom_right = QtCore.QPoint(self.dragbox.pos[0], self.dragbox.pos[1])
rect = QtCore.QRect(top_left, bottom_right)
qp.drawRect(rect)
@ -479,10 +528,18 @@ class FrequencyChart(Chart):
headline += f" ({self.name_unit})"
qp.drawText(3, 15, headline)
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin, 20,
self.leftMargin, self.topMargin + self.dim.height + 5)
qp.drawLine(self.leftMargin - 5, self.topMargin + self.dim.height,
self.leftMargin + self.dim.width, self.topMargin + self.dim.height)
qp.drawLine(
self.leftMargin,
20,
self.leftMargin,
self.topMargin + self.dim.height + 5,
)
qp.drawLine(
self.leftMargin - 5,
self.topMargin + self.dim.height,
self.leftMargin + self.dim.width,
self.topMargin + self.dim.height,
)
self.drawTitle(qp)
def drawValues(self, qp: QtGui.QPainter):
@ -507,7 +564,9 @@ class FrequencyChart(Chart):
span = max_value - min_value
if span == 0:
logger.info(
"Span is zero for %s-Chart, setting to a small value.", self.name)
"Span is zero for %s-Chart, setting to a small value.",
self.name,
)
span = 1e-15
self.span = span
@ -515,30 +574,37 @@ class FrequencyChart(Chart):
fmt = Format(max_nr_digits=1)
for i in range(target_ticks):
val = min_value + (i / target_ticks) * span
y = self.topMargin + \
round((self.maxValue - val) / self.span * self.dim.height)
y = self.topMargin + round(
(self.maxValue - val) / self.span * self.dim.height
)
qp.setPen(Chart.color.text)
if val != min_value:
valstr = str(Value(val, fmt=fmt))
qp.drawText(3, y + 3, valstr)
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin - 5, y,
self.leftMargin + self.dim.width, y)
qp.drawLine(
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
)
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin - 5, self.topMargin,
self.leftMargin + self.dim.width, self.topMargin)
qp.drawLine(
self.leftMargin - 5,
self.topMargin,
self.leftMargin + self.dim.width,
self.topMargin,
)
qp.setPen(Chart.color.text)
qp.drawText(3, self.topMargin + 4, str(Value(max_value, fmt=fmt)))
qp.drawText(3, self.dim.height + self.topMargin,
str(Value(min_value, fmt=fmt)))
qp.drawText(
3, self.dim.height + self.topMargin, str(Value(min_value, fmt=fmt))
)
self.drawFrequencyTicks(qp)
self.drawData(qp, self.data, Chart.color.sweep)
self.drawData(qp, self.reference, Chart.color.reference)
self.drawMarkers(qp)
def _find_scaling(self) -> Tuple[float, float]:
def _find_scaling(self) -> tuple[float, float]:
min_value = self.minDisplayValue / 10e11
max_value = self.maxDisplayValue / 10e11
if self.fixedValues:
@ -562,30 +628,36 @@ class FrequencyChart(Chart):
ticks = math.floor(self.dim.width / 100)
# try to adapt format to span
if int(fspan / ticks / self.fstart * 10000) > 2:
if self.fstart == 0 or int(fspan / ticks / self.fstart * 10000) > 2:
my_format_frequency = format_frequency_chart
else:
my_format_frequency = format_frequency_chart_2
qp.drawText(self.leftMargin - 20,
self.topMargin + self.dim.height + 15,
my_format_frequency(self.fstart))
qp.drawText(
self.leftMargin - 20,
self.topMargin + self.dim.height + 15,
my_format_frequency(self.fstart),
)
for i in range(ticks):
x = self.leftMargin + round((i + 1) * self.dim.width / ticks)
if self.logarithmicX:
fspan = math.log(self.fstop) - math.log(self.fstart)
freq = round(
math.exp(((i + 1) * fspan / ticks) + math.log(self.fstart)))
math.exp(((i + 1) * fspan / ticks) + math.log(self.fstart))
)
else:
freq = round(fspan / ticks * (i + 1) + self.fstart)
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(x, self.topMargin, x,
self.topMargin + self.dim.height + 5)
qp.drawLine(
x, self.topMargin, x, self.topMargin + self.dim.height + 5
)
qp.setPen(Chart.color.text)
qp.drawText(x - 20,
self.topMargin + self.dim.height + 15,
my_format_frequency(freq))
qp.drawText(
x - 20,
self.topMargin + self.dim.height + 15,
my_format_frequency(freq),
)
def drawBands(self, qp, fstart, fstop):
qp.setBrush(self.bands.color)
@ -599,17 +671,24 @@ class FrequencyChart(Chart):
# don't draw if either band not in chart or completely in band
if start < fstart < fstop < end or end < fstart or start > fstop:
continue
x_start = max(self.leftMargin + 1,
self.getXPosition(Datapoint(start, 0, 0)))
x_stop = min(self.leftMargin + self.dim.width,
self.getXPosition(Datapoint(end, 0, 0)))
qp.drawRect(x_start,
self.topMargin,
x_stop - x_start,
self.dim.height)
x_start = max(
self.leftMargin + 1, self.getXPosition(Datapoint(start, 0, 0))
)
x_stop = min(
self.leftMargin + self.dim.width,
self.getXPosition(Datapoint(end, 0, 0)),
)
qp.drawRect(
x_start, self.topMargin, x_stop - x_start, self.dim.height
)
def drawData(self, qp: QtGui.QPainter, data: List[Datapoint],
color: QtGui.QColor, y_function=None):
def drawData(
self,
qp: QtGui.QPainter,
data: list[Datapoint],
color: QtGui.QColor,
y_function=None,
):
if y_function is None:
y_function = self.getYPosition
pen = QtGui.QPen(color)
@ -630,12 +709,13 @@ class FrequencyChart(Chart):
if prevy is None:
continue
qp.setPen(line_pen)
if self.isPlotable(x, y) and self.isPlotable(prevx, prevy):
qp.drawLine(x, y, prevx, prevy)
elif self.isPlotable(x, y) and not self.isPlotable(prevx, prevy):
new_x, new_y = self.getPlotable(x, y, prevx, prevy)
qp.drawLine(x, y, new_x, new_y)
elif not self.isPlotable(x, y) and self.isPlotable(prevx, prevy):
if self.isPlotable(x, y):
if self.isPlotable(prevx, prevy):
qp.drawLine(x, y, prevx, prevy)
else:
new_x, new_y = self.getPlotable(x, y, prevx, prevy)
qp.drawLine(x, y, new_x, new_y)
elif self.isPlotable(prevx, prevy):
new_x, new_y = self.getPlotable(prevx, prevy, x, y)
qp.drawLine(prevx, prevy, new_x, new_y)
qp.setPen(pen)
@ -652,13 +732,17 @@ class FrequencyChart(Chart):
x = self.getXPosition(data[m.location])
y = y_function(data[m.location])
if self.isPlotable(x, y):
self.drawMarker(x, y, qp, m.color,
self.markers.index(m) + 1)
self.drawMarker(
x, y, qp, m.color, self.markers.index(m) + 1
)
def isPlotable(self, x, y):
return y is not None and x is not None and \
self.leftMargin <= x <= self.leftMargin + self.dim.width and \
self.topMargin <= y <= self.topMargin + self.dim.height
return (
y is not None
and x is not None
and self.leftMargin <= x <= self.leftMargin + self.dim.width
and self.topMargin <= y <= self.topMargin + self.dim.height
)
def getPlotable(self, x, y, distantx, distanty):
p1 = np.array([x, y])
@ -669,8 +753,12 @@ class FrequencyChart(Chart):
p4 = np.array([self.leftMargin + self.dim.width, self.topMargin])
elif distanty > self.topMargin + self.dim.height:
p3 = np.array([self.leftMargin, self.topMargin + self.dim.height])
p4 = np.array([self.leftMargin + self.dim.width,
self.topMargin + self.dim.height])
p4 = np.array(
[
self.leftMargin + self.dim.width,
self.topMargin + self.dim.height,
]
)
else:
return x, y
@ -717,12 +805,14 @@ class FrequencyChart(Chart):
def keyPressEvent(self, a0: QtGui.QKeyEvent) -> None:
m = self.getActiveMarker()
if m is not None and a0.modifiers() == QtCore.Qt.NoModifier:
if a0.key() in [QtCore.Qt.Key_Down, QtCore.Qt.Key_Left]:
m.frequencyInput.keyPressEvent(QtGui.QKeyEvent(
a0.type(), QtCore.Qt.Key_Down, a0.modifiers()))
elif a0.key() in [QtCore.Qt.Key_Up, QtCore.Qt.Key_Right]:
m.frequencyInput.keyPressEvent(QtGui.QKeyEvent(
a0.type(), QtCore.Qt.Key_Up, a0.modifiers()))
if m is not None and a0.modifiers() == Qt.KeyboardModifier.NoModifier:
if a0.key() in [Qt.Key.Key_Down, Qt.Key.Key_Left]:
m.frequencyInput.keyPressEvent(
QtGui.QKeyEvent(a0.type(), Qt.Key.Key_Down, a0.modifiers())
)
elif a0.key() in [Qt.Key.Key_Up, Qt.Key.Key_Right]:
m.frequencyInput.keyPressEvent(
QtGui.QKeyEvent(a0.type(), Qt.Key.Key_Up, a0.modifiers())
)
else:
super().keyPressEvent(a0)

Wyświetl plik

@ -18,15 +18,15 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import math
import logging
from typing import List
import numpy as np
from PyQt5 import QtGui
from PyQt6 import QtGui
from NanoVNASaver.Charts.Chart import Chart
from NanoVNASaver.RFTools import Datapoint
from .Frequency import FrequencyChart
logger = logging.getLogger(__name__)
@ -73,7 +73,7 @@ class GroupDelayChart(FrequencyChart):
self.groupDelayReference = self.calc_data(self.reference)
self.update()
def calc_data(self, data: List[Datapoint]):
def calc_data(self, data: list[Datapoint]):
data_len = len(data)
if data_len <= 1:
return []
@ -88,8 +88,8 @@ class GroupDelayChart(FrequencyChart):
phase_change = unwrapped[-1] - unwrapped[-2]
freq_change = d.freq - data[-2].freq
else:
phase_change = unwrapped[i+1] - unwrapped[i-1]
freq_change = data[i+1].freq - data[i-1].freq
phase_change = unwrapped[i + 1] - unwrapped[i - 1]
freq_change = data[i + 1].freq - data[i - 1].freq
delay = (-phase_change / (freq_change * 360)) * 10e8
if not self.reflective:
delay /= 2
@ -124,21 +124,30 @@ class GroupDelayChart(FrequencyChart):
tickcount = math.floor(self.dim.height / 60)
for i in range(tickcount):
delay = min_delay + span * i / tickcount
y = self.topMargin + round((self.maxDelay - delay) / self.span * self.dim.height)
y = self.topMargin + round(
(self.maxDelay - delay) / self.span * self.dim.height
)
if delay not in {min_delay, max_delay}:
qp.setPen(QtGui.QPen(Chart.color.text))
# TODO use format class
digits = 0 if delay == 0 else max(
0, min(2, math.floor(3 - math.log10(abs(delay)))))
digits = (
0
if delay == 0
else max(0, min(2, math.floor(3 - math.log10(abs(delay)))))
)
delaystr = str(round(delay, digits if digits != 0 else None))
qp.drawText(3, y + 3, delaystr)
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.dim.width, y)
qp.drawLine(
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
)
qp.drawLine(self.leftMargin - 5,
self.topMargin,
self.leftMargin + self.dim.width,
self.topMargin)
qp.drawLine(
self.leftMargin - 5,
self.topMargin,
self.leftMargin + self.dim.width,
self.topMargin,
)
qp.setPen(Chart.color.text)
qp.drawText(3, self.topMargin + 5, str(max_delay))
qp.drawText(3, self.dim.height + self.topMargin, str(min_delay))
@ -151,15 +160,20 @@ class GroupDelayChart(FrequencyChart):
self.drawFrequencyTicks(qp)
self.draw_data(qp, Chart.color.sweep,
self.data, self.groupDelay)
self.draw_data(qp, Chart.color.reference,
self.reference, self.groupDelayReference)
self.draw_data(qp, Chart.color.sweep, self.data, self.groupDelay)
self.draw_data(
qp, Chart.color.reference, self.reference, self.groupDelayReference
)
self.drawMarkers(qp)
def draw_data(self, qp: QtGui.QPainter, color: QtGui.QColor,
data: List[Datapoint], delay: List[Datapoint]):
def draw_data(
self,
qp: QtGui.QPainter,
color: QtGui.QColor,
data: list[Datapoint],
delay: list[Datapoint],
):
pen = QtGui.QPen(color)
pen.setWidth(self.dim.point)
line_pen = QtGui.QPen(color)
@ -174,12 +188,13 @@ class GroupDelayChart(FrequencyChart):
prevx = self.getXPosition(data[i - 1])
prevy = self.getYPositionFromDelay(delay[i - 1])
qp.setPen(line_pen)
if self.isPlotable(x, y) and self.isPlotable(prevx, prevy):
qp.drawLine(x, y, prevx, prevy)
elif self.isPlotable(x, y) and not self.isPlotable(prevx, prevy):
new_x, new_y = self.getPlotable(x, y, prevx, prevy)
qp.drawLine(x, y, new_x, new_y)
elif not self.isPlotable(x, y) and self.isPlotable(prevx, prevy):
if self.isPlotable(x, y):
if self.isPlotable(prevx, prevy):
qp.drawLine(x, y, prevx, prevy)
else:
new_x, new_y = self.getPlotable(x, y, prevx, prevy)
qp.drawLine(x, y, new_x, new_y)
elif self.isPlotable(prevx, prevy):
new_x, new_y = self.getPlotable(prevx, prevy, x, y)
qp.drawLine(prevx, prevy, new_x, new_y)
qp.setPen(pen)
@ -195,10 +210,12 @@ class GroupDelayChart(FrequencyChart):
delay = 0
return self.getYPositionFromDelay(delay)
def getYPositionFromDelay(self, delay: float):
return self.topMargin + round((self.maxDelay - delay) / self.span * self.dim.height)
def getYPositionFromDelay(self, delay: float) -> int:
return self.topMargin + int(
(self.maxDelay - delay) / self.span * self.dim.height
)
def valueAtPosition(self, y) -> List[float]:
def valueAtPosition(self, y) -> list[float]:
absy = y - self.topMargin
val = -1 * ((absy / self.dim.height * self.span) - self.maxDelay)
return [val]

Wyświetl plik

@ -19,39 +19,31 @@
from dataclasses import dataclass
import math
import logging
from typing import List
from PyQt5 import QtGui
from PyQt6 import QtGui
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.Charts.Chart import Chart
from NanoVNASaver.Charts.Frequency import FrequencyChart
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.SITools import log_floor_125
logger = logging.getLogger(__name__)
@dataclass
class TickVal:
count: int
first: float
step: float
count: int = 0
first: float = 0.0
step: float = 0.0
def span2ticks(span: float, min_val: float) -> TickVal:
for spn, dbs in ((50.0, 10.0),
(20.0, 5.0),
(10.0, 2.0),
(5.0, 1.0),
(2.0, 0.5),
(1.0, 0.2),
(0.0, 0.1)):
if span >= spn:
count = math.floor(span / dbs)
first = math.ceil(min_val / dbs) * dbs
step = dbs
if first == min_val:
first += dbs
break
span = abs(span)
step = log_floor_125(span / 5)
count = math.floor(span / step)
first = math.ceil(min_val / step) * step
if first == min_val:
first += step
return TickVal(count, first, step)
@ -122,8 +114,12 @@ class LogMagChart(FrequencyChart):
self.draw_db_lines(qp, self.maxValue, self.minValue, ticks)
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin - 5, self.topMargin,
self.leftMargin + self.dim.width, self.topMargin)
qp.drawLine(
self.leftMargin - 5,
self.topMargin,
self.leftMargin + self.dim.width,
self.topMargin,
)
qp.setPen(Chart.color.text)
qp.drawText(3, self.topMargin + 4, f"{self.maxValue}")
qp.drawText(3, self.dim.height + self.topMargin, f"{self.minValue}")
@ -134,14 +130,17 @@ class LogMagChart(FrequencyChart):
for i in range(ticks.count):
db = ticks.first + i * ticks.step
y = self.topMargin + round(
(maxValue - db) / self.span * self.dim.height)
(maxValue - db) / self.span * self.dim.height
)
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin - 5, y,
self.leftMargin + self.dim.width, y)
qp.drawLine(
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
)
if db > minValue and db != maxValue:
qp.setPen(QtGui.QPen(Chart.color.text))
qp.drawText(3, y + 4,
f"{round(db, 1)}" if ticks.step < 1 else f"{db}")
qp.drawText(
3, y + 4, f"{round(db, 1)}" if ticks.step < 1 else f"{db}"
)
def draw_swr_markers(self, qp) -> None:
qp.setPen(Chart.color.swr)
@ -152,19 +151,20 @@ class LogMagChart(FrequencyChart):
if self.isInverted:
logMag = logMag * -1
y = self.topMargin + round(
(self.maxValue - logMag) / self.span * self.dim.height)
qp.drawLine(self.leftMargin, y,
self.leftMargin + self.dim.width, y)
(self.maxValue - logMag) / self.span * self.dim.height
)
qp.drawLine(self.leftMargin, y, self.leftMargin + self.dim.width, y)
qp.drawText(self.leftMargin + 3, y - 1, f"VSWR: {vswr}")
def getYPosition(self, d: Datapoint) -> int:
logMag = self.logMag(d)
if math.isinf(logMag):
return None
return self.topMargin + round(
(self.maxValue - logMag) / self.span * self.dim.height)
return self.topMargin
return self.topMargin + int(
(self.maxValue - logMag) / self.span * self.dim.height
)
def valueAtPosition(self, y) -> List[float]:
def valueAtPosition(self, y) -> list[float]:
absy = y - self.topMargin
val = -1 * ((absy / self.dim.height * self.span) - self.maxValue)
return [val]

Wyświetl plik

@ -18,13 +18,13 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import math
import logging
from typing import List
from PyQt5 import QtGui
from PyQt6 import QtGui
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.Charts.Chart import Chart
from NanoVNASaver.Charts.Frequency import FrequencyChart
logger = logging.getLogger(__name__)
@ -33,18 +33,15 @@ class MagnitudeChart(FrequencyChart):
super().__init__(name)
self.minDisplayValue = 0
self.maxDisplayValue = 1
self.fixedValues = True
self.y_action_fixed_span.setChecked(True)
self.y_action_automatic.setChecked(False)
self.minValue = 0
self.maxValue = 1
self.span = 1
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
if not self.data and not self.reference:
return
self._set_start_stop()
@ -54,71 +51,70 @@ class MagnitudeChart(FrequencyChart):
self.drawBands(qp, self.fstart, self.fstop)
if self.fixedValues:
maxValue = self.maxDisplayValue
minValue = self.minDisplayValue
self.maxValue = maxValue
self.minValue = minValue
max_value = self.maxDisplayValue
min_value = self.minDisplayValue
else:
# Find scaling
minValue = 100
maxValue = 0
min_value = 100
max_value = 0
for d in self.data:
mag = self.magnitude(d)
if mag > maxValue:
maxValue = mag
if mag < minValue:
minValue = mag
for d in self.reference: # Also check min/max for the reference sweep
max_value = max(max_value, mag)
min_value = min(min_value, mag)
# Also check min/max for the reference sweep
for d in self.reference:
if d.freq < self.fstart or d.freq > self.fstop:
continue
mag = self.magnitude(d)
if mag > maxValue:
maxValue = mag
if mag < minValue:
minValue = mag
max_value = max(max_value, mag)
min_value = min(min_value, mag)
min_value = 10 * math.floor(min_value / 10)
max_value = 10 * math.ceil(max_value / 10)
minValue = 10*math.floor(minValue/10)
self.minValue = minValue
maxValue = 10*math.ceil(maxValue/10)
self.maxValue = maxValue
self.maxValue = max_value
self.minValue = min_value
span = maxValue-minValue
if span == 0:
span = 0.01
self.span = span
target_ticks = math.floor(self.dim.height / 60)
self.span = (max_value - min_value) or 0.01
target_ticks = int(self.dim.height // 60)
for i in range(target_ticks):
val = minValue + i / target_ticks * span
y = self.topMargin + round((self.maxValue - val) / self.span * self.dim.height)
val = min_value + i / target_ticks * self.span
y = self.topMargin + int(
(self.maxValue - val) / self.span * self.dim.height
)
qp.setPen(Chart.color.text)
if val != minValue:
if val != min_value:
digits = max(0, min(2, math.floor(3 - math.log10(abs(val)))))
if digits == 0:
vswrstr = str(round(val))
else:
vswrstr = str(round(val, digits))
vswrstr = (
str(round(val)) if digits == 0 else str(round(val, digits))
)
qp.drawText(3, y + 3, vswrstr)
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.dim.width, y)
qp.drawLine(
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
)
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin - 5, self.topMargin,
self.leftMargin + self.dim.width, self.topMargin)
qp.drawLine(
self.leftMargin - 5,
self.topMargin,
self.leftMargin + self.dim.width,
self.topMargin,
)
qp.setPen(Chart.color.text)
qp.drawText(3, self.topMargin + 4, str(maxValue))
qp.drawText(3, self.dim.height+self.topMargin, str(minValue))
qp.drawText(3, self.topMargin + 4, str(max_value))
qp.drawText(3, self.dim.height + self.topMargin, str(min_value))
self.drawFrequencyTicks(qp)
qp.setPen(Chart.color.swr)
for vswr in self.swrMarkers:
if vswr <= 1:
continue
mag = (vswr-1)/(vswr+1)
y = self.topMargin + round((self.maxValue - mag) / self.span * self.dim.height)
mag = (vswr - 1) / (vswr + 1)
y = self.topMargin + int(
(self.maxValue - mag) / self.span * self.dim.height
)
qp.drawLine(self.leftMargin, y, self.leftMargin + self.dim.width, y)
qp.drawText(self.leftMargin + 3, y - 1, "VSWR: " + str(vswr))
qp.drawText(self.leftMargin + 3, y - 1, f"VSWR: {vswr}")
self.drawData(qp, self.data, Chart.color.sweep)
self.drawData(qp, self.reference, Chart.color.reference)
@ -126,9 +122,11 @@ class MagnitudeChart(FrequencyChart):
def getYPosition(self, d: Datapoint) -> int:
mag = self.magnitude(d)
return self.topMargin + round((self.maxValue - mag) / self.span * self.dim.height)
return self.topMargin + int(
(self.maxValue - mag) / self.span * self.dim.height
)
def valueAtPosition(self, y) -> List[float]:
def valueAtPosition(self, y) -> list[float]:
absy = y - self.topMargin
val = -1 * ((absy / self.dim.height * self.span) - self.maxValue)
return [val]

Wyświetl plik

@ -18,13 +18,11 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import math
import logging
from typing import List
from PyQt5 import QtGui
from PyQt6 import QtGui
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.SITools import (
Format, Value, round_ceil, round_floor)
from NanoVNASaver.SITools import Format, Value, round_ceil, round_floor
from NanoVNASaver.Charts.Chart import Chart
from NanoVNASaver.Charts.Frequency import FrequencyChart
from NanoVNASaver.Charts.LogMag import LogMagChart
@ -45,7 +43,7 @@ class MagnitudeZChart(FrequencyChart):
self.span = 1
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
if not self.data and not self.reference:
return
self._set_start_stop()
@ -56,8 +54,11 @@ class MagnitudeZChart(FrequencyChart):
if self.fixedValues:
self.maxValue = self.maxDisplayValue
self.minValue = max(
self.minDisplayValue, 0.01) if self.logarithmicY else self.minDisplayValue
self.minValue = (
max(self.minDisplayValue, 0.01)
if self.logarithmicY
else self.minDisplayValue
)
else:
# Find scaling
self.minValue = 100
@ -68,7 +69,8 @@ class MagnitudeZChart(FrequencyChart):
continue
self.maxValue = max(self.maxValue, mag)
self.minValue = min(self.minValue, mag)
for d in self.reference: # Also check min/max for the reference sweep
# Also check min/max for the reference sweep
for d in self.reference:
if d.freq < self.fstart or d.freq > self.fstop:
continue
mag = self.magnitude(d)
@ -85,20 +87,23 @@ class MagnitudeZChart(FrequencyChart):
self.span = (self.maxValue - self.minValue) or 0.01
# We want one horizontal tick per 50 pixels, at most
horizontal_ticks = math.floor(self.dim.height/50)
horizontal_ticks = int(self.dim.height / 50)
fmt = Format(max_nr_digits=4)
for i in range(horizontal_ticks):
y = self.topMargin + round(i * self.dim.height / horizontal_ticks)
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin - 5, y,
self.leftMargin + self.dim.width + 5, y)
qp.drawLine(
self.leftMargin - 5, y, self.leftMargin + self.dim.width + 5, y
)
qp.setPen(QtGui.QPen(Chart.color.text))
val = Value(self.valueAtPosition(y)[0], fmt=fmt)
qp.drawText(3, y + 4, str(val))
qp.drawText(3,
self.dim.height + self.topMargin,
str(Value(self.minValue, fmt=fmt)))
qp.drawText(
3,
self.dim.height + self.topMargin,
str(Value(self.minValue, fmt=fmt)),
)
self.drawFrequencyTicks(qp)
@ -113,16 +118,23 @@ class MagnitudeZChart(FrequencyChart):
if math.isfinite(mag):
if self.logarithmicY:
span = math.log(self.maxValue) - math.log(self.minValue)
return self.topMargin + round(
(math.log(self.maxValue) - math.log(mag)) / span * self.dim.height)
return self.topMargin + round((self.maxValue - mag) / self.span * self.dim.height)
return self.topMargin + int(
(math.log(self.maxValue) - math.log(mag))
/ span
* self.dim.height
)
return self.topMargin + int(
(self.maxValue - mag) / self.span * self.dim.height
)
return self.topMargin
def valueAtPosition(self, y) -> List[float]:
def valueAtPosition(self, y) -> list[float]:
absy = y - self.topMargin
if self.logarithmicY:
span = math.log(self.maxValue) - math.log(self.minValue)
val = math.exp(math.log(self.maxValue) - absy * span / self.dim.height)
val = math.exp(
math.log(self.maxValue) - absy * span / self.dim.height
)
else:
val = self.maxValue - (absy / self.dim.height * self.span)
return [val]

Wyświetl plik

@ -1,4 +1,3 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
@ -27,7 +26,6 @@ logger = logging.getLogger(__name__)
class MagnitudeZSeriesChart(MagnitudeZChart):
@staticmethod
def magnitude(p: Datapoint) -> float:
return abs(p.seriesImpedance())

Wyświetl plik

@ -1,4 +1,3 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
@ -26,7 +25,6 @@ logger = logging.getLogger(__name__)
class MagnitudeZShuntChart(MagnitudeZChart):
@staticmethod
def magnitude(p: Datapoint) -> float:
return abs(p.shuntImpedance())

Wyświetl plik

@ -18,15 +18,15 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import math
import logging
from typing import List
from PyQt5 import QtGui
from PyQt6 import QtGui
from NanoVNASaver.Marker import Marker
from NanoVNASaver.Marker.Widget import Marker
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.SITools import Format, Value
from NanoVNASaver.Charts.Chart import Chart
from NanoVNASaver.Charts.Frequency import FrequencyChart
logger = logging.getLogger(__name__)
@ -46,27 +46,36 @@ class PermeabilityChart(FrequencyChart):
self.minDisplayValue = -100
def logarithmicYAllowed(self) -> bool:
return True;
def copy(self):
new_chart: PermeabilityChart = super().copy()
return new_chart
return True
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(Chart.color.text))
qp.drawText(self.leftMargin + 5, 15, self.name + " (\N{MICRO SIGN}\N{OHM SIGN} / Hz)")
qp.drawText(
self.leftMargin + 5,
15,
self.name + " (\N{MICRO SIGN}\N{OHM SIGN} / Hz)",
)
qp.drawText(10, 15, "R")
qp.drawText(self.leftMargin + self.dim.width + 10, 15, "X")
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin, self.topMargin - 5,
self.leftMargin, self.topMargin + self.dim.height + 5)
qp.drawLine(self.leftMargin-5, self.topMargin + self.dim.height,
self.leftMargin + self.dim.width + 5, self.topMargin + self.dim.height)
qp.drawLine(
self.leftMargin,
self.topMargin - 5,
self.leftMargin,
self.topMargin + self.dim.height + 5,
)
qp.drawLine(
self.leftMargin - 5,
self.topMargin + self.dim.height,
self.leftMargin + self.dim.width + 5,
self.topMargin + self.dim.height,
)
self.drawTitle(qp)
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
if not self.data and not self.reference:
return
pen = QtGui.QPen(Chart.color.sweep)
pen.setWidth(self.dim.point)
line_pen = QtGui.QPen(Chart.color.sweep)
@ -90,61 +99,51 @@ class PermeabilityChart(FrequencyChart):
re, im = imp.real, imp.imag
re = re * 10e6 / d.freq
im = im * 10e6 / d.freq
if re > max_val:
max_val = re
if re < min_val:
min_val = re
if im > max_val:
max_val = im
if im < min_val:
min_val = im
for d in self.reference: # Also check min/max for the reference sweep
max_val = max(max_val, re)
max_val = max(max_val, im)
min_val = min(min_val, re)
min_val = min(min_val, im)
# Also check min/max for the reference sweep
for d in self.reference:
if d.freq < self.fstart or d.freq > self.fstop:
continue
imp = d.impedance()
re, im = imp.real, imp.imag
re = re * 10e6 / d.freq
im = im * 10e6 / d.freq
if re > max_val:
max_val = re
if re < min_val:
min_val = re
if im > max_val:
max_val = im
if im < min_val:
min_val = im
max_val = max(max_val, re)
max_val = max(max_val, im)
min_val = min(min_val, re)
min_val = min(min_val, im)
if self.logarithmicY:
min_val = max(0.01, min_val)
self.max = max_val
span = max_val - min_val
if span == 0:
span = 0.01
self.span = span
self.span = (max_val - min_val) or 0.01
# We want one horizontal tick per 50 pixels, at most
horizontal_ticks = math.floor(self.dim.height/50)
horizontal_ticks = math.floor(self.dim.height / 50)
fmt = Format(max_nr_digits=4)
for i in range(horizontal_ticks):
y = self.topMargin + round(i * self.dim.height / horizontal_ticks)
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin - 5, y,
self.leftMargin + self.dim.width + 5, y)
qp.drawLine(
self.leftMargin - 5, y, self.leftMargin + self.dim.width + 5, y
)
qp.setPen(QtGui.QPen(Chart.color.text))
val = Value(self.valueAtPosition(y)[0], fmt=fmt)
qp.drawText(3, y + 4, str(val))
qp.drawText(3,
self.dim.height + self.topMargin,
str(Value(min_val, fmt=fmt)))
qp.drawText(
3, self.dim.height + self.topMargin, str(Value(min_val, fmt=fmt))
)
self.drawFrequencyTicks(qp)
primary_pen = pen
secondary_pen = QtGui.QPen(Chart.color.sweep_secondary)
if len(self.data) > 0:
if self.data:
c = QtGui.QColor(Chart.color.sweep)
c.setAlpha(255)
pen = QtGui.QPen(c)
@ -156,17 +155,20 @@ class PermeabilityChart(FrequencyChart):
pen.setColor(c)
qp.setPen(pen)
qp.drawLine(
self.leftMargin + self.dim.width, 9,
self.leftMargin + self.dim.width + 5, 9)
self.leftMargin + self.dim.width,
9,
self.leftMargin + self.dim.width + 5,
9,
)
primary_pen.setWidth(self.dim.point)
secondary_pen.setWidth(self.dim.point)
line_pen.setWidth(self.dim.line)
for i in range(len(self.data)):
x = self.getXPosition(self.data[i])
y_re = self.getReYPosition(self.data[i])
y_im = self.getImYPosition(self.data[i])
for i, data in enumerate(self.data):
x = self.getXPosition(data)
y_re = self.getReYPosition(data)
y_im = self.getImYPosition(data)
qp.setPen(primary_pen)
if self.isPlotable(x, y_re):
qp.drawPoint(x, y_re)
@ -175,30 +177,36 @@ class PermeabilityChart(FrequencyChart):
qp.drawPoint(x, y_im)
if self.flag.draw_lines and i > 0:
prev_x = self.getXPosition(self.data[i - 1])
prev_y_re = self.getReYPosition(self.data[i-1])
prev_y_im = self.getImYPosition(self.data[i-1])
prev_y_re = self.getReYPosition(self.data[i - 1])
prev_y_im = self.getImYPosition(self.data[i - 1])
# Real part first
line_pen.setColor(Chart.color.sweep)
qp.setPen(line_pen)
if self.isPlotable(x, y_re) and self.isPlotable(prev_x, prev_y_re):
qp.drawLine(x, y_re, prev_x, prev_y_re)
elif self.isPlotable(x, y_re) and not self.isPlotable(prev_x, prev_y_re):
new_x, new_y = self.getPlotable(x, y_re, prev_x, prev_y_re)
qp.drawLine(x, y_re, new_x, new_y)
elif not self.isPlotable(x, y_re) and self.isPlotable(prev_x, prev_y_re):
if self.isPlotable(x, y_re):
if self.isPlotable(prev_x, prev_y_re):
qp.drawLine(x, y_re, prev_x, prev_y_re)
else:
new_x, new_y = self.getPlotable(
x, y_re, prev_x, prev_y_re
)
qp.drawLine(x, y_re, new_x, new_y)
elif self.isPlotable(prev_x, prev_y_re):
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
qp.drawLine(prev_x, prev_y_re, new_x, new_y)
# Imag part second
line_pen.setColor(Chart.color.sweep_secondary)
qp.setPen(line_pen)
if self.isPlotable(x, y_im) and self.isPlotable(prev_x, prev_y_im):
qp.drawLine(x, y_im, prev_x, prev_y_im)
elif self.isPlotable(x, y_im) and not self.isPlotable(prev_x, prev_y_im):
new_x, new_y = self.getPlotable(x, y_im, prev_x, prev_y_im)
qp.drawLine(x, y_im, new_x, new_y)
elif not self.isPlotable(x, y_im) and self.isPlotable(prev_x, prev_y_im):
if self.isPlotable(x, y_im):
if self.isPlotable(prev_x, prev_y_im):
qp.drawLine(x, y_im, prev_x, prev_y_im)
else:
new_x, new_y = self.getPlotable(
x, y_im, prev_x, prev_y_im
)
qp.drawLine(x, y_im, new_x, new_y)
elif self.isPlotable(prev_x, prev_y_im):
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
qp.drawLine(prev_x, prev_y_im, new_x, new_y)
@ -206,7 +214,7 @@ class PermeabilityChart(FrequencyChart):
line_pen.setColor(Chart.color.reference)
secondary_pen.setColor(Chart.color.reference_secondary)
qp.setPen(primary_pen)
if len(self.reference) > 0:
if self.reference:
c = QtGui.QColor(Chart.color.reference)
c.setAlpha(255)
pen = QtGui.QPen(c)
@ -218,15 +226,19 @@ class PermeabilityChart(FrequencyChart):
pen = QtGui.QPen(c)
pen.setWidth(2)
qp.setPen(pen)
qp.drawLine(self.leftMargin + self.dim.width, 14,
self.leftMargin + self.dim.width + 5, 14)
qp.drawLine(
self.leftMargin + self.dim.width,
14,
self.leftMargin + self.dim.width + 5,
14,
)
for i in range(len(self.reference)):
if self.reference[i].freq < self.fstart or self.reference[i].freq > self.fstop:
for i, reference in enumerate(self.reference):
if reference.freq < self.fstart or reference.freq > self.fstop:
continue
x = self.getXPosition(self.reference[i])
y_re = self.getReYPosition(self.reference[i])
y_im = self.getImYPosition(self.reference[i])
x = self.getXPosition(reference)
y_re = self.getReYPosition(reference)
y_im = self.getImYPosition(reference)
qp.setPen(primary_pen)
if self.isPlotable(x, y_re):
qp.drawPoint(x, y_re)
@ -235,30 +247,36 @@ class PermeabilityChart(FrequencyChart):
qp.drawPoint(x, y_im)
if self.flag.draw_lines and i > 0:
prev_x = self.getXPosition(self.reference[i - 1])
prev_y_re = self.getReYPosition(self.reference[i-1])
prev_y_im = self.getImYPosition(self.reference[i-1])
prev_y_re = self.getReYPosition(self.reference[i - 1])
prev_y_im = self.getImYPosition(self.reference[i - 1])
line_pen.setColor(Chart.color.reference)
qp.setPen(line_pen)
# Real part first
if self.isPlotable(x, y_re) and self.isPlotable(prev_x, prev_y_re):
qp.drawLine(x, y_re, prev_x, prev_y_re)
elif self.isPlotable(x, y_re) and not self.isPlotable(prev_x, prev_y_re):
new_x, new_y = self.getPlotable(x, y_re, prev_x, prev_y_re)
qp.drawLine(x, y_re, new_x, new_y)
elif not self.isPlotable(x, y_re) and self.isPlotable(prev_x, prev_y_re):
if self.isPlotable(x, y_re):
if self.isPlotable(prev_x, prev_y_re):
qp.drawLine(x, y_re, prev_x, prev_y_re)
else:
new_x, new_y = self.getPlotable(
x, y_re, prev_x, prev_y_re
)
qp.drawLine(x, y_re, new_x, new_y)
elif self.isPlotable(prev_x, prev_y_re):
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
qp.drawLine(prev_x, prev_y_re, new_x, new_y)
line_pen.setColor(Chart.color.reference_secondary)
qp.setPen(line_pen)
# Imag part second
if self.isPlotable(x, y_im) and self.isPlotable(prev_x, prev_y_im):
qp.drawLine(x, y_im, prev_x, prev_y_im)
elif self.isPlotable(x, y_im) and not self.isPlotable(prev_x, prev_y_im):
new_x, new_y = self.getPlotable(x, y_im, prev_x, prev_y_im)
qp.drawLine(x, y_im, new_x, new_y)
elif not self.isPlotable(x, y_im) and self.isPlotable(prev_x, prev_y_im):
if self.isPlotable(x, y_im):
if self.isPlotable(prev_x, prev_y_im):
qp.drawLine(x, y_im, prev_x, prev_y_im)
else:
new_x, new_y = self.getPlotable(
x, y_im, prev_x, prev_y_im
)
qp.drawLine(x, y_im, new_x, new_y)
elif self.isPlotable(prev_x, prev_y_im):
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
qp.drawLine(prev_x, prev_y_im, new_x, new_y)
@ -269,8 +287,8 @@ class PermeabilityChart(FrequencyChart):
y_re = self.getReYPosition(self.data[m.location])
y_im = self.getImYPosition(self.data[m.location])
self.drawMarker(x, y_re, qp, m.color, self.markers.index(m)+1)
self.drawMarker(x, y_im, qp, m.color, self.markers.index(m)+1)
self.drawMarker(x, y_re, qp, m.color, self.markers.index(m) + 1)
self.drawMarker(x, y_im, qp, m.color, self.markers.index(m) + 1)
def getImYPosition(self, d: Datapoint) -> int:
im = d.impedance().imag
@ -281,11 +299,13 @@ class PermeabilityChart(FrequencyChart):
span = math.log(self.max) - math.log(min_val)
else:
return -1
return self.topMargin + round(
(math.log(self.max) - math.log(im)) /
span * self.dim.height)
return self.topMargin + round(
(self.max - im) / self.span * self.dim.height)
return int(
self.topMargin
+ (math.log(self.max) - math.log(im)) / span * self.dim.height
)
return int(
self.topMargin + (self.max - im) / self.span * self.dim.height
)
def getReYPosition(self, d: Datapoint) -> int:
re = d.impedance().real
@ -296,13 +316,15 @@ class PermeabilityChart(FrequencyChart):
span = math.log(self.max) - math.log(min_val)
else:
return -1
return self.topMargin + round(
(math.log(self.max) - math.log(re)) /
span * self.dim.height)
return self.topMargin + round(
(self.max - re) / self.span * self.dim.height)
return int(
self.topMargin
+ (math.log(self.max) - math.log(re)) / span * self.dim.height
)
return int(
self.topMargin + (self.max - re) / self.span * self.dim.height
)
def valueAtPosition(self, y) -> List[float]:
def valueAtPosition(self, y) -> list[float]:
absy = y - self.topMargin
if self.logarithmicY:
min_val = self.max - self.span
@ -326,7 +348,7 @@ class PermeabilityChart(FrequencyChart):
myr = self.getReYPosition(self.data[m.location])
myi = self.getImYPosition(self.data[m.location])
dx = abs(x - mx)
dy = min(abs(y - myr), abs(y-myi))
dy = min(abs(y - myr), abs(y - myi))
distance = math.sqrt(dx**2 + dy**2)
if distance < shortest:
shortest = distance

Wyświetl plik

@ -19,10 +19,9 @@
import math
import logging
from typing import List
import numpy as np
from PyQt5 import QtWidgets, QtGui
from PyQt6.QtGui import QAction, QPainter, QPen
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.Charts.Chart import Chart
@ -47,9 +46,11 @@ class PhaseChart(FrequencyChart):
self.maxDisplayValue = 180
self.y_menu.addSeparator()
self.action_unwrap = QtWidgets.QAction("Unwrap")
self.action_unwrap = QAction("Unwrap")
self.action_unwrap.setCheckable(True)
self.action_unwrap.triggered.connect(lambda: self.setUnwrap(self.action_unwrap.isChecked()))
self.action_unwrap.triggered.connect(
lambda: self.setUnwrap(self.action_unwrap.isChecked())
)
self.y_menu.addAction(self.action_unwrap)
def copy(self):
@ -62,19 +63,13 @@ class PhaseChart(FrequencyChart):
self.unwrap = unwrap
self.update()
def drawValues(self, qp: QtGui.QPainter):
def drawValues(self, qp: QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
return
if self.unwrap:
rawData = []
for d in self.data:
rawData.append(d.phase)
rawReference = []
for d in self.reference:
rawReference.append(d.phase)
rawData = [d.phase for d in self.data]
rawReference = [d.phase for d in self.reference]
self.unwrappedData = np.degrees(np.unwrap(rawData))
self.unwrappedReference = np.degrees(np.unwrap(rawReference))
@ -102,27 +97,36 @@ class PhaseChart(FrequencyChart):
for i in range(tickcount):
angle = minAngle + span * i / tickcount
y = self.topMargin + round((self.maxAngle - angle) / self.span * self.dim.height)
if angle != minAngle and angle != maxAngle:
qp.setPen(QtGui.QPen(Chart.color.text))
y = self.topMargin + int(
(self.maxAngle - angle) / self.span * self.dim.height
)
if angle not in [minAngle, maxAngle]:
qp.setPen(QPen(Chart.color.text))
if angle != 0:
digits = max(0, min(2, math.floor(3 - math.log10(abs(angle)))))
if digits == 0:
anglestr = str(round(angle))
else:
anglestr = str(round(angle, digits))
digits = max(
0, min(2, math.floor(3 - math.log10(abs(angle))))
)
anglestr = (
str(round(angle))
if digits == 0
else str(round(angle, digits))
)
else:
anglestr = "0"
qp.drawText(3, y + 3, anglestr + "°")
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin - 5, y, self.leftMargin + self.dim.width, y)
qp.drawLine(self.leftMargin - 5,
self.topMargin,
self.leftMargin + self.dim.width,
self.topMargin)
qp.drawText(3, y + 3, f"{anglestr}°")
qp.setPen(QPen(Chart.color.foreground))
qp.drawLine(
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
)
qp.drawLine(
self.leftMargin - 5,
self.topMargin,
self.leftMargin + self.dim.width,
self.topMargin,
)
qp.setPen(Chart.color.text)
qp.drawText(3, self.topMargin + 5, str(maxAngle) + "°")
qp.drawText(3, self.dim.height + self.topMargin, str(minAngle) + "°")
qp.drawText(3, self.topMargin + 5, f"{maxAngle}°")
qp.drawText(3, self.dim.height + self.topMargin, f"{minAngle}°")
self._set_start_stop()
@ -136,18 +140,17 @@ class PhaseChart(FrequencyChart):
self.drawMarkers(qp)
def getYPosition(self, d: Datapoint) -> int:
if self.unwrap:
if d in self.data:
angle = self.unwrappedData[self.data.index(d)]
elif d in self.reference:
angle = self.unwrappedReference[self.reference.index(d)]
else:
angle = math.degrees(d.phase)
if self.unwrap and d in self.data:
angle = self.unwrappedData[self.data.index(d)]
elif self.unwrap and d in self.reference:
angle = self.unwrappedReference[self.reference.index(d)]
else:
angle = math.degrees(d.phase)
return self.topMargin + round((self.maxAngle - angle) / self.span * self.dim.height)
return self.topMargin + int(
(self.maxAngle - angle) / self.span * self.dim.height
)
def valueAtPosition(self, y) -> List[float]:
def valueAtPosition(self, y) -> list[float]:
absy = y - self.topMargin
val = -1 * ((absy / self.dim.height * self.span) - self.maxAngle)
return [val]

Wyświetl plik

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
from PyQt5 import QtGui, QtCore
from PyQt6 import QtGui, QtCore
from NanoVNASaver.Charts.Chart import Chart
from NanoVNASaver.Charts.Square import SquareChart
@ -39,16 +39,25 @@ class PolarChart(SquareChart):
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawEllipse(QtCore.QPoint(center_x, center_y), width_2, height_2)
qp.drawEllipse(QtCore.QPoint(center_x, center_y),
width_2 // 2, height_2 // 2)
qp.drawEllipse(
QtCore.QPoint(center_x, center_y), width_2 // 2, height_2 // 2
)
qp.drawLine(center_x - width_2, center_y,
center_x + width_2, center_y)
qp.drawLine(center_x, center_y - height_2,
center_x, center_y + height_2)
qp.drawLine(center_x + width_45, center_y + height_45,
center_x - width_45, center_y - height_45)
qp.drawLine(center_x + width_45, center_y - height_45,
center_x - width_45, center_y + height_45)
qp.drawLine(center_x - width_2, center_y, center_x + width_2, center_y)
qp.drawLine(
center_x, center_y - height_2, center_x, center_y + height_2
)
qp.drawLine(
center_x + width_45,
center_y + height_45,
center_x - width_45,
center_y - height_45,
)
qp.drawLine(
center_x + width_45,
center_y - height_45,
center_x - width_45,
center_y + height_45,
)
self.drawTitle(qp)

Wyświetl plik

@ -18,9 +18,8 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import math
import logging
from typing import List
from PyQt5 import QtGui
from PyQt6 import QtGui
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.Charts.Chart import Chart
@ -49,19 +48,17 @@ class QualityFactorChart(FrequencyChart):
# Make up some sensible scaling here
if self.fixedValues:
maxQ = self.maxDisplayValue
minQ = self.minDisplayValue
else:
minQ = 0
maxQ = 0
for d in self.data:
Q = d.qFactor()
if Q > maxQ:
maxQ = Q
maxQ = max(maxQ, Q)
scale = 0
if maxQ > 0:
scale = max(scale, math.floor(math.log10(maxQ)))
maxQ = math.ceil(maxQ / 10 ** scale) * 10 ** scale
self.minQ = minQ
maxQ = math.ceil(maxQ / 10**scale) * 10**scale
self.minQ = self.minDisplayValue
self.maxQ = maxQ
self.span = self.maxQ - self.minQ
if self.span == 0:
@ -71,28 +68,34 @@ class QualityFactorChart(FrequencyChart):
for i in range(tickcount):
q = self.minQ + i * self.span / tickcount
y = self.topMargin + round((self.maxQ - q) / self.span * self.dim.height)
y = self.topMargin + int(
(self.maxQ - q) / self.span * self.dim.height
)
q = round(q)
if q < 10:
q = round(q, 2)
elif q < 20:
if q < 20:
q = round(q, 1)
else:
q = round(q)
qp.setPen(QtGui.QPen(Chart.color.text))
qp.drawText(3, y+3, str(q))
qp.drawText(3, y + 3, str(q))
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin-5, y, self.leftMargin + self.dim.width, y)
qp.drawLine(self.leftMargin - 5,
self.topMargin,
self.leftMargin + self.dim.width, self.topMargin)
qp.drawLine(
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
)
qp.drawLine(
self.leftMargin - 5,
self.topMargin,
self.leftMargin + self.dim.width,
self.topMargin,
)
qp.setPen(Chart.color.text)
max_q = round(maxQ)
if maxQ < 10:
qstr = str(round(maxQ, 2))
max_q = round(maxQ, 2)
elif maxQ < 20:
qstr = str(round(maxQ, 1))
else:
qstr = str(round(maxQ))
qp.drawText(3, 35, qstr)
max_q = round(maxQ, 1)
qp.drawText(3, 35, f"{max_q}")
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
@ -119,9 +122,11 @@ class QualityFactorChart(FrequencyChart):
def getYPosition(self, d: Datapoint) -> int:
Q = d.qFactor()
return self.topMargin + round((self.maxQ - Q) / self.span * self.dim.height)
return self.topMargin + int(
(self.maxQ - Q) / self.span * self.dim.height
)
def valueAtPosition(self, y) -> List[float]:
def valueAtPosition(self, y) -> list[float]:
absy = y - self.topMargin
val = -1 * ((absy / self.dim.height * self.span) - self.maxQ)
return [val]

Wyświetl plik

@ -18,12 +18,11 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import math
import logging
from typing import List
from PyQt5 import QtWidgets, QtGui
from PyQt6 import QtWidgets, QtGui
from NanoVNASaver.Formatting import format_frequency_chart
from NanoVNASaver.Marker import Marker
from NanoVNASaver.Marker.Widget import Marker
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.SITools import Format, Value
@ -58,47 +57,22 @@ class RealImaginaryChart(FrequencyChart):
self.y_menu.clear()
self.y_action_automatic = QtWidgets.QAction("Automatic")
self.y_action_automatic = QtGui.QAction("Automatic")
self.y_action_automatic.setCheckable(True)
self.y_action_automatic.setChecked(True)
self.y_action_automatic.changed.connect(
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked()))
self.y_action_fixed_span = QtWidgets.QAction("Fixed span")
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked())
)
self.y_action_fixed_span = QtGui.QAction("Fixed span")
self.y_action_fixed_span.setCheckable(True)
self.y_action_fixed_span.changed.connect(
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked()))
mode_group = QtWidgets.QActionGroup(self)
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked())
)
mode_group = QtGui.QActionGroup(self)
mode_group.addAction(self.y_action_automatic)
mode_group.addAction(self.y_action_fixed_span)
self.y_menu.addAction(self.y_action_automatic)
self.y_menu.addAction(self.y_action_fixed_span)
self.y_menu.addSeparator()
self.action_set_fixed_maximum_real = QtWidgets.QAction(
f"Maximum R ({self.maxDisplayReal})")
self.action_set_fixed_maximum_real.triggered.connect(
self.setMaximumRealValue)
self.action_set_fixed_minimum_real = QtWidgets.QAction(
f"Minimum R ({self.minDisplayReal})")
self.action_set_fixed_minimum_real.triggered.connect(
self.setMinimumRealValue)
self.action_set_fixed_maximum_imag = QtWidgets.QAction(
f"Maximum jX ({self.maxDisplayImag})")
self.action_set_fixed_maximum_imag.triggered.connect(
self.setMaximumImagValue)
self.action_set_fixed_minimum_imag = QtWidgets.QAction(
f"Minimum jX ({self.minDisplayImag})")
self.action_set_fixed_minimum_imag.triggered.connect(
self.setMinimumImagValue)
self.y_menu.addAction(self.action_set_fixed_maximum_real)
self.y_menu.addAction(self.action_set_fixed_minimum_real)
self.y_menu.addSeparator()
self.y_menu.addAction(self.action_set_fixed_maximum_imag)
self.y_menu.addAction(self.action_set_fixed_minimum_imag)
def copy(self):
new_chart: RealImaginaryChart = super().copy()
@ -109,26 +83,10 @@ class RealImaginaryChart(FrequencyChart):
new_chart.minDisplayImag = self.minDisplayImag
return new_chart
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(Chart.color.text))
qp.drawText(self.leftMargin + 5, 15,
f"{self.name} (\N{OHM SIGN})")
qp.drawText(10, 15, "R")
qp.drawText(self.leftMargin + self.dim.width + 10, 15, "X")
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin,
self.topMargin - 5,
self.leftMargin,
self.topMargin + self.dim.height + 5)
qp.drawLine(self.leftMargin-5,
self.topMargin + self.dim.height,
self.leftMargin + self.dim.width + 5,
self.topMargin + self.dim.height)
self.drawTitle(qp)
def drawValues(self, qp: QtGui.QPainter):
if len(self.data) == 0 and len(self.reference) == 0:
if not self.data and not self.reference:
return
pen = QtGui.QPen(Chart.color.sweep)
pen.setWidth(self.dim.point)
line_pen = QtGui.QPen(Chart.color.sweep)
@ -142,68 +100,7 @@ class RealImaginaryChart(FrequencyChart):
if self.bands.enabled:
self.drawBands(qp, self.fstart, self.fstop)
# Find scaling
if self.fixedValues:
min_real = self.minDisplayReal
max_real = self.maxDisplayReal
min_imag = self.minDisplayImag
max_imag = self.maxDisplayImag
else:
min_real = 1000
min_imag = 1000
max_real = 0
max_imag = -1000
for d in self.data:
imp = self.impedance(d)
re, im = imp.real, imp.imag
if math.isinf(re): # Avoid infinite scales
continue
max_real = max(max_real, re)
min_real = min(min_real, re)
max_imag = max(max_imag, im)
min_imag = min(min_imag, im)
for d in self.reference: # Also check min/max for the reference sweep
if d.freq < self.fstart or d.freq > self.fstop:
continue
imp = self.impedance(d)
re, im = imp.real, imp.imag
if math.isinf(re): # Avoid infinite scales
continue
max_real = max(max_real, re)
min_real = min(min_real, re)
max_imag = max(max_imag, im)
min_imag = min(min_imag, im)
# Always have at least 8 numbered horizontal lines
max_real = math.ceil(max_real)
min_real = math.floor(min_real)
max_imag = math.ceil(max_imag)
min_imag = math.floor(min_imag)
if max_imag - min_imag < 8:
missing = 8 - (max_imag - min_imag)
max_imag += math.ceil(missing / 2)
min_imag -= math.floor(missing / 2)
if 0 > max_imag > -2:
max_imag = 0
if 0 < min_imag < 2:
min_imag = 0
if (max_imag - min_imag) > 8 and min_imag < 0 < max_imag:
# We should show a "0" line for the reactive part
span = max_imag - min_imag
step_size = span / 8
if max_imag < step_size:
# The 0 line is the first step after the top. Scale accordingly.
max_imag = -min_imag/7
elif -min_imag < step_size:
# The 0 line is the last step before the bottom. Scale accordingly.
min_imag = -max_imag/7
else:
# Scale max_imag to be a whole factor of min_imag
num_min = math.floor(min_imag/step_size * -1)
num_max = 8 - num_min
max_imag = num_max * (min_imag / num_min) * -1
min_real, max_real, min_imag, max_imag = self.find_scaling()
self.max_real = max_real
self.max_imag = max_imag
@ -211,35 +108,23 @@ class RealImaginaryChart(FrequencyChart):
self.span_real = (max_real - min_real) or 0.01
self.span_imag = (max_imag - min_imag) or 0.01
# We want one horizontal tick per 50 pixels, at most
horizontal_ticks = self.dim.height // 50
self.drawHorizontalTicks(qp)
fmt = Format(max_nr_digits=3)
for i in range(horizontal_ticks):
y = self.topMargin + i * self.dim.height // horizontal_ticks
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(self.leftMargin - 5, y,
self.leftMargin + self.dim.width + 5, y)
qp.setPen(QtGui.QPen(Chart.color.text))
re = max_real - i * self.span_real / horizontal_ticks
im = max_imag - i * self.span_imag / horizontal_ticks
qp.drawText(3, y + 4, f"{Value(re, fmt=fmt)}")
qp.drawText(
self.leftMargin + self.dim.width + 8,
y + 4,
f"{Value(im, fmt=fmt)}")
qp.drawText(3, self.dim.height + self.topMargin,
str(Value(min_real, fmt=fmt)))
qp.drawText(self.leftMargin + self.dim.width + 8,
self.dim.height + self.topMargin,
str(Value(min_imag, fmt=fmt)))
qp.drawText(
3, self.dim.height + self.topMargin, str(Value(min_real, fmt=fmt))
)
qp.drawText(
self.leftMargin + self.dim.width + 8,
self.dim.height + self.topMargin,
str(Value(min_imag, fmt=fmt)),
)
self.drawFrequencyTicks(qp)
primary_pen = pen
secondary_pen = QtGui.QPen(Chart.color.sweep_secondary)
if len(self.data) > 0:
if self.data:
c = QtGui.QColor(Chart.color.sweep)
c.setAlpha(255)
pen = QtGui.QPen(c)
@ -250,17 +135,21 @@ class RealImaginaryChart(FrequencyChart):
c.setAlpha(255)
pen.setColor(c)
qp.setPen(pen)
qp.drawLine(self.leftMargin + self.dim.width, 9,
self.leftMargin + self.dim.width + 5, 9)
qp.drawLine(
self.leftMargin + self.dim.width,
9,
self.leftMargin + self.dim.width + 5,
9,
)
primary_pen.setWidth(self.dim.point)
secondary_pen.setWidth(self.dim.point)
line_pen.setWidth(self.dim.line)
for i in range(len(self.data)):
x = self.getXPosition(self.data[i])
y_re = self.getReYPosition(self.data[i])
y_im = self.getImYPosition(self.data[i])
for i, data in enumerate(self.data):
x = self.getXPosition(data)
y_re = self.getReYPosition(data)
y_im = self.getImYPosition(data)
qp.setPen(primary_pen)
if self.isPlotable(x, y_re):
qp.drawPoint(x, y_re)
@ -269,30 +158,36 @@ class RealImaginaryChart(FrequencyChart):
qp.drawPoint(x, y_im)
if self.flag.draw_lines and i > 0:
prev_x = self.getXPosition(self.data[i - 1])
prev_y_re = self.getReYPosition(self.data[i-1])
prev_y_im = self.getImYPosition(self.data[i-1])
prev_y_re = self.getReYPosition(self.data[i - 1])
prev_y_im = self.getImYPosition(self.data[i - 1])
# Real part first
line_pen.setColor(Chart.color.sweep)
qp.setPen(line_pen)
if self.isPlotable(x, y_re) and self.isPlotable(prev_x, prev_y_re):
qp.drawLine(x, y_re, prev_x, prev_y_re)
elif self.isPlotable(x, y_re) and not self.isPlotable(prev_x, prev_y_re):
new_x, new_y = self.getPlotable(x, y_re, prev_x, prev_y_re)
qp.drawLine(x, y_re, new_x, new_y)
elif not self.isPlotable(x, y_re) and self.isPlotable(prev_x, prev_y_re):
if self.isPlotable(x, y_re):
if self.isPlotable(prev_x, prev_y_re):
qp.drawLine(x, y_re, prev_x, prev_y_re)
else:
new_x, new_y = self.getPlotable(
x, y_re, prev_x, prev_y_re
)
qp.drawLine(x, y_re, new_x, new_y)
elif self.isPlotable(prev_x, prev_y_re):
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
qp.drawLine(prev_x, prev_y_re, new_x, new_y)
# Imag part second
line_pen.setColor(Chart.color.sweep_secondary)
qp.setPen(line_pen)
if self.isPlotable(x, y_im) and self.isPlotable(prev_x, prev_y_im):
qp.drawLine(x, y_im, prev_x, prev_y_im)
elif self.isPlotable(x, y_im) and not self.isPlotable(prev_x, prev_y_im):
new_x, new_y = self.getPlotable(x, y_im, prev_x, prev_y_im)
qp.drawLine(x, y_im, new_x, new_y)
elif not self.isPlotable(x, y_im) and self.isPlotable(prev_x, prev_y_im):
if self.isPlotable(x, y_im):
if self.isPlotable(prev_x, prev_y_im):
qp.drawLine(x, y_im, prev_x, prev_y_im)
else:
new_x, new_y = self.getPlotable(
x, y_im, prev_x, prev_y_im
)
qp.drawLine(x, y_im, new_x, new_y)
elif self.isPlotable(prev_x, prev_y_im):
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
qp.drawLine(prev_x, prev_y_im, new_x, new_y)
@ -300,7 +195,7 @@ class RealImaginaryChart(FrequencyChart):
line_pen.setColor(Chart.color.reference)
secondary_pen.setColor(Chart.color.reference_secondary)
qp.setPen(primary_pen)
if len(self.reference) > 0:
if self.reference:
c = QtGui.QColor(Chart.color.reference)
c.setAlpha(255)
pen = QtGui.QPen(c)
@ -312,15 +207,19 @@ class RealImaginaryChart(FrequencyChart):
pen = QtGui.QPen(c)
pen.setWidth(2)
qp.setPen(pen)
qp.drawLine(self.leftMargin + self.dim.width, 14,
self.leftMargin + self.dim.width + 5, 14)
qp.drawLine(
self.leftMargin + self.dim.width,
14,
self.leftMargin + self.dim.width + 5,
14,
)
for i in range(len(self.reference)):
if self.reference[i].freq < self.fstart or self.reference[i].freq > self.fstop:
for i, reference in enumerate(self.reference):
if reference.freq < self.fstart or reference.freq > self.fstop:
continue
x = self.getXPosition(self.reference[i])
y_re = self.getReYPosition(self.reference[i])
y_im = self.getImYPosition(self.reference[i])
x = self.getXPosition(reference)
y_re = self.getReYPosition(reference)
y_im = self.getImYPosition(reference)
qp.setPen(primary_pen)
if self.isPlotable(x, y_re):
qp.drawPoint(x, y_re)
@ -329,30 +228,36 @@ class RealImaginaryChart(FrequencyChart):
qp.drawPoint(x, y_im)
if self.flag.draw_lines and i > 0:
prev_x = self.getXPosition(self.reference[i - 1])
prev_y_re = self.getReYPosition(self.reference[i-1])
prev_y_im = self.getImYPosition(self.reference[i-1])
prev_y_re = self.getReYPosition(self.reference[i - 1])
prev_y_im = self.getImYPosition(self.reference[i - 1])
line_pen.setColor(Chart.color.reference)
qp.setPen(line_pen)
# Real part first
if self.isPlotable(x, y_re) and self.isPlotable(prev_x, prev_y_re):
qp.drawLine(x, y_re, prev_x, prev_y_re)
elif self.isPlotable(x, y_re) and not self.isPlotable(prev_x, prev_y_re):
new_x, new_y = self.getPlotable(x, y_re, prev_x, prev_y_re)
qp.drawLine(x, y_re, new_x, new_y)
elif not self.isPlotable(x, y_re) and self.isPlotable(prev_x, prev_y_re):
if self.isPlotable(x, y_re):
if self.isPlotable(prev_x, prev_y_re):
qp.drawLine(x, y_re, prev_x, prev_y_re)
else:
new_x, new_y = self.getPlotable(
x, y_re, prev_x, prev_y_re
)
qp.drawLine(x, y_re, new_x, new_y)
elif self.isPlotable(prev_x, prev_y_re):
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
qp.drawLine(prev_x, prev_y_re, new_x, new_y)
line_pen.setColor(Chart.color.reference_secondary)
qp.setPen(line_pen)
# Imag part second
if self.isPlotable(x, y_im) and self.isPlotable(prev_x, prev_y_im):
qp.drawLine(x, y_im, prev_x, prev_y_im)
elif self.isPlotable(x, y_im) and not self.isPlotable(prev_x, prev_y_im):
new_x, new_y = self.getPlotable(x, y_im, prev_x, prev_y_im)
qp.drawLine(x, y_im, new_x, new_y)
elif not self.isPlotable(x, y_im) and self.isPlotable(prev_x, prev_y_im):
if self.isPlotable(x, y_im):
if self.isPlotable(prev_x, prev_y_im):
qp.drawLine(x, y_im, prev_x, prev_y_im)
else:
new_x, new_y = self.getPlotable(
x, y_im, prev_x, prev_y_im
)
qp.drawLine(x, y_im, new_x, new_y)
elif self.isPlotable(prev_x, prev_y_im):
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
qp.drawLine(prev_x, prev_y_im, new_x, new_y)
@ -363,25 +268,122 @@ class RealImaginaryChart(FrequencyChart):
y_re = self.getReYPosition(self.data[m.location])
y_im = self.getImYPosition(self.data[m.location])
self.drawMarker(x, y_re, qp, m.color, self.markers.index(m)+1)
self.drawMarker(x, y_im, qp, m.color, self.markers.index(m)+1)
self.drawMarker(x, y_re, qp, m.color, self.markers.index(m) + 1)
self.drawMarker(x, y_im, qp, m.color, self.markers.index(m) + 1)
def drawHorizontalTicks(self, qp):
# We want one horizontal tick per 50 pixels, at most
fmt = Format(max_nr_digits=3)
horizontal_ticks = self.dim.height // 50
for i in range(horizontal_ticks):
y = self.topMargin + i * self.dim.height // horizontal_ticks
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(
self.leftMargin - 5, y, self.leftMargin + self.dim.width + 5, y
)
qp.setPen(QtGui.QPen(Chart.color.text))
re = self.max_real - i * self.span_real / horizontal_ticks
im = self.max_imag - i * self.span_imag / horizontal_ticks
qp.drawText(3, y + 4, f"{Value(re, fmt=fmt)}")
qp.drawText(
self.leftMargin + self.dim.width + 8,
y + 4,
f"{Value(im, fmt=fmt)}",
)
def find_scaling(self):
# Find scaling
if self.fixedValues:
min_real = self.minDisplayReal
max_real = self.maxDisplayReal
min_imag = self.minDisplayImag
max_imag = self.maxDisplayImag
return min_real, max_real, min_imag, max_imag
min_real = 1000
min_imag = 1000
max_real = 0
max_imag = -1000
for d in self.data:
imp = self.value(d)
re, im = imp.real, imp.imag
if math.isinf(re): # Avoid infinite scales
continue
max_real = max(max_real, re)
min_real = min(min_real, re)
max_imag = max(max_imag, im)
min_imag = min(min_imag, im)
# Also check min/max for the reference sweep
for d in self.reference:
if d.freq < self.fstart or d.freq > self.fstop:
continue
imp = self.value(d)
re, im = imp.real, imp.imag
if math.isinf(re): # Avoid infinite scales
continue
max_real = max(max_real, re)
min_real = min(min_real, re)
max_imag = max(max_imag, im)
min_imag = min(min_imag, im)
# Always have at least 8 numbered horizontal lines
max_real = math.ceil(max_real)
min_real = math.floor(min_real)
max_imag = math.ceil(max_imag)
min_imag = math.floor(min_imag)
min_imag, max_imag = self.imag_scaling_constraints(min_imag, max_imag)
return min_real, max_real, min_imag, max_imag
def imag_scaling_constraints(self, min_imag, max_imag):
if max_imag - min_imag < 8:
missing = 8 - (max_imag - min_imag)
max_imag += math.ceil(missing / 2)
min_imag -= math.floor(missing / 2)
if 0 > max_imag > -2:
max_imag = 0
if 0 < min_imag < 2:
min_imag = 0
if (max_imag - min_imag) > 8 and min_imag < 0 < max_imag:
# We should show a "0" line for the reactive part
span = max_imag - min_imag
step_size = span / 8
if max_imag < step_size:
# The 0 line is the first step after the top.
# Scale accordingly.
max_imag = -min_imag / 7
elif -min_imag < step_size:
# The 0 line is the last step before the bottom.
# Scale accordingly.
min_imag = -max_imag / 7
else:
# Scale max_imag to be a whole factor of min_imag
num_min = math.floor(min_imag / step_size * -1)
num_max = 8 - num_min
max_imag = num_max * (min_imag / num_min) * -1
return min_imag, max_imag
def getImYPosition(self, d: Datapoint) -> int:
im = self.impedance(d).imag
return (self.topMargin + int(self.max_imag - im) // self.span_imag
* self.dim.height)
im = self.value(d).imag
return int(
self.topMargin
+ (self.max_imag - im) / self.span_imag * self.dim.height
)
def getReYPosition(self, d: Datapoint) -> int:
re = self.impedance(d).real
return (self.topMargin + int(self.max_real - re) // self.span_real
* self.dim.height if math.isfinite(re) else self.topMargin)
re = self.value(d).real
return int(
self.topMargin
+ (self.max_real - re) / self.span_real * self.dim.height
if math.isfinite(re)
else self.topMargin
)
def valueAtPosition(self, y) -> List[float]:
def valueAtPosition(self, y) -> list[float]:
absy = y - self.topMargin
valRe = -1 * ((absy / self.dim.height *
self.span_real) - self.max_real)
valIm = -1 * ((absy / self.dim.height *
self.span_imag) - self.max_imag)
valRe = -1 * ((absy / self.dim.height * self.span_real) - self.max_real)
valIm = -1 * ((absy / self.dim.height * self.span_imag) - self.max_imag)
return [valRe, valIm]
def zoomTo(self, x1, y1, x2, y2):
@ -405,17 +407,17 @@ class RealImaginaryChart(FrequencyChart):
self.update()
def getNearestMarker(self, x, y) -> Marker:
if len(self.data) == 0:
def getNearestMarker(self, x, y) -> Marker | None:
if not self.data:
return None
shortest = 10**6
shortest = 10e6
nearest = None
for m in self.markers:
mx, _ = self.getPosition(self.data[m.location])
myr = self.getReYPosition(self.data[m.location])
myi = self.getImYPosition(self.data[m.location])
dx = abs(x - mx)
dy = min(abs(y - myr), abs(y-myi))
dy = min(abs(y - myr), abs(y - myi))
distance = math.sqrt(dx**2 + dy**2)
if distance < shortest:
shortest = distance
@ -424,9 +426,12 @@ class RealImaginaryChart(FrequencyChart):
def setMinimumRealValue(self):
min_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Minimum real value",
"Set minimum real value", value=self.minDisplayReal,
decimals=2)
self,
"Minimum real value",
"Set minimum real value",
value=self.minDisplayReal,
decimals=2,
)
if not selected:
return
if not (self.fixedValues and min_val >= self.maxDisplayReal):
@ -436,9 +441,12 @@ class RealImaginaryChart(FrequencyChart):
def setMaximumRealValue(self):
max_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Maximum real value",
"Set maximum real value", value=self.maxDisplayReal,
decimals=2)
self,
"Maximum real value",
"Set maximum real value",
value=self.maxDisplayReal,
decimals=2,
)
if not selected:
return
if not (self.fixedValues and max_val <= self.minDisplayReal):
@ -448,9 +456,12 @@ class RealImaginaryChart(FrequencyChart):
def setMinimumImagValue(self):
min_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Minimum imaginary value",
"Set minimum imaginary value", value=self.minDisplayImag,
decimals=2)
self,
"Minimum imaginary value",
"Set minimum imaginary value",
value=self.minDisplayImag,
decimals=2,
)
if not selected:
return
if not (self.fixedValues and min_val >= self.maxDisplayImag):
@ -460,9 +471,12 @@ class RealImaginaryChart(FrequencyChart):
def setMaximumImagValue(self):
max_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Maximum imaginary value",
"Set maximum imaginary value", value=self.maxDisplayImag,
decimals=2)
self,
"Maximum imaginary value",
"Set maximum imaginary value",
value=self.maxDisplayImag,
decimals=2,
)
if not selected:
return
if not (self.fixedValues and max_val <= self.minDisplayImag):
@ -472,9 +486,10 @@ class RealImaginaryChart(FrequencyChart):
def setFixedValues(self, fixed_values: bool):
self.fixedValues = fixed_values
if (fixed_values and
(self.minDisplayReal >= self.maxDisplayReal or
self.minDisplayImag > self.maxDisplayImag)):
if fixed_values and (
self.minDisplayReal >= self.maxDisplayReal
or self.minDisplayImag > self.maxDisplayImag
):
self.fixedValues = False
self.y_action_automatic.setChecked(True)
self.y_action_fixed_span.setChecked(False)
@ -482,18 +497,24 @@ class RealImaginaryChart(FrequencyChart):
def contextMenuEvent(self, event):
self.action_set_fixed_start.setText(
f"Start ({format_frequency_chart(self.minFrequency)})")
f"Start ({format_frequency_chart(self.minFrequency)})"
)
self.action_set_fixed_stop.setText(
f"Stop ({format_frequency_chart(self.maxFrequency)})")
f"Stop ({format_frequency_chart(self.maxFrequency)})"
)
self.action_set_fixed_minimum_real.setText(
f"Minimum R ({self.minDisplayReal})")
f"Minimum R ({self.minDisplayReal})"
)
self.action_set_fixed_maximum_real.setText(
f"Maximum R ({self.maxDisplayReal})")
f"Maximum R ({self.maxDisplayReal})"
)
self.action_set_fixed_minimum_imag.setText(
f"Minimum jX ({self.minDisplayImag})")
f"Minimum jX ({self.minDisplayImag})"
)
self.action_set_fixed_maximum_imag.setText(
f"Maximum jX ({self.maxDisplayImag})")
self.menu.exec_(event.globalPos())
f"Maximum jX ({self.maxDisplayImag})"
)
self.menu.exec(event.globalPos())
def impedance(self, p: Datapoint) -> complex:
return p.impedance()
def value(self, p: Datapoint) -> complex:
raise NotImplementedError()

Wyświetl plik

@ -0,0 +1,201 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import math
import numpy as np
import logging
from scipy.constants import mu_0
from PyQt6 import QtWidgets, QtGui
from NanoVNASaver.Formatting import format_frequency_chart
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.Charts.Chart import Chart
from NanoVNASaver.Charts.RI import RealImaginaryChart
logger = logging.getLogger(__name__)
MU = "\N{GREEK SMALL LETTER MU}"
class RealImaginaryMuChart(RealImaginaryChart):
def __init__(self, name=""):
super().__init__(name)
self.y_menu.addSeparator()
self.action_set_fixed_maximum_real = QtGui.QAction(
f"Maximum {MU}' ({self.maxDisplayReal})"
)
self.action_set_fixed_maximum_real.triggered.connect(
self.setMaximumRealValue
)
self.action_set_fixed_minimum_real = QtGui.QAction(
f"Minimum {MU}' ({self.minDisplayReal})"
)
self.action_set_fixed_minimum_real.triggered.connect(
self.setMinimumRealValue
)
self.action_set_fixed_maximum_imag = QtGui.QAction(
f"Maximum {MU}'' ({self.maxDisplayImag})"
)
self.action_set_fixed_maximum_imag.triggered.connect(
self.setMaximumImagValue
)
self.action_set_fixed_minimum_imag = QtGui.QAction(
f"Minimum {MU}'' ({self.minDisplayImag})"
)
self.action_set_fixed_minimum_imag.triggered.connect(
self.setMinimumImagValue
)
self.y_menu.addAction(self.action_set_fixed_maximum_real)
self.y_menu.addAction(self.action_set_fixed_minimum_real)
self.y_menu.addSeparator()
self.y_menu.addAction(self.action_set_fixed_maximum_imag)
self.y_menu.addAction(self.action_set_fixed_minimum_imag)
# Manage core parameters
# TODO pick some sane default values?
self.coreLength = 1.0
self.coreArea = 1.0
self.coreWindings = 1
self.menu.addSeparator()
self.action_set_core_length = QtGui.QAction("Core effective length")
self.action_set_core_length.triggered.connect(self.setCoreLength)
self.action_set_core_area = QtGui.QAction("Core area")
self.action_set_core_area.triggered.connect(self.setCoreArea)
self.action_set_core_windings = QtGui.QAction("Core number of windings")
self.action_set_core_windings.triggered.connect(self.setCoreWindings)
self.menu.addAction(self.action_set_core_length)
self.menu.addAction(self.action_set_core_area)
self.menu.addAction(self.action_set_core_windings)
def copy(self):
new_chart: RealImaginaryMuChart = super().copy()
new_chart.coreLength = self.coreLength
new_chart.coreArea = self.coreArea
new_chart.coreWindings = self.coreWindings
return new_chart
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(Chart.color.text))
qp.drawText(self.leftMargin + 5, 15, f"{self.name}")
qp.drawText(5, 15, f"{MU}'")
qp.drawText(self.leftMargin + self.dim.width + 10, 15, f"{MU}''")
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(
self.leftMargin,
self.topMargin - 5,
self.leftMargin,
self.topMargin + self.dim.height + 5,
)
qp.drawLine(
self.leftMargin - 5,
self.topMargin + self.dim.height,
self.leftMargin + self.dim.width + 5,
self.topMargin + self.dim.height,
)
self.drawTitle(qp)
def contextMenuEvent(self, event):
self.action_set_fixed_start.setText(
f"Start ({format_frequency_chart(self.minFrequency)})"
)
self.action_set_fixed_stop.setText(
f"Stop ({format_frequency_chart(self.maxFrequency)})"
)
self.action_set_fixed_minimum_real.setText(
f"Minimum {MU}' ({self.minDisplayReal})"
)
self.action_set_fixed_maximum_real.setText(
f"Maximum {MU}' ({self.maxDisplayReal})"
)
self.action_set_fixed_minimum_imag.setText(
f"Minimum {MU}'' ({self.minDisplayImag})"
)
self.action_set_fixed_maximum_imag.setText(
f"Maximum {MU}'' ({self.maxDisplayImag})"
)
self.menu.exec(event.globalPos())
def setCoreLength(self):
val, selected = QtWidgets.QInputDialog.getDouble(
self,
"Core effective length",
"Set core effective length in mm",
value=self.coreLength,
decimals=2,
)
if not selected:
return
if not (self.fixedValues and val >= 0):
self.coreLength = val
if self.fixedValues:
self.update()
def setCoreArea(self):
val, selected = QtWidgets.QInputDialog.getDouble(
self,
"Core effective area",
"Set core cross section area length in mm\N{SUPERSCRIPT TWO}",
value=self.coreArea,
decimals=2,
)
if not selected:
return
if not (self.fixedValues and val >= 0):
self.coreArea = val
if self.fixedValues:
self.update()
def setCoreWindings(self):
val, selected = QtWidgets.QInputDialog.getInt(
self,
"Core number of windings",
"Set core number of windings",
value=self.coreWindings,
)
if not selected:
return
if not (self.fixedValues and val >= 0):
self.coreWindings = val
if self.fixedValues:
self.update()
def value(self, p: Datapoint) -> complex:
return self.mu_r(p)
def mu_r(self, p: Datapoint) -> complex:
inductance = p.impedance() / (2j * math.pi * p.freq)
# Core length and core area are in mm and mm2 respectively
# note: mu_r = mu' - j * mu ''
return np.conj(
inductance
* (self.coreLength / 1e3)
/ (mu_0 * self.coreWindings**2 * (self.coreArea / 1e6))
)

Wyświetl plik

@ -0,0 +1,116 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
from PyQt6 import QtGui
from NanoVNASaver.Formatting import format_frequency_chart
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.Charts.Chart import Chart
from .RI import RealImaginaryChart
logger = logging.getLogger(__name__)
class RealImaginaryZChart(RealImaginaryChart):
def __init__(self, name=""):
super().__init__(name)
self.y_menu.addSeparator()
self.action_set_fixed_maximum_real = QtGui.QAction(
f"Maximum R ({self.maxDisplayReal})"
)
self.action_set_fixed_maximum_real.triggered.connect(
self.setMaximumRealValue
)
self.action_set_fixed_minimum_real = QtGui.QAction(
f"Minimum R ({self.minDisplayReal})"
)
self.action_set_fixed_minimum_real.triggered.connect(
self.setMinimumRealValue
)
self.action_set_fixed_maximum_imag = QtGui.QAction(
f"Maximum jX ({self.maxDisplayImag})"
)
self.action_set_fixed_maximum_imag.triggered.connect(
self.setMaximumImagValue
)
self.action_set_fixed_minimum_imag = QtGui.QAction(
f"Minimum jX ({self.minDisplayImag})"
)
self.action_set_fixed_minimum_imag.triggered.connect(
self.setMinimumImagValue
)
self.y_menu.addAction(self.action_set_fixed_maximum_real)
self.y_menu.addAction(self.action_set_fixed_minimum_real)
self.y_menu.addSeparator()
self.y_menu.addAction(self.action_set_fixed_maximum_imag)
self.y_menu.addAction(self.action_set_fixed_minimum_imag)
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(Chart.color.text))
qp.drawText(self.leftMargin + 5, 15, f"{self.name} (\N{OHM SIGN})")
qp.drawText(10, 15, "R")
qp.drawText(self.leftMargin + self.dim.width + 10, 15, "X")
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(
self.leftMargin,
self.topMargin - 5,
self.leftMargin,
self.topMargin + self.dim.height + 5,
)
qp.drawLine(
self.leftMargin - 5,
self.topMargin + self.dim.height,
self.leftMargin + self.dim.width + 5,
self.topMargin + self.dim.height,
)
self.drawTitle(qp)
def contextMenuEvent(self, event):
self.action_set_fixed_start.setText(
f"Start ({format_frequency_chart(self.minFrequency)})"
)
self.action_set_fixed_stop.setText(
f"Stop ({format_frequency_chart(self.maxFrequency)})"
)
self.action_set_fixed_minimum_real.setText(
f"Minimum R ({self.minDisplayReal})"
)
self.action_set_fixed_maximum_real.setText(
f"Maximum R ({self.maxDisplayReal})"
)
self.action_set_fixed_minimum_imag.setText(
f"Minimum jX ({self.minDisplayImag})"
)
self.action_set_fixed_maximum_imag.setText(
f"Maximum jX ({self.maxDisplayImag})"
)
self.menu.exec(event.globalPos())
def value(self, p: Datapoint) -> complex:
return self.impedance(p)
def impedance(self, p: Datapoint) -> complex:
return p.impedance()

Some files were not shown because too many files have changed in this diff Show More