kopia lustrzana https://github.com/NanoVNA-Saver/nanovna-saver
Porównaj commity
97 Commity
Autor | SHA1 | Data |
---|---|---|
Holger Müller | a04d6d9b39 | |
Holger Müller | 00dd59ffc6 | |
Holger Müller | d3216d2ddb | |
t52ta6ek | 96dd23211a | |
Holger Müller | 2f8c5346eb | |
t52ta6ek | 4257ac152a | |
Holger Müller | 21e85bdb49 | |
Holger Müller | b4800102d8 | |
Name | abb80a5160 | |
t52ta6ek | 5bed1bc6cc | |
t52ta6ek | 20c1e4ec7c | |
Name | 21ba0ef665 | |
t52ta6ek | eff83097f8 | |
Name | dbea311a02 | |
t52ta6ek | a4a923a649 | |
Martin | ce0c7dd226 | |
Martin | 546d3b188a | |
Martin | 1f233819d2 | |
Martin | a8ffbc3aee | |
Holger Müller | ce8a59d478 | |
Crispin Tschirky | aab2a15f69 | |
Sascha Silbe | 9b4575e307 | |
Henk Vergonet | 8f86722c1e | |
Henk Vergonet | d09b55e1ae | |
Name | 6eb24f2315 | |
Name | d89c9f9d94 | |
Name | f34f3d1f67 | |
Name | 1cd5c052db | |
Holger Müller | 52cdac4f52 | |
Holger Müller | fafe0b2536 | |
Holger Müller | c5e00666aa | |
Holger Müller | 8dec23296e | |
Holger Müller | d1592ac1a3 | |
Holger Müller | 4e06fc53cf | |
Holger Müller | 45c2338196 | |
Holger Müller | 2bab4d4b0d | |
Holger Müller | b3a9f6d8cb | |
Holger Müller | 3c752a9731 | |
Holger Müller | b2c2598d3c | |
dependabot[bot] | c18a6c226f | |
Holger Müller | dd2f5b8a5d | |
Holger Müller | b322d3dc09 | |
Holger Müller | 5b21315a11 | |
Holger Müller | 9ace7d8cd4 | |
Holger Müller | b768a8e01b | |
Holger Müller | 2c58b2ba8f | |
MarcFontaine | a45baea9e2 | |
Holger Müller | db5cd98e03 | |
Holger Müller | 74792b3192 | |
Holger Müller | 50b540a832 | |
Holger Müller | 094b0185e7 | |
Holger Müller | b0110002ec | |
Holger Müller | c0e177bf1a | |
Holger Müller | 185a64b5ae | |
Holger Müller | f7d72d4320 | |
Holger Müller | 82e582b9c0 | |
Holger Müller | 59e7e1809a | |
Holger Müller | 0b82754350 | |
Holger Müller | 6f6f6c65e1 | |
Holger Müller | 92a8a0e39d | |
Holger Müller | b47e665575 | |
Holger Müller | 7f920249b1 | |
Holger Müller | 5860b04ce6 | |
Holger Müller | 9231737b70 | |
Holger Müller | 29518eef00 | |
Holger Müller | 8e9976a540 | |
Holger Müller | 0fbb301435 | |
Holger Müller | 93ee51d236 | |
Holger Müller | 925cf6d4e1 | |
Holger Müller | 09246b6a34 | |
Roel Jordans | dc8874c1c9 | |
Roel Jordans | c4623ddd90 | |
Roel Jordans | 02371bc56b | |
Roel Jordans | 0ffe0eaf72 | |
Roel Jordans | ee3467e5ec | |
Roel Jordans | 3d3e31e176 | |
Martin | f377c999fa | |
Holger Müller | 4cebe94b87 | |
Holger Müller | e4bd720160 | |
Martin | a437029fcd | |
Holger Müller | d7867b7535 | |
Holger Müller | 09d8b2b866 | |
Roel Jordans | 69f5089c1f | |
Roel Jordans | 044c1c885e | |
Roel Jordans | 0c3f179303 | |
Roel Jordans | 9b199b53a9 | |
Roel Jordans | dc44d33786 | |
Roel Jordans | 3265d0368b | |
Holger Müller | a9d0e02e4d | |
ikatkov | 2c868d818f | |
Martin | f996ee9ceb | |
Holger Müller | d313911840 | |
Holger Müller | 7c86009b3e | |
Martin | c536de6dc8 | |
Holger Müller | d6b2f8119b | |
Holger Müller | d654ea0441 | |
Jaroslav Škarvada | 4d21d6dfdc |
21
.coveragerc
21
.coveragerc
|
@ -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
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
|
||||
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):
|
||||
- [] 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?
|
||||
|
||||
|
@ -30,8 +30,8 @@ Issue Number: N/A
|
|||
|
||||
## Does this introduce a breaking change?
|
||||
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
- [] Yes
|
||||
- [] No
|
||||
|
||||
<!-- If this does introduce a breaking change, please describe the impact and migration path for existing applications below. -->
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
name: Linux Release
|
||||
name: Modern Linux Release
|
||||
|
||||
on:
|
||||
push:
|
||||
|
@ -8,30 +8,41 @@ on:
|
|||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-20.04
|
||||
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.3.1 setuptools==65.6.3
|
||||
python -m pip install pip==23.3.2 setuptools==69.0.3
|
||||
pip install -r requirements.txt
|
||||
pip install PyInstaller==5.7.0
|
||||
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
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
name: Modern Linux Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Install python
|
||||
run: |
|
||||
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||
sudo apt-get update
|
||||
sudo apt install -y python3.11 python3-pip python3.11-venv \
|
||||
python3.11-dev \
|
||||
python3-pyqt5
|
||||
- name: Install dependencies and pyinstall
|
||||
run: |
|
||||
python3.11 -m venv build
|
||||
. build/bin/activate
|
||||
python -m pip install pip==22.3.1 setuptools==65.6.3
|
||||
pip install -r requirements.txt
|
||||
pip install PyInstaller==5.7.0
|
||||
- name: Build binary
|
||||
run: |
|
||||
. build/bin/activate
|
||||
pyinstaller --onefile -n nanovna-saver nanovna-saver.py
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: NanoVNASaver.linux_modern
|
||||
path: dist/nanovna-saver
|
|
@ -11,19 +11,22 @@ jobs:
|
|||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.11
|
||||
- name: Install dependencies and pyinstall
|
||||
run: |
|
||||
python -m pip install pip==22.3.1 setuptools==65.6.3
|
||||
python -m pip install pip==23.3.2 setuptools==69.0.3
|
||||
pip install -r requirements.txt
|
||||
pip install PyInstaller==5.7.0
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -11,24 +11,32 @@ jobs:
|
|||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [x64, x86]
|
||||
arch: [x64, ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
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.3.1 setuptools==65.6.3
|
||||
pip install -r requirements.txt
|
||||
pip install PyInstaller==5.7.0
|
||||
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:
|
||||
|
|
|
@ -1,26 +1,56 @@
|
|||
/venv/
|
||||
/env/
|
||||
.idea/
|
||||
.tox/
|
||||
.vscode/
|
||||
/build/
|
||||
/dist/
|
||||
/nanovna-saver.spec
|
||||
*.egg-info/
|
||||
*.pyc
|
||||
*.cal
|
||||
settings.json
|
||||
.gitignore
|
||||
.coverage
|
||||
.flatpak-builder
|
||||
/nanovna-saver.exe.spec
|
||||
/deb_dist/
|
||||
*.deb
|
||||
*.rpm
|
||||
*.tar.gz
|
||||
# Temporary and binary files
|
||||
*~
|
||||
.*~
|
||||
*.bak
|
||||
*.new
|
||||
*.old
|
||||
*.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
|
||||
.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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
|
@ -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>
|
|
@ -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 you’re 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
|
|
@ -1,19 +1,16 @@
|
|||
[Desktop Entry]
|
||||
Categories=Electronics
|
||||
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
|
||||
Encoding=UTF-8
|
||||
Exec=NanoVNASaver
|
||||
GenericName[de_DE]=
|
||||
GenericName=
|
||||
Icon=NanoVNASaver_48x48.png
|
||||
Icon=NanoVNASaver_48x48
|
||||
MimeType=
|
||||
Name[de_DE]=NanoVNASaver
|
||||
Name=NanoVNASaver
|
||||
Path=
|
||||
StartupNotify=true
|
||||
Terminal=false
|
||||
TerminalOptions=
|
||||
Type=Application
|
||||
X-DBUS-ServiceName=
|
||||
X-DBUS-StartupType=
|
||||
|
|
|
@ -1,119 +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 typing import Callable, List, Tuple
|
||||
|
||||
from PyQt5 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)
|
|
@ -1,435 +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 dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
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_LINE = {
|
||||
"short": 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+ # optional for backword compatibility
|
||||
(?P<throughr>[-0-9Ee.]+) \s+ (?P<throughi>[-0-9Ee.]+) \s+
|
||||
(?P<isolationr>[-0-9Ee.]+) \s+ (?P<isolationi>[-0-9Ee.]+)
|
||||
)? \s* $
|
||||
""", re.VERBOSE),
|
||||
"long": 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),
|
||||
}
|
||||
|
||||
|
||||
RXP_CAL_HEADER = re.compile(r"""
|
||||
^ \# \s+ Hz \s+
|
||||
ShortR \s+ ShortI \s+ OpenR \s+ OpenI \s+
|
||||
LoadR \s+ LoadI \s+ ThroughR \s+ ThroughI \s+
|
||||
(?P<t_refl>ThrureflR \s+ ThrureflI \s+)? IsolationR \s+ IsolationI \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.data: defaultdict[int, CalData] = defaultdict(CalData)
|
||||
|
||||
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)
|
||||
|
||||
# TODO: implement tests
|
||||
def save(self, filename: str):
|
||||
# Save the calibration data to file
|
||||
if not self.isValid1Port():
|
||||
raise ValueError("Not a valid 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
|
||||
def load(self, filename):
|
||||
self.source = os.path.basename(filename)
|
||||
self.dataset = CalDataSet()
|
||||
self.notes = []
|
||||
|
||||
header = ""
|
||||
cols = {
|
||||
"": (),
|
||||
"sol": ("short", "open", "load"),
|
||||
"short": ("short", "open", "load",
|
||||
"through", "isolation"),
|
||||
"long": ("short", "open", "load",
|
||||
"through", "thrurefl", "isolation"),
|
||||
|
||||
}
|
||||
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 m := RXP_CAL_HEADER.search(line):
|
||||
header = "long" if m.group(1) else "short"
|
||||
columns = cols[header]
|
||||
logger.debug("found %s header type", header)
|
||||
continue
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
if not header:
|
||||
logger.warning(
|
||||
"Warning: Read line without having read header: %s",
|
||||
line)
|
||||
continue
|
||||
m = RXP_CAL_LINE[header].search(line)
|
||||
if not m:
|
||||
logger.warning("Illegal data in cal file. Line %i", i + 1)
|
||||
continue
|
||||
if (header == "short" and not m.group(8) and
|
||||
columns != cols["sol"]):
|
||||
logger.debug("only SOL cal data")
|
||||
columns = cols["sol"]
|
||||
cal = m.groupdict()
|
||||
|
||||
for name in columns:
|
||||
self.dataset.insert(
|
||||
name,
|
||||
Datapoint(int(cal["freq"]),
|
||||
float(cal[f"{name}r"]),
|
||||
float(cal[f"{name}i"])))
|
|
@ -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 = int(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}")
|
|
@ -1,490 +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
|
|
@ -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
|
|
@ -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"]
|
|
@ -1,162 +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
|
14
Pipfile
14
Pipfile
|
@ -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"
|
262
README.md
262
README.md
|
@ -1,262 +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¤cy_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
|
||||
|
||||
<a href="#built-with"></a>
|
||||
It's written in __Python 3__ using __PyQt5__ and __scipy__.
|
||||
|
||||
<details open="open">
|
||||
<summary>Table of Contents</summary>
|
||||
|
||||
- [About](#nanovnasaver)
|
||||
- [Built With](#built-with)
|
||||
- [Introduction](#introduction)
|
||||
- [Current Features](#current-features)
|
||||
- [Screenshot](#screenshot)
|
||||
- [Binary Releases](#binary-releases)
|
||||
- [Installation](#installation)
|
||||
- [Detailed Installation Instructions](docs/INSTALLATION.md)
|
||||
- [Usage](#using-the-software)
|
||||
- [Calibration](#calibration)
|
||||
- [TDR](#tdr)
|
||||
- [Latest Changes](#latest-changes)
|
||||
- [Contributing](#contributing)
|
||||
- [Contribution Guidlines](docs/CONTRIBUTING.md)
|
||||
- [License](#license)
|
||||
- [References](#references)
|
||||
- [Acknowledgements](#acknowledgements)
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
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.
|
||||
|
||||
<a href="#current-features"></a>
|
||||
### 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
|
||||
|
||||
### Screenshot
|
||||
![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 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.
|
||||
|
||||
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.
|
||||
|
||||
### 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.4.tar.gz
|
||||
|
||||
Once completed run with the following command
|
||||
|
||||
NanoVNASaver
|
||||
|
||||
[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.
|
||||
|
||||
![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.
|
||||
|
||||
Latest Changes
|
||||
--------------
|
||||
|
||||
### 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
|
||||
|
||||
### Changes in 0.5.3
|
||||
|
||||
- Python 3.10 compatability fixes
|
||||
- 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
|
||||
- Fixed man float related crashes with Qt and
|
||||
Python 3.10
|
||||
- Using more integer divisions to get right type for QPainter
|
||||
points
|
||||
- No more long lines in code (pycodestyle)
|
||||
|
||||
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:
|
||||
|
||||
[![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¤cy_code=EUR&source=url)
|
|
@ -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¤cy_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¤cy_code=EUR&source=url
|
||||
:alt: Paypal
|
||||
|
|
@ -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}
|
1
debug.sh
1
debug.sh
|
@ -1,2 +1,3 @@
|
|||
#!/bin/sh
|
||||
export PYTHONPATH="src"
|
||||
exec python -m debugpy --listen 5678 --wait-for-client $@
|
||||
|
|
|
@ -1,12 +1,68 @@
|
|||
Installation Instructions
|
||||
=========================
|
||||
# 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
|
||||
|
||||
1. Install python3.8 and pip
|
||||
|
||||
sudo apt install python3.8 python3-pip
|
||||
sudo apt install python3 python3-pip
|
||||
python3 -m venv ~/.venv_nano
|
||||
. ~/.venv_nano/bin/activate
|
||||
pip install -U pip
|
||||
|
@ -29,7 +85,6 @@ Installation Instructions
|
|||
. ~/.venv_nano/bin/activate
|
||||
python3 nanovna-saver.py
|
||||
|
||||
|
||||
## MacPorts
|
||||
|
||||
Via a MacPorts distribution maintained by @ra1nb0w.
|
||||
|
@ -49,7 +104,6 @@ Via a MacPorts distribution maintained by @ra1nb0w.
|
|||
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
|
||||
|
||||
|
||||
2. Python :
|
||||
|
||||
brew install python
|
||||
|
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
|||
# Empty directory
|
|
@ -0,0 +1,2 @@
|
|||
.. _authors:
|
||||
.. include:: ../AUTHORS.rst
|
|
@ -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 don’t 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)
|
|
@ -0,0 +1 @@
|
|||
.. include:: ../CONTRIBUTING.rst
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
|||
.. _license:
|
||||
|
||||
=======
|
||||
License
|
||||
=======
|
||||
|
||||
.. include:: ../LICENSE.txt
|
|
@ -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.
|
|
@ -0,0 +1,2 @@
|
|||
.. _readme:
|
||||
.. include:: ../README.rst
|
|
@ -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
|
|
@ -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
|
||||
|
|
Plik binarny nie jest wyświetlany.
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 109 KiB |
|
@ -16,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()
|
||||
|
|
|
@ -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",
|
||||
]
|
|
@ -1,5 +1,9 @@
|
|||
pyserial==3.5
|
||||
PyQt5==5.15.7
|
||||
numpy==1.24.1
|
||||
scipy==1.9.3
|
||||
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
|
||||
|
|
94
setup.cfg
94
setup.cfg
|
@ -1,3 +1,8 @@
|
|||
# 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
|
||||
|
@ -5,26 +10,95 @@ author_email= NanoVNA-Saver@users.noreply.github.com
|
|||
license = GNU GPL V3
|
||||
license_files = LICENSE,
|
||||
description = GUI for the NanoVNA and derivates
|
||||
long_description = file: README.md
|
||||
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]
|
||||
# do not use "find_namespace:" because this may recursively include "build"
|
||||
packages = find:
|
||||
install_requires=
|
||||
zip_safe = False
|
||||
packages = find_namespace:
|
||||
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
|
||||
|
||||
# without this option the rpm-build includes also the "test" directory
|
||||
[options.packages.find]
|
||||
exclude = test
|
||||
[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
|
||||
|
|
45
setup.py
45
setup.py
|
@ -1,28 +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(
|
||||
data_files=[
|
||||
( "share/doc/nanovnasaver/", [ "CHANGELOG.md", "LICENSE", "README.md" ] ),
|
||||
( "share/applications/", [ "NanoVNASaver.desktop" ] ),
|
||||
( "share/icons/hicolor/48x48/apps/", [ "NanoVNASaver_48x48.png" ] ),
|
||||
]
|
||||
)
|
||||
|
||||
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
|
||||
|
|
|
@ -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.4"
|
||||
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"
|
|
@ -21,7 +21,7 @@
|
|||
import logging
|
||||
from time import sleep
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
from PyQt6 import QtWidgets
|
||||
|
||||
from NanoVNASaver.Analysis.VSWRAnalysis import VSWRAnalysis
|
||||
|
||||
|
@ -35,6 +35,7 @@ class MagLoopAnalysis(VSWRAnalysis):
|
|||
Useful for tuning magloop.
|
||||
|
||||
"""
|
||||
|
||||
max_dips_shown = 1
|
||||
|
||||
vswr_bandwith_value = 2.56 # -3 dB ?!?
|
||||
|
@ -56,12 +57,17 @@ 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:
|
||||
|
@ -73,22 +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)
|
||||
self.vswr_limit_value,
|
||||
)
|
||||
else:
|
||||
new_start = new_start - 5 * self.bandwith
|
||||
new_end = new_end + 5 * self.bandwith
|
||||
|
@ -100,14 +109,17 @@ class MagLoopAnalysis(VSWRAnalysis):
|
|||
self.input_vswr_limit.setValue(self.vswr_limit_value)
|
||||
logger.debug(
|
||||
"no minimum found, looking for higher value %s",
|
||||
self.vswr_limit_value)
|
||||
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)
|
|
@ -18,9 +18,8 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, List
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
from PyQt6 import QtWidgets
|
||||
|
||||
import NanoVNASaver.AnalyticTools as at
|
||||
from NanoVNASaver.Analysis.Base import Analysis, CUTOFF_VALS
|
||||
|
@ -33,42 +32,52 @@ 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'):
|
||||
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(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'])
|
||||
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("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("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'])
|
||||
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")
|
||||
|
||||
|
@ -103,71 +112,90 @@ class BandPassAnalysis(Analysis):
|
|||
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']),
|
||||
"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["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"])
|
||||
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'):
|
||||
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}")
|
||||
self.label["q_factor"].setText(f"{result['q_factor']:.2f}")
|
||||
|
||||
for label in ('octave_l', 'decade_l', 'octave_r', 'decade_r'):
|
||||
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:
|
||||
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'])
|
||||
"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.")
|
||||
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]):
|
||||
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])
|
||||
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'])
|
||||
)))
|
||||
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:
|
||||
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)
|
||||
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
|
||||
|
||||
|
@ -177,13 +205,15 @@ class BandPassAnalysis(Analysis):
|
|||
return -1
|
||||
return peak
|
||||
|
||||
def find_bounderies(self,
|
||||
gains: List[float],
|
||||
peak: int, peak_db: float) -> Dict[str, int]:
|
||||
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)
|
||||
gains, peak, peak_db, attn
|
||||
)
|
||||
cutoff_pos[f"{attn:.1f}dB_r"] = at.cut_off_right(
|
||||
gains, peak, peak_db, attn)
|
||||
gains, peak, peak_db, attn
|
||||
)
|
||||
return cutoff_pos
|
|
@ -17,7 +17,6 @@
|
|||
# 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 Dict, List
|
||||
|
||||
import NanoVNASaver.AnalyticTools as at
|
||||
from NanoVNASaver.Analysis.Base import CUTOFF_VALS
|
||||
|
@ -31,14 +30,16 @@ class BandStopAnalysis(BandPassAnalysis):
|
|||
super().__init__(app)
|
||||
self.set_titel("Band stop filter analysis")
|
||||
|
||||
def find_center(self, gains: List[float]) -> int:
|
||||
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]:
|
||||
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))
|
||||
(
|
||||
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
|
|
@ -17,8 +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 typing import Dict
|
||||
from PyQt5 import QtWidgets
|
||||
from PyQt6 import QtWidgets
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -28,15 +27,15 @@ 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.HLine)
|
||||
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.label: dict[str, QtWidgets.QLabel] = {
|
||||
"titel": QtWidgets.QLabel(),
|
||||
"result": QtWidgets.QLabel(),
|
||||
}
|
||||
self.layout = QtWidgets.QFormLayout()
|
||||
self._widget = QtWidgets.QWidget()
|
||||
|
@ -53,7 +52,7 @@ class Analysis:
|
|||
label.clear()
|
||||
|
||||
def set_result(self, text):
|
||||
self.label['result'].setText(text)
|
||||
self.label["result"].setText(text)
|
||||
|
||||
def set_titel(self, text):
|
||||
self.label['titel'].setText(text)
|
||||
self.label["titel"].setText(text)
|
|
@ -19,14 +19,18 @@
|
|||
import csv
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
from PyQt6 import QtWidgets
|
||||
|
||||
import NanoVNASaver.AnalyticTools as at
|
||||
from NanoVNASaver.Analysis.ResonanceAnalysis import (
|
||||
ResonanceAnalysis, format_resistence_neg
|
||||
ResonanceAnalysis,
|
||||
format_resistence_neg,
|
||||
)
|
||||
from NanoVNASaver.Formatting import (
|
||||
format_frequency, format_complex_imp, format_frequency_short)
|
||||
format_frequency,
|
||||
format_complex_imp,
|
||||
format_frequency_short,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -43,11 +47,11 @@ class EFHWAnalysis(ResonanceAnalysis):
|
|||
def do_resonance_analysis(self):
|
||||
s11 = self.app.data.s11
|
||||
maximums = sorted(
|
||||
at.maxima([d.impedance().real for d in s11],
|
||||
threshold=500))
|
||||
at.maxima([d.impedance().real for d in s11], threshold=500)
|
||||
)
|
||||
extended_data = {}
|
||||
logger.info("TO DO: find near data")
|
||||
for lowest in self.crossing:
|
||||
for lowest in self.crossings:
|
||||
my_data = self._get_data(lowest)
|
||||
if lowest in extended_data:
|
||||
extended_data[lowest].update(my_data)
|
||||
|
@ -61,12 +65,14 @@ class EFHWAnalysis(ResonanceAnalysis):
|
|||
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))]
|
||||
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)
|
||||
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)
|
||||
|
@ -76,14 +82,17 @@ class EFHWAnalysis(ResonanceAnalysis):
|
|||
QtWidgets.QLabel(
|
||||
f" ({diff[i]['freq']})"
|
||||
f" {format_complex_imp(s11[idx].impedance())}"
|
||||
f" ({diff[i]['r']}) {diff[i]['lambda']} m"))
|
||||
f" ({diff[i]['r']}) {diff[i]['lambda']} m"
|
||||
),
|
||||
)
|
||||
|
||||
if self.filename and extended_data:
|
||||
with open(
|
||||
self.filename, 'w', newline='', encoding='utf-8'
|
||||
self.filename, "w", newline="", encoding="utf-8"
|
||||
) as csvfile:
|
||||
fieldnames = extended_data[sorted(
|
||||
extended_data.keys())[0]].keys()
|
||||
fieldnames = extended_data[
|
||||
sorted(extended_data.keys())[0]
|
||||
].keys()
|
||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
for idx in sorted(extended_data.keys()):
|
||||
|
@ -99,10 +108,11 @@ class EFHWAnalysis(ResonanceAnalysis):
|
|||
:param old:
|
||||
:param new:
|
||||
"""
|
||||
fields = fields or [("freq", str), ]
|
||||
fields = fields or [
|
||||
("freq", str),
|
||||
]
|
||||
|
||||
def no_compare():
|
||||
|
||||
return {k: "-" for k, _ in fields}
|
||||
|
||||
old_idx = sorted(old.keys())
|
||||
|
@ -113,8 +123,9 @@ class EFHWAnalysis(ResonanceAnalysis):
|
|||
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))
|
||||
logger.warning(
|
||||
"resonances changed from %s to %s", len(old_idx), len(new_idx)
|
||||
)
|
||||
|
||||
split = 0
|
||||
max_delta_f = 1_000_000
|
||||
|
@ -135,15 +146,19 @@ class EFHWAnalysis(ResonanceAnalysis):
|
|||
logger.debug("Deltas %s", diff[i])
|
||||
continue
|
||||
|
||||
logger.debug("can't compare, %s is too much ",
|
||||
format_frequency(delta_f))
|
||||
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):
|
||||
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 ?!?
|
|
@ -18,9 +18,8 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, List
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
from PyQt6 import QtWidgets
|
||||
|
||||
import NanoVNASaver.AnalyticTools as at
|
||||
from NanoVNASaver.Analysis.Base import Analysis, CUTOFF_VALS
|
||||
|
@ -41,9 +40,12 @@ class HighPassAnalysis(Analysis):
|
|||
|
||||
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(
|
||||
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"])
|
||||
|
@ -51,7 +53,7 @@ class HighPassAnalysis(Analysis):
|
|||
layout.addRow("Roll-off:", self.label["octave"])
|
||||
layout.addRow("Roll-off:", self.label["decade"])
|
||||
|
||||
self.set_titel('Highpass analysis')
|
||||
self.set_titel("Highpass analysis")
|
||||
|
||||
def runAnalysis(self):
|
||||
if not self.app.data.s21:
|
||||
|
@ -81,29 +83,32 @@ class HighPassAnalysis(Analysis):
|
|||
logger.debug("Cuttoff gains: %s", cutoff_gain)
|
||||
|
||||
octave, decade = at.calculate_rolloff(
|
||||
s21, cutoff_pos["10.0dB"], cutoff_pos["20.0dB"])
|
||||
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'])
|
||||
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)")
|
||||
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.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.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:
|
||||
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:
|
||||
|
@ -111,11 +116,10 @@ class HighPassAnalysis(Analysis):
|
|||
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]:
|
||||
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)
|
||||
f"{attn:.1f}dB": at.cut_off_left(gains, peak, peak_db, attn)
|
||||
for attn in CUTOFF_VALS
|
||||
}
|
|
@ -17,7 +17,6 @@
|
|||
# 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 Dict, List
|
||||
|
||||
import NanoVNASaver.AnalyticTools as at
|
||||
from NanoVNASaver.Analysis.Base import CUTOFF_VALS
|
||||
|
@ -30,13 +29,12 @@ class LowPassAnalysis(HighPassAnalysis):
|
|||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
|
||||
self.set_titel('Lowpass filter analysis')
|
||||
self.set_titel("Lowpass filter analysis")
|
||||
|
||||
def find_cutoffs(self,
|
||||
gains: List[float],
|
||||
peak: int, peak_db: float) -> Dict[str, int]:
|
||||
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)
|
||||
f"{attn:.1f}dB": at.cut_off_right(gains, peak, peak_db, attn)
|
||||
for attn in CUTOFF_VALS
|
||||
}
|
|
@ -18,14 +18,16 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
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)
|
||||
SimplePeakSearchAnalysis,
|
||||
)
|
||||
|
||||
from NanoVNASaver.Formatting import format_frequency_short
|
||||
|
||||
|
@ -34,7 +36,6 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class PeakSearchAnalysis(SimplePeakSearchAnalysis):
|
||||
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
|
||||
|
@ -48,7 +49,7 @@ class PeakSearchAnalysis(SimplePeakSearchAnalysis):
|
|||
self.layout.addRow(QtWidgets.QLabel("<b>Results</b>"))
|
||||
self.results_header = self.layout.rowCount()
|
||||
|
||||
self.set_titel('Peak search')
|
||||
self.set_titel("Peak search")
|
||||
|
||||
def runAnalysis(self):
|
||||
if not self.app.data.s11:
|
||||
|
@ -59,18 +60,18 @@ class PeakSearchAnalysis(SimplePeakSearchAnalysis):
|
|||
data, fmt_fnc = self.data_and_format()
|
||||
|
||||
inverted = False
|
||||
if self.button['peak_l'].isChecked():
|
||||
if self.button["peak_l"].isChecked():
|
||||
inverted = True
|
||||
peaks, _ = find_peaks(
|
||||
-np.array(data), width=3, distance=3, prominence=1)
|
||||
-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)
|
||||
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 %i at %d", i, p)
|
||||
logger.debug("Peak %s at %s", i, p)
|
||||
prominences = peak_prominences(data, peaks)[0]
|
||||
logger.debug("%d prominences", len(prominences))
|
||||
|
||||
|
@ -89,19 +90,24 @@ class PeakSearchAnalysis(SimplePeakSearchAnalysis):
|
|||
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 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))
|
||||
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())
|
||||
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)
|
|
@ -19,15 +19,12 @@
|
|||
import os
|
||||
import csv
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
from PyQt6 import QtWidgets
|
||||
|
||||
import NanoVNASaver.AnalyticTools as at
|
||||
from NanoVNASaver.Analysis.Base import Analysis, QHLine
|
||||
from NanoVNASaver.Formatting import (
|
||||
format_frequency, format_complex_imp,
|
||||
format_resistance)
|
||||
from NanoVNASaver.Formatting import format_frequency, format_resistance
|
||||
from NanoVNASaver.RFTools import reflection_coefficient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -44,10 +41,9 @@ def vswr_transformed(z, ratio=49) -> float:
|
|||
|
||||
|
||||
class ResonanceAnalysis(Analysis):
|
||||
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
self.crossing: List[int] = []
|
||||
self.crossings: list[int] = []
|
||||
self.filename = ""
|
||||
self._widget = QtWidgets.QWidget()
|
||||
self.layout = QtWidgets.QFormLayout()
|
||||
|
@ -72,10 +68,8 @@ class ResonanceAnalysis(Analysis):
|
|||
"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["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
|
||||
|
||||
|
@ -83,52 +77,48 @@ class ResonanceAnalysis(Analysis):
|
|||
|
||||
def runAnalysis(self):
|
||||
self.reset()
|
||||
self.filename = os.path.join(
|
||||
"/tmp/", f"{self.input_description.text()}.csv"
|
||||
) if self.input_description.text() else ""
|
||||
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())
|
||||
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.crossing = at.zero_crossings([d.phase for d in self.app.data.s11])
|
||||
logger.debug("Found %d sections ",
|
||||
len(self.crossing))
|
||||
if not self.crossing:
|
||||
self.layout.addRow(QtWidgets.QLabel(
|
||||
"No resonance found"))
|
||||
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 m in self.crossing:
|
||||
start, lowest, end = m
|
||||
my_data = self._get_data(lowest)
|
||||
s11_low = self.app.data.s11[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(s11_low.freq)}"
|
||||
f" ({format_complex_imp(s11_low.impedance())})"))
|
||||
else:
|
||||
self.layout.addRow("Resonance", QtWidgets.QLabel(
|
||||
format_frequency(self.app.data.s11[lowest].freq)))
|
||||
self.layout.addWidget(QHLine())
|
||||
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=''
|
||||
self.filename, "w", encoding="utf-8", newline=""
|
||||
) as csvfile:
|
||||
fieldnames = extended_data[0].keys()
|
||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
@ -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)
|
|
@ -17,9 +17,8 @@
|
|||
# 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 List
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
from PyQt6 import QtWidgets
|
||||
|
||||
import NanoVNASaver.AnalyticTools as at
|
||||
from NanoVNASaver.Analysis.Base import Analysis, QHLine
|
||||
|
@ -54,7 +53,7 @@ class VSWRAnalysis(Analysis):
|
|||
self.results_label = QtWidgets.QLabel("<b>Results</b>")
|
||||
self.layout.addRow(self.results_label)
|
||||
|
||||
self.minimums: List[int] = []
|
||||
self.minimums: list[int] = []
|
||||
|
||||
def runAnalysis(self):
|
||||
if not self.app.data.s11:
|
||||
|
@ -64,34 +63,50 @@ class VSWRAnalysis(Analysis):
|
|||
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]
|
||||
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())
|
||||
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)}."))
|
||||
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))))
|
||||
"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)
|
|
@ -18,23 +18,24 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import itertools as it
|
||||
import math
|
||||
from typing import Callable, List, Tuple
|
||||
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]:
|
||||
def zero_crossings(data: list[float]) -> list[int]:
|
||||
"""find zero crossings
|
||||
|
||||
Args:
|
||||
data (List[float]): data list execute
|
||||
data (list[float]): data list execute
|
||||
|
||||
Returns:
|
||||
List[int]: sorted indices of zero crossing points
|
||||
list[int]: sorted indices of zero crossing points
|
||||
"""
|
||||
if not data:
|
||||
return []
|
||||
|
@ -42,8 +43,9 @@ def zero_crossings(data: List[float]) -> List[int]:
|
|||
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}]
|
||||
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
|
||||
|
@ -52,69 +54,68 @@ def zero_crossings(data: List[float]) -> List[int]:
|
|||
return sorted(real_zeros + crossings)
|
||||
|
||||
|
||||
def maxima(data: List[float], threshold: float = 0.0) -> List[int]:
|
||||
def maxima(data: list[float], threshold: float = 0.0) -> list[int]:
|
||||
"""maxima
|
||||
|
||||
Args:
|
||||
data (List[float]): data list to execute
|
||||
data (list[float]): data list to execute
|
||||
|
||||
Returns:
|
||||
List[int]: indices of maxima
|
||||
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
|
||||
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]:
|
||||
def minima(data: list[float], threshold: float = 0.0) -> list[int]:
|
||||
"""minima
|
||||
|
||||
Args:
|
||||
data (List[float]): data list to execute
|
||||
data (list[float]): data list to execute
|
||||
|
||||
Returns:
|
||||
List[int]: indices of minima
|
||||
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
|
||||
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]:
|
||||
def take_from_idx(
|
||||
data: list[float], idx: int, predicate: Callable
|
||||
) -> list[int]:
|
||||
"""take_from_center
|
||||
|
||||
Args:
|
||||
data (List[float]): data list to execute
|
||||
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
|
||||
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))]
|
||||
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:
|
||||
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
|
||||
gains (list[float]): gain values
|
||||
idx (int): start position to search from
|
||||
delta (float, optional): max gain delta from start. Defaults to 3.0.
|
||||
|
||||
|
@ -122,18 +123,18 @@ def center_from_idx(gains: List[float],
|
|||
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)
|
||||
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:
|
||||
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
|
||||
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.
|
||||
|
@ -143,18 +144,18 @@ def cut_off_left(gains: List[float], idx: int,
|
|||
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)
|
||||
(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:
|
||||
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
|
||||
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.
|
||||
|
@ -165,19 +166,20 @@ def cut_off_right(gains: List[float], idx: int,
|
|||
"""
|
||||
|
||||
return next(
|
||||
(i for i in range(idx, len(gains)) if
|
||||
(peak_gain - gains[i]) > attn),
|
||||
-1)
|
||||
(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]:
|
||||
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]:
|
||||
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
|
|
@ -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()
|
|
@ -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,20 +60,24 @@ class CombinedLogMagChart(LogMagChart):
|
|||
|
||||
def drawChart(self, qp: QtGui.QPainter):
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
qp.drawText(int(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:
|
||||
|
@ -117,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)
|
||||
|
@ -132,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)
|
|
@ -19,11 +19,11 @@
|
|||
import logging
|
||||
|
||||
from dataclasses import dataclass, field, replace
|
||||
from typing import List, Set, Tuple, ClassVar, Any, Optional
|
||||
from typing import ClassVar, Any
|
||||
|
||||
from PyQt5 import QtWidgets, QtGui, QtCore
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtGui import QColor
|
||||
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
|
||||
|
@ -34,17 +34,24 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
@dataclass
|
||||
class ChartColors: # pylint: disable=too-many-instance-attributes
|
||||
background: QColor = field(default_factory=lambda: QColor(QtCore.Qt.white))
|
||||
background: QColor = field(
|
||||
default_factory=lambda: QColor(QColorConstants.White)
|
||||
)
|
||||
foreground: QColor = field(
|
||||
default_factory=lambda: QColor(QtCore.Qt.lightGray))
|
||||
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(QtCore.Qt.darkYellow))
|
||||
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(QtCore.Qt.darkMagenta))
|
||||
default_factory=lambda: QColor(QColorConstants.DarkMagenta)
|
||||
)
|
||||
swr: QColor = field(default_factory=lambda: QColor(255, 0, 0, 128))
|
||||
text: QColor = field(default_factory=lambda: QColor(QtCore.Qt.black))
|
||||
text: QColor = field(default_factory=lambda: QColor(QColorConstants.Black))
|
||||
bands: QColor = field(default_factory=lambda: QColor(128, 128, 128, 48))
|
||||
|
||||
|
||||
|
@ -60,8 +67,8 @@ class ChartDimensions:
|
|||
|
||||
@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
|
||||
|
@ -97,8 +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 - int(text_width // 2),
|
||||
y - 3 - offset, text)
|
||||
self.qp.drawText(x - int(text_width // 2), y - 3 - offset, text)
|
||||
|
||||
|
||||
class Chart(QtWidgets.QWidget):
|
||||
|
@ -109,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
|
||||
|
@ -122,22 +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 = QAction("Popout chart")
|
||||
self.action_popout.triggered.connect(
|
||||
lambda: self.popoutRequested.emit(self))
|
||||
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
|
||||
|
@ -185,7 +192,7 @@ class Chart(QtWidgets.QWidget):
|
|||
None,
|
||||
)
|
||||
|
||||
def getNearestMarker(self, x, y) -> Optional[Marker]:
|
||||
def getNearestMarker(self, x, y) -> Marker | None:
|
||||
if not self.data:
|
||||
return None
|
||||
shortest = 10**6
|
||||
|
@ -198,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):
|
||||
|
@ -206,22 +213,27 @@ 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):
|
||||
|
@ -230,7 +242,9 @@ class Chart(QtWidgets.QWidget):
|
|||
self.zoomTo(
|
||||
self.dragbox.pos_start[0],
|
||||
self.dragbox.pos_start[1],
|
||||
a0.x(), a0.y())
|
||||
a0.position().x(),
|
||||
a0.position().y(),
|
||||
)
|
||||
self.dragbox.state = False
|
||||
self.dragbox.pos = (-1, -1)
|
||||
self.dragbox.pos_start = (0, 0)
|
||||
|
@ -243,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
|
||||
|
@ -252,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
|
||||
|
@ -262,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()
|
||||
|
||||
|
@ -272,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:
|
||||
|
@ -314,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}")
|
||||
|
||||
|
@ -330,6 +346,6 @@ class Chart(QtWidgets.QWidget):
|
|||
|
||||
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()
|
|
@ -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,20 +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.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)
|
||||
|
||||
|
@ -197,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)
|
||||
|
@ -215,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
|
||||
|
@ -242,8 +265,11 @@ class FrequencyChart(Chart):
|
|||
|
||||
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()
|
||||
|
@ -435,10 +477,10 @@ class FrequencyChart(Chart):
|
|||
m.setFrequency(str(f))
|
||||
|
||||
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
|
||||
self.dim.width = (
|
||||
a0.size().width() - self.rightMargin - self.leftMargin)
|
||||
self.dim.width = a0.size().width() - self.rightMargin - self.leftMargin
|
||||
self.dim.height = (
|
||||
a0.size().height() - self.bottomMargin - self.topMargin)
|
||||
a0.size().height() - self.bottomMargin - self.topMargin
|
||||
)
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, _: QtGui.QPaintEvent) -> None:
|
||||
|
@ -450,25 +492,31 @@ class FrequencyChart(Chart):
|
|||
self.drawDragbog(qp)
|
||||
qp.end()
|
||||
|
||||
def _data_oob(self, data: List[Datapoint]) -> bool:
|
||||
return (data[0].freq > self.fstop or self.data[-1].freq < self.fstart)
|
||||
def _data_oob(self, data: list[Datapoint]) -> bool:
|
||||
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 + int(self.dim.width // 2) - 70,
|
||||
self.topMargin + int(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)
|
||||
|
@ -480,14 +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):
|
||||
|
@ -513,7 +565,8 @@ class FrequencyChart(Chart):
|
|||
if span == 0:
|
||||
logger.info(
|
||||
"Span is zero for %s-Chart, setting to a small value.",
|
||||
self.name)
|
||||
self.name,
|
||||
)
|
||||
span = 1e-15
|
||||
self.span = span
|
||||
|
||||
|
@ -521,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:
|
||||
|
@ -568,32 +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)
|
||||
|
@ -607,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)
|
||||
|
@ -642,8 +713,7 @@ class FrequencyChart(Chart):
|
|||
if self.isPlotable(prevx, prevy):
|
||||
qp.drawLine(x, y, prevx, prevy)
|
||||
else:
|
||||
new_x, new_y = self.getPlotable(
|
||||
x, y, prevx, prevy)
|
||||
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)
|
||||
|
@ -662,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])
|
||||
|
@ -679,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
|
||||
|
||||
|
@ -727,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)
|
|
@ -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 []
|
||||
|
@ -124,23 +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))
|
||||
|
@ -153,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)
|
||||
|
@ -200,9 +212,10 @@ class GroupDelayChart(FrequencyChart):
|
|||
|
||||
def getYPositionFromDelay(self, delay: float) -> int:
|
||||
return self.topMargin + int(
|
||||
(self.maxDelay - delay) / self.span * self.dim.height)
|
||||
(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]
|
|
@ -19,9 +19,8 @@
|
|||
from dataclasses import dataclass
|
||||
import math
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from PyQt5 import QtGui
|
||||
from PyQt6 import QtGui
|
||||
|
||||
from NanoVNASaver.Charts.Chart import Chart
|
||||
from NanoVNASaver.Charts.Frequency import FrequencyChart
|
||||
|
@ -115,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}")
|
||||
|
@ -127,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)
|
||||
|
@ -145,9 +151,9 @@ 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:
|
||||
|
@ -155,9 +161,10 @@ class LogMagChart(FrequencyChart):
|
|||
if math.isinf(logMag):
|
||||
return self.topMargin
|
||||
return self.topMargin + int(
|
||||
(self.maxValue - logMag) / self.span * self.dim.height)
|
||||
(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]
|
|
@ -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__)
|
||||
|
||||
|
||||
|
@ -78,21 +78,28 @@ class MagnitudeChart(FrequencyChart):
|
|||
target_ticks = int(self.dim.height // 60)
|
||||
for i in range(target_ticks):
|
||||
val = min_value + i / target_ticks * self.span
|
||||
y = self.topMargin + int((self.maxValue - val) / self.span
|
||||
* self.dim.height)
|
||||
y = self.topMargin + int(
|
||||
(self.maxValue - val) / self.span * self.dim.height
|
||||
)
|
||||
qp.setPen(Chart.color.text)
|
||||
if val != min_value:
|
||||
digits = max(0, min(2, math.floor(3 - math.log10(abs(val)))))
|
||||
vswrstr = (str(round(val)) if digits == 0 else
|
||||
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(max_value))
|
||||
qp.drawText(3, self.dim.height + self.topMargin, str(min_value))
|
||||
|
@ -103,10 +110,10 @@ class MagnitudeChart(FrequencyChart):
|
|||
if vswr <= 1:
|
||||
continue
|
||||
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)
|
||||
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, f"VSWR: {vswr}")
|
||||
|
||||
self.drawData(qp, self.data, Chart.color.sweep)
|
||||
|
@ -116,9 +123,10 @@ class MagnitudeChart(FrequencyChart):
|
|||
def getYPosition(self, d: Datapoint) -> int:
|
||||
mag = self.magnitude(d)
|
||||
return self.topMargin + int(
|
||||
(self.maxValue - mag) / self.span * self.dim.height)
|
||||
(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]
|
|
@ -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
|
||||
|
@ -57,8 +55,10 @@ class MagnitudeZChart(FrequencyChart):
|
|||
if self.fixedValues:
|
||||
self.maxValue = self.maxDisplayValue
|
||||
self.minValue = (
|
||||
max(self.minDisplayValue, 0.01) if self.logarithmicY else
|
||||
self.minDisplayValue)
|
||||
max(self.minDisplayValue, 0.01)
|
||||
if self.logarithmicY
|
||||
else self.minDisplayValue
|
||||
)
|
||||
else:
|
||||
# Find scaling
|
||||
self.minValue = 100
|
||||
|
@ -92,15 +92,18 @@ class MagnitudeZChart(FrequencyChart):
|
|||
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)
|
||||
|
||||
|
@ -116,18 +119,22 @@ class MagnitudeZChart(FrequencyChart):
|
|||
if self.logarithmicY:
|
||||
span = math.log(self.maxValue) - math.log(self.minValue)
|
||||
return self.topMargin + int(
|
||||
(math.log(self.maxValue) - math.log(mag)) /
|
||||
span * self.dim.height)
|
||||
(math.log(self.maxValue) - math.log(mag))
|
||||
/ span
|
||||
* self.dim.height
|
||||
)
|
||||
return self.topMargin + int(
|
||||
(self.maxValue - mag) / self.span * self.dim.height)
|
||||
(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]
|
|
@ -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())
|
|
@ -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())
|
|
@ -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.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__)
|
||||
|
||||
|
||||
|
@ -50,19 +50,26 @@ class PermeabilityChart(FrequencyChart):
|
|||
|
||||
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):
|
||||
|
@ -121,21 +128,22 @@ class PermeabilityChart(FrequencyChart):
|
|||
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)
|
||||
|
@ -147,8 +155,11 @@ 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)
|
||||
|
@ -177,7 +188,8 @@ class PermeabilityChart(FrequencyChart):
|
|||
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)
|
||||
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)
|
||||
|
@ -191,7 +203,8 @@ class PermeabilityChart(FrequencyChart):
|
|||
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)
|
||||
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)
|
||||
|
@ -201,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)
|
||||
|
@ -213,8 +226,12 @@ 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, reference in enumerate(self.reference):
|
||||
if reference.freq < self.fstart or reference.freq > self.fstop:
|
||||
|
@ -241,7 +258,8 @@ class PermeabilityChart(FrequencyChart):
|
|||
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)
|
||||
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)
|
||||
|
@ -255,7 +273,8 @@ class PermeabilityChart(FrequencyChart):
|
|||
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)
|
||||
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)
|
||||
|
@ -268,10 +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
|
||||
|
@ -283,10 +300,12 @@ class PermeabilityChart(FrequencyChart):
|
|||
else:
|
||||
return -1
|
||||
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)
|
||||
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
|
||||
|
@ -298,12 +317,14 @@ class PermeabilityChart(FrequencyChart):
|
|||
else:
|
||||
return -1
|
||||
return int(
|
||||
self.topMargin + (math.log(self.max) - math.log(re)) /
|
||||
span * self.dim.height)
|
||||
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)
|
||||
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
|
|
@ -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,10 +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()))
|
||||
lambda: self.setUnwrap(self.action_unwrap.isChecked())
|
||||
)
|
||||
self.y_menu.addAction(self.action_unwrap)
|
||||
|
||||
def copy(self):
|
||||
|
@ -63,7 +63,7 @@ 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
|
||||
|
||||
|
@ -98,24 +98,32 @@ class PhaseChart(FrequencyChart):
|
|||
for i in range(tickcount):
|
||||
angle = minAngle + span * i / tickcount
|
||||
y = self.topMargin + int(
|
||||
(self.maxAngle - angle) / self.span * self.dim.height)
|
||||
(self.maxAngle - angle) / self.span * self.dim.height
|
||||
)
|
||||
if angle not in [minAngle, maxAngle]:
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
qp.setPen(QPen(Chart.color.text))
|
||||
if angle != 0:
|
||||
digits = max(
|
||||
0, min(2, math.floor(3 - math.log10(abs(angle)))))
|
||||
anglestr = str(round(angle)) if digits == 0 else str(
|
||||
round(angle, digits))
|
||||
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, f"{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.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, f"{maxAngle}°")
|
||||
qp.drawText(3, self.dim.height + self.topMargin, f"{minAngle}°")
|
||||
|
@ -139,9 +147,10 @@ class PhaseChart(FrequencyChart):
|
|||
else:
|
||||
angle = math.degrees(d.phase)
|
||||
return self.topMargin + int(
|
||||
(self.maxAngle - angle) / self.span * self.dim.height)
|
||||
(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]
|
|
@ -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)
|
|
@ -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
|
||||
|
@ -57,7 +56,7 @@ class QualityFactorChart(FrequencyChart):
|
|||
scale = 0
|
||||
if maxQ > 0:
|
||||
scale = max(scale, math.floor(math.log10(maxQ)))
|
||||
maxQ = math.ceil(maxQ / 10 ** scale) * 10 ** scale
|
||||
maxQ = math.ceil(maxQ / 10**scale) * 10**scale
|
||||
|
||||
self.minQ = self.minDisplayValue
|
||||
self.maxQ = maxQ
|
||||
|
@ -69,8 +68,9 @@ class QualityFactorChart(FrequencyChart):
|
|||
|
||||
for i in range(tickcount):
|
||||
q = self.minQ + i * self.span / tickcount
|
||||
y = self.topMargin + int((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)
|
||||
|
@ -79,12 +79,15 @@ class QualityFactorChart(FrequencyChart):
|
|||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
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)
|
||||
|
@ -119,10 +122,11 @@ class QualityFactorChart(FrequencyChart):
|
|||
|
||||
def getYPosition(self, d: Datapoint) -> int:
|
||||
Q = d.qFactor()
|
||||
return self.topMargin + int((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]
|
|
@ -18,9 +18,8 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import math
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from PyQt5 import QtWidgets, QtGui
|
||||
from PyQt6 import QtWidgets, QtGui
|
||||
|
||||
from NanoVNASaver.Formatting import format_frequency_chart
|
||||
from NanoVNASaver.Marker.Widget import Marker
|
||||
|
@ -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,23 +83,6 @@ 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 not self.data and not self.reference:
|
||||
return
|
||||
|
@ -154,11 +111,14 @@ class RealImaginaryChart(FrequencyChart):
|
|||
self.drawHorizontalTicks(qp)
|
||||
|
||||
fmt = Format(max_nr_digits=3)
|
||||
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)
|
||||
|
||||
|
@ -175,8 +135,12 @@ 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)
|
||||
|
@ -205,7 +169,8 @@ class RealImaginaryChart(FrequencyChart):
|
|||
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)
|
||||
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)
|
||||
|
@ -219,7 +184,8 @@ class RealImaginaryChart(FrequencyChart):
|
|||
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)
|
||||
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)
|
||||
|
@ -241,8 +207,12 @@ 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, reference in enumerate(self.reference):
|
||||
if reference.freq < self.fstart or reference.freq > self.fstop:
|
||||
|
@ -269,7 +239,8 @@ class RealImaginaryChart(FrequencyChart):
|
|||
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)
|
||||
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)
|
||||
|
@ -283,7 +254,8 @@ class RealImaginaryChart(FrequencyChart):
|
|||
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)
|
||||
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)
|
||||
|
@ -296,10 +268,8 @@ 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
|
||||
|
@ -308,8 +278,9 @@ class RealImaginaryChart(FrequencyChart):
|
|||
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.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
|
||||
|
@ -317,7 +288,8 @@ class RealImaginaryChart(FrequencyChart):
|
|||
qp.drawText(
|
||||
self.leftMargin + self.dim.width + 8,
|
||||
y + 4,
|
||||
f"{Value(im, fmt=fmt)}")
|
||||
f"{Value(im, fmt=fmt)}",
|
||||
)
|
||||
|
||||
def find_scaling(self):
|
||||
# Find scaling
|
||||
|
@ -333,7 +305,7 @@ class RealImaginaryChart(FrequencyChart):
|
|||
max_real = 0
|
||||
max_imag = -1000
|
||||
for d in self.data:
|
||||
imp = self.impedance(d)
|
||||
imp = self.value(d)
|
||||
re, im = imp.real, imp.imag
|
||||
if math.isinf(re): # Avoid infinite scales
|
||||
continue
|
||||
|
@ -345,7 +317,7 @@ class RealImaginaryChart(FrequencyChart):
|
|||
for d in self.reference:
|
||||
if d.freq < self.fstart or d.freq > self.fstop:
|
||||
continue
|
||||
imp = self.impedance(d)
|
||||
imp = self.value(d)
|
||||
re, im = imp.real, imp.imag
|
||||
if math.isinf(re): # Avoid infinite scales
|
||||
continue
|
||||
|
@ -393,21 +365,25 @@ class RealImaginaryChart(FrequencyChart):
|
|||
return min_imag, max_imag
|
||||
|
||||
def getImYPosition(self, d: Datapoint) -> int:
|
||||
im = self.impedance(d).imag
|
||||
return int(self.topMargin + (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 int(self.topMargin + (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):
|
||||
|
@ -431,7 +407,7 @@ class RealImaginaryChart(FrequencyChart):
|
|||
|
||||
self.update()
|
||||
|
||||
def getNearestMarker(self, x, y) -> Optional[Marker]:
|
||||
def getNearestMarker(self, x, y) -> Marker | None:
|
||||
if not self.data:
|
||||
return None
|
||||
shortest = 10e6
|
||||
|
@ -450,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):
|
||||
|
@ -462,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):
|
||||
|
@ -474,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):
|
||||
|
@ -486,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):
|
||||
|
@ -498,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)
|
||||
|
@ -508,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()
|
|
@ -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))
|
||||
)
|
|
@ -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()
|
|
@ -19,12 +19,11 @@
|
|||
import logging
|
||||
|
||||
from NanoVNASaver.RFTools import Datapoint
|
||||
from .RI import RealImaginaryChart
|
||||
from .RIZ import RealImaginaryZChart
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RealImaginarySeriesChart(RealImaginaryChart):
|
||||
|
||||
class RealImaginaryZSeriesChart(RealImaginaryZChart):
|
||||
def impedance(self, p: Datapoint) -> complex:
|
||||
return p.seriesImpedance()
|
|
@ -19,12 +19,11 @@
|
|||
import logging
|
||||
|
||||
from NanoVNASaver.RFTools import Datapoint
|
||||
from .RI import RealImaginaryChart
|
||||
from .RIZ import RealImaginaryZChart
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RealImaginaryShuntChart(RealImaginaryChart):
|
||||
|
||||
class RealImaginaryZShuntChart(RealImaginaryZChart):
|
||||
def impedance(self, p: Datapoint) -> complex:
|
||||
return p.shuntImpedance()
|
|
@ -17,9 +17,8 @@
|
|||
# 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 List
|
||||
|
||||
from PyQt5 import QtGui
|
||||
from PyQt6 import QtGui
|
||||
|
||||
from NanoVNASaver.RFTools import Datapoint
|
||||
from NanoVNASaver.Charts.Chart import Chart
|
||||
|
@ -52,14 +51,18 @@ class SParameterChart(FrequencyChart):
|
|||
qp.drawText(10, 15, "Real")
|
||||
qp.drawText(self.leftMargin + self.dim.width - 15, 15, "Imag")
|
||||
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.data) == 0 and len(self.reference) == 0:
|
||||
|
@ -82,49 +85,63 @@ class SParameterChart(FrequencyChart):
|
|||
tick_count = self.dim.height // 60
|
||||
tick_step = self.span / tick_count
|
||||
for i in range(tick_count):
|
||||
val = minValue + i * tick_step
|
||||
val = int(minValue + i * tick_step)
|
||||
y = self.topMargin + (maxValue - val) // 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 val > minValue and val != maxValue:
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
qp.drawText(3, y + 4, str(round(val, 2)))
|
||||
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"{maxValue}")
|
||||
qp.drawText(3, self.dim.height + self.topMargin, f"{minValue}")
|
||||
self.drawFrequencyTicks(qp)
|
||||
self.drawData(qp, self.data, Chart.color.sweep, self.getReYPosition)
|
||||
self.drawData(qp, self.reference, Chart.color.reference,
|
||||
self.getReYPosition)
|
||||
self.drawData(qp, self.data, Chart.color.sweep_secondary,
|
||||
self.getImYPosition)
|
||||
self.drawData(qp, self.reference,
|
||||
Chart.color.reference_secondary, self.getImYPosition)
|
||||
self.drawData(
|
||||
qp, self.reference, Chart.color.reference, self.getReYPosition
|
||||
)
|
||||
self.drawData(
|
||||
qp, self.data, Chart.color.sweep_secondary, self.getImYPosition
|
||||
)
|
||||
self.drawData(
|
||||
qp,
|
||||
self.reference,
|
||||
Chart.color.reference_secondary,
|
||||
self.getImYPosition,
|
||||
)
|
||||
|
||||
self.drawMarkers(qp, y_function=self.getReYPosition)
|
||||
self.drawMarkers(qp, y_function=self.getImYPosition)
|
||||
|
||||
def getYPosition(self, d: Datapoint) -> int:
|
||||
return int(
|
||||
self.topMargin + (self.maxValue - d.re) / self.span *
|
||||
self.dim.height)
|
||||
self.topMargin
|
||||
+ (self.maxValue - d.re) / self.span * self.dim.height
|
||||
)
|
||||
|
||||
def getReYPosition(self, d: Datapoint) -> int:
|
||||
return int(
|
||||
self.topMargin + (self.maxValue - d.re) / self.span *
|
||||
self.dim.height)
|
||||
self.topMargin
|
||||
+ (self.maxValue - d.re) / self.span * self.dim.height
|
||||
)
|
||||
|
||||
def getImYPosition(self, d: Datapoint) -> int:
|
||||
return int(
|
||||
self.topMargin + (self.maxValue - d.im) / self.span *
|
||||
self.dim.height)
|
||||
self.topMargin
|
||||
+ (self.maxValue - d.im) / 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]
|
|
@ -0,0 +1,165 @@
|
|||
# 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 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 = int(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.AlignmentFlag.AlignCenter,
|
||||
f"{swr}",
|
||||
)
|
|
@ -18,9 +18,8 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
import math
|
||||
from typing import List
|
||||
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
from PyQt6 import QtGui, QtCore, QtWidgets
|
||||
|
||||
from NanoVNASaver.Charts.Chart import Chart
|
||||
from NanoVNASaver.RFTools import Datapoint
|
||||
|
@ -29,18 +28,19 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class SquareChart(Chart):
|
||||
def __init__(self, name=''):
|
||||
def __init__(self, name=""):
|
||||
super().__init__(name)
|
||||
sizepolicy = QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.Fixed,
|
||||
QtWidgets.QSizePolicy.MinimumExpanding)
|
||||
QtWidgets.QSizePolicy.Policy.Fixed,
|
||||
QtWidgets.QSizePolicy.Policy.MinimumExpanding,
|
||||
)
|
||||
self.setSizePolicy(sizepolicy)
|
||||
self.dim.width = 250
|
||||
self.dim.height = 250
|
||||
self.setMinimumSize(self.dim.width + 40, self.dim.height + 40)
|
||||
|
||||
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)
|
||||
|
||||
|
@ -53,8 +53,14 @@ class SquareChart(Chart):
|
|||
def drawChart(self, qp: QtGui.QPainter) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def draw_data(self, qp: QtGui.QPainter, color: QtGui.QColor,
|
||||
data: List[Datapoint], fstart: int = 0, fstop: int = 0):
|
||||
def draw_data(
|
||||
self,
|
||||
qp: QtGui.QPainter,
|
||||
color: QtGui.QColor,
|
||||
data: list[Datapoint],
|
||||
fstart: int = 0,
|
||||
fstop: int = 0,
|
||||
):
|
||||
if not data:
|
||||
return
|
||||
fstop = fstop or data[-1].freq
|
||||
|
@ -65,8 +71,7 @@ class SquareChart(Chart):
|
|||
|
||||
qp.setPen(pen)
|
||||
prev_x = self.getXPosition(data[0])
|
||||
prev_y = int(self.height() / 2 + data[0].im * -1 *
|
||||
self.dim.height / 2)
|
||||
prev_y = int(self.height() / 2 + data[0].im * -1 * self.dim.height / 2)
|
||||
for i, d in enumerate(data):
|
||||
x = self.getXPosition(d)
|
||||
y = int(self.height() / 2 + d.im * -1 * self.dim.height / 2)
|
||||
|
@ -85,14 +90,15 @@ class SquareChart(Chart):
|
|||
|
||||
fstart = self.data[0].freq if self.data else 0
|
||||
fstop = self.data[-1].freq if self.data else 0
|
||||
self.draw_data(qp, Chart.color.reference,
|
||||
self.reference, fstart, fstop)
|
||||
self.draw_data(qp, Chart.color.reference, self.reference, fstart, fstop)
|
||||
|
||||
for m in self.markers:
|
||||
if m.location != -1 and m.location < len(self.data):
|
||||
x = self.getXPosition(self.data[m.location])
|
||||
y = int(self.height() // 2 -
|
||||
self.data[m.location].im * self.dim.height // 2)
|
||||
y = int(
|
||||
self.height() // 2
|
||||
- self.data[m.location].im * self.dim.height // 2
|
||||
)
|
||||
self.drawMarker(x, y, qp, m.color, self.markers.index(m) + 1)
|
||||
|
||||
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
|
||||
|
@ -106,19 +112,21 @@ class SquareChart(Chart):
|
|||
self.update()
|
||||
|
||||
def mouseMoveEvent(self, a0: QtGui.QMouseEvent):
|
||||
if a0.buttons() == QtCore.Qt.RightButton:
|
||||
if a0.buttons() == QtCore.Qt.MouseButton.RightButton:
|
||||
a0.ignore()
|
||||
return
|
||||
|
||||
x = a0.x()
|
||||
y = a0.y()
|
||||
x = a0.position().x()
|
||||
y = a0.position().y()
|
||||
absx = x - (self.width() - self.dim.width) / 2
|
||||
absy = y - (self.height() - self.dim.height) / 2
|
||||
if (absx < 0 or
|
||||
absx > self.dim.width or
|
||||
absy < 0 or
|
||||
absy > self.dim.height or
|
||||
(not self.data and not self.reference)):
|
||||
if (
|
||||
absx < 0
|
||||
or absx > self.dim.width
|
||||
or absy < 0
|
||||
or absy > self.dim.height
|
||||
or (not self.data and not self.reference)
|
||||
):
|
||||
a0.ignore()
|
||||
return
|
||||
a0.accept()
|
||||
|
@ -133,8 +141,9 @@ class SquareChart(Chart):
|
|||
|
||||
positions = [
|
||||
math.sqrt(
|
||||
(x - (width_2 + d.re * dim_x_2))**2 +
|
||||
(y - (height_2 - d.im * dim_y_2))**2)
|
||||
(x - (width_2 + d.re * dim_x_2)) ** 2
|
||||
+ (y - (height_2 - d.im * dim_y_2)) ** 2
|
||||
)
|
||||
for d in target
|
||||
]
|
||||
|
|
@ -0,0 +1,560 @@
|
|||
# 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 PyQt6.QtCore import QPoint, QRect, Qt
|
||||
from PyQt6.QtGui import (
|
||||
QAction,
|
||||
QActionGroup,
|
||||
QMouseEvent,
|
||||
QPalette,
|
||||
QPainter,
|
||||
QPaintEvent,
|
||||
QPen,
|
||||
QResizeEvent,
|
||||
)
|
||||
from PyQt6.QtWidgets import QInputDialog, QMenu, QSizePolicy
|
||||
|
||||
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(
|
||||
QSizePolicy(
|
||||
QSizePolicy.Policy.MinimumExpanding,
|
||||
QSizePolicy.Policy.MinimumExpanding,
|
||||
)
|
||||
)
|
||||
pal = QPalette()
|
||||
pal.setColor(QPalette.ColorRole.Window, Chart.color.background)
|
||||
self.setPalette(pal)
|
||||
self.setAutoFillBackground(True)
|
||||
|
||||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.DefaultContextMenu)
|
||||
self.menu = QMenu()
|
||||
|
||||
self.reset = QAction("Reset")
|
||||
self.reset.triggered.connect(self.resetDisplayLimits)
|
||||
self.menu.addAction(self.reset)
|
||||
|
||||
self.x_menu = QMenu("Length axis")
|
||||
self.mode_group = QActionGroup(self.x_menu)
|
||||
self.action_automatic = 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 = 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 = QAction(
|
||||
f"Start ({self.minDisplayLength})"
|
||||
)
|
||||
self.action_set_fixed_start.triggered.connect(self.setMinimumLength)
|
||||
|
||||
self.action_set_fixed_stop = 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 = QMenu("Impedance axis")
|
||||
self.y_mode_group = QActionGroup(self.y_menu)
|
||||
self.y_action_automatic = 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 = 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 = QAction(
|
||||
f"Maximum ({self.maxImpedance})"
|
||||
)
|
||||
self.y_action_set_fixed_maximum.triggered.connect(
|
||||
self.setMaximumImpedance
|
||||
)
|
||||
|
||||
self.y_action_set_fixed_minimum = 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 = 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 = 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 = 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 = 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 = 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: QMouseEvent) -> None:
|
||||
if a0.buttons() == Qt.MouseButton.RightButton:
|
||||
a0.ignore()
|
||||
return
|
||||
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.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.position().x()
|
||||
self.dragbox.move_y = a0.position().y()
|
||||
return
|
||||
if a0.modifiers() == Qt.KeyboardModifier.ControlModifier:
|
||||
# Dragging a box
|
||||
if not self.dragbox.state:
|
||||
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.position().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:
|
||||
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 _draw_ticks(self, height, width, x_step, min_index):
|
||||
ticks = (self.width() - self.leftMargin) // 100
|
||||
qp = QPainter(self)
|
||||
for i in range(ticks):
|
||||
x = self.leftMargin + round((i + 1) * width / ticks)
|
||||
qp.setPen(QPen(Chart.color.foreground))
|
||||
qp.drawLine(x, self.topMargin, x, self.topMargin + height)
|
||||
qp.setPen(QPen(Chart.color.text))
|
||||
distance = (
|
||||
self.tdrWindow.distance_axis[
|
||||
min_index + int((x - self.leftMargin) * x_step) - 1
|
||||
]
|
||||
/ 2
|
||||
)
|
||||
qp.drawText(
|
||||
x - 15, self.topMargin + height + 15, f"{round(distance, 1)}m"
|
||||
)
|
||||
qp.setPen(QPen(Chart.color.text))
|
||||
qp.drawText(
|
||||
self.leftMargin - 10,
|
||||
self.topMargin + height + 15,
|
||||
f"{str(round(self.tdrWindow.distance_axis[min_index] / 2, 1))}m",
|
||||
)
|
||||
|
||||
def _draw_y_ticks(self, height, width, min_impedance, max_impedance):
|
||||
qp = QPainter(self)
|
||||
y_step = (max_impedance - min_impedance) / height
|
||||
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_step * i * y_tick_step
|
||||
qp.setPen(Chart.color.text)
|
||||
qp.drawText(3, y + 3, str(round(y_val, 1)))
|
||||
qp.setPen(Chart.color.text)
|
||||
qp.drawText(
|
||||
3, self.topMargin + height + 3, f"{round(min_impedance, 1)}"
|
||||
)
|
||||
|
||||
def _draw_max_point(self, height, x_step, y_step, min_index):
|
||||
qp = QPainter(self)
|
||||
id_max = np.argmax(self.tdrWindow.td)
|
||||
|
||||
max_point = 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,
|
||||
f"{round(self.tdrWindow.distance_axis[id_max] / 2, 2)}m",
|
||||
)
|
||||
|
||||
def _draw_marker(self, height, x_step, y_step, min_index):
|
||||
qp = QPainter(self)
|
||||
marker_point = 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,
|
||||
f"""{round(
|
||||
self.tdrWindow.distance_axis[self.markerLocation] / 2,
|
||||
2)}m""",
|
||||
)
|
||||
|
||||
def _draw_graph(self, height, width):
|
||||
min_index = 0
|
||||
max_index = math.ceil(len(self.tdrWindow.distance_axis) / 2)
|
||||
|
||||
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
|
||||
|
||||
# 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)
|
||||
if self.fixedValues:
|
||||
min_impedance = max(0, self.minImpedance)
|
||||
max_impedance = max(0.1, self.maxImpedance)
|
||||
|
||||
y_step = max(self.tdrWindow.td) * 1.1 / height or 1.0e-30
|
||||
|
||||
self._draw_ticks(height, width, x_step, min_index)
|
||||
self._draw_y_ticks(height, width, min_impedance, max_impedance)
|
||||
|
||||
qp = QPainter(self)
|
||||
pen = QPen(Chart.color.sweep)
|
||||
pen.setWidth(self.dim.point)
|
||||
qp.setPen(pen)
|
||||
|
||||
y_step = (max_impedance - min_impedance) / height
|
||||
for i in range(min_index, max_index):
|
||||
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_step
|
||||
)
|
||||
if self.isPlotable(x, y):
|
||||
pen.setColor(Chart.color.sweep_secondary)
|
||||
qp.setPen(pen)
|
||||
qp.drawPoint(x, y)
|
||||
|
||||
self._draw_max_point(height, x_step, y_step, min_index)
|
||||
|
||||
if self.markerLocation != -1:
|
||||
self._draw_marker(height, x_step, y_step, min_index)
|
||||
|
||||
def paintEvent(self, _: QPaintEvent) -> None:
|
||||
qp = QPainter(self)
|
||||
qp.setPen(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(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
|
||||
self.drawTitle(qp)
|
||||
|
||||
if self.tdrWindow.td:
|
||||
self._draw_graph(height, width)
|
||||
|
||||
if self.dragbox.state and self.dragbox.pos[0] != -1:
|
||||
dashed_pen = QPen(Chart.color.foreground, 1, Qt.PenStyle.DashLine)
|
||||
qp.setPen(dashed_pen)
|
||||
qp.drawRect(
|
||||
QRect(
|
||||
QPoint(*self.dragbox.pos_start),
|
||||
QPoint(*self.dragbox.pos),
|
||||
)
|
||||
)
|
||||
|
||||
qp.end()
|
||||
|
||||
def valueAtPosition(self, y):
|
||||
if self.tdrWindow.td:
|
||||
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:
|
||||
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: QResizeEvent) -> None:
|
||||
super().resizeEvent(a0)
|
||||
self.dim.width = self.width() - self.leftMargin - self.rightMargin
|
||||
self.dim.height = self.height() - self.bottomMargin - self.topMargin
|
|
@ -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
|
||||
|
@ -30,7 +29,6 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class VSWRChart(FrequencyChart):
|
||||
|
||||
def __init__(self, name=""):
|
||||
super().__init__(name)
|
||||
|
||||
|
@ -90,19 +88,22 @@ class VSWRChart(FrequencyChart):
|
|||
qp.setPen(Chart.color.text)
|
||||
if vswr != 0:
|
||||
digits = max(
|
||||
0, min(2, math.floor(3 - math.log10(abs(vswr)))))
|
||||
0, min(2, math.floor(3 - math.log10(abs(vswr))))
|
||||
)
|
||||
v_text = f"{round(vswr, digits)}" if digits else "0"
|
||||
qp.drawText(3, y + 3, v_text)
|
||||
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.dim.height,
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin + self.dim.height)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||
)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5,
|
||||
self.topMargin + self.dim.height,
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin + self.dim.height,
|
||||
)
|
||||
qp.setPen(Chart.color.text)
|
||||
digits = max(
|
||||
0, min(2, math.floor(3 - math.log10(abs(minVSWR)))))
|
||||
digits = max(0, min(2, math.floor(3 - math.log10(abs(minVSWR)))))
|
||||
v_text = f"{round(minVSWR, digits)}" if digits else "0"
|
||||
qp.drawText(3, self.topMargin + self.dim.height, v_text)
|
||||
else:
|
||||
|
@ -112,16 +113,20 @@ class VSWRChart(FrequencyChart):
|
|||
qp.setPen(Chart.color.text)
|
||||
if vswr != 0:
|
||||
digits = max(
|
||||
0, min(2, math.floor(3 - math.log10(abs(vswr)))))
|
||||
0, min(2, math.floor(3 - math.log10(abs(vswr))))
|
||||
)
|
||||
vswrstr = f"{round(vswr, digits)}" if digits else "0"
|
||||
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,
|
||||
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)
|
||||
digits = max(0, min(2, math.floor(3 - math.log10(abs(maxVSWR)))))
|
||||
v_text = f"{round(maxVSWR, digits)}" if digits else "0"
|
||||
|
@ -130,8 +135,7 @@ class VSWRChart(FrequencyChart):
|
|||
qp.setPen(Chart.color.swr)
|
||||
for vswr in self.swrMarkers:
|
||||
y = self.getYPositionFromValue(vswr)
|
||||
qp.drawLine(self.leftMargin, y,
|
||||
self.leftMargin + self.dim.width, y)
|
||||
qp.drawLine(self.leftMargin, y, self.leftMargin + self.dim.width, y)
|
||||
qp.drawText(self.leftMargin + 3, y - 1, str(vswr))
|
||||
|
||||
self.drawFrequencyTicks(qp)
|
||||
|
@ -146,20 +150,22 @@ class VSWRChart(FrequencyChart):
|
|||
span = math.log(self.maxVSWR) - math.log(min_val)
|
||||
else:
|
||||
return -1
|
||||
return (
|
||||
self.topMargin + int(
|
||||
(math.log(self.maxVSWR) - math.log(vswr)) /
|
||||
span * self.dim.height))
|
||||
return self.topMargin + int(
|
||||
(math.log(self.maxVSWR) - math.log(vswr))
|
||||
/ span
|
||||
* self.dim.height
|
||||
)
|
||||
try:
|
||||
return self.topMargin + int(
|
||||
(self.maxVSWR - vswr) / self.span * self.dim.height)
|
||||
(self.maxVSWR - vswr) / self.span * self.dim.height
|
||||
)
|
||||
except OverflowError:
|
||||
return self.topMargin
|
||||
|
||||
def getYPosition(self, d: Datapoint) -> int:
|
||||
return self.getYPositionFromValue(d.vswr)
|
||||
|
||||
def valueAtPosition(self, y) -> List[float]:
|
||||
def valueAtPosition(self, y) -> list[float]:
|
||||
absy = y - self.topMargin
|
||||
if self.logarithmicY:
|
||||
min_val = self.maxVSWR - self.span
|
|
@ -15,9 +15,39 @@ from .Permeability import PermeabilityChart
|
|||
from .Phase import PhaseChart
|
||||
from .QFactor import QualityFactorChart
|
||||
from .RI import RealImaginaryChart
|
||||
from .RIShunt import RealImaginaryShuntChart
|
||||
from .RISeries import RealImaginarySeriesChart
|
||||
from .RIMu import RealImaginaryMuChart
|
||||
from .RIZ import RealImaginaryZChart
|
||||
from .RIZShunt import RealImaginaryZShuntChart
|
||||
from .RIZSeries import RealImaginaryZSeriesChart
|
||||
from .Smith import SmithChart
|
||||
from .SParam import SParameterChart
|
||||
from .TDR import TDRChart
|
||||
from .VSWR import VSWRChart
|
||||
|
||||
__all__ = [
|
||||
"Chart",
|
||||
"FrequencyChart",
|
||||
"PolarChart",
|
||||
"SquareChart",
|
||||
"CapacitanceChart",
|
||||
"InductanceChart",
|
||||
"GroupDelayChart",
|
||||
"LogMagChart",
|
||||
"CombinedLogMagChart",
|
||||
"MagnitudeChart",
|
||||
"MagnitudeZChart",
|
||||
"MagnitudeZShuntChart",
|
||||
"MagnitudeZSeriesChart",
|
||||
"PermeabilityChart",
|
||||
"PhaseChart",
|
||||
"QualityFactorChart",
|
||||
"RealImaginaryChart",
|
||||
"RealImaginaryMuChart",
|
||||
"RealImaginaryZChart",
|
||||
"RealImaginaryZShuntChart",
|
||||
"RealImaginaryZSeriesChart",
|
||||
"SmithChart",
|
||||
"SParameterChart",
|
||||
"TDRChart",
|
||||
"VSWRChart",
|
||||
]
|
|
@ -18,7 +18,7 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
from PyQt6 import QtWidgets, QtCore
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -29,6 +29,6 @@ class Control(QtWidgets.QGroupBox):
|
|||
def __init__(self, app: QtWidgets.QWidget, title: str = ""):
|
||||
super().__init__()
|
||||
self.app = app
|
||||
self.setMaximumWidth(240)
|
||||
self.setMaximumWidth(250)
|
||||
self.setTitle(title)
|
||||
self.layout = QtWidgets.QFormLayout(self)
|
|
@ -18,8 +18,8 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
from PyQt5.QtWidgets import QCheckBox
|
||||
from PyQt6 import QtWidgets, QtCore
|
||||
from PyQt6.QtWidgets import QCheckBox, QSizePolicy
|
||||
|
||||
from NanoVNASaver import Defaults
|
||||
from NanoVNASaver.Marker.Widget import Marker
|
||||
|
@ -29,16 +29,16 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class ShowButton(QtWidgets.QPushButton):
|
||||
def setText(self, text: str = ''):
|
||||
def setText(self, text: str = ""):
|
||||
if not text:
|
||||
text = ("Show data"
|
||||
if Defaults.cfg.gui.markers_hidden else "Hide data")
|
||||
text = (
|
||||
"Show data" if Defaults.cfg.gui.markers_hidden else "Hide data"
|
||||
)
|
||||
super().setText(text)
|
||||
self.setToolTip("Toggle visibility of marker readings area")
|
||||
|
||||
|
||||
class MarkerControl(Control):
|
||||
|
||||
def __init__(self, app: QtWidgets.QWidget):
|
||||
super().__init__(app, "Markers")
|
||||
|
||||
|
@ -55,7 +55,7 @@ class MarkerControl(Control):
|
|||
self.check_delta = QCheckBox("Enable Delta Marker")
|
||||
self.check_delta.toggled.connect(self.toggle_delta)
|
||||
|
||||
self.check_delta_reference = QCheckBox("reference")
|
||||
self.check_delta_reference = QCheckBox("Reference")
|
||||
self.check_delta_reference.toggled.connect(self.toggle_delta_reference)
|
||||
|
||||
layout2 = QtWidgets.QHBoxLayout()
|
||||
|
@ -70,9 +70,12 @@ class MarkerControl(Control):
|
|||
self.showMarkerButton.clicked.connect(self.toggle_frame)
|
||||
|
||||
lock_radiobutton = QtWidgets.QRadioButton("Locked")
|
||||
lock_radiobutton.setLayoutDirection(QtCore.Qt.RightToLeft)
|
||||
lock_radiobutton.setLayoutDirection(
|
||||
QtCore.Qt.LayoutDirection.RightToLeft
|
||||
)
|
||||
lock_radiobutton.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred)
|
||||
QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred
|
||||
)
|
||||
|
||||
hbox = QtWidgets.QHBoxLayout()
|
||||
hbox.addWidget(self.showMarkerButton)
|
||||
|
@ -82,8 +85,7 @@ class MarkerControl(Control):
|
|||
def toggle_frame(self):
|
||||
def settings(hidden: bool):
|
||||
Defaults.cfg.gui.markers_hidden = not hidden
|
||||
self.app.marker_frame.setHidden(
|
||||
Defaults.cfg.gui.markers_hidden)
|
||||
self.app.marker_frame.setHidden(Defaults.cfg.gui.markers_hidden)
|
||||
self.showMarkerButton.setText()
|
||||
self.showMarkerButton.repaint()
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
import logging
|
||||
from time import sleep
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
from PyQt6 import QtWidgets
|
||||
|
||||
from NanoVNASaver.Hardware.Hardware import Interface, get_interfaces, get_VNA
|
||||
from NanoVNASaver.Controls.Control import Control
|
||||
|
@ -28,7 +28,6 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class SerialControl(Control):
|
||||
|
||||
def __init__(self, app: QtWidgets.QWidget):
|
||||
super().__init__(app, "Serial port control")
|
||||
|
||||
|
@ -58,7 +57,8 @@ class SerialControl(Control):
|
|||
self.btn_settings.setMinimumHeight(20)
|
||||
self.btn_settings.setFixedWidth(60)
|
||||
self.btn_settings.clicked.connect(
|
||||
lambda: self.app.display_window("device_settings"))
|
||||
lambda: self.app.display_window("device_settings")
|
||||
)
|
||||
|
||||
button_layout.addWidget(self.btn_settings, stretch=0)
|
||||
self.layout.addRow(button_layout)
|
||||
|
@ -82,8 +82,9 @@ class SerialControl(Control):
|
|||
try:
|
||||
self.interface.open()
|
||||
except (IOError, AttributeError) as exc:
|
||||
logger.error("Tried to open %s and failed: %s",
|
||||
self.interface, exc)
|
||||
logger.error(
|
||||
"Tried to open %s and failed: %s", self.interface, exc
|
||||
)
|
||||
return
|
||||
if not self.interface.isOpen():
|
||||
logger.error("Unable to open port %s", self.interface)
|
||||
|
@ -96,7 +97,8 @@ class SerialControl(Control):
|
|||
logger.error("Unable to connect to VNA: %s", exc)
|
||||
|
||||
self.app.vna.validateInput = self.app.settings.value(
|
||||
"SerialInputValidation", True, bool)
|
||||
"SerialInputValidation", False, bool
|
||||
)
|
||||
|
||||
# connected
|
||||
self.btn_toggle.setText("Disconnect")
|
||||
|
@ -106,16 +108,20 @@ class SerialControl(Control):
|
|||
if not frequencies:
|
||||
logger.warning("No frequencies read")
|
||||
return
|
||||
logger.info("Read starting frequency %s and end frequency %s",
|
||||
frequencies[0], frequencies[-1])
|
||||
logger.info(
|
||||
"Read starting frequency %s and end frequency %s",
|
||||
frequencies[0],
|
||||
frequencies[-1],
|
||||
)
|
||||
self.app.sweep_control.set_start(frequencies[0])
|
||||
if frequencies[0] < frequencies[-1]:
|
||||
self.app.sweep_control.set_end(frequencies[-1])
|
||||
else:
|
||||
self.app.sweep_control.set_end(
|
||||
frequencies[0] +
|
||||
self.app.vna.datapoints *
|
||||
self.app.sweep_control.get_segments())
|
||||
frequencies[0]
|
||||
+ self.app.vna.datapoints
|
||||
* self.app.sweep_control.get_segments()
|
||||
)
|
||||
|
||||
self.app.sweep_control.set_segments(1) # speed up things
|
||||
self.app.sweep_control.update_center_span()
|
|
@ -18,11 +18,13 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
from PyQt6 import QtWidgets, QtCore
|
||||
|
||||
from NanoVNASaver.Formatting import (
|
||||
format_frequency_sweep, format_frequency_short,
|
||||
parse_frequency)
|
||||
format_frequency_sweep,
|
||||
format_frequency_short,
|
||||
parse_frequency,
|
||||
)
|
||||
from NanoVNASaver.Inputs import FrequencyInputWidget
|
||||
from NanoVNASaver.Controls.Control import Control
|
||||
|
||||
|
@ -30,12 +32,11 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class SweepControl(Control):
|
||||
|
||||
def __init__(self, app: QtWidgets.QWidget):
|
||||
super().__init__(app, "Sweep control")
|
||||
|
||||
line = QtWidgets.QFrame()
|
||||
line.setFrameShape(QtWidgets.QFrame.VLine)
|
||||
line.setFrameShape(QtWidgets.QFrame.Shape.VLine)
|
||||
|
||||
input_layout = QtWidgets.QHBoxLayout()
|
||||
input_left_layout = QtWidgets.QFormLayout()
|
||||
|
@ -48,14 +49,14 @@ class SweepControl(Control):
|
|||
self.input_start = FrequencyInputWidget()
|
||||
self.input_start.setFixedHeight(20)
|
||||
self.input_start.setMinimumWidth(60)
|
||||
self.input_start.setAlignment(QtCore.Qt.AlignRight)
|
||||
self.input_start.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
|
||||
self.input_start.textEdited.connect(self.update_center_span)
|
||||
self.input_start.textChanged.connect(self.update_step_size)
|
||||
input_left_layout.addRow(QtWidgets.QLabel("Start"), self.input_start)
|
||||
|
||||
self.input_end = FrequencyInputWidget()
|
||||
self.input_end.setFixedHeight(20)
|
||||
self.input_end.setAlignment(QtCore.Qt.AlignRight)
|
||||
self.input_end.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
|
||||
self.input_end.textEdited.connect(self.update_center_span)
|
||||
self.input_end.textChanged.connect(self.update_step_size)
|
||||
input_left_layout.addRow(QtWidgets.QLabel("Stop"), self.input_end)
|
||||
|
@ -63,29 +64,31 @@ class SweepControl(Control):
|
|||
self.input_center = FrequencyInputWidget()
|
||||
self.input_center.setFixedHeight(20)
|
||||
self.input_center.setMinimumWidth(60)
|
||||
self.input_center.setAlignment(QtCore.Qt.AlignRight)
|
||||
self.input_center.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
|
||||
self.input_center.textEdited.connect(self.update_start_end)
|
||||
|
||||
input_right_layout.addRow(QtWidgets.QLabel(
|
||||
"Center"), self.input_center)
|
||||
input_right_layout.addRow(QtWidgets.QLabel("Center"), self.input_center)
|
||||
|
||||
self.input_span = FrequencyInputWidget()
|
||||
self.input_span.setFixedHeight(20)
|
||||
self.input_span.setAlignment(QtCore.Qt.AlignRight)
|
||||
self.input_span.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
|
||||
self.input_span.textEdited.connect(self.update_start_end)
|
||||
|
||||
input_right_layout.addRow(QtWidgets.QLabel("Span"), self.input_span)
|
||||
|
||||
self.input_segments = QtWidgets.QLineEdit(
|
||||
self.app.settings.value("Segments", "1"))
|
||||
self.input_segments.setAlignment(QtCore.Qt.AlignRight)
|
||||
self.app.settings.value("Segments", "1")
|
||||
)
|
||||
self.input_segments.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
|
||||
self.input_segments.setFixedHeight(20)
|
||||
self.input_segments.setFixedWidth(60)
|
||||
self.input_segments.textEdited.connect(self.update_step_size)
|
||||
|
||||
self.label_step = QtWidgets.QLabel("Hz/step")
|
||||
self.label_step.setAlignment(
|
||||
QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
QtCore.Qt.AlignmentFlag.AlignRight
|
||||
| QtCore.Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
|
||||
segment_layout = QtWidgets.QHBoxLayout()
|
||||
segment_layout.addWidget(self.input_segments)
|
||||
|
@ -95,7 +98,8 @@ class SweepControl(Control):
|
|||
btn_settings_window = QtWidgets.QPushButton("Sweep settings ...")
|
||||
btn_settings_window.setFixedHeight(20)
|
||||
btn_settings_window.clicked.connect(
|
||||
lambda: self.app.display_window("sweep_settings"))
|
||||
lambda: self.app.display_window("sweep_settings")
|
||||
)
|
||||
|
||||
self.layout.addRow(btn_settings_window)
|
||||
|
||||
|
@ -107,11 +111,13 @@ class SweepControl(Control):
|
|||
self.btn_start = QtWidgets.QPushButton("Sweep")
|
||||
self.btn_start.setFixedHeight(20)
|
||||
self.btn_start.clicked.connect(self.app.sweep_start)
|
||||
self.btn_start.setShortcut(QtCore.Qt.Key_W | QtCore.Qt.CTRL)
|
||||
self.btn_start.setShortcut(
|
||||
QtCore.Qt.Key.Key_Control + QtCore.Qt.Key.Key_W
|
||||
)
|
||||
self.btn_stop = QtWidgets.QPushButton("Stop")
|
||||
self.btn_stop.setFixedHeight(20)
|
||||
self.btn_stop.clicked.connect(self.app.sweep_stop)
|
||||
self.btn_stop.setShortcut(QtCore.Qt.Key_Escape)
|
||||
self.btn_stop.setShortcut(QtCore.Qt.Key.Key_Escape)
|
||||
self.btn_stop.setDisabled(True)
|
||||
btn_layout = QtWidgets.QHBoxLayout()
|
||||
btn_layout.addWidget(self.btn_start)
|
||||
|
@ -206,14 +212,13 @@ class SweepControl(Control):
|
|||
segments = self.get_segments()
|
||||
if segments > 0:
|
||||
fstep = fspan / (segments * self.app.vna.datapoints - 1)
|
||||
self.label_step.setText(
|
||||
f"{format_frequency_short(fstep)}/step")
|
||||
self.label_step.setText(f"{format_frequency_short(fstep)}/step")
|
||||
self.update_sweep()
|
||||
|
||||
def update_sweep(self):
|
||||
sweep = self.app.sweep
|
||||
with sweep.lock:
|
||||
sweep.start = self.get_start()
|
||||
sweep.end = self.get_end()
|
||||
sweep.segments = self.get_segments()
|
||||
sweep.points = self.app.vna.datapoints
|
||||
self.app.sweep.update(
|
||||
start=self.get_start(),
|
||||
end=self.get_end(),
|
||||
segments=self.get_segments(),
|
||||
points=self.app.vna.datapoints,
|
||||
)
|
|
@ -21,9 +21,8 @@ import dataclasses as DC
|
|||
import logging
|
||||
from ast import literal_eval
|
||||
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5.QtCore import QSettings, QByteArray
|
||||
from PyQt5.QtGui import QColor
|
||||
from PyQt6.QtCore import QSettings, QByteArray
|
||||
from PyQt6.QtGui import QColor, QColorConstants
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -43,12 +42,12 @@ class GUI:
|
|||
|
||||
@DC.dataclass
|
||||
class ChartsSelected:
|
||||
chart_00: str = 'S11 Smith Chart'
|
||||
chart_01: str = 'S11 Return Loss'
|
||||
chart_02: str = 'None'
|
||||
chart_10: str = 'S21 Polar Plot'
|
||||
chart_11: str = 'S21 Gain'
|
||||
chart_12: str = 'None'
|
||||
chart_00: str = "S11 Smith Chart"
|
||||
chart_01: str = "S11 Return Loss"
|
||||
chart_02: str = "None"
|
||||
chart_10: str = "S21 Polar Plot"
|
||||
chart_11: str = "S21 Gain"
|
||||
chart_12: str = "None"
|
||||
|
||||
|
||||
@DC.dataclass
|
||||
|
@ -63,39 +62,57 @@ class Chart:
|
|||
marker_size: int = 8
|
||||
returnloss_is_positive: bool = False
|
||||
show_bands: bool = False
|
||||
vswr_lines: list = DC.field(default_factory=lambda: [])
|
||||
vswr_lines: list = DC.field(default_factory=list)
|
||||
|
||||
|
||||
@DC.dataclass
|
||||
class ChartColors: # pylint: disable=too-many-instance-attributes
|
||||
background: QColor = DC.field(
|
||||
default_factory=lambda: QColor(QtCore.Qt.white))
|
||||
default_factory=lambda: QColor(QColorConstants.White)
|
||||
)
|
||||
foreground: QColor = DC.field(
|
||||
default_factory=lambda: QColor(QtCore.Qt.lightGray))
|
||||
default_factory=lambda: QColor(QColorConstants.LightGray)
|
||||
)
|
||||
reference: QColor = DC.field(default_factory=lambda: QColor(0, 0, 255, 64))
|
||||
reference_secondary: QColor = DC.field(
|
||||
default_factory=lambda: QColor(0, 0, 192, 48))
|
||||
default_factory=lambda: QColor(0, 0, 192, 48)
|
||||
)
|
||||
sweep: QColor = DC.field(
|
||||
default_factory=lambda: QColor(QtCore.Qt.darkYellow))
|
||||
default_factory=lambda: QColor(QColorConstants.DarkYellow)
|
||||
)
|
||||
sweep_secondary: QColor = DC.field(
|
||||
default_factory=lambda: QColor(QtCore.Qt.darkMagenta))
|
||||
swr: QColor = DC.field(
|
||||
default_factory=lambda: QColor(255, 0, 0, 128))
|
||||
default_factory=lambda: QColor(QColorConstants.DarkMagenta)
|
||||
)
|
||||
swr: QColor = DC.field(default_factory=lambda: QColor(255, 0, 0, 128))
|
||||
text: QColor = DC.field(
|
||||
default_factory=lambda: QColor(QtCore.Qt.black))
|
||||
bands: QColor = DC.field(
|
||||
default_factory=lambda: QColor(128, 128, 128, 48))
|
||||
default_factory=lambda: QColor(QColorConstants.Black)
|
||||
)
|
||||
bands: QColor = DC.field(default_factory=lambda: QColor(128, 128, 128, 48))
|
||||
|
||||
|
||||
@DC.dataclass
|
||||
class Markers:
|
||||
active_labels: list = DC.field(default_factory=lambda: [
|
||||
"actualfreq", "impedance", "serr", "serl", "serc", "parr", "parlc",
|
||||
"vswr", "returnloss", "s11q", "s11phase", "s21gain", "s21phase",
|
||||
])
|
||||
active_labels: list = DC.field(
|
||||
default_factory=lambda: [
|
||||
"actualfreq",
|
||||
"impedance",
|
||||
"serr",
|
||||
"serl",
|
||||
"serc",
|
||||
"parr",
|
||||
"parlc",
|
||||
"vswr",
|
||||
"returnloss",
|
||||
"s11q",
|
||||
"s11phase",
|
||||
"s21gain",
|
||||
"s21phase",
|
||||
]
|
||||
)
|
||||
colored_names: bool = True
|
||||
color_0: QColor = DC.field(
|
||||
default_factory=lambda: QColor(QtCore.Qt.darkGray))
|
||||
default_factory=lambda: QColor(QColorConstants.DarkGray)
|
||||
)
|
||||
color_1: QColor = DC.field(default_factory=lambda: QColor(255, 0, 0))
|
||||
color_2: QColor = DC.field(default_factory=lambda: QColor(0, 255, 0))
|
||||
color_3: QColor = DC.field(default_factory=lambda: QColor(0, 0, 255))
|
||||
|
@ -103,37 +120,34 @@ class Markers:
|
|||
color_5: QColor = DC.field(default_factory=lambda: QColor(255, 0, 255))
|
||||
color_6: QColor = DC.field(default_factory=lambda: QColor(255, 255, 0))
|
||||
color_7: QColor = DC.field(
|
||||
default_factory=lambda: QColor(QtCore.Qt.lightGray))
|
||||
default_factory=lambda: QColor(QColorConstants.LightGray)
|
||||
)
|
||||
|
||||
|
||||
@DC.dataclass
|
||||
class CFG:
|
||||
gui: object = DC.field(
|
||||
default_factory=lambda: GUI())
|
||||
charts_selected: object = DC.field(
|
||||
default_factory=lambda: ChartsSelected())
|
||||
chart: object = DC.field(
|
||||
default_factory=lambda: Chart())
|
||||
chart_colors: object = DC.field(
|
||||
default_factory=lambda: ChartColors())
|
||||
markers: object = DC.field(
|
||||
default_factory=lambda: Markers())
|
||||
gui: object = DC.field(default_factory=GUI)
|
||||
charts_selected: object = DC.field(default_factory=ChartsSelected)
|
||||
chart: object = DC.field(default_factory=Chart)
|
||||
chart_colors: object = DC.field(default_factory=ChartColors)
|
||||
markers: object = DC.field(default_factory=Markers)
|
||||
|
||||
|
||||
cfg = CFG()
|
||||
|
||||
|
||||
def restore(settings: 'AppSettings') -> CFG:
|
||||
def restore(settings: "AppSettings") -> CFG:
|
||||
result = CFG()
|
||||
for field in DC.fields(result):
|
||||
value = settings.restore_dataclass(field.name.upper(),
|
||||
getattr(result, field.name))
|
||||
value = settings.restore_dataclass(
|
||||
field.name.upper(), getattr(result, field.name)
|
||||
)
|
||||
setattr(result, field.name, value)
|
||||
logger.debug("restored\n(\n%s\n)", result)
|
||||
return result
|
||||
|
||||
|
||||
def store(settings: 'AppSettings', data: CFG = None) -> None:
|
||||
def store(settings: "AppSettings", data: CFG = None) -> None:
|
||||
data = data or cfg
|
||||
logger.debug("storing\n(\n%s\n)", data)
|
||||
assert isinstance(data, CFG)
|
||||
|
@ -145,27 +159,27 @@ def store(settings: 'AppSettings', data: CFG = None) -> None:
|
|||
|
||||
def from_type(data) -> str:
|
||||
type_map = {
|
||||
bytearray: lambda x: x.hex(),
|
||||
QColor: lambda x: x.getRgb(),
|
||||
QByteArray: lambda x: x.toHex()
|
||||
bytearray: bytearray.hex,
|
||||
QColor: QColor.getRgb,
|
||||
QByteArray: QByteArray.toHex,
|
||||
}
|
||||
return (f"{type_map[type(data)](data)}" if
|
||||
type(data) in type_map else
|
||||
f"{data}")
|
||||
return (
|
||||
f"{type_map[type(data)](data)}" if type(data) in type_map else f"{data}"
|
||||
)
|
||||
|
||||
|
||||
def to_type(data: object, data_type: type) -> object:
|
||||
type_map = {
|
||||
bool: lambda x: x.lower() == 'true',
|
||||
bool: lambda x: x.lower() == "true",
|
||||
bytearray: bytearray.fromhex,
|
||||
list: literal_eval,
|
||||
tuple: literal_eval,
|
||||
QColor: lambda x: QColor.fromRgb(*literal_eval(x)),
|
||||
QByteArray: lambda x: QByteArray.fromHex(literal_eval(x))
|
||||
QByteArray: lambda x: QByteArray.fromHex(literal_eval(x)),
|
||||
}
|
||||
return (type_map[data_type](data) if
|
||||
data_type in type_map else
|
||||
data_type(data))
|
||||
return (
|
||||
type_map[data_type](data) if data_type in type_map else data_type(data)
|
||||
)
|
||||
|
||||
|
||||
# noinspection PyDataclass
|
||||
|
@ -178,8 +192,13 @@ class AppSettings(QSettings):
|
|||
try:
|
||||
assert isinstance(value, field.type)
|
||||
except AssertionError as exc:
|
||||
logger.error("%s: %s of type %s is not a %s",
|
||||
name, field.name, type(value), field.type)
|
||||
logger.error(
|
||||
"%s: %s of type %s is not a %s",
|
||||
name,
|
||||
field.name,
|
||||
type(value),
|
||||
field.type,
|
||||
)
|
||||
raise TypeError from exc
|
||||
self.setValue(field.name, from_type(value))
|
||||
self.endGroup()
|
|
@ -18,7 +18,6 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import math
|
||||
from numbers import Number
|
||||
from typing import Union
|
||||
|
||||
from NanoVNASaver import SITools
|
||||
|
||||
|
@ -27,22 +26,27 @@ FMT_FREQ_SHORT = SITools.Format(max_nr_digits=4)
|
|||
FMT_FREQ_SPACE = SITools.Format(space_str=" ")
|
||||
FMT_FREQ_SWEEP = SITools.Format(max_nr_digits=9, allow_strip=True)
|
||||
FMT_FREQ_INPUTS = SITools.Format(
|
||||
max_nr_digits=10, allow_strip=True,
|
||||
printable_min=0, unprintable_under="- ")
|
||||
max_nr_digits=10, allow_strip=True, printable_min=0, unprintable_under="- "
|
||||
)
|
||||
FMT_Q_FACTOR = SITools.Format(
|
||||
max_nr_digits=4, assume_infinity=False,
|
||||
min_offset=0, max_offset=0, allow_strip=True)
|
||||
max_nr_digits=4,
|
||||
assume_infinity=False,
|
||||
min_offset=0,
|
||||
max_offset=0,
|
||||
allow_strip=True,
|
||||
)
|
||||
FMT_GROUP_DELAY = SITools.Format(max_nr_digits=5, space_str=" ")
|
||||
FMT_REACT = SITools.Format(max_nr_digits=5, space_str=" ", allow_strip=True)
|
||||
FMT_COMPLEX = SITools.Format(max_nr_digits=3, allow_strip=True,
|
||||
printable_min=0, unprintable_under="- ")
|
||||
FMT_COMPLEX = SITools.Format(
|
||||
max_nr_digits=3, allow_strip=True, printable_min=0, unprintable_under="- "
|
||||
)
|
||||
FMT_COMPLEX_NEG = SITools.Format(max_nr_digits=3, allow_strip=True)
|
||||
FMT_SHORT = SITools.Format(max_nr_digits=4)
|
||||
FMT_WAVELENGTH = SITools.Format(max_nr_digits=4, space_str=" ")
|
||||
FMT_PARSE = SITools.Format(parse_sloppy_unit=True, parse_sloppy_kilo=True,
|
||||
parse_clamp_min=0)
|
||||
FMT_PARSE_VALUE = SITools.Format(
|
||||
parse_sloppy_unit=True, parse_sloppy_kilo=True)
|
||||
FMT_PARSE = SITools.Format(
|
||||
parse_sloppy_unit=True, parse_sloppy_kilo=True, parse_clamp_min=0
|
||||
)
|
||||
FMT_PARSE_VALUE = SITools.Format(parse_sloppy_unit=True, parse_sloppy_kilo=True)
|
||||
FMT_VSWR = SITools.Format(max_nr_digits=3)
|
||||
|
||||
|
||||
|
@ -50,7 +54,7 @@ def format_frequency(freq: Number) -> str:
|
|||
return str(SITools.Value(freq, "Hz", FMT_FREQ))
|
||||
|
||||
|
||||
def format_frequency_inputs(freq: Union[Number, str]) -> str:
|
||||
def format_frequency_inputs(freq: Number | str) -> str:
|
||||
return str(SITools.Value(freq, "Hz", FMT_FREQ_INPUTS))
|
||||
|
||||
|
||||
|
@ -117,7 +121,7 @@ def format_group_delay(val: float) -> str:
|
|||
|
||||
|
||||
def format_phase(val: float) -> str:
|
||||
return f"{math.degrees(val):.2f}""\N{DEGREE SIGN}"
|
||||
return f"{math.degrees(val):.2f}" "\N{DEGREE SIGN}"
|
||||
|
||||
|
||||
def format_complex_adm(z: complex, allow_negative: bool = False) -> str:
|
||||
|
@ -135,7 +139,7 @@ def format_complex_imp(z: complex, allow_negative: bool = False) -> str:
|
|||
fmt_re = FMT_COMPLEX_NEG if allow_negative else FMT_COMPLEX
|
||||
re = SITools.Value(z.real, fmt=fmt_re)
|
||||
im = SITools.Value(abs(z.imag), fmt=FMT_COMPLEX)
|
||||
return f"{re}{'-' if z.imag < 0 else '+'}j{im} ""\N{OHM SIGN}"
|
||||
return f"{re}{'-' if z.imag < 0 else '+'}j{im} " "\N{OHM SIGN}"
|
||||
|
||||
|
||||
def format_wavelength(length: Number) -> str:
|
||||
|
@ -153,10 +157,11 @@ def parse_frequency(freq: str) -> int:
|
|||
return -1
|
||||
|
||||
|
||||
def parse_value(val: str, unit: str = "",
|
||||
fmt: SITools.Format = FMT_PARSE_VALUE) -> float:
|
||||
def parse_value(
|
||||
val: str, unit: str = "", fmt: SITools.Format = FMT_PARSE_VALUE
|
||||
) -> float:
|
||||
try:
|
||||
val.replace(',', '.')
|
||||
val.replace(",", ".")
|
||||
return float(SITools.Value(val, unit, fmt))
|
||||
except (ValueError, IndexError):
|
||||
return 0.0
|
|
@ -20,7 +20,6 @@ import logging
|
|||
import platform
|
||||
from collections import namedtuple
|
||||
from time import sleep
|
||||
from typing import List
|
||||
|
||||
import serial
|
||||
from serial.tools import list_ports
|
||||
|
@ -34,7 +33,10 @@ from NanoVNASaver.Hardware.NanoVNA_F_V2 import NanoVNA_F_V2
|
|||
from NanoVNASaver.Hardware.NanoVNA_H import NanoVNA_H
|
||||
from NanoVNASaver.Hardware.NanoVNA_H4 import NanoVNA_H4
|
||||
from NanoVNASaver.Hardware.NanoVNA_V2 import NanoVNA_V2
|
||||
from NanoVNASaver.Hardware.TinySA import TinySA
|
||||
from NanoVNASaver.Hardware.TinySA import TinySA, TinySA_Ultra
|
||||
from NanoVNASaver.Hardware.JNCRadio_VNA_3G import JNCRadio_VNA_3G
|
||||
from NanoVNASaver.Hardware.SV4401A import SV4401A
|
||||
from NanoVNASaver.Hardware.SV6301A import SV6301A
|
||||
from NanoVNASaver.Hardware.Serial import drain_serial, Interface
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -43,8 +45,8 @@ USBDevice = namedtuple("Device", "vid pid name")
|
|||
|
||||
USBDEVICETYPES = (
|
||||
USBDevice(0x0483, 0x5740, "NanoVNA"),
|
||||
USBDevice(0x16c0, 0x0483, "AVNA"),
|
||||
USBDevice(0x04b4, 0x0008, "S-A-A-2"),
|
||||
USBDevice(0x16C0, 0x0483, "AVNA"),
|
||||
USBDevice(0x04B4, 0x0008, "S-A-A-2"),
|
||||
)
|
||||
RETRIES = 3
|
||||
TIMEOUT = 0.2
|
||||
|
@ -59,6 +61,10 @@ NAME2DEVICE = {
|
|||
"F": NanoVNA_F,
|
||||
"NanoVNA": NanoVNA,
|
||||
"tinySA": TinySA,
|
||||
"tinySA_Ultra": TinySA_Ultra,
|
||||
"JNCRadio": JNCRadio_VNA_3G,
|
||||
"SV4401A": SV4401A,
|
||||
"SV6301A": SV6301A,
|
||||
"Unknown": NanoVNA,
|
||||
}
|
||||
|
||||
|
@ -71,30 +77,41 @@ NAME2DEVICE = {
|
|||
|
||||
def _fix_v2_hwinfo(dev):
|
||||
# if dev.hwid == r'PORTS\VID_04B4&PID_0008\DEMO':
|
||||
if r'PORTS\VID_04B4&PID_0008' in dev.hwid:
|
||||
dev.vid, dev.pid = 0x04b4, 0x0008
|
||||
if r"PORTS\VID_04B4&PID_0008" in dev.hwid:
|
||||
dev.vid, dev.pid = 0x04B4, 0x0008
|
||||
return dev
|
||||
|
||||
|
||||
def usb_typename(device: ListPortInfo) -> str:
|
||||
return next((t.name for t in USBDEVICETYPES if
|
||||
device.vid == t.vid and device.pid == t.pid),
|
||||
"")
|
||||
return next(
|
||||
(
|
||||
t.name
|
||||
for t in USBDEVICETYPES
|
||||
if device.vid == t.vid and device.pid == t.pid
|
||||
),
|
||||
"",
|
||||
)
|
||||
|
||||
|
||||
# Get list of interfaces with VNAs connected
|
||||
|
||||
|
||||
def get_interfaces() -> List[Interface]:
|
||||
def get_interfaces() -> list[Interface]:
|
||||
interfaces = []
|
||||
# serial like usb interfaces
|
||||
for d in list_ports.comports():
|
||||
if platform.system() == 'Windows' and d.vid is None:
|
||||
if platform.system() == "Windows" and d.vid is None:
|
||||
d = _fix_v2_hwinfo(d)
|
||||
if not (typename := usb_typename(d)):
|
||||
continue
|
||||
logger.debug("Found %s USB:(%04x:%04x) on port %s",
|
||||
typename, d.vid, d.pid, d.device)
|
||||
iface = Interface('serial', typename)
|
||||
logger.debug(
|
||||
"Found %s USB:(%04x:%04x) on port %s",
|
||||
typename,
|
||||
d.vid,
|
||||
d.pid,
|
||||
d.device,
|
||||
)
|
||||
iface = Interface("serial", typename)
|
||||
iface.port = d.device
|
||||
iface.open()
|
||||
iface.comment = get_comment(iface)
|
||||
|
@ -105,13 +122,12 @@ def get_interfaces() -> List[Interface]:
|
|||
return interfaces
|
||||
|
||||
|
||||
def get_portinfos() -> List[str]:
|
||||
def get_portinfos() -> list[str]:
|
||||
portinfos = []
|
||||
# serial like usb interfaces
|
||||
for d in list_ports.comports():
|
||||
logger.debug("Found USB:(%04x:%04x) on port %s",
|
||||
d.vid, d.pid, d.device)
|
||||
iface = Interface('serial', "DEBUG")
|
||||
logger.debug("Found USB:(%04x:%04x) on port %s", d.vid, d.pid, d.device)
|
||||
iface = Interface("serial", "DEBUG")
|
||||
iface.port = d.device
|
||||
iface.open()
|
||||
version = detect_version(iface)
|
||||
|
@ -130,19 +146,23 @@ def get_comment(iface: Interface) -> str:
|
|||
with iface.lock:
|
||||
vna_version = detect_version(iface)
|
||||
|
||||
if vna_version == 'v2':
|
||||
if vna_version == "v2":
|
||||
return "S-A-A-2"
|
||||
|
||||
logger.info("Finding firmware variant...")
|
||||
info = get_info(iface)
|
||||
for search, name in (
|
||||
("AVNA + Teensy", "AVNA"),
|
||||
("NanoVNA-H 4", "H4"),
|
||||
("NanoVNA-H", "H"),
|
||||
("NanoVNA-F_V2", "F_V2"),
|
||||
("NanoVNA-F", "F"),
|
||||
("NanoVNA", "NanoVNA"),
|
||||
("tinySA", "tinySA"),
|
||||
("AVNA + Teensy", "AVNA"),
|
||||
("NanoVNA-H 4", "H4"),
|
||||
("NanoVNA-H", "H"),
|
||||
("NanoVNA-F_V2", "F_V2"),
|
||||
("NanoVNA-F", "F"),
|
||||
("NanoVNA", "NanoVNA"),
|
||||
("tinySA4", "tinySA_Ultra"),
|
||||
("tinySA", "tinySA"),
|
||||
("JNCRadio_VNA_3G", "JNCRadio"),
|
||||
("SV4401A", "SV4401A"),
|
||||
("SV6301A", "SV6301A"),
|
||||
):
|
||||
if info.find(search) >= 0:
|
||||
return name
|
||||
|
@ -171,7 +191,7 @@ def detect_version(serial_port: serial.Serial) -> str:
|
|||
if data.startswith("2"):
|
||||
return "v2"
|
||||
logger.debug("Retry detection: %s", i + 1)
|
||||
logger.error('No VNA detected. Hardware responded to CR with: %s', data)
|
||||
logger.error("No VNA detected. Hardware responded to CR with: %s", data)
|
||||
return ""
|
||||
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
# 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 serial
|
||||
from PyQt6.QtGui import QImage, QPixmap
|
||||
|
||||
from NanoVNASaver.Hardware.NanoVNA import NanoVNA
|
||||
from NanoVNASaver.Hardware.Serial import Interface
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class JNCRadio_VNA_3G(NanoVNA):
|
||||
name = "JNCRadio_VNA_3G"
|
||||
screenwidth = 800
|
||||
screenheight = 480
|
||||
valid_datapoints = (501, 11, 101, 1001)
|
||||
sweep_points_min = 11
|
||||
sweep_points_max = 1001
|
||||
|
||||
def __init__(self, iface: Interface):
|
||||
super().__init__(iface)
|
||||
self.sweep_max_freq_Hz = 3e9
|
||||
|
||||
def getScreenshot(self) -> QPixmap:
|
||||
logger.debug("Capturing screenshot...")
|
||||
self.serial.timeout = 8
|
||||
if not self.connected():
|
||||
return QPixmap()
|
||||
try:
|
||||
rgba_array = self._capture_data()
|
||||
image = QImage(
|
||||
rgba_array,
|
||||
self.screenwidth,
|
||||
self.screenheight,
|
||||
QImage.Format.Format_RGB16,
|
||||
)
|
||||
logger.debug("Captured screenshot")
|
||||
return QPixmap(image)
|
||||
except serial.SerialException as exc:
|
||||
logger.exception("Exception while capturing screenshot: %s", exc)
|
||||
return QPixmap()
|
||||
|
||||
def setSweep(self, start, stop):
|
||||
self.start = start
|
||||
self.stop = stop
|
||||
list(self.exec_command(f"scan {start} {stop} {self.datapoints}"))
|
|
@ -18,11 +18,10 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
import struct
|
||||
from typing import List
|
||||
|
||||
import serial
|
||||
import numpy as np
|
||||
from PyQt5 import QtGui
|
||||
from PyQt6.QtGui import QImage, QPixmap
|
||||
|
||||
from NanoVNASaver.Hardware.Serial import drain_serial, Interface
|
||||
from NanoVNASaver.Hardware.VNA import VNA
|
||||
|
@ -46,7 +45,6 @@ class NanoVNA(VNA):
|
|||
self._sweepdata = []
|
||||
|
||||
def _get_running_frequencies(self):
|
||||
|
||||
logger.debug("Reading values: frequencies")
|
||||
try:
|
||||
frequencies = super().readValues("frequencies")
|
||||
|
@ -61,42 +59,45 @@ class NanoVNA(VNA):
|
|||
timeout = self.serial.timeout
|
||||
with self.serial.lock:
|
||||
drain_serial(self.serial)
|
||||
self.serial.write("capture\r".encode('ascii'))
|
||||
self.serial.write("capture\r".encode("ascii"))
|
||||
self.serial.readline()
|
||||
self.serial.timeout = 4
|
||||
image_data = self.serial.read(
|
||||
self.screenwidth * self.screenheight * 2)
|
||||
self.screenwidth * self.screenheight * 2
|
||||
)
|
||||
self.serial.timeout = timeout
|
||||
self.serial.timeout = timeout
|
||||
return image_data
|
||||
|
||||
def _convert_data(self, image_data: bytes) -> bytes:
|
||||
rgb_data = struct.unpack(
|
||||
f">{self.screenwidth * self.screenheight}H",
|
||||
image_data)
|
||||
f">{self.screenwidth * self.screenheight}H", image_data
|
||||
)
|
||||
rgb_array = np.array(rgb_data, dtype=np.uint32)
|
||||
return (0xFF000000 +
|
||||
((rgb_array & 0xF800) << 8) +
|
||||
((rgb_array & 0x07E0) << 5) +
|
||||
((rgb_array & 0x001F) << 3))
|
||||
return (
|
||||
0xFF000000
|
||||
+ ((rgb_array & 0xF800) << 8)
|
||||
+ ((rgb_array & 0x07E0) << 5)
|
||||
+ ((rgb_array & 0x001F) << 3)
|
||||
)
|
||||
|
||||
def getScreenshot(self) -> QtGui.QPixmap:
|
||||
def getScreenshot(self) -> QPixmap:
|
||||
logger.debug("Capturing screenshot...")
|
||||
if not self.connected():
|
||||
return QtGui.QPixmap()
|
||||
return QPixmap()
|
||||
try:
|
||||
rgba_array = self._convert_data(self._capture_data())
|
||||
image = QtGui.QImage(
|
||||
image = QImage(
|
||||
rgba_array,
|
||||
self.screenwidth,
|
||||
self.screenheight,
|
||||
QtGui.QImage.Format_ARGB32)
|
||||
QImage.Format.Format_ARGB32,
|
||||
)
|
||||
logger.debug("Captured screenshot")
|
||||
return QtGui.QPixmap(image)
|
||||
return QPixmap(image)
|
||||
except serial.SerialException as exc:
|
||||
logger.exception(
|
||||
"Exception while capturing screenshot: %s", exc)
|
||||
return QtGui.QPixmap()
|
||||
logger.exception("Exception while capturing screenshot: %s", exc)
|
||||
return QPixmap()
|
||||
|
||||
def resetSweep(self, start: int, stop: int):
|
||||
list(self.exec_command(f"sweep {start} {stop} {self.datapoints}"))
|
||||
|
@ -121,14 +122,18 @@ class NanoVNA(VNA):
|
|||
self.features.add("Scan command")
|
||||
self.sweep_method = "scan"
|
||||
|
||||
def readFrequencies(self) -> List[int]:
|
||||
def readFrequencies(self) -> list[int]:
|
||||
logger.debug("readFrequencies: %s", self.sweep_method)
|
||||
if self.sweep_method != "scan_mask":
|
||||
return super().readFrequencies()
|
||||
return [int(line) for line in self.exec_command(
|
||||
f"scan {self.start} {self.stop} {self.datapoints} 0b001")]
|
||||
return [
|
||||
int(line)
|
||||
for line in self.exec_command(
|
||||
f"scan {self.start} {self.stop} {self.datapoints} 0b001"
|
||||
)
|
||||
]
|
||||
|
||||
def readValues(self, value) -> List[str]:
|
||||
def readValues(self, value) -> list[str]:
|
||||
if self.sweep_method != "scan_mask":
|
||||
return super().readValues(value)
|
||||
logger.debug("readValue with scan mask (%s)", value)
|
||||
|
@ -137,11 +142,12 @@ class NanoVNA(VNA):
|
|||
if value == "data 0":
|
||||
self._sweepdata = []
|
||||
for line in self.exec_command(
|
||||
f"scan {self.start} {self.stop} {self.datapoints} 0b110"):
|
||||
f"scan {self.start} {self.stop} {self.datapoints} 0b110"
|
||||
):
|
||||
data = line.split()
|
||||
self._sweepdata.append((
|
||||
f"{data[0]} {data[1]}",
|
||||
f"{data[2]} {data[3]}"))
|
||||
self._sweepdata.append(
|
||||
(f"{data[0]} {data[1]}", f"{data[2]} {data[3]}")
|
||||
)
|
||||
if value == "data 0":
|
||||
return [x[0] for x in self._sweepdata]
|
||||
if value == "data 1":
|
|
@ -19,7 +19,7 @@
|
|||
import logging
|
||||
|
||||
import serial
|
||||
from PyQt5 import QtGui
|
||||
from PyQt6.QtGui import QImage, QPixmap
|
||||
|
||||
from NanoVNASaver.Hardware.NanoVNA import NanoVNA
|
||||
from NanoVNASaver.Hardware.Serial import Interface
|
||||
|
@ -36,20 +36,20 @@ class NanoVNA_F_V2(NanoVNA):
|
|||
super().__init__(iface)
|
||||
self.sweep_max_freq_Hz = 3e9
|
||||
|
||||
def getScreenshot(self) -> QtGui.QPixmap:
|
||||
def getScreenshot(self) -> QPixmap:
|
||||
logger.debug("Capturing screenshot...")
|
||||
if not self.connected():
|
||||
return QtGui.QPixmap()
|
||||
return QPixmap()
|
||||
try:
|
||||
rgba_array = self._capture_data()
|
||||
image = QtGui.QImage(
|
||||
image = QImage(
|
||||
rgba_array,
|
||||
self.screenwidth,
|
||||
self.screenheight,
|
||||
QtGui.QImage.Format_RGB16)
|
||||
QImage.Format.Format_RGB16,
|
||||
)
|
||||
logger.debug("Captured screenshot")
|
||||
return QtGui.QPixmap(image)
|
||||
return QPixmap(image)
|
||||
except serial.SerialException as exc:
|
||||
logger.exception(
|
||||
"Exception while capturing screenshot: %s", exc)
|
||||
return QtGui.QPixmap()
|
||||
logger.exception("Exception while capturing screenshot: %s", exc)
|
||||
return QPixmap()
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue