Merge pull request #612 from NanoVNA-Saver/feature/v0.6.0

Feature/v0.6.0
pull/614/head
Holger Müller 2023-03-08 13:57:16 +01:00 zatwierdzone przez GitHub
commit db5cd98e03
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
150 zmienionych plików z 4597 dodań i 2311 usunięć

Wyświetl plik

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

76
.gitignore vendored
Wyświetl plik

@ -1,26 +1,54 @@
/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
# Per-project virtualenvs
.venv*/
.conda*/
.python-version

27
.readthedocs.yml 100644
Wyświetl plik

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

42
AUTHORS.rst 100644
Wyświetl plik

@ -0,0 +1,42 @@
============
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>
* zstadler <zeev.stadler@gmail.com>

322
CONTRIBUTING.rst 100644
Wyświetl plik

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

Wyświetl plik

@ -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)

Wyświetl plik

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

14
Pipfile
Wyświetl plik

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

233
README.md
Wyświetl plik

@ -1,233 +0,0 @@
[![Latest Release](https://img.shields.io/github/v/release/NanoVNA-Saver/nanovna-saver.svg)](https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest)
[![License](https://img.shields.io/github/license/NanoVNA-Saver/nanovna-saver.svg)](https://github.com/NanoVNA-Saver/nanovna-saver/blob/master/LICENSE)
[![Downloads](https://img.shields.io/github/downloads/NanoVNA-Saver/nanovna-saver/total.svg)](https://github.com/NanoVNA-Saver/nanovna-saver/releases/)
[![GitHub Releases](https://img.shields.io/github/downloads/NanoVNA-Saver/nanovna-saver/latest/total)](https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest)
[![Donate](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=T8KTGVDQF5K6E&item_name=NanoVNASaver+Development&currency_code=EUR&source=url)
# NanoVNASaver
A multiplatform tool to save Touchstone files from the NanoVNA,
sweep frequency spans in segments to gain more than 101 data
points, and generally display and analyze the resulting data.
- Copyright 2019, 2020 Rune B. Broberg
- Copyright 2020ff NanoVNA-Saver Authors
<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)
- [Measuring inductor core permeability](#measuring-inductor-core-permeability)
- [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)
## 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.
![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.
### 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
### 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:
[![Paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=T8KTGVDQF5K6E&item_name=NanoVNASaver+Development&currency_code=EUR&source=url)

274
README.rst 100644
Wyświetl plik

@ -0,0 +1,274 @@
.. role:: raw-html-m2r(raw)
:format: html
.. image:: https://img.shields.io/github/v/release/NanoVNA-Saver/nanovna-saver.svg
:target: https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest
:alt: Latest Release
.. image:: https://img.shields.io/github/license/NanoVNA-Saver/nanovna-saver.svg
:target: https://github.com/NanoVNA-Saver/nanovna-saver/blob/master/LICENSE.txt
:alt: License
.. image:: https://img.shields.io/github/downloads/NanoVNA-Saver/nanovna-saver/total.svg
:target: https://github.com/NanoVNA-Saver/nanovna-saver/releases/
:alt: Downloads
.. image:: https://img.shields.io/github/downloads/NanoVNA-Saver/nanovna-saver/latest/total
:target: https://github.com/NanoVNA-Saver/nanovna-saver/releases/latest
:alt: GitHub Releases
.. image:: https://img.shields.io/badge/paypal-donate-yellow.svg
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=T8KTGVDQF5K6E&item_name=NanoVNASaver+Development&currency_code=EUR&source=url
:alt: Donate
NanoVNASaver
============
A multiplatform tool to save Touchstone files from the NanoVNA,
sweep frequency spans in segments to gain more than 101 data
points, and generally display and analyze the resulting data.
* Copyright 2019, 2020 Rune B. Broberg
* Copyright 2020ff NanoVNA-Saver Authors
It's written in **Python 3** using **PyQt5** 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
* 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
-----------------------
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)
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
^^^^^^^^^^^^^^
Changes in 0.5.5
^^^^^^^^^^^^^^^^
* Measuring inductor core permeability
* Bugfixes for calibration data loading and saving
* Let V2 Devices more time for usb-serial setup
* Make some windows scrollable
Changes in 0.5.4
^^^^^^^^^^^^^^^^
* Bugfixes for Python3.11 compatability
* Bugfix for Python3.8 compatability
* use math instead of table for log step calculation
* Support of NanoVNA V2 Plus5 on Windows
* New SI prefixes added - Ronna, Quetta
* addes a Makefile to build a packages
* Simplyfied sweep worker
* Fixed calibration data loading
* Explicit import of scipy functions - #555
* Refactoring of Analysis modules
Contributing
------------
First off, thanks for taking the time to contribute! Contributions are what
make the open-source community such an amazing place to learn, inspire, and
create. Any contributions you make will benefit everybody else and are
**greatly appreciated**.
Please read `our contribution guidelines <docs/CONTRIBUTING.md>`_\ , and thank
you for being involved!
License
-------
This software is licensed under version 3 of the GNU General Public License. It
comes with NO WARRANTY.
You can use it, commercially as well. You may make changes to the code, but I
(and the license) ask that you give these changes back to the community.
References
----------
* Ohan Smit wrote an introduction to using the application:
[https://zs1sci.com/blog/nanovnasaver/]
* HexAndFlex wrote a 3-part (thus far) series on Getting Started with the
NanoVNA:
[https://hexandflex.com/2019/08/31/getting-started-with-the-nanovna-part-1/]
- Part 3 is dedicated to NanoVNASaver:
[https://hexandflex.com/2019/09/15/getting-started-with-the-nanovna-part-3-pc-software/]
* Gunthard Kraus did documentation in English and German:
[http://www.gunthard-kraus.de/fertig_NanoVNA/English/]
[http://www.gunthard-kraus.de/fertig_NanoVNA/Deutsch/]
Acknowledgements
----------------
Original application by Rune B. Broberg (5Q5R)
Contributions and changes by Holger Müller (DG5DBH), David Hunt and others.
TDR inspiration shamelessly stolen from the work of Salil (VU2CWA) at
https://nuclearrambo.com/wordpress/accurately-measuring-cable-length-with-nanovna/
TDR cable types by Larry Goga.
Bugfixes and Python installation work by Ohan Smit.
Thanks to everyone who have tested, commented and inspired. Particular thanks
go to the alpha testing crew who suffer the early instability of new versions.
This software is available free of charge. If you read all this way, and you
*still* want to support it, you may donate to the developer using the button
below:
.. image:: https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=T8KTGVDQF5K6E&item_name=NanoVNASaver+Development&currency_code=EUR&source=url
:alt: Paypal

29
docs/Makefile 100644
Wyświetl plik

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

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

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

2
docs/authors.rst 100644
Wyświetl plik

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

286
docs/conf.py 100644
Wyświetl plik

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

Wyświetl plik

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

60
docs/index.rst 100644
Wyświetl plik

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

7
docs/license.rst 100644
Wyświetl plik

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

2
docs/readme.rst 100644
Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -24,7 +24,14 @@ with suppress(ImportError):
# pyright: reportMissingImports=false
import pkg_resources.py2_warn
from NanoVNASaver.__main__ import main
try:
from NanoVNASaver.__main__ import main
except ModuleNotFoundError:
import sys
if __name__ == '__main__':
sys.path.append("src")
from NanoVNASaver.__main__ import main
if __name__ == "__main__":
main()

14
pyproject.toml 100644
Wyświetl plik

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

100
setup.cfg
Wyświetl plik

@ -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,101 @@ 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
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
numpy>=1.21.1
scipy>=1.7.1
Cython>=0.29.24
python_requires = >=3.8, <4
[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
# Add here console scripts like:
# console_scripts =
# script_name = NanoVNASaver.module:function
# For example:
# console_scripts =
# fibonacci = NanoVNASaver.skeleton:run
# And any other entry points, for example:
# pyscaffold.cli =
# awesome = pyscaffoldext.awesome.extension:AwesomeExtension
# 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

Wyświetl plik

@ -1,27 +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/", ["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

Wyświetl plik

@ -17,10 +17,11 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
VERSION = "0.5.5"
VERSION = "0.6.0-pre"
VERSION_URL = (
"https://raw.githubusercontent.com/"
"NanoVNA-Saver/nanovna-saver/master/NanoVNASaver/About.py")
"NanoVNA-Saver/nanovna-saver/master/NanoVNASaver/About.py"
)
INFO_URL = "https://github.com/NanoVNA-Saver/nanovna-saver"
INFO = f"""NanoVNASaver {VERSION}

Wyświetl plik

@ -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)

Wyświetl plik

@ -33,42 +33,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,72 +113,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])
"""
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:
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
@ -178,13 +206,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

Wyświetl plik

@ -34,11 +34,13 @@ class BandStopAnalysis(BandPassAnalysis):
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

Wyświetl plik

@ -35,8 +35,8 @@ class Analysis:
def __init__(self, app: QtWidgets.QWidget):
self.app = app
self.label: Dict[str, QtWidgets.QLabel] = {
'titel': QtWidgets.QLabel(),
'result': QtWidgets.QLabel(),
"titel": QtWidgets.QLabel(),
"result": QtWidgets.QLabel(),
}
self.layout = QtWidgets.QFormLayout()
self._widget = QtWidgets.QWidget()
@ -53,7 +53,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)

Wyświetl plik

@ -23,10 +23,14 @@ from PyQt5 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 ?!?

Wyświetl plik

@ -41,9 +41,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 +54,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,25 +84,28 @@ 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)")
@ -111,11 +117,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
}

Wyświetl plik

@ -30,13 +30,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
}

Wyświetl plik

@ -20,12 +20,14 @@ import logging
from PyQt5 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,14 +60,14 @@ 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):
@ -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)

Wyświetl plik

@ -25,9 +25,7 @@ from PyQt5 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 +42,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 +69,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 +78,50 @@ 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
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)

Wyświetl plik

@ -0,0 +1,123 @@
# NanoVNASaver
#
# A python program to view and export Touchstone data from a NanoVNA
# Copyright (C) 2019, 2020 Rune B. Broberg
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
from typing import Callable, 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)

Wyświetl plik

@ -64,34 +64,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)

Wyświetl plik

@ -21,6 +21,7 @@ import math
from typing import Callable, List, Tuple
import numpy as np
# pylint: disable=import-error, no-name-in-module
from scipy.signal import find_peaks
@ -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
@ -61,11 +63,8 @@ def maxima(data: List[float], threshold: float = 0.0) -> List[int]:
Returns:
List[int]: indices of maxima
"""
peaks = find_peaks(
data, width=2, distance=3, prominence=1)[0].tolist()
return [
i for i in peaks if data[i] > threshold
] if threshold else peaks
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]:
@ -77,16 +76,15 @@ def minima(data: List[float], threshold: float = 0.0) -> List[int]:
Returns:
List[int]: indices of minima
"""
bottoms = find_peaks(
-np.array(data), width=2, distance=3, prominence=1)[0].tolist()
return [
i for i in bottoms if data[i] < threshold
] if threshold else bottoms
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:
@ -99,18 +97,21 @@ def take_from_idx(data: List[float],
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:
@ -122,13 +123,13 @@ 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
@ -143,13 +144,13 @@ 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
@ -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

Wyświetl plik

@ -35,7 +35,8 @@ IDEAL_OPEN = complex(1, 0)
IDEAL_LOAD = complex(0, 0)
IDEAL_THROUGH = complex(1, 0)
RXP_CAL_HEADER = re.compile(r"""
RXP_CAL_HEADER = re.compile(
r"""
^ \# \s+ Hz \s+
ShortR \s+ ShortI \s+ OpenR \s+ OpenI \s+
LoadR \s+ LoadI
@ -43,9 +44,12 @@ RXP_CAL_HEADER = re.compile(r"""
(?P<thrurefl> \s+ ThrureflR \s+ ThrureflI)?
(?P<isolation> \s+ IsolationR \s+ IsolationI)?
\s* $
""", re.VERBOSE | re.IGNORECASE)
""",
re.VERBOSE | re.IGNORECASE,
)
RXP_CAL_LINE = re.compile(r"""
RXP_CAL_LINE = re.compile(
r"""
^ \s*
(?P<freq>\d+) \s+
(?P<shortr>[-0-9Ee.]+) \s+ (?P<shorti>[-0-9Ee.]+) \s+
@ -55,7 +59,9 @@ RXP_CAL_LINE = re.compile(r"""
( \s+ (?P<thrureflr>[-0-9Ee.]+) \s+ (?P<thrurefli>[-0-9Ee.]+))?
( \s+ (?P<isolationr>[-0-9Ee.]+) \s+ (?P<isolationi>[-0-9Ee.]+))?
\s* $
""", re.VERBOSE)
""",
re.VERBOSE,
)
logger = logging.getLogger(__name__)
@ -63,7 +69,8 @@ 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)
complex(0, 1) * 2 * math.pi * d.freq * delay * -1 * mult
)
return Datapoint(d.freq, corr_data.real, corr_data.imag)
@ -88,14 +95,16 @@ class CalData:
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 ''
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 ""
)
)
@ -138,26 +147,32 @@ class CalDataSet(UserDict):
(
"# 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"
+ "\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 ""
if self.complete1port()
else ""
)
def _append_match(self, m: re.Match, header: str,
line_nr: int, line: str) -> None:
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"
}
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)
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"]
@ -166,11 +181,14 @@ class CalDataSet(UserDict):
for name in columns:
self.insert(
name,
Datapoint(int(cal["freq"]),
float(cal[f"{name}r"]),
float(cal[f"{name}i"])))
Datapoint(
int(cal["freq"]),
float(cal[f"{name}r"]),
float(cal[f"{name}i"]),
),
)
def from_str(self, text: str) -> 'CalDataSet':
def from_str(self, text: str) -> "CalDataSet":
# reset data
self.notes = ""
self.data = defaultdict(CalData)
@ -185,7 +203,8 @@ class CalDataSet(UserDict):
if m := RXP_CAL_HEADER.search(line):
if header:
logger.warning(
"Duplicate header in cal data. %i: %s", i, line)
"Duplicate header in cal data. %i: %s", i, line
)
header = "through" if m.group("through") else "sol"
continue
if not line or line.startswith("#"):
@ -197,13 +216,20 @@ class CalDataSet(UserDict):
continue
if not header:
logger.warning(
"Caldata without having read header: %i: %s", i, line)
"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'}:
if name not in {
"short",
"open",
"load",
"through",
"thrurefl",
"isolation",
}:
raise KeyError(name)
freq = dp.freq
setattr(self.data[freq], name, (dp.z))
@ -223,9 +249,7 @@ class CalDataSet(UserDict):
yield self.get(freq)
def size_of(self, name: str) -> int:
return len(
[True for val in self.data.values() if getattr(val, name)]
)
return len([True for val in self.data.values() if getattr(val, name)])
def complete1port(self) -> bool:
for val in self.data.values():
@ -244,7 +268,6 @@ class CalDataSet(UserDict):
class Calibration:
def __init__(self):
self.notes = []
self.dataset = CalDataSet()
self.cal_element = CalElement()
@ -278,18 +301,30 @@ class Calibration:
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
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)
@ -301,18 +336,16 @@ class Calibration:
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
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.")
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.")
"must be completed for calibration to be applied."
)
logger.debug("Calculating calibration for %d points.", self.size())
for freq, caldata in self.dataset.items():
@ -324,10 +357,12 @@ class Calibration:
self.isCalculated = False
logger.error(
"Division error - did you use the same measurement"
" for two of short, open and load?")
" 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
f" values at frequency {freq}Hz."
) from exc
self.gen_interpolation()
self.isCalculated = True
@ -338,25 +373,47 @@ class Calibration:
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))
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))
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))
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))
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:
@ -367,11 +424,17 @@ class Calibration:
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)
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))
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:
@ -379,59 +442,103 @@ class Calibration:
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))
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()])
(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])),
"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))
(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)))
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:
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:
with open(filename, encoding="utf-8") as calfile:
self.dataset = CalDataSet().from_str(calfile.read())
self.notes = self.dataset.notes.splitlines()

Wyświetl plik

@ -61,20 +61,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 +121,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 +140,12 @@ class CombinedLogMagChart(LogMagChart):
pen = QtGui.QPen(c)
pen.setWidth(2)
qp.setPen(pen)
qp.drawLine(self.leftMargin + self.dim.width - 20, 14,
self.leftMargin + self.dim.width - 15, 14)
qp.drawLine(
self.leftMargin + self.dim.width - 20,
14,
self.leftMargin + self.dim.width - 15,
14,
)
self.drawData(qp, self.data11, Chart.color.sweep)
self.drawData(qp, self.data21, Chart.color.sweep_secondary)

Wyświetl plik

@ -36,13 +36,16 @@ logger = logging.getLogger(__name__)
class ChartColors: # pylint: disable=too-many-instance-attributes
background: QColor = field(default_factory=lambda: QColor(QtCore.Qt.white))
foreground: QColor = field(
default_factory=lambda: QColor(QtCore.Qt.lightGray))
default_factory=lambda: QColor(QtCore.Qt.lightGray)
)
reference: QColor = field(default_factory=lambda: QColor(0, 0, 255, 64))
reference_secondary: QColor = field(
default_factory=lambda: QColor(0, 0, 192, 48))
default_factory=lambda: QColor(0, 0, 192, 48)
)
sweep: QColor = field(default_factory=lambda: QColor(QtCore.Qt.darkYellow))
sweep_secondary: QColor = field(
default_factory=lambda: QColor(QtCore.Qt.darkMagenta))
default_factory=lambda: QColor(QtCore.Qt.darkMagenta)
)
swr: QColor = field(default_factory=lambda: QColor(255, 0, 0, 128))
text: QColor = field(default_factory=lambda: QColor(QtCore.Qt.black))
bands: QColor = field(default_factory=lambda: QColor(128, 128, 128, 48))
@ -97,8 +100,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 +111,7 @@ class Chart(QtWidgets.QWidget):
def __init__(self, name):
super().__init__()
self.name = name
self.sweepTitle = ''
self.sweepTitle = ""
self.leftMargin = 30
self.rightMargin = 20
@ -130,7 +132,8 @@ class Chart(QtWidgets.QWidget):
self.action_popout = QtWidgets.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")
@ -230,7 +233,9 @@ class Chart(QtWidgets.QWidget):
self.zoomTo(
self.dragbox.pos_start[0],
self.dragbox.pos_start[1],
a0.x(), a0.y())
a0.x(),
a0.y(),
)
self.dragbox.state = False
self.dragbox.pos = (-1, -1)
self.dragbox.pos_start = (0, 0)
@ -262,7 +267,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 +277,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 +321,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}")

Wyświetl plik

@ -25,9 +25,12 @@ from PyQt5 import QtWidgets, QtGui, QtCore
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
@ -79,11 +81,13 @@ class FrequencyChart(Chart):
self.action_automatic.setCheckable(True)
self.action_automatic.setChecked(True)
self.action_automatic.changed.connect(
lambda: self.setFixedSpan(self.action_fixed_span.isChecked()))
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()))
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)
@ -91,11 +95,13 @@ class FrequencyChart(Chart):
self.x_menu.addSeparator()
self.action_set_fixed_start = QtWidgets.QAction(
f"Start ({format_frequency_chart(self.minFrequency)})")
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)})")
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)
@ -110,9 +116,11 @@ class FrequencyChart(Chart):
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)
@ -122,11 +130,13 @@ class FrequencyChart(Chart):
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()))
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked())
)
self.y_action_fixed_span = QtWidgets.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()))
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked())
)
mode_group = QtWidgets.QActionGroup(self)
mode_group.addAction(self.y_action_automatic)
mode_group.addAction(self.y_action_fixed_span)
@ -135,11 +145,13 @@ class FrequencyChart(Chart):
self.y_menu.addSeparator()
self.action_set_fixed_minimum = QtWidgets.QAction(
f"Minimum ({self.minDisplayValue})")
f"Minimum ({self.minDisplayValue})"
)
self.action_set_fixed_minimum.triggered.connect(self.setMinimumValue)
self.action_set_fixed_maximum = QtWidgets.QAction(
f"Maximum ({self.maxDisplayValue})")
f"Maximum ({self.maxDisplayValue})"
)
self.action_set_fixed_maximum.triggered.connect(self.setMaximumValue)
self.y_menu.addAction(self.action_set_fixed_maximum)
@ -155,9 +167,11 @@ class FrequencyChart(Chart):
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)
@ -168,16 +182,21 @@ class FrequencyChart(Chart):
self.menu.addAction(self.action_save_screenshot)
self.action_popout = QtWidgets.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.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.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding,
)
)
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, Chart.color.background)
self.setPalette(pal)
@ -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)
@ -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,9 +303,11 @@ 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
min_val = parse_value(text)
@ -292,9 +323,11 @@ class FrequencyChart(Chart):
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),
)
if not selected:
return
max_val = parse_value(text)
@ -323,18 +356,21 @@ 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
@ -410,9 +446,12 @@ class FrequencyChart(Chart):
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.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()
@ -436,10 +475,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:
@ -452,24 +491,30 @@ class FrequencyChart(Chart):
qp.end()
def _data_oob(self, data: List[Datapoint]) -> bool:
return (data[0].freq > self.fstop or self.data[-1].freq < self.fstart)
return data[0].freq > self.fstop or self.data[-1].freq < self.fstart
def _check_frequency_boundaries(self, qp: QtGui.QPainter):
if (self.data and self._data_oob(self.data) and
(not self.reference or self._data_oob(self.reference))):
if (
self.data
and self._data_oob(self.data)
and (not self.reference or self._data_oob(self.reference))
):
# Data outside frequency range
qp.setBackgroundMode(QtCore.Qt.OpaqueMode)
qp.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)
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)
@ -481,14 +526,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):
@ -514,7 +563,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
@ -522,23 +572,30 @@ 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)
@ -574,27 +631,31 @@ class FrequencyChart(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)
@ -608,17 +669,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)
@ -643,8 +711,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)
@ -663,13 +730,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])
@ -680,8 +751,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
@ -730,10 +805,14 @@ class FrequencyChart(Chart):
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()))
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()))
m.frequencyInput.keyPressEvent(
QtGui.QKeyEvent(a0.type(), QtCore.Qt.Key_Up, a0.modifiers())
)
else:
super().keyPressEvent(a0)

Wyświetl plik

@ -27,6 +27,7 @@ from PyQt5 import QtGui
from NanoVNASaver.Charts.Chart import Chart
from NanoVNASaver.RFTools import Datapoint
from .Frequency import FrequencyChart
logger = logging.getLogger(__name__)
@ -124,23 +125,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 +161,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,7 +213,8 @@ 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]:
absy = y - self.topMargin

Wyświetl plik

@ -115,8 +115,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 +131,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 +152,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,7 +162,8 @@ 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]:
absy = y - self.topMargin

Wyświetl plik

@ -25,6 +25,7 @@ from PyQt5 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 +79,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 +111,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,7 +124,8 @@ 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]:
absy = y - self.topMargin

Wyświetl plik

@ -23,8 +23,7 @@ from typing import List
from PyQt5 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 +56,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 +93,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 +120,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]:
absy = y - self.topMargin
if self.logarithmicY:
span = math.log(self.maxValue) - math.log(self.minValue)
val = math.exp(math.log(self.maxValue) -
absy * span / self.dim.height)
val = math.exp(
math.log(self.maxValue) - absy * span / self.dim.height
)
else:
val = self.maxValue - (absy / self.dim.height * self.span)
return [val]

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -27,6 +27,7 @@ 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 +51,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,15 +129,16 @@ 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)
@ -147,8 +156,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 +189,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 +204,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)
@ -213,8 +227,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 +259,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 +274,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 +288,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 +301,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,10 +318,12 @@ 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]:
absy = y - self.topMargin

Wyświetl plik

@ -50,7 +50,8 @@ class PhaseChart(FrequencyChart):
self.action_unwrap = QtWidgets.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):
@ -98,24 +99,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))
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.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,7 +148,8 @@ 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]:
absy = y - self.topMargin

Wyświetl plik

@ -39,16 +39,25 @@ class PolarChart(SquareChart):
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawEllipse(QtCore.QPoint(center_x, center_y), width_2, height_2)
qp.drawEllipse(QtCore.QPoint(center_x, center_y),
width_2 // 2, height_2 // 2)
qp.drawEllipse(
QtCore.QPoint(center_x, center_y), width_2 // 2, height_2 // 2
)
qp.drawLine(center_x - width_2, center_y,
center_x + width_2, center_y)
qp.drawLine(center_x, center_y - height_2,
center_x, center_y + height_2)
qp.drawLine(center_x + width_45, center_y + height_45,
center_x - width_45, center_y - height_45)
qp.drawLine(center_x + width_45, center_y - height_45,
center_x - width_45, center_y + height_45)
qp.drawLine(center_x - width_2, center_y, center_x + width_2, center_y)
qp.drawLine(
center_x, center_y - height_2, center_x, center_y + height_2
)
qp.drawLine(
center_x + width_45,
center_y + height_45,
center_x - width_45,
center_y - height_45,
)
qp.drawLine(
center_x + width_45,
center_y - height_45,
center_x - width_45,
center_y + height_45,
)
self.drawTitle(qp)

Wyświetl plik

@ -57,7 +57,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 +69,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 +80,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,8 +123,9 @@ 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]:
absy = y - self.topMargin

Wyświetl plik

@ -62,11 +62,13 @@ class RealImaginaryChart(FrequencyChart):
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()))
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked())
)
self.y_action_fixed_span = QtWidgets.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()))
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked())
)
mode_group = QtWidgets.QActionGroup(self)
mode_group.addAction(self.y_action_automatic)
mode_group.addAction(self.y_action_fixed_span)
@ -110,11 +112,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)
@ -131,8 +136,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)
@ -161,7 +170,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)
@ -175,7 +185,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)
@ -197,8 +208,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:
@ -225,7 +240,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)
@ -239,7 +255,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)
@ -252,10 +269,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
@ -264,8 +279,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
@ -273,7 +289,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
@ -350,20 +367,24 @@ class RealImaginaryChart(FrequencyChart):
def getImYPosition(self, d: Datapoint) -> int:
im = self.value(d).imag
return int(self.topMargin + (self.max_imag - im) / self.span_imag
* self.dim.height)
return int(
self.topMargin
+ (self.max_imag - im) / self.span_imag * self.dim.height
)
def getReYPosition(self, d: Datapoint) -> int:
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)
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]:
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):
@ -406,9 +427,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):
@ -418,9 +442,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):
@ -430,9 +457,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):
@ -442,9 +472,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):
@ -454,9 +487,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)
@ -464,17 +498,23 @@ 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})")
f"Maximum jX ({self.maxDisplayImag})"
)
self.menu.exec_(event.globalPos())
def value(self, p: Datapoint) -> complex:

Wyświetl plik

@ -34,30 +34,37 @@ 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 = QtWidgets.QAction(
f"Maximum {MU}' ({self.maxDisplayReal})")
f"Maximum {MU}' ({self.maxDisplayReal})"
)
self.action_set_fixed_maximum_real.triggered.connect(
self.setMaximumRealValue)
self.setMaximumRealValue
)
self.action_set_fixed_minimum_real = QtWidgets.QAction(
f"Minimum {MU}' ({self.minDisplayReal})")
f"Minimum {MU}' ({self.minDisplayReal})"
)
self.action_set_fixed_minimum_real.triggered.connect(
self.setMinimumRealValue)
self.setMinimumRealValue
)
self.action_set_fixed_maximum_imag = QtWidgets.QAction(
f"Maximum {MU}'' ({self.maxDisplayImag})")
f"Maximum {MU}'' ({self.maxDisplayImag})"
)
self.action_set_fixed_maximum_imag.triggered.connect(
self.setMaximumImagValue)
self.setMaximumImagValue
)
self.action_set_fixed_minimum_imag = QtWidgets.QAction(
f"Minimum {MU}'' ({self.minDisplayImag})")
f"Minimum {MU}'' ({self.minDisplayImag})"
)
self.action_set_fixed_minimum_imag.triggered.connect(
self.setMinimumImagValue)
self.setMinimumImagValue
)
self.y_menu.addAction(self.action_set_fixed_maximum_real)
self.y_menu.addAction(self.action_set_fixed_minimum_real)
@ -67,25 +74,21 @@ class RealImaginaryMuChart(RealImaginaryChart):
# Manage core parameters
# TODO pick some sane default values?
self.coreLength = 1.
self.coreArea = 1.
self.coreLength = 1.0
self.coreArea = 1.0
self.coreWindings = 1
self.menu.addSeparator()
self.action_set_core_length = QtWidgets.QAction(
"Core effective length")
self.action_set_core_length.triggered.connect(
self.setCoreLength)
self.action_set_core_length = QtWidgets.QAction("Core effective length")
self.action_set_core_length.triggered.connect(self.setCoreLength)
self.action_set_core_area = QtWidgets.QAction(
"Core area")
self.action_set_core_area.triggered.connect(
self.setCoreArea)
self.action_set_core_area = QtWidgets.QAction("Core area")
self.action_set_core_area.triggered.connect(self.setCoreArea)
self.action_set_core_windings = QtWidgets.QAction(
"Core number of windings")
self.action_set_core_windings.triggered.connect(
self.setCoreWindings)
"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)
@ -102,41 +105,53 @@ class RealImaginaryMuChart(RealImaginaryChart):
def drawChart(self, qp: QtGui.QPainter):
qp.setPen(QtGui.QPen(Chart.color.text))
qp.drawText(self.leftMargin + 5, 15,
f"{self.name}")
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)
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)})")
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 {MU}' ({self.minDisplayReal})")
f"Minimum {MU}' ({self.minDisplayReal})"
)
self.action_set_fixed_maximum_real.setText(
f"Maximum {MU}' ({self.maxDisplayReal})")
f"Maximum {MU}' ({self.maxDisplayReal})"
)
self.action_set_fixed_minimum_imag.setText(
f"Minimum {MU}'' ({self.minDisplayImag})")
f"Minimum {MU}'' ({self.minDisplayImag})"
)
self.action_set_fixed_maximum_imag.setText(
f"Maximum {MU}'' ({self.maxDisplayImag})")
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)
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):
@ -146,9 +161,12 @@ class RealImaginaryMuChart(RealImaginaryChart):
def setCoreArea(self):
val, selected = QtWidgets.QInputDialog.getDouble(
self, "Core effective area",
self,
"Core effective area",
"Set core cross section area length in mm\N{SUPERSCRIPT TWO}",
value=self.coreArea, decimals=2)
value=self.coreArea,
decimals=2,
)
if not selected:
return
if not (self.fixedValues and val >= 0):
@ -158,8 +176,11 @@ class RealImaginaryMuChart(RealImaginaryChart):
def setCoreWindings(self):
val, selected = QtWidgets.QInputDialog.getInt(
self, "Core number of windings",
"Set core number of windings", value=self.coreWindings)
self,
"Core number of windings",
"Set core number of windings",
value=self.coreWindings,
)
if not selected:
return
if not (self.fixedValues and val >= 0):
@ -176,6 +197,7 @@ class RealImaginaryMuChart(RealImaginaryChart):
# 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))
inductance
* (self.coreLength / 1e3)
/ (mu_0 * self.coreWindings**2 * (self.coreArea / 1e6))
)

Wyświetl plik

@ -35,24 +35,32 @@ class RealImaginaryZChart(RealImaginaryChart):
self.y_menu.addSeparator()
self.action_set_fixed_maximum_real = QtWidgets.QAction(
f"Maximum R ({self.maxDisplayReal})")
f"Maximum R ({self.maxDisplayReal})"
)
self.action_set_fixed_maximum_real.triggered.connect(
self.setMaximumRealValue)
self.setMaximumRealValue
)
self.action_set_fixed_minimum_real = QtWidgets.QAction(
f"Minimum R ({self.minDisplayReal})")
f"Minimum R ({self.minDisplayReal})"
)
self.action_set_fixed_minimum_real.triggered.connect(
self.setMinimumRealValue)
self.setMinimumRealValue
)
self.action_set_fixed_maximum_imag = QtWidgets.QAction(
f"Maximum jX ({self.maxDisplayImag})")
f"Maximum jX ({self.maxDisplayImag})"
)
self.action_set_fixed_maximum_imag.triggered.connect(
self.setMaximumImagValue)
self.setMaximumImagValue
)
self.action_set_fixed_minimum_imag = QtWidgets.QAction(
f"Minimum jX ({self.minDisplayImag})")
f"Minimum jX ({self.minDisplayImag})"
)
self.action_set_fixed_minimum_imag.triggered.connect(
self.setMinimumImagValue)
self.setMinimumImagValue
)
self.y_menu.addAction(self.action_set_fixed_maximum_real)
self.y_menu.addAction(self.action_set_fixed_minimum_real)
@ -62,34 +70,43 @@ class RealImaginaryZChart(RealImaginaryChart):
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(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)
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)})")
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})")
f"Maximum jX ({self.maxDisplayImag})"
)
self.menu.exec_(event.globalPos())
def value(self, p: Datapoint) -> complex:

Wyświetl plik

@ -25,6 +25,5 @@ logger = logging.getLogger(__name__)
class RealImaginaryZSeriesChart(RealImaginaryZChart):
def impedance(self, p: Datapoint) -> complex:
return p.seriesImpedance()

Wyświetl plik

@ -25,6 +25,5 @@ logger = logging.getLogger(__name__)
class RealImaginaryZShuntChart(RealImaginaryZChart):
def impedance(self, p: Datapoint) -> complex:
return p.shuntImpedance()

Wyświetl plik

@ -52,14 +52,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:
@ -85,44 +89,58 @@ class SParameterChart(FrequencyChart):
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]:
absy = y - self.topMargin

Wyświetl plik

@ -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 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}",
)

Wyświetl plik

@ -29,11 +29,11 @@ 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.Fixed, QtWidgets.QSizePolicy.MinimumExpanding
)
self.setSizePolicy(sizepolicy)
self.dim.width = 250
self.dim.height = 250
@ -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:
@ -114,11 +120,13 @@ class SquareChart(Chart):
y = a0.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
]

Wyświetl plik

@ -49,7 +49,9 @@ class TDRChart(Chart):
self.setSizePolicy(
QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding))
QtWidgets.QSizePolicy.MinimumExpanding,
)
)
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Background, Chart.color.background)
self.setPalette(pal)
@ -68,11 +70,13 @@ class TDRChart(Chart):
self.action_automatic.setCheckable(True)
self.action_automatic.setChecked(True)
self.action_automatic.changed.connect(
lambda: self.setFixedSpan(self.action_fixed_span.isChecked()))
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()))
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)
@ -80,11 +84,13 @@ class TDRChart(Chart):
self.x_menu.addSeparator()
self.action_set_fixed_start = QtWidgets.QAction(
f"Start ({self.minDisplayLength})")
f"Start ({self.minDisplayLength})"
)
self.action_set_fixed_start.triggered.connect(self.setMinimumLength)
self.action_set_fixed_stop = QtWidgets.QAction(
f"Stop ({self.maxDisplayLength})")
f"Stop ({self.maxDisplayLength})"
)
self.action_set_fixed_stop.triggered.connect(self.setMaximumLength)
self.x_menu.addAction(self.action_set_fixed_start)
@ -96,11 +102,13 @@ class TDRChart(Chart):
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()))
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()))
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)
@ -108,14 +116,18 @@ class TDRChart(Chart):
self.y_menu.addSeparator()
self.y_action_set_fixed_maximum = QtWidgets.QAction(
f"Maximum ({self.maxImpedance})")
f"Maximum ({self.maxImpedance})"
)
self.y_action_set_fixed_maximum.triggered.connect(
self.setMaximumImpedance)
self.setMaximumImpedance
)
self.y_action_set_fixed_minimum = QtWidgets.QAction(
f"Minimum ({self.minImpedance})")
f"Minimum ({self.minImpedance})"
)
self.y_action_set_fixed_minimum.triggered.connect(
self.setMinimumImpedance)
self.setMinimumImpedance
)
self.y_menu.addAction(self.y_action_set_fixed_maximum)
self.y_menu.addAction(self.y_action_set_fixed_minimum)
@ -126,26 +138,29 @@ class TDRChart(Chart):
self.menu.addAction(self.action_save_screenshot)
self.action_popout = QtWidgets.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.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.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})")
f"Minimum ({self.minImpedance})"
)
self.y_action_set_fixed_maximum.setText(
f"Maximum ({self.maxImpedance})")
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
return (
self.leftMargin <= x <= self.width() - self.rightMargin
and self.topMargin <= y <= self.height() - self.bottomMargin
)
def resetDisplayLimits(self):
self.fixedSpan = False
@ -162,9 +177,13 @@ class TDRChart(Chart):
def setMinimumLength(self):
min_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Start length (m)",
"Set start length (m)", value=self.minDisplayLength,
min=0, decimals=1)
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):
@ -174,9 +193,13 @@ class TDRChart(Chart):
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)
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):
@ -190,10 +213,13 @@ class TDRChart(Chart):
def setMinimumImpedance(self):
min_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Minimum impedance (\N{OHM SIGN})",
self,
"Minimum impedance (\N{OHM SIGN})",
"Set minimum impedance (\N{OHM SIGN})",
value=self.minDisplayLength,
min=0, decimals=1)
min=0,
decimals=1,
)
if not selected:
return
if not (self.fixedValues and min_val >= self.maxImpedance):
@ -203,10 +229,13 @@ class TDRChart(Chart):
def setMaximumImpedance(self):
max_val, selected = QtWidgets.QInputDialog.getDouble(
self, "Maximum impedance (\N{OHM SIGN})",
self,
"Maximum impedance (\N{OHM SIGN})",
"Set maximum impedance (\N{OHM SIGN})",
value=self.minDisplayLength,
min=0.1, decimals=1)
min=0.1,
decimals=1,
)
if not selected:
return
if not (self.fixedValues and max_val <= self.minImpedance):
@ -236,9 +265,12 @@ class TDRChart(Chart):
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.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
@ -261,13 +293,14 @@ class TDRChart(Chart):
if self.tdrWindow.td:
if self.fixedSpan:
max_index = np.searchsorted(
self.tdrWindow.distance_axis, self.maxDisplayLength * 2)
self.tdrWindow.distance_axis, self.maxDisplayLength * 2
)
min_index = np.searchsorted(
self.tdrWindow.distance_axis, self.minDisplayLength * 2)
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)
max_index = math.ceil(len(self.tdrWindow.distance_axis) / 2)
x_step = max_index / width
self.markerLocation = int(round(absx * x_step))
@ -282,17 +315,21 @@ class TDRChart(Chart):
qp.setPen(QtGui.QPen(Chart.color.foreground))
qp.drawLine(x, self.topMargin, x, self.topMargin + height)
qp.setPen(QtGui.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")
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(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")
str(round(self.tdrWindow.distance_axis[min_index] / 2, 1)) + "m",
)
def _draw_y_ticks(self, height, width, min_impedance, max_impedance):
qp = QtGui.QPainter(self)
@ -308,7 +345,8 @@ class TDRChart(Chart):
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)}")
3, self.topMargin + height + 3, f"{round(min_impedance, 1)}"
)
def _draw_max_point(self, height, x_step, y_step, min_index):
qp = QtGui.QPainter(self)
@ -316,22 +354,25 @@ class TDRChart(Chart):
max_point = QtCore.QPoint(
self.leftMargin + int((id_max - min_index) / x_step),
(self.topMargin + height) - int(
self.tdrWindow.td[id_max] / y_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")
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 = QtGui.QPainter(self)
marker_point = QtCore.QPoint(
self.leftMargin +
int((self.markerLocation - min_index) / x_step),
(self.topMargin + height) -
int(self.tdrWindow.td[self.markerLocation] / y_step))
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(
@ -339,19 +380,21 @@ class TDRChart(Chart):
marker_point.y() - 5,
f"""{round(
self.tdrWindow.distance_axis[self.markerLocation] / 2,
2)}m""")
2)}m""",
)
def _draw_graph(self, height, width):
min_index = 0
max_index = math.ceil(
len(self.tdrWindow.distance_axis) / 2)
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)
self.tdrWindow.distance_axis, max_length * 2
)
min_index = np.searchsorted(
self.tdrWindow.distance_axis, self.minDisplayLength * 2)
self.tdrWindow.distance_axis, self.minDisplayLength * 2
)
if max_index == min_index:
if max_index < len(self.tdrWindow.distance_axis) - 1:
max_index += 1
@ -361,8 +404,7 @@ class TDRChart(Chart):
# 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)
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)
@ -370,7 +412,7 @@ class TDRChart(Chart):
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)
self._draw_y_ticks(height, width, min_impedance, max_impedance)
qp = QtGui.QPainter(self)
pen = QtGui.QPen(Chart.color.sweep)
@ -388,7 +430,8 @@ class TDRChart(Chart):
x = self.leftMargin + int((i - min_index) / x_step)
y = (self.topMargin + height) - int(
(self.tdrWindow.step_response_Z[i] - min_impedance) / y_step)
(self.tdrWindow.step_response_Z[i] - min_impedance) / y_step
)
if self.isPlotable(x, y):
pen.setColor(Chart.color.sweep_secondary)
qp.setPen(pen)
@ -408,14 +451,18 @@ class TDRChart(Chart):
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)
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)
@ -424,12 +471,13 @@ class TDRChart(Chart):
if self.dragbox.state and self.dragbox.pos[0] != -1:
dashed_pen = QtGui.QPen(
Chart.color.foreground, 1, QtCore.Qt.DashLine)
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)
QtCore.QPoint(*self.dragbox.pos),
)
)
@ -444,11 +492,11 @@ class TDRChart(Chart):
max_impedance = self.maxImpedance
else:
min_impedance = max(
0,
np.min(self.tdrWindow.step_response_Z) / 1.05)
0, np.min(self.tdrWindow.step_response_Z) / 1.05
)
max_impedance = min(
1000,
np.max(self.tdrWindow.step_response_Z) * 1.05)
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
@ -459,20 +507,28 @@ class TDRChart(Chart):
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)
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)
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)
"Zoom to (x,y) by (x,y): (%d, %d) by (%d, %d)", x1, y1, x2, y2
)
val1 = self.valueAtPosition(y1)
val2 = self.valueAtPosition(y2)

Wyświetl plik

@ -30,7 +30,6 @@ logger = logging.getLogger(__name__)
class VSWRChart(FrequencyChart):
def __init__(self, name=""):
super().__init__(name)
@ -90,19 +89,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 +114,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 +136,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,13 +151,15 @@ 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

Wyświetl plik

@ -23,3 +23,31 @@ 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",
]

Wyświetl plik

@ -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")
@ -72,7 +72,8 @@ class MarkerControl(Control):
lock_radiobutton = QtWidgets.QRadioButton("Locked")
lock_radiobutton.setLayoutDirection(QtCore.Qt.RightToLeft)
lock_radiobutton.setSizePolicy(
QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred)
QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred
)
hbox = QtWidgets.QHBoxLayout()
hbox.addWidget(self.showMarkerButton)
@ -82,8 +83,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()

Wyświetl plik

@ -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", True, 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()

Wyświetl plik

@ -21,8 +21,10 @@ import logging
from PyQt5 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,7 +32,6 @@ logger = logging.getLogger(__name__)
class SweepControl(Control):
def __init__(self, app: QtWidgets.QWidget):
super().__init__(app, "Sweep control")
@ -66,8 +67,7 @@ class SweepControl(Control):
self.input_center.setAlignment(QtCore.Qt.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)
@ -77,7 +77,8 @@ class SweepControl(Control):
input_right_layout.addRow(QtWidgets.QLabel("Span"), self.input_span)
self.input_segments = QtWidgets.QLineEdit(
self.app.settings.value("Segments", "1"))
self.app.settings.value("Segments", "1")
)
self.input_segments.setAlignment(QtCore.Qt.AlignRight)
self.input_segments.setFixedHeight(20)
self.input_segments.setFixedWidth(60)
@ -85,7 +86,8 @@ class SweepControl(Control):
self.label_step = QtWidgets.QLabel("Hz/step")
self.label_step.setAlignment(
QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter
)
segment_layout = QtWidgets.QHBoxLayout()
segment_layout.addWidget(self.input_segments)
@ -95,7 +97,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)
@ -206,8 +209,7 @@ 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):

Wyświetl plik

@ -43,12 +43,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
@ -69,33 +69,49 @@ class Chart:
@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(QtCore.Qt.white)
)
foreground: QColor = DC.field(
default_factory=lambda: QColor(QtCore.Qt.lightGray))
default_factory=lambda: QColor(QtCore.Qt.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(QtCore.Qt.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))
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(QtCore.Qt.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))
@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(QtCore.Qt.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 +119,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(QtCore.Qt.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=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())
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)
@ -147,25 +160,25 @@ def from_type(data) -> str:
type_map = {
bytearray: lambda x: x.hex(),
QColor: lambda x: x.getRgb(),
QByteArray: lambda x: x.toHex()
QByteArray: lambda x: x.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 +191,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()

Wyświetl plik

@ -27,22 +27,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)
@ -117,7 +122,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 +140,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 +158,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

Wyświetl plik

@ -43,8 +43,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
@ -71,15 +71,21 @@ 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
@ -88,13 +94,18 @@ 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)
@ -109,9 +120,8 @@ 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 +140,19 @@ 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"),
("tinySA", "tinySA"),
):
if info.find(search) >= 0:
return name
@ -171,7 +181,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 ""

Wyświetl plik

@ -46,7 +46,6 @@ class NanoVNA(VNA):
self._sweepdata = []
def _get_running_frequencies(self):
logger.debug("Reading values: frequencies")
try:
frequencies = super().readValues("frequencies")
@ -61,24 +60,27 @@ 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:
logger.debug("Capturing screenshot...")
@ -90,12 +92,12 @@ class NanoVNA(VNA):
rgba_array,
self.screenwidth,
self.screenheight,
QtGui.QImage.Format_ARGB32)
QtGui.QImage.Format_ARGB32,
)
logger.debug("Captured screenshot")
return QtGui.QPixmap(image)
except serial.SerialException as exc:
logger.exception(
"Exception while capturing screenshot: %s", exc)
logger.exception("Exception while capturing screenshot: %s", exc)
return QtGui.QPixmap()
def resetSweep(self, start: int, stop: int):
@ -125,8 +127,12 @@ class NanoVNA(VNA):
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]:
if self.sweep_method != "scan_mask":
@ -137,11 +143,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":

Wyświetl plik

@ -46,10 +46,10 @@ class NanoVNA_F_V2(NanoVNA):
rgba_array,
self.screenwidth,
self.screenheight,
QtGui.QImage.Format_RGB16)
QtGui.QImage.Format_RGB16,
)
logger.debug("Captured screenshot")
return QtGui.QPixmap(image)
except serial.SerialException as exc:
logger.exception(
"Exception while capturing screenshot: %s", exc)
logger.exception("Exception while capturing screenshot: %s", exc)
return QtGui.QPixmap()

Wyświetl plik

@ -26,13 +26,13 @@ from NanoVNASaver.Hardware.Serial import Interface
from NanoVNASaver.Hardware.VNA import VNA
from NanoVNASaver.Version import Version
if platform.system() != 'Windows':
if platform.system() != "Windows":
import tty
logger = logging.getLogger(__name__)
_CMD_NOP = 0x00
_CMD_INDICATE = 0x0d
_CMD_INDICATE = 0x0D
_CMD_READ = 0x10
_CMD_READ2 = 0x11
_CMD_READ4 = 0x12
@ -49,22 +49,23 @@ _ADDR_SWEEP_POINTS = 0x20
_ADDR_SWEEP_VALS_PER_FREQ = 0x22
_ADDR_RAW_SAMPLES_MODE = 0x26
_ADDR_VALUES_FIFO = 0x30
_ADDR_DEVICE_VARIANT = 0xf0
_ADDR_PROTOCOL_VERSION = 0xf1
_ADDR_HARDWARE_REVISION = 0xf2
_ADDR_FW_MAJOR = 0xf3
_ADDR_FW_MINOR = 0xf4
_ADDR_DEVICE_VARIANT = 0xF0
_ADDR_PROTOCOL_VERSION = 0xF1
_ADDR_HARDWARE_REVISION = 0xF2
_ADDR_FW_MAJOR = 0xF3
_ADDR_FW_MINOR = 0xF4
WRITE_SLEEP = 0.05
_ADF4350_TXPOWER_DESC_MAP = {
0: '9dB attenuation',
1: '6dB attenuation',
2: '3dB attenuation',
3: 'Maximum',
0: "9dB attenuation",
1: "6dB attenuation",
2: "3dB attenuation",
3: "Maximum",
}
_ADF4350_TXPOWER_DESC_REV_MAP = {
value: key for key, value in _ADF4350_TXPOWER_DESC_MAP.items()}
value: key for key, value in _ADF4350_TXPOWER_DESC_MAP.items()
}
class NanoVNA_V2(VNA):
@ -76,7 +77,7 @@ class NanoVNA_V2(VNA):
def __init__(self, iface: Interface):
super().__init__(iface)
if platform.system() != 'Windows':
if platform.system() != "Windows":
tty.setraw(self.serial.fd)
# reset protocol to known state
@ -85,8 +86,8 @@ class NanoVNA_V2(VNA):
sleep(WRITE_SLEEP)
# firmware major version of 0xff indicates dfu mode
if self.version.data["major"] == 0xff:
raise IOError('Device is in DFU mode')
if self.version.data["major"] == 0xFF:
raise IOError("Device is in DFU mode")
if "S21 hack" in self.features:
self.valid_datapoints = (101, 11, 51, 201, 301, 501, 1021)
@ -116,8 +117,13 @@ class NanoVNA_V2(VNA):
self.features.update({"Set TX power partial", "Set Average"})
# Can only set ADF4350 power, i.e. for >= 140MHz
self.txPowerRanges = [
((140e6, self.sweep_max_freq_Hz),
[_ADF4350_TXPOWER_DESC_MAP[value] for value in (3, 2, 1, 0)]),
(
(140e6, self.sweep_max_freq_Hz),
[
_ADF4350_TXPOWER_DESC_MAP[value]
for value in (3, 2, 1, 0)
],
),
]
def readFirmware(self) -> str:
@ -135,9 +141,15 @@ class NanoVNA_V2(VNA):
freq_index = -1
for i in range(pointstoread):
(fwd_real, fwd_imag, rev0_real, rev0_imag, rev1_real,
rev1_imag, freq_index) = unpack_from(
"<iiiiiihxxxxxx", arr, i * 32)
(
fwd_real,
fwd_imag,
rev0_real,
rev0_imag,
rev1_real,
rev1_imag,
freq_index,
) = unpack_from("<iiiiiihxxxxxx", arr, i * 32)
fwd = complex(fwd_real, fwd_imag)
refl = complex(rev0_real, rev0_imag)
thru = complex(rev1_real, rev1_imag)
@ -158,12 +170,14 @@ class NanoVNA_V2(VNA):
self.serial.write(pack("<Q", 0))
sleep(WRITE_SLEEP)
# cmd: write register 0x30 to clear FIFO
self.serial.write(pack("<BBB",
_CMD_WRITE, _ADDR_VALUES_FIFO, 0))
self.serial.write(
pack("<BBB", _CMD_WRITE, _ADDR_VALUES_FIFO, 0)
)
sleep(WRITE_SLEEP)
# clear sweepdata
self._sweepdata = [(complex(), complex())] * (
self.datapoints + s21hack)
self.datapoints + s21hack
)
pointstodo = self.datapoints + s21hack
# we read at most 255 values at a time and the time required
# empirically is just over 3 seconds for 101 points or
@ -174,9 +188,13 @@ class NanoVNA_V2(VNA):
pointstoread = min(255, pointstodo)
# cmd: read FIFO, addr 0x30
self.serial.write(
pack("<BBB",
_CMD_READFIFO, _ADDR_VALUES_FIFO,
pointstoread))
pack(
"<BBB",
_CMD_READFIFO,
_ADDR_VALUES_FIFO,
pointstoread,
)
)
sleep(WRITE_SLEEP)
# each value is 32 bytes
nBytes = pointstoread * 32
@ -185,8 +203,9 @@ class NanoVNA_V2(VNA):
# timeout secs
arr = self.serial.read(nBytes)
if nBytes != len(arr):
logger.warning("expected %d bytes, got %d",
nBytes, len(arr))
logger.warning(
"expected %d bytes, got %d", nBytes, len(arr)
)
# the way to retry on timeout is keep the data
# already read then try to read the rest of
# the data into the array
@ -205,8 +224,7 @@ class NanoVNA_V2(VNA):
idx = 1 if value == "data 1" else 0
return [
f'{str(x[idx].real)} {str(x[idx].imag)}'
for x in self._sweepdata
f"{str(x[idx].real)} {str(x[idx].imag)}" for x in self._sweepdata
]
def resetSweep(self, start: int, stop: int):
@ -225,15 +243,15 @@ class NanoVNA_V2(VNA):
raise IOError("Timeout reading version registers")
return Version(f"{resp[0]}.0.{resp[1]}")
def readVersion(self) -> 'Version':
result = self._read_version(_ADDR_FW_MAJOR,
_ADDR_FW_MINOR)
def readVersion(self) -> "Version":
result = self._read_version(_ADDR_FW_MAJOR, _ADDR_FW_MINOR)
logger.debug("readVersion: %s", result)
return result
def read_board_revision(self) -> 'Version':
result = self._read_version(_ADDR_DEVICE_VARIANT,
_ADDR_HARDWARE_REVISION)
def read_board_revision(self) -> "Version":
result = self._read_version(
_ADDR_DEVICE_VARIANT, _ADDR_HARDWARE_REVISION
)
logger.debug("read_board_revision: %s", result)
return result
@ -243,34 +261,41 @@ class NanoVNA_V2(VNA):
return
self.sweepStartHz = start
self.sweepStepHz = step
logger.info('NanoVNAV2: set sweep start %d step %d',
self.sweepStartHz, self.sweepStepHz)
logger.info(
"NanoVNAV2: set sweep start %d step %d",
self.sweepStartHz,
self.sweepStepHz,
)
self._updateSweep()
return
def _updateSweep(self):
s21hack = "S21 hack" in self.features
cmd = pack("<BBQ", _CMD_WRITE8, _ADDR_SWEEP_START,
max(50000,
int(self.sweepStartHz - (self.sweepStepHz * s21hack))))
cmd += pack("<BBQ", _CMD_WRITE8,
_ADDR_SWEEP_STEP, int(self.sweepStepHz))
cmd += pack("<BBH", _CMD_WRITE2,
_ADDR_SWEEP_POINTS, self.datapoints + s21hack)
cmd += pack("<BBH", _CMD_WRITE2,
_ADDR_SWEEP_VALS_PER_FREQ, 1)
cmd = pack(
"<BBQ",
_CMD_WRITE8,
_ADDR_SWEEP_START,
max(50000, int(self.sweepStartHz - (self.sweepStepHz * s21hack))),
)
cmd += pack(
"<BBQ", _CMD_WRITE8, _ADDR_SWEEP_STEP, int(self.sweepStepHz)
)
cmd += pack(
"<BBH", _CMD_WRITE2, _ADDR_SWEEP_POINTS, self.datapoints + s21hack
)
cmd += pack("<BBH", _CMD_WRITE2, _ADDR_SWEEP_VALS_PER_FREQ, 1)
with self.serial.lock:
self.serial.write(cmd)
sleep(WRITE_SLEEP)
def setTXPower(self, freq_range, power_desc):
if freq_range[0] != 140e6:
raise ValueError('Invalid TX power frequency range')
raise ValueError("Invalid TX power frequency range")
# 140MHz..max => ADF4350
self._set_register(0x42, _ADF4350_TXPOWER_DESC_REV_MAP[power_desc], 1)
def _set_register(self, addr, value, size):
packet = b''
packet = b""
if size == 1:
packet = pack("<BBB", _CMD_WRITE, addr, value)
elif size == 2:

Wyświetl plik

@ -41,7 +41,7 @@ def drain_serial(serial_port: serial.Serial):
class Interface(serial.Serial):
def __init__(self, interface_type: str, comment, *args, **kwargs):
super().__init__(*args, **kwargs)
assert interface_type in {'serial', 'usb', 'bt', 'network'}
assert interface_type in {"serial", "usb", "bt", "network"}
self.type = interface_type
self.comment = comment
self.port = None

Wyświetl plik

@ -34,18 +34,17 @@ class TinySA(VNA):
name = "tinySA"
screenwidth = 320
screenheight = 240
valid_datapoints = (290, )
valid_datapoints = (290,)
def __init__(self, iface: Interface):
super().__init__(iface)
self.features = {'Screenshots'}
self.features = {"Screenshots"}
logger.debug("Setting initial start,stop")
self.start, self.stop = self._get_running_frequencies()
self.sweep_max_freq_Hz = 950e6
self._sweepdata = []
def _get_running_frequencies(self):
logger.debug("Reading values: frequencies")
try:
frequencies = super().readValues("frequencies")
@ -60,24 +59,27 @@ class TinySA(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:
logger.debug("Capturing screenshot...")
@ -89,12 +91,12 @@ class TinySA(VNA):
rgba_array,
self.screenwidth,
self.screenheight,
QtGui.QImage.Format_ARGB32)
QtGui.QImage.Format_ARGB32,
)
logger.debug("Captured screenshot")
return QtGui.QPixmap(image)
except serial.SerialException as exc:
logger.exception(
"Exception while capturing screenshot: %s", exc)
logger.exception("Exception while capturing screenshot: %s", exc)
return QtGui.QPixmap()
def resetSweep(self, start: int, stop: int):
@ -113,6 +115,7 @@ class TinySA(VNA):
def readValues(self, value) -> List[str]:
logger.debug("Read: %s", value)
if value == "data 0":
self._sweepdata = [f"0 {line.strip()}"
for line in self.exec_command("data")]
self._sweepdata = [
f"0 {line.strip()}" for line in self.exec_command("data")
]
return self._sweepdata

Wyświetl plik

@ -44,8 +44,11 @@ WAIT = 0.05
def _max_retries(bandwidth: int, datapoints: int) -> int:
return round(20 + 20 * (datapoints / 101) +
(1000 / bandwidth) ** 1.30 * (datapoints / 101))
return round(
20
+ 20 * (datapoints / 101)
+ (1000 / bandwidth) ** 1.30 * (datapoints / 101)
)
class VNA:
@ -94,7 +97,7 @@ class VNA:
logger.debug("exec_command(%s)", command)
with self.serial.lock:
drain_serial(self.serial)
self.serial.write(f"{command}\r".encode('ascii'))
self.serial.write(f"{command}\r".encode("ascii"))
sleep(wait)
retries = 0
max_retries = _max_retries(self.bandwidth, self.datapoints)
@ -137,11 +140,14 @@ class VNA:
result = result.split(" {")[1].strip("}")
return sorted([int(i) for i in result.split("|")])
except IndexError:
return [1000, ]
return [
1000,
]
def set_bandwidth(self, bandwidth: int):
bw_val = DISLORD_BW[bandwidth] \
if self.bw_method == "dislord" else bandwidth
bw_val = (
DISLORD_BW[bandwidth] if self.bw_method == "dislord" else bandwidth
)
result = " ".join(self.exec_command(f"bandwidth {bw_val}"))
if self.bw_method == "ttrftech" and result:
raise IOError(f"set_bandwith({bandwidth}: {result}")
@ -191,11 +197,10 @@ class VNA:
def readValues(self, value) -> List[str]:
logger.debug("VNA reading %s", value)
result = list(self.exec_command(value))
logger.debug("VNA done reading %s (%d values)",
value, len(result))
logger.debug("VNA done reading %s (%d values)", value, len(result))
return result
def readVersion(self) -> 'Version':
def readVersion(self) -> "Version":
result = list(self.exec_command("version"))
logger.debug("result:\n%s", result)
return Version(result[0])

Wyświetl plik

@ -61,71 +61,91 @@ class DeltaMarker(Marker):
imp = imp_b - imp_a
cap_str = format_capacitance(
RFTools.impedance_to_capacitance(imp_b, s11_b.freq) -
RFTools.impedance_to_capacitance(imp_a, s11_a.freq))
RFTools.impedance_to_capacitance(imp_b, s11_b.freq)
- RFTools.impedance_to_capacitance(imp_a, s11_a.freq)
)
ind_str = format_inductance(
RFTools.impedance_to_inductance(imp_b, s11_b.freq) -
RFTools.impedance_to_inductance(imp_a, s11_a.freq))
RFTools.impedance_to_inductance(imp_b, s11_b.freq)
- RFTools.impedance_to_inductance(imp_a, s11_a.freq)
)
imp_p_a = RFTools.serial_to_parallel(imp_a)
imp_p_b = RFTools.serial_to_parallel(imp_b)
imp_p = imp_p_b - imp_p_a
cap_p_str = format_capacitance(
RFTools.impedance_to_capacitance(imp_p_b, s11_b.freq) -
RFTools.impedance_to_capacitance(imp_p_a, s11_a.freq))
RFTools.impedance_to_capacitance(imp_p_b, s11_b.freq)
- RFTools.impedance_to_capacitance(imp_p_a, s11_a.freq)
)
ind_p_str = format_inductance(
RFTools.impedance_to_inductance(imp_p_b, s11_b.freq) -
RFTools.impedance_to_inductance(imp_p_a, s11_a.freq))
RFTools.impedance_to_inductance(imp_p_b, s11_b.freq)
- RFTools.impedance_to_inductance(imp_p_a, s11_a.freq)
)
x_str = cap_str if imp.imag < 0 else ind_str
x_p_str = cap_p_str if imp_p.imag < 0 else ind_p_str
self.label['actualfreq'].setText(
format_frequency_space(s11_b.freq - s11_a.freq))
self.label['lambda'].setText(
format_wavelength(s11_b.wavelength - s11_a.wavelength))
self.label['admittance'].setText(format_complex_adm(imp_p, True))
self.label['impedance'].setText(format_complex_imp(imp, True))
self.label["actualfreq"].setText(
format_frequency_space(s11_b.freq - s11_a.freq)
)
self.label["lambda"].setText(
format_wavelength(s11_b.wavelength - s11_a.wavelength)
)
self.label["admittance"].setText(format_complex_adm(imp_p, True))
self.label["impedance"].setText(format_complex_imp(imp, True))
self.label['parc'].setText(cap_p_str)
self.label['parl'].setText(ind_p_str)
self.label['parlc'].setText(x_p_str)
self.label["parc"].setText(cap_p_str)
self.label["parl"].setText(ind_p_str)
self.label["parlc"].setText(x_p_str)
self.label['parr'].setText(format_resistance(imp_p.real, True))
self.label['returnloss'].setText(
format_gain(s11_b.gain - s11_a.gain, self.returnloss_is_positive))
self.label['s11groupdelay'].setText(format_group_delay(
RFTools.groupDelay(b.s11, 1) -
RFTools.groupDelay(a.s11, 1)))
self.label["parr"].setText(format_resistance(imp_p.real, True))
self.label["returnloss"].setText(
format_gain(s11_b.gain - s11_a.gain, self.returnloss_is_positive)
)
self.label["s11groupdelay"].setText(
format_group_delay(
RFTools.groupDelay(b.s11, 1) - RFTools.groupDelay(a.s11, 1)
)
)
self.label['s11mag'].setText(
format_magnitude(abs(s11_b.z) - abs(s11_a.z)))
self.label['s11phase'].setText(format_phase(s11_b.phase - s11_a.phase))
self.label['s11polar'].setText(
self.label["s11mag"].setText(
format_magnitude(abs(s11_b.z) - abs(s11_a.z))
)
self.label["s11phase"].setText(format_phase(s11_b.phase - s11_a.phase))
self.label["s11polar"].setText(
f"{round(abs(s11_b.z) - abs(s11_a.z), 2)}"
f"{format_phase(s11_b.phase - s11_a.phase)}")
self.label['s11q'].setText(format_q_factor(
s11_b.qFactor() - s11_a.qFactor(), True))
self.label['s11z'].setText(format_resistance(abs(imp)))
self.label['serc'].setText(cap_str)
self.label['serl'].setText(ind_str)
self.label['serlc'].setText(x_str)
self.label['serr'].setText(format_resistance(imp.real, True))
self.label['vswr'].setText(format_vswr(s11_b.vswr - s11_a.vswr))
f"{format_phase(s11_b.phase - s11_a.phase)}"
)
self.label["s11q"].setText(
format_q_factor(s11_b.qFactor() - s11_a.qFactor(), True)
)
self.label["s11z"].setText(format_resistance(abs(imp)))
self.label["serc"].setText(cap_str)
self.label["serl"].setText(ind_str)
self.label["serlc"].setText(x_str)
self.label["serr"].setText(format_resistance(imp.real, True))
self.label["vswr"].setText(format_vswr(s11_b.vswr - s11_a.vswr))
if len(a.s21) == len(a.s11):
s21_a = a.s21[1]
s21_b = b.s21[1]
self.label['s21gain'].setText(format_gain(
s21_b.gain - s21_a.gain))
self.label['s21groupdelay'].setText(format_group_delay(
(RFTools.groupDelay(b.s21, 1) -
RFTools.groupDelay(a.s21, 1)) / 2))
self.label['s21mag'].setText(format_magnitude(
abs(s21_b.z) - abs(s21_a.z)))
self.label['s21phase'].setText(format_phase(
s21_b.phase - s21_a.phase))
self.label['s21polar'].setText(
self.label["s21gain"].setText(format_gain(s21_b.gain - s21_a.gain))
self.label["s21groupdelay"].setText(
format_group_delay(
(
RFTools.groupDelay(b.s21, 1)
- RFTools.groupDelay(a.s21, 1)
)
/ 2
)
)
self.label["s21mag"].setText(
format_magnitude(abs(s21_b.z) - abs(s21_a.z))
)
self.label["s21phase"].setText(
format_phase(s21_b.phase - s21_a.phase)
)
self.label["s21polar"].setText(
f"{round(abs(s21_b.z) - abs(s21_a.z), 2)}"
f"{format_phase(s21_b.phase - s21_a.phase)}")
f"{format_phase(s21_b.phase - s21_a.phase)}"
)

Wyświetl plik

@ -56,10 +56,10 @@ TYPES = (
Label("s21groupdelay", "S21 Group Delay", "S21 Group Delay", False),
Label("s21magshunt", "S21 |Z| shunt", "S21 Z Magnitude shunt", False),
Label("s21magseries", "S21 |Z| series", "S21 Z Magnitude series", False),
Label("s21realimagshunt", "S21 R+jX shunt",
"S21 Z Real+Imag shunt", False),
Label("s21realimagseries", "S21 R+jX series",
"S21 Z Real+Imag series", False),
Label("s21realimagshunt", "S21 R+jX shunt", "S21 Z Real+Imag shunt", False),
Label(
"s21realimagseries", "S21 R+jX series", "S21 Z Real+Imag series", False
),
)
@ -67,31 +67,40 @@ def default_label_ids() -> str:
return [label.label_id for label in TYPES if label.default_active]
class Value():
class Value:
"""Contains the data area to calculate marker values from"""
def __init__(self, freq: int = 0,
s11: List[Datapoint] = None,
s21: List[Datapoint] = None):
def __init__(
self,
freq: int = 0,
s11: List[Datapoint] = None,
s21: List[Datapoint] = None,
):
self.freq = freq
self.s11 = [] if s11 is None else s11[:]
self.s21 = [] if s21 is None else s21[:]
def store(self, index: int,
s11: List[Datapoint],
s21: List[Datapoint]):
def store(self, index: int, s11: List[Datapoint], s21: List[Datapoint]):
# handle boundaries
if index == 0:
index = 1
s11 = [s11[0], ] + s11
s11 = [
s11[0],
] + s11
if s21:
s21 = [s21[0], ] + s21
s21 = [
s21[0],
] + s21
if index == len(s11):
s11 += [s11[-1], ]
s11 += [
s11[-1],
]
if s21:
s21 += [s21[-1], ]
s21 += [
s21[-1],
]
self.freq = s11[1].freq
self.s11 = s11[index - 1:index + 2]
self.s11 = s11[index - 1 : index + 2]
if s21:
self.s21 = s21[index - 1:index + 2]
self.s21 = s21[index - 1 : index + 2]

Wyświetl plik

@ -81,7 +81,8 @@ class Marker(QtCore.QObject, Value):
if self.qsettings:
Marker._instances += 1
Marker.active_labels = self.qsettings.value(
"MarkerFields", defaultValue=default_label_ids())
"MarkerFields", defaultValue=default_label_ids()
)
self.index = Marker._instances
if not self.name:
@ -92,7 +93,9 @@ class Marker(QtCore.QObject, Value):
self.frequencyInput.setAlignment(QtCore.Qt.AlignRight)
self.frequencyInput.editingFinished.connect(
lambda: self.setFrequency(
parse_frequency(self.frequencyInput.text())))
parse_frequency(self.frequencyInput.text())
)
)
###############################################################
# Data display labels
@ -101,8 +104,8 @@ class Marker(QtCore.QObject, Value):
self.label = {
label.label_id: MarkerLabel(label.name) for label in TYPES
}
self.label['actualfreq'].setMinimumWidth(100)
self.label['returnloss'].setMinimumWidth(80)
self.label["actualfreq"].setMinimumWidth(100)
self.label["returnloss"].setMinimumWidth(80)
###############################################################
# Marker control layout
@ -112,8 +115,11 @@ class Marker(QtCore.QObject, Value):
self.btnColorPicker.setMinimumHeight(20)
self.btnColorPicker.setFixedWidth(20)
self.btnColorPicker.clicked.connect(
lambda: self.setColor(QtWidgets.QColorDialog.getColor(
self.color, options=QtWidgets.QColorDialog.ShowAlphaChannel))
lambda: self.setColor(
QtWidgets.QColorDialog.getColor(
self.color, options=QtWidgets.QColorDialog.ShowAlphaChannel
)
)
)
self.isMouseControlledRadioButton = QtWidgets.QRadioButton()
@ -133,7 +139,9 @@ class Marker(QtCore.QObject, Value):
try:
self.setColor(
self.qsettings.value(
f"Marker{self.count()}Color", COLORS[self.count()]))
f"Marker{self.count()}Color", COLORS[self.count()]
)
)
except AttributeError: # happens when qsettings == None
self.setColor(COLORS[1])
except IndexError:
@ -159,8 +167,7 @@ class Marker(QtCore.QObject, Value):
def _add_active_labels(self, label_id, form):
if label_id in self.label:
form.addRow(
f"{self.label[label_id].name}:", self.label[label_id])
form.addRow(f"{self.label[label_id].name}:", self.label[label_id])
self.label[label_id].show()
def _size_str(self) -> str:
@ -171,9 +178,9 @@ class Marker(QtCore.QObject, Value):
def setScale(self, scale):
self.group_box.setMaximumWidth(int(340 * scale))
self.label['actualfreq'].setMinimumWidth(int(100 * scale))
self.label['actualfreq'].setMinimumWidth(int(100 * scale))
self.label['returnloss'].setMinimumWidth(int(80 * scale))
self.label["actualfreq"].setMinimumWidth(int(100 * scale))
self.label["actualfreq"].setMinimumWidth(int(100 * scale))
self.label["returnloss"].setMinimumWidth(int(80 * scale))
if self.coloredText:
color_string = QtCore.QVariant(self.color)
color_string.convert(QtCore.QVariant.String)
@ -259,8 +266,10 @@ class Marker(QtCore.QObject, Value):
upper_stepsize = data[-1].freq - data[-2].freq
# We are outside the bounds of the data, so we can't put in a marker
if (self.freq + lower_stepsize / 2 < min_freq or
self.freq - upper_stepsize / 2 > max_freq):
if (
self.freq + lower_stepsize / 2 < min_freq
or self.freq - upper_stepsize / 2 > max_freq
):
return
min_distance = max_freq
@ -286,15 +295,16 @@ class Marker(QtCore.QObject, Value):
for v in self.label.values():
v.setText("")
def updateLabels(self,
s11: List[RFTools.Datapoint],
s21: List[RFTools.Datapoint]):
def updateLabels(
self, s11: List[RFTools.Datapoint], s21: List[RFTools.Datapoint]
):
if not s11:
return
if self.location == -1: # initial position
try:
location = (self.index - 1) / (
(self._instances - 1) * (len(s11) - 1))
(self._instances - 1) * (len(s11) - 1)
)
self.location = int(location)
except ZeroDivisionError:
self.location = 0
@ -309,63 +319,72 @@ class Marker(QtCore.QObject, Value):
imp = _s11.impedance()
cap_str = format_capacitance(
RFTools.impedance_to_capacitance(imp, _s11.freq))
RFTools.impedance_to_capacitance(imp, _s11.freq)
)
ind_str = format_inductance(
RFTools.impedance_to_inductance(imp, _s11.freq))
RFTools.impedance_to_inductance(imp, _s11.freq)
)
imp_p = RFTools.serial_to_parallel(imp)
cap_p_str = format_capacitance(
RFTools.impedance_to_capacitance(imp_p, _s11.freq))
RFTools.impedance_to_capacitance(imp_p, _s11.freq)
)
ind_p_str = format_inductance(
RFTools.impedance_to_inductance(imp_p, _s11.freq))
RFTools.impedance_to_inductance(imp_p, _s11.freq)
)
x_str = cap_str if imp.imag < 0 else ind_str
x_p_str = cap_p_str if imp_p.imag < 0 else ind_p_str
self.label['actualfreq'].setText(format_frequency_space(_s11.freq))
self.label['lambda'].setText(format_wavelength(_s11.wavelength))
self.label['admittance'].setText(format_complex_adm(imp))
self.label['impedance'].setText(format_complex_imp(imp))
self.label['parc'].setText(cap_p_str)
self.label['parl'].setText(ind_p_str)
self.label['parlc'].setText(x_p_str)
self.label['parr'].setText(format_resistance(imp_p.real))
self.label['returnloss'].setText(
format_gain(_s11.gain, self.returnloss_is_positive))
self.label['s11groupdelay'].setText(
format_group_delay(RFTools.groupDelay(s11, self.location)))
self.label['s11mag'].setText(format_magnitude(abs(_s11.z)))
self.label['s11phase'].setText(format_phase(_s11.phase))
self.label['s11polar'].setText(
f'{str(round(abs(_s11.z), 2))}{format_phase(_s11.phase)}'
self.label["actualfreq"].setText(format_frequency_space(_s11.freq))
self.label["lambda"].setText(format_wavelength(_s11.wavelength))
self.label["admittance"].setText(format_complex_adm(imp))
self.label["impedance"].setText(format_complex_imp(imp))
self.label["parc"].setText(cap_p_str)
self.label["parl"].setText(ind_p_str)
self.label["parlc"].setText(x_p_str)
self.label["parr"].setText(format_resistance(imp_p.real))
self.label["returnloss"].setText(
format_gain(_s11.gain, self.returnloss_is_positive)
)
self.label["s11groupdelay"].setText(
format_group_delay(RFTools.groupDelay(s11, self.location))
)
self.label["s11mag"].setText(format_magnitude(abs(_s11.z)))
self.label["s11phase"].setText(format_phase(_s11.phase))
self.label["s11polar"].setText(
f"{str(round(abs(_s11.z), 2))}{format_phase(_s11.phase)}"
)
self.label['s11q'].setText(format_q_factor(_s11.qFactor()))
self.label['s11z'].setText(format_resistance(abs(imp)))
self.label['serc'].setText(cap_str)
self.label['serl'].setText(ind_str)
self.label['serlc'].setText(x_str)
self.label['serr'].setText(format_resistance(imp.real))
self.label['vswr'].setText(format_vswr(_s11.vswr))
self.label["s11q"].setText(format_q_factor(_s11.qFactor()))
self.label["s11z"].setText(format_resistance(abs(imp)))
self.label["serc"].setText(cap_str)
self.label["serl"].setText(ind_str)
self.label["serlc"].setText(x_str)
self.label["serr"].setText(format_resistance(imp.real))
self.label["vswr"].setText(format_vswr(_s11.vswr))
if len(s21) == len(s11):
_s21 = s21[self.location]
self.label['s21gain'].setText(format_gain(_s21.gain))
self.label['s21groupdelay'].setText(
format_group_delay(RFTools.groupDelay(s21, self.location) / 2))
self.label['s21mag'].setText(format_magnitude(abs(_s21.z)))
self.label['s21phase'].setText(format_phase(_s21.phase))
self.label['s21polar'].setText(
f'{str(round(abs(_s21.z), 2))}{format_phase(_s21.phase)}'
self.label["s21gain"].setText(format_gain(_s21.gain))
self.label["s21groupdelay"].setText(
format_group_delay(RFTools.groupDelay(s21, self.location) / 2)
)
self.label["s21mag"].setText(format_magnitude(abs(_s21.z)))
self.label["s21phase"].setText(format_phase(_s21.phase))
self.label["s21polar"].setText(
f"{str(round(abs(_s21.z), 2))}{format_phase(_s21.phase)}"
)
self.label['s21magshunt'].setText(
format_magnitude(abs(_s21.shuntImpedance())))
self.label['s21magseries'].setText(
format_magnitude(abs(_s21.seriesImpedance())))
self.label['s21realimagshunt'].setText(
format_complex_imp(
_s21.shuntImpedance(), allow_negative=True))
self.label['s21realimagseries'].setText(
format_complex_imp(
_s21.seriesImpedance(), allow_negative=True))
self.label["s21magshunt"].setText(
format_magnitude(abs(_s21.shuntImpedance()))
)
self.label["s21magseries"].setText(
format_magnitude(abs(_s21.seriesImpedance()))
)
self.label["s21realimagshunt"].setText(
format_complex_imp(_s21.shuntImpedance(), allow_negative=True)
)
self.label["s21realimagseries"].setText(
format_complex_imp(_s21.seriesImpedance(), allow_negative=True)
)

Wyświetl plik

@ -26,9 +26,14 @@ from PyQt5 import QtWidgets, QtCore, QtGui
from NanoVNASaver import Defaults
from .Windows import (
AboutWindow, AnalysisWindow, CalibrationWindow,
DeviceSettingsWindow, DisplaySettingsWindow, SweepSettingsWindow,
TDRWindow, FilesWindow
AboutWindow,
AnalysisWindow,
CalibrationWindow,
DeviceSettingsWindow,
DisplaySettingsWindow,
SweepSettingsWindow,
TDRWindow,
FilesWindow,
)
from .Controls.MarkerControl import MarkerControl
from .Controls.SweepControl import SweepControl
@ -40,14 +45,26 @@ from .RFTools import corr_att_data
from .Charts.Chart import Chart
from .Charts import (
CapacitanceChart,
CombinedLogMagChart, GroupDelayChart, InductanceChart,
LogMagChart, PhaseChart,
MagnitudeChart, MagnitudeZChart, MagnitudeZShuntChart,
CombinedLogMagChart,
GroupDelayChart,
InductanceChart,
LogMagChart,
PhaseChart,
MagnitudeChart,
MagnitudeZChart,
MagnitudeZShuntChart,
MagnitudeZSeriesChart,
QualityFactorChart, VSWRChart, PermeabilityChart, PolarChart,
QualityFactorChart,
VSWRChart,
PermeabilityChart,
PolarChart,
RealImaginaryMuChart,
RealImaginaryZChart, RealImaginaryZShuntChart, RealImaginaryZSeriesChart,
SmithChart, SParameterChart, TDRChart,
RealImaginaryZChart,
RealImaginaryZShuntChart,
RealImaginaryZSeriesChart,
SmithChart,
SParameterChart,
TDRChart,
)
from .Calibration import Calibration
from .Marker.Widget import Marker
@ -69,10 +86,11 @@ class NanoVNASaver(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.s21att = 0.0
if getattr(sys, 'frozen', False):
if getattr(sys, "frozen", False):
logger.debug("Running from pyinstaller bundle")
self.icon = QtGui.QIcon(
f"{sys._MEIPASS}/icon_48x48.png") # pylint: disable=no-member
f"{sys._MEIPASS}/icon_48x48.png"
) # pylint: disable=no-member
else:
self.icon = QtGui.QIcon("icon_48x48.png")
self.setWindowIcon(self.icon)
@ -80,7 +98,8 @@ class NanoVNASaver(QtWidgets.QWidget):
QtCore.QSettings.IniFormat,
QtCore.QSettings.UserScope,
"NanoVNASaver",
"NanoVNASaver")
"NanoVNASaver",
)
logger.info("Settings from: %s", self.settings.fileName())
Defaults.cfg = Defaults.restore(self.settings)
self.threadpool = QtCore.QThreadPool()
@ -128,13 +147,17 @@ class NanoVNASaver(QtWidgets.QWidget):
outer.addWidget(scrollarea)
self.setLayout(outer)
scrollarea.setWidgetResizable(True)
self.resize(Defaults.cfg.gui.window_width,
Defaults.cfg.gui.window_height)
self.resize(
Defaults.cfg.gui.window_width, Defaults.cfg.gui.window_height
)
scrollarea.setSizePolicy(
QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding)
self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding)
QtWidgets.QSizePolicy.MinimumExpanding,
)
self.setSizePolicy(
QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding,
)
widget = QtWidgets.QWidget()
widget.setLayout(layout)
scrollarea.setWidget(widget)
@ -149,25 +172,30 @@ class NanoVNASaver(QtWidgets.QWidget):
"magnitude_z": MagnitudeZChart("S11 |Z|"),
"permeability": PermeabilityChart(
"S11 R/\N{GREEK SMALL LETTER OMEGA} &"
" X/\N{GREEK SMALL LETTER OMEGA}"),
" X/\N{GREEK SMALL LETTER OMEGA}"
),
"phase": PhaseChart("S11 Phase"),
"q_factor": QualityFactorChart("S11 Quality Factor"),
"real_imag": RealImaginaryZChart("S11 R+jX"),
"real_imag_mu": RealImaginaryMuChart("S11 \N{GREEK SMALL LETTER MU}"),
"real_imag_mu": RealImaginaryMuChart(
"S11 \N{GREEK SMALL LETTER MU}"
),
"smith": SmithChart("S11 Smith Chart"),
"s_parameter": SParameterChart("S11 Real/Imaginary"),
"vswr": VSWRChart("S11 VSWR"),
},
"s21": {
"group_delay": GroupDelayChart("S21 Group Delay",
reflective=False),
"group_delay": GroupDelayChart(
"S21 Group Delay", reflective=False
),
"log_mag": LogMagChart("S21 Gain"),
"magnitude": MagnitudeChart("|S21|"),
"magnitude_z_shunt": MagnitudeZShuntChart("S21 |Z| shunt"),
"magnitude_z_series": MagnitudeZSeriesChart("S21 |Z| series"),
"real_imag_shunt": RealImaginaryZShuntChart("S21 R+jX shunt"),
"real_imag_series": RealImaginaryZSeriesChart(
"S21 R+jX series"),
"S21 R+jX series"
),
"phase": PhaseChart("S21 Phase"),
"polar": PolarChart("S21 Polar Plot"),
"s_parameter": SParameterChart("S21 Real/Imaginary"),
@ -190,8 +218,13 @@ class NanoVNASaver(QtWidgets.QWidget):
# List of all charts that can be selected for display
self.selectable_charts = (
self.s11charts + self.s21charts +
self.combinedCharts + [self.tdr_mainwindow_chart, ])
self.s11charts
+ self.s21charts
+ self.combinedCharts
+ [
self.tdr_mainwindow_chart,
]
)
# List of all charts that subscribe to updates (including duplicates!)
self.subscribing_charts = []
@ -314,7 +347,8 @@ class NanoVNASaver(QtWidgets.QWidget):
btn_show_analysis = QtWidgets.QPushButton("Analysis ...")
btn_show_analysis.setMinimumHeight(20)
btn_show_analysis.clicked.connect(
lambda: self.display_window("analysis"))
lambda: self.display_window("analysis")
)
self.marker_column.addWidget(btn_show_analysis)
###############################################################
@ -335,10 +369,10 @@ class NanoVNASaver(QtWidgets.QWidget):
self.tdr_result_label = QtWidgets.QLabel()
self.tdr_result_label.setMinimumHeight(20)
tdr_control_layout.addRow(
"Estimated cable length:", self.tdr_result_label)
"Estimated cable length:", self.tdr_result_label
)
self.tdr_button = QtWidgets.QPushButton(
"Time Domain Reflectometry ...")
self.tdr_button = QtWidgets.QPushButton("Time Domain Reflectometry ...")
self.tdr_button.setMinimumHeight(20)
self.tdr_button.clicked.connect(lambda: self.display_window("tdr"))
@ -351,8 +385,13 @@ class NanoVNASaver(QtWidgets.QWidget):
###############################################################
left_column.addSpacerItem(
QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Fixed,
QtWidgets.QSizePolicy.Expanding))
QtWidgets.QSpacerItem(
1,
1,
QtWidgets.QSizePolicy.Fixed,
QtWidgets.QSizePolicy.Expanding,
)
)
###############################################################
# Reference control
@ -390,7 +429,8 @@ class NanoVNASaver(QtWidgets.QWidget):
btnOpenCalibrationWindow.setMinimumHeight(20)
self.calibrationWindow = CalibrationWindow(self)
btnOpenCalibrationWindow.clicked.connect(
lambda: self.display_window("calibration"))
lambda: self.display_window("calibration")
)
###############################################################
# Display setup
@ -399,22 +439,21 @@ class NanoVNASaver(QtWidgets.QWidget):
btn_display_setup = QtWidgets.QPushButton("Display setup ...")
btn_display_setup.setMinimumHeight(20)
btn_display_setup.setMaximumWidth(240)
btn_display_setup.clicked.connect(
lambda: self.display_window("setup"))
btn_display_setup.clicked.connect(lambda: self.display_window("setup"))
btn_about = QtWidgets.QPushButton("About ...")
btn_about.setMinimumHeight(20)
btn_about.setMaximumWidth(240)
btn_about.clicked.connect(
lambda: self.display_window("about"))
btn_about.clicked.connect(lambda: self.display_window("about"))
btn_open_file_window = QtWidgets.QPushButton("Files")
btn_open_file_window.setMinimumHeight(20)
btn_open_file_window.setMaximumWidth(240)
btn_open_file_window.clicked.connect(
lambda: self.display_window("file"))
lambda: self.display_window("file")
)
button_grid = QtWidgets.QGridLayout()
button_grid.addWidget(btn_open_file_window, 0, 0)
@ -425,16 +464,19 @@ class NanoVNASaver(QtWidgets.QWidget):
logger.debug("Finished building interface")
def _sweep_control(self, start: bool = True) -> None:
self.sweep_control.progress_bar.setValue(0 if start else 100)
self.sweep_control.btn_start.setDisabled(start)
self.sweep_control.btn_stop.setDisabled(not start)
self.sweep_control.toggle_settings(start)
def sweep_start(self):
# Run the device data update
if not self.vna.connected():
return
self.worker.stopped = False
self.sweep_control.progress_bar.setValue(0)
self.sweep_control.btn_start.setDisabled(True)
self.sweep_control.btn_stop.setDisabled(False)
self.sweep_control.toggle_settings(True)
self._sweep_control(start=True)
for m in self.markers:
m.resetLabels()
@ -481,8 +523,7 @@ class NanoVNASaver(QtWidgets.QWidget):
m2 = Marker("Reference")
m2.location = self.markers[0].location
m2.resetLabels()
m2.updateLabels(self.ref_data.s11,
self.ref_data.s21)
m2.updateLabels(self.ref_data.s11, self.ref_data.s21)
else:
logger.warning("No reference data for marker")
@ -522,7 +563,8 @@ class NanoVNASaver(QtWidgets.QWidget):
min_vswr = min(s11, key=lambda data: data.vswr)
self.s11_min_swr_label.setText(
f"{format_vswr(min_vswr.vswr)} @"
f" {format_frequency(min_vswr.freq)}")
f" {format_frequency(min_vswr.freq)}"
)
self.s11_min_rl_label.setText(format_gain(min_vswr.gain))
else:
self.s11_min_swr_label.setText("")
@ -533,10 +575,12 @@ class NanoVNASaver(QtWidgets.QWidget):
max_gain = max(s21, key=lambda data: data.gain)
self.s21_min_gain_label.setText(
f"{format_gain(min_gain.gain)}"
f" @ {format_frequency(min_gain.freq)}")
f" @ {format_frequency(min_gain.freq)}"
)
self.s21_max_gain_label.setText(
f"{format_gain(max_gain.gain)}"
f" @ {format_frequency(max_gain.freq)}")
f" @ {format_frequency(max_gain.freq)}"
)
else:
self.s21_min_gain_label.setText("")
self.s21_max_gain_label.setText("")
@ -545,14 +589,10 @@ class NanoVNASaver(QtWidgets.QWidget):
self.dataAvailable.emit()
def sweepFinished(self):
self.sweep_control.progress_bar.setValue(100)
self.sweep_control.btn_start.setDisabled(False)
self.sweep_control.btn_stop.setDisabled(True)
self.sweep_control.toggle_settings(False)
self._sweep_control(start=False)
for marker in self.markers:
marker.frequencyInput.textEdited.emit(
marker.frequencyInput.text())
marker.frequencyInput.textEdited.emit(marker.frequencyInput.text())
def setReference(self, s11=None, s21=None, source=None):
if not s11:
@ -581,11 +621,13 @@ class NanoVNASaver(QtWidgets.QWidget):
if self.sweepSource != "":
insert += (
f"Sweep: {self.sweepSource} @ {len(self.data.s11)} points"
f"{', ' if self.referenceSource else ''}")
f"{', ' if self.referenceSource else ''}"
)
if self.referenceSource != "":
insert += (
f"Reference: {self.referenceSource} @"
f" {len(self.ref_data.s11)} points")
f" {len(self.ref_data.s11)} points"
)
insert += ")"
title = f"{self.baseTitle} {insert or ''}"
self.setWindowTitle(title)
@ -612,7 +654,7 @@ class NanoVNASaver(QtWidgets.QWidget):
self.showError(self.worker.error_message)
with contextlib.suppress(IOError):
self.vna.flushSerialBuffers() # Remove any left-over data
self.vna.reconnect() # try reconnection
self.vna.reconnect() # try reconnection
self.sweepFinished()
def popoutChart(self, chart: Chart):
@ -661,8 +703,12 @@ class NanoVNASaver(QtWidgets.QWidget):
new_width = qf_new.horizontalAdvance(standard_string)
old_width = qf_normal.horizontalAdvance(standard_string)
self.scaleFactor = new_width / old_width
logger.debug("New font width: %f, normal font: %f, factor: %f",
new_width, old_width, self.scaleFactor)
logger.debug(
"New font width: %f, normal font: %f, factor: %f",
new_width,
old_width,
self.scaleFactor,
)
# TODO: Update all the fixed widths to account for the scaling
for m in self.markers:
m.get_data_layout().setFont(font)

Wyświetl plik

@ -34,12 +34,12 @@ class Datapoint(NamedTuple):
@property
def z(self) -> complex:
""" return the s value complex number """
"""return the s value complex number"""
return complex(self.re, self.im)
@property
def phase(self) -> float:
""" return the datapoint's phase value """
"""return the datapoint's phase value"""
return cmath.phase(self.z)
@property
@ -77,11 +77,11 @@ class Datapoint(NamedTuple):
def capacitiveEquivalent(self, ref_impedance: float = 50) -> float:
return impedance_to_capacitance(
self.impedance(ref_impedance), self.freq)
self.impedance(ref_impedance), self.freq
)
def inductiveEquivalent(self, ref_impedance: float = 50) -> float:
return impedance_to_inductance(
self.impedance(ref_impedance), self.freq)
return impedance_to_inductance(self.impedance(ref_impedance), self.freq)
def gamma_to_impedance(gamma: complex, ref_impedance: float = 50) -> complex:
@ -124,9 +124,10 @@ def norm_to_impedance(z: complex, ref_impedance: float = 50) -> complex:
def parallel_to_serial(z: complex) -> complex:
"""Convert parallel impedance to serial impedance equivalent"""
z_sq_sum = z.real ** 2 + z.imag ** 2 or 10.0e-30
return complex(z.real * z.imag ** 2 / z_sq_sum,
z.real ** 2 * z.imag / z_sq_sum)
z_sq_sum = z.real**2 + z.imag**2 or 10.0e-30
return complex(
z.real * z.imag**2 / z_sq_sum, z.real**2 * z.imag / z_sq_sum
)
def reflection_coefficient(z: complex, ref_impedance: float = 50) -> complex:
@ -136,7 +137,7 @@ def reflection_coefficient(z: complex, ref_impedance: float = 50) -> complex:
def serial_to_parallel(z: complex) -> complex:
"""Convert serial impedance to parallel impedance equivalent"""
z_sq_sum = z.real ** 2 + z.imag ** 2
z_sq_sum = z.real**2 + z.imag**2
if z.real == 0 and z.imag == 0:
return complex(math.inf, math.inf)
if z.imag == 0:
@ -150,7 +151,7 @@ def corr_att_data(data: List[Datapoint], att: float) -> List[Datapoint]:
"""Correct the ratio for a given attenuation on s21 input"""
if att <= 0:
return data
att = 10**(att / 20)
att = 10 ** (att / 20)
ndata = []
for dp in data:
corrected = dp.z * att

Wyświetl plik

@ -22,8 +22,29 @@ from decimal import Context, Decimal, InvalidOperation
from typing import NamedTuple
from numbers import Number, Real
PREFIXES = ("q", "r", "y", "z", "a", "f", "p", "n", "µ", "m",
"", "k", "M", "G", "T", "P", "E", "Z", "Y", "R", "Q")
PREFIXES = (
"q",
"r",
"y",
"z",
"a",
"f",
"p",
"n",
"µ",
"m",
"",
"k",
"M",
"G",
"T",
"P",
"E",
"Z",
"Y",
"R",
"Q",
)
def clamp_value(value: Real, rmin: Real, rmax: Real) -> Real:
@ -32,17 +53,17 @@ def clamp_value(value: Real, rmin: Real, rmax: Real) -> Real:
def round_ceil(value: Real, digits: int = 0) -> Real:
factor = 10 ** -digits
factor = 10**-digits
return factor * math.ceil(value / factor)
def round_floor(value: Real, digits: int = 0) -> Real:
factor = 10 ** -digits
factor = 10**-digits
return factor * math.floor(value / factor)
def log_floor_125(x: float) -> float:
log_base = 10**(math.floor(math.log10(x)))
log_base = 10 ** (math.floor(math.log10(x)))
log_factor = x / log_base
if log_factor >= 5:
return 5 * log_base
@ -80,31 +101,44 @@ class Value:
self.fmt = fmt
if isinstance(value, str):
self._value = Decimal(math.nan)
if value.lower() != 'nan':
if value.lower() != "nan":
self.parse(value)
else:
self._value = Decimal(value, context=Value.CTX)
def __repr__(self) -> str:
return (f"{self.__class__.__name__}("
f"{repr(self._value)}, '{self._unit}', {self.fmt})")
return (
f"{self.__class__.__name__}("
f"{repr(self._value)}, '{self._unit}', {self.fmt})"
)
def __str__(self) -> str:
fmt = self.fmt
if math.isnan(self._value):
return f"-{fmt.space_str}{self._unit}"
if (fmt.assume_infinity and
abs(self._value) >= 10 ** ((fmt.max_offset + 1) * 3)):
return (("-" if self._value < 0 else "") +
"\N{INFINITY}" + fmt.space_str + self._unit)
if fmt.assume_infinity and abs(self._value) >= 10 ** (
(fmt.max_offset + 1) * 3
):
return (
("-" if self._value < 0 else "")
+ "\N{INFINITY}"
+ fmt.space_str
+ self._unit
)
if self._value < fmt.printable_min:
return fmt.unprintable_under + self._unit
if self._value > fmt.printable_max:
return fmt.unprintable_over + self._unit
offset = clamp_value(
int(math.log10(abs(self._value)) // 3),
fmt.min_offset, fmt.max_offset) if self._value else 0
offset = (
clamp_value(
int(math.log10(abs(self._value)) // 3),
fmt.min_offset,
fmt.max_offset,
)
if self._value
else 0
)
real = float(self._value) / (10 ** (offset * 3))
@ -112,8 +146,9 @@ class Value:
formstr = ".0f"
else:
max_digits = fmt.max_nr_digits + (
(1 if not fmt.fix_decimals and abs(real) < 10 else 0) +
(1 if not fmt.fix_decimals and abs(real) < 100 else 0))
(1 if not fmt.fix_decimals and abs(real) < 10 else 0)
+ (1 if not fmt.fix_decimals and abs(real) < 100 else 0)
)
formstr = f".{max_digits - 3}f"
if self.fmt.allways_signed:
@ -150,10 +185,13 @@ class Value:
value = value.replace(" ", "") # Ignore spaces
if self._unit and (
value.endswith(self._unit) or
(self.fmt.parse_sloppy_unit and
value.lower().endswith(self._unit.lower()))): # strip unit
value = value[:-len(self._unit)]
value.endswith(self._unit)
or (
self.fmt.parse_sloppy_unit
and value.lower().endswith(self._unit.lower())
)
): # strip unit
value = value[: -len(self._unit)]
factor = 1
# fix for e.g. KHz, mHz gHz as milli-Hertz mostly makes no
@ -170,13 +208,14 @@ class Value:
self._value = -math.inf
else:
try:
self._value = (Decimal(value, context=Value.CTX)
* Decimal(factor, context=Value.CTX))
self._value = Decimal(value, context=Value.CTX) * Decimal(
factor, context=Value.CTX
)
except InvalidOperation as exc:
raise ValueError() from exc
self._value = clamp_value(self._value,
self.fmt.parse_clamp_min,
self.fmt.parse_clamp_max)
self._value = clamp_value(
self._value, self.fmt.parse_clamp_min, self.fmt.parse_clamp_max
)
return self
@property

Wyświetl plik

@ -57,9 +57,12 @@ class BandsModel(QtCore.QAbstractTableModel):
# These bands correspond broadly to the Danish Amateur Radio allocation
def __init__(self):
super().__init__()
self.settings = QtCore.QSettings(QtCore.QSettings.IniFormat,
QtCore.QSettings.UserScope,
"NanoVNASaver", "Bands")
self.settings = QtCore.QSettings(
QtCore.QSettings.IniFormat,
QtCore.QSettings.UserScope,
"NanoVNASaver",
"Bands",
)
self.settings.setIniCodec("UTF-8")
self.enabled = self.settings.value("ShowBands", False, bool)
@ -71,7 +74,8 @@ class BandsModel(QtCore.QAbstractTableModel):
def saveSettings(self):
self.settings.setValue(
"bands",
[f"{name};{start};{end}" for name, start, end in self.bands])
[f"{name};{start};{end}" for name, start, end in self.bands],
)
self.settings.sync()
def resetBands(self):
@ -87,18 +91,22 @@ class BandsModel(QtCore.QAbstractTableModel):
def data(self, index: QModelIndex, role: int = ...) -> QtCore.QVariant:
if role in [
QtCore.Qt.DisplayRole, QtCore.Qt.ItemDataRole, QtCore.Qt.EditRole,
QtCore.Qt.DisplayRole,
QtCore.Qt.ItemDataRole,
QtCore.Qt.EditRole,
]:
return QtCore.QVariant(self.bands[index.row()][index.column()])
if role == QtCore.Qt.TextAlignmentRole:
if index.column() == 0:
return QtCore.QVariant(QtCore.Qt.AlignCenter)
return QtCore.QVariant(
QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter
)
return QtCore.QVariant()
def setData(self, index: QModelIndex,
value: typing.Any, role: int = ...) -> bool:
def setData(
self, index: QModelIndex, value: typing.Any, role: int = ...
) -> bool:
if role == QtCore.Qt.EditRole and index.isValid():
t = self.bands[index.row()]
name = t[0]
@ -116,14 +124,14 @@ class BandsModel(QtCore.QAbstractTableModel):
return True
return False
def index(self, row: int,
column: int, _: QModelIndex = ...) -> QModelIndex:
def index(self, row: int, column: int, _: QModelIndex = ...) -> QModelIndex:
return self.createIndex(row, column)
def addRow(self):
self.bands.append(("New", 0, 0))
self.dataChanged.emit(self.index(len(self.bands), 0),
self.index(len(self.bands), 2))
self.dataChanged.emit(
self.index(len(self.bands), 0), self.index(len(self.bands), 2)
)
self.layoutChanged.emit()
def removeRow(self, row: int, _: QModelIndex = ...) -> bool:
@ -132,10 +140,13 @@ class BandsModel(QtCore.QAbstractTableModel):
self.saveSettings()
return True
def headerData(self, section: int,
orientation: QtCore.Qt.Orientation, role: int = ...):
if (role == QtCore.Qt.DisplayRole and
orientation == QtCore.Qt.Horizontal):
def headerData(
self, section: int, orientation: QtCore.Qt.Orientation, role: int = ...
):
if (
role == QtCore.Qt.DisplayRole
and orientation == QtCore.Qt.Horizontal
):
with contextlib.suppress(IndexError):
return _HEADER_DATA[section]
return None
@ -143,9 +154,10 @@ class BandsModel(QtCore.QAbstractTableModel):
def flags(self, index: QModelIndex) -> QtCore.Qt.ItemFlags:
if index.isValid():
return QtCore.Qt.ItemFlags(
QtCore.Qt.ItemIsEditable |
QtCore.Qt.ItemIsEnabled |
QtCore.Qt.ItemIsSelectable)
QtCore.Qt.ItemIsEditable
| QtCore.Qt.ItemIsEnabled
| QtCore.Qt.ItemIsSelectable
)
super().flags(index)
def setColor(self, color):

Wyświetl plik

@ -32,10 +32,13 @@ class SweepMode(Enum):
class Properties:
def __init__(self, name: str = "",
mode: 'SweepMode' = SweepMode.SINGLE,
averages: Tuple[int, int] = (3, 0),
logarithmic: bool = False):
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
@ -44,13 +47,19 @@ class Properties:
def __repr__(self):
return (
f"Properties('{self.name}', {self.mode}, {self.averages},"
f" {self.logarithmic})")
f" {self.logarithmic})"
)
class Sweep:
def __init__(self, start: int = 3600000, end: int = 30000000,
points: int = 101, segments: int = 1,
properties: 'Properties' = Properties()):
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
@ -63,18 +72,22 @@ class Sweep:
def __repr__(self) -> str:
return (
f"Sweep({self.start}, {self.end}, {self.points}, {self.segments},"
f" {self.properties})")
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)
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)
def copy(self) -> "Sweep":
return Sweep(
self.start, self.end, self.points, self.segments, self.properties
)
@property
def span(self) -> int:
@ -86,11 +99,11 @@ class Sweep:
def check(self):
if (
self.segments <= 0
or self.points <= 0
or self.start <= 0
or self.end <= 0
or self.stepsize < 1
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}")

Wyświetl plik

@ -42,9 +42,8 @@ def truncate(values: List[List[Tuple]], count: int) -> List[List[Tuple]]:
for valueset in np.swapaxes(values, 0, 1).tolist():
avg = complex(*np.average(valueset, 0))
truncated.append(
sorted(valueset,
key=lambda v, a=avg:
abs(a - complex(*v)))[:keep])
sorted(valueset, key=lambda v, a=avg: abs(a - complex(*v)))[:keep]
)
return np.swapaxes(truncated, 0, 1).tolist()
@ -87,7 +86,8 @@ class SweepWorker(QtCore.QRunnable):
logger.info("Initializing SweepWorker")
if not self.app.vna.connected():
logger.debug(
"Attempted to run without being connected to the NanoVNA")
"Attempted to run without being connected to the NanoVNA"
)
self.running = False
return
@ -106,8 +106,9 @@ class SweepWorker(QtCore.QRunnable):
if sweep.segments > 1:
start = sweep.start
end = sweep.end
logger.debug("Resetting NanoVNA sweep to full range: %d to %d",
start, end)
logger.debug(
"Resetting NanoVNA sweep to full range: %d to %d", start, end
)
self.app.vna.resetSweep(start, end)
self.percentage = 100
@ -117,9 +118,11 @@ class SweepWorker(QtCore.QRunnable):
def _run_loop(self) -> None:
sweep = self.sweep
averages = (sweep.properties.averages[0]
if sweep.properties.mode == SweepMode.AVERAGE
else 1)
averages = (
sweep.properties.averages[0]
if sweep.properties.mode == SweepMode.AVERAGE
else 1
)
logger.info("%d averages", averages)
while True:
@ -131,7 +134,8 @@ class SweepWorker(QtCore.QRunnable):
start, stop = sweep.get_index_range(i)
freq, values11, values21 = self.readAveragedSegment(
start, stop, averages)
start, stop, averages
)
self.percentage = (i + 1) * 100 / sweep.segments
self.updateData(freq, values11, values21, i)
if sweep.properties.mode != SweepMode.CONTINOUS or self.stopped:
@ -152,14 +156,18 @@ class SweepWorker(QtCore.QRunnable):
def updateData(self, frequencies, values11, values21, index):
# Update the data from (i*101) to (i+1)*101
logger.debug(
"Calculating data and inserting in existing data at index %d",
index)
"Calculating data and inserting in existing data at index %d", index
)
offset = self.sweep.points * index
raw_data11 = [Datapoint(freq, values11[i][0], values11[i][1])
for i, freq in enumerate(frequencies)]
raw_data21 = [Datapoint(freq, values21[i][0], values21[i][1])
for i, freq in enumerate(frequencies)]
raw_data11 = [
Datapoint(freq, values11[i][0], values11[i][1])
for i, freq in enumerate(frequencies)
]
raw_data21 = [
Datapoint(freq, values21[i][0], values21[i][1])
for i, freq in enumerate(frequencies)
]
data11, data21 = self.applyCalibration(raw_data11, raw_data21)
logger.debug("update Freqs: %s, Offset: %s", len(frequencies), offset)
@ -169,16 +177,18 @@ class SweepWorker(QtCore.QRunnable):
self.rawData11[offset + i] = raw_data11[i]
self.rawData21[offset + i] = raw_data21[i]
logger.debug("Saving data to application (%d and %d points)",
len(self.data11), len(self.data21))
logger.debug(
"Saving data to application (%d and %d points)",
len(self.data11),
len(self.data21),
)
self.app.saveData(self.data11, self.data21)
logger.debug('Sending "updated" signal')
self.signals.updated.emit()
def applyCalibration(self,
raw_data11: List[Datapoint],
raw_data21: List[Datapoint]
) -> Tuple[List[Datapoint], List[Datapoint]]:
def applyCalibration(
self, raw_data11: List[Datapoint], raw_data21: List[Datapoint]
) -> Tuple[List[Datapoint], List[Datapoint]]:
data11: List[Datapoint] = []
data21: List[Datapoint] = []
@ -186,8 +196,9 @@ class SweepWorker(QtCore.QRunnable):
data11 = raw_data11.copy()
data21 = raw_data21.copy()
elif self.app.calibration.isValid1Port():
data11.extend(self.app.calibration.correct11(dp)
for dp in raw_data11)
data11.extend(
self.app.calibration.correct11(dp) for dp in raw_data11
)
else:
data11 = raw_data11.copy()
@ -199,8 +210,10 @@ class SweepWorker(QtCore.QRunnable):
data21 = raw_data21
if self.offsetDelay != 0:
data11 = [correct_delay(dp, self.offsetDelay, reflect=True)
for dp in data11]
data11 = [
correct_delay(dp, self.offsetDelay, reflect=True)
for dp in data11
]
data21 = [correct_delay(dp, self.offsetDelay) for dp in data21]
return data11, data21
@ -209,8 +222,9 @@ class SweepWorker(QtCore.QRunnable):
values11 = []
values21 = []
freq = []
logger.info("Reading from %d to %d. Averaging %d values",
start, stop, averages)
logger.info(
"Reading from %d to %d. Averaging %d values", start, stop, averages
)
for i in range(averages):
if self.stopped:
logger.debug("Stopping averaging as signalled.")
@ -227,8 +241,9 @@ class SweepWorker(QtCore.QRunnable):
retry += 1
freq, tmp11, tmp21 = self.readSegment(start, stop)
if retry > 1:
logger.error("retry %s readSegment(%s,%s)",
retry, start, stop)
logger.error(
"retry %s readSegment(%s,%s)", retry, start, stop
)
sleep(0.5)
values11.append(tmp11)
values21.append(tmp21)
@ -240,8 +255,7 @@ class SweepWorker(QtCore.QRunnable):
truncates = self.sweep.properties.averages[1]
if truncates > 0 and averages > 1:
logger.debug("Truncating %d values by %d",
len(values11), truncates)
logger.debug("Truncating %d values by %d", len(values11), truncates)
values11 = truncate(values11, truncates)
values21 = truncate(values21, truncates)
@ -278,36 +292,42 @@ class SweepWorker(QtCore.QRunnable):
a, b = d.split(" ")
try:
if self.app.vna.validateInput and (
abs(float(a)) > 9.5 or
abs(float(b)) > 9.5):
abs(float(a)) > 9.5 or abs(float(b)) > 9.5
):
logger.warning(
"Got a non plausible data value: (%s)", d)
"Got a non plausible data value: (%s)", d
)
done = False
break
returndata.append((float(a), float(b)))
except ValueError as exc:
logger.exception("An exception occurred reading %s: %s",
data, exc)
logger.exception(
"An exception occurred reading %s: %s", data, exc
)
done = False
if not done:
logger.debug("Re-reading %s", data)
sleep(0.2)
count += 1
if count == 5:
logger.error("Tried and failed to read %s %d times.",
data, count)
logger.error(
"Tried and failed to read %s %d times.", data, count
)
logger.debug("trying to reconnect")
self.app.vna.reconnect()
if count >= 10:
logger.critical(
"Tried and failed to read %s %d times. Giving up.",
data, count)
data,
count,
)
raise IOError(
f"Failed reading {data} {count} times.\n"
f"Data outside expected valid ranges,"
f" or in an unexpected format.\n\n"
f"You can disable data validation on the"
f"device settings screen.")
f"device settings screen."
)
return returndata
def gui_error(self, message: str):

Wyświetl plik

@ -35,20 +35,22 @@ class Options:
# Fun fact: In Touchstone 1.1 spec all params are optional unordered.
# Just the line has to start with "#"
UNIT_TO_FACTOR = {
"ghz": 10 ** 9,
"mhz": 10 ** 6,
"khz": 10 ** 3,
"hz": 10 ** 0,
"ghz": 10**9,
"mhz": 10**6,
"khz": 10**3,
"hz": 10**0,
}
VALID_UNITS = UNIT_TO_FACTOR.keys()
VALID_PARAMETERS = "syzgh"
VALID_FORMATS = ("ma", "db", "ri")
def __init__(self,
unit: str = "GHZ",
parameter: str = "S",
t_format: str = "ma",
resistance: int = 50):
def __init__(
self,
unit: str = "GHZ",
parameter: str = "S",
t_format: str = "ma",
resistance: int = 50,
):
# set defaults
assert unit.lower() in Options.VALID_UNITS
assert parameter.lower() in Options.VALID_PARAMETERS
@ -145,9 +147,11 @@ class Touchstone:
return self.sdata[Touchstone.FIELD_ORDER.index(name)]
def s_freq(self, name: str, freq: int) -> Datapoint:
return Datapoint(freq,
float(self._interp[name]["real"](freq)),
float(self._interp[name]["imag"](freq)))
return Datapoint(
freq,
float(self._interp[name]["real"](freq)),
float(self._interp[name]["imag"](freq)),
)
def swap(self):
self.sdata = [self.s22, self.s12, self.s21, self.s11]
@ -170,12 +174,20 @@ class Touchstone:
imag.append(dp.im)
self._interp[i] = {
"real": interp1d(freq, real,
kind="slinear", bounds_error=False,
fill_value=(real[0], real[-1])),
"imag": interp1d(freq, imag,
kind="slinear", bounds_error=False,
fill_value=(imag[0], imag[-1])),
"real": interp1d(
freq,
real,
kind="slinear",
bounds_error=False,
fill_value=(real[0], real[-1]),
),
"imag": interp1d(
freq,
imag,
kind="slinear",
bounds_error=False,
fill_value=(imag[0], imag[-1]),
),
}
def _parse_comments(self, fp) -> str:
@ -192,27 +204,29 @@ class Touchstone:
vals = iter(data)
for v in vals:
if self.opts.format == "ri":
next(data_list).append(Datapoint(freq, float(v),
float(next(vals))))
next(data_list).append(
Datapoint(freq, float(v), float(next(vals)))
)
if self.opts.format == "ma":
z = cmath.rect(float(v), math.radians(float(next(vals))))
next(data_list).append(Datapoint(freq, z.real, z.imag))
if self.opts.format == "db":
z = cmath.rect(10 ** (float(v) / 20),
math.radians(float(next(vals))))
z = cmath.rect(
10 ** (float(v) / 20), math.radians(float(next(vals)))
)
next(data_list).append(Datapoint(freq, z.real, z.imag))
def load(self):
logger.info("Attempting to open file %s", self.filename)
try:
with open(self.filename, encoding='utf-8') as infile:
with open(self.filename, encoding="utf-8") as infile:
self.loads(infile.read())
except IOError as e:
logger.exception("Failed to open %s: %s", self.filename, e)
def loads(self, s: str):
"""Parse touchstone 1.1 string input
appends to existing sdata if Touchstone object exists
appends to existing sdata if Touchstone object exists
"""
try:
self._loads(s)
@ -239,7 +253,7 @@ class Touchstone:
continue
# ignore comments at data end
data = line.split('!')[0]
data = line.split("!")[0]
data = data.split()
freq, data = round(float(data[0]) * self.opts.factor), data[1:]
data_len = len(data)
@ -270,8 +284,7 @@ class Touchstone:
nr_params: Number of s-parameters. 2 for s1p, 4 for s2p
"""
logger.info("Attempting to open file %s for writing",
self.filename)
logger.info("Attempting to open file %s for writing", self.filename)
with open(self.filename, "w", encoding="utf-8") as outfile:
outfile.write(self.saves(nr_params))

Wyświetl plik

@ -22,13 +22,16 @@ logger = logging.getLogger(__name__)
class Version:
RXP = re.compile(r"""^
RXP = re.compile(
r"""^
\D*
(?P<major>\d+)\.
(?P<minor>\d+)\.?
(?P<revision>\d+)?
(?P<note>.*)
$""", re.VERBOSE)
$""",
re.VERBOSE,
)
def __init__(self, vstring: str = "0.0.0"):
self.data = {
@ -47,11 +50,11 @@ class Version:
logger.error("Unable to parse version: %s", vstring)
def __gt__(self, other: "Version") -> bool:
l, r = self.data, other.data
left, right = self.data, other.data
for name in ("major", "minor", "revision"):
if l[name] > r[name]:
if left[name] > right[name]:
return True
if l[name] < r[name]:
if left[name] < right[name]:
return False
return False
@ -68,8 +71,10 @@ class Version:
return self.data == other.data
def __str__(self) -> str:
return (f'{self.data["major"]}.{self.data["minor"]}'
f'.{self.data["revision"]}{self.data["note"]}')
return (
f'{self.data["major"]}.{self.data["minor"]}'
f'.{self.data["revision"]}{self.data["note"]}'
)
@property
def major(self) -> int:

Wyświetl plik

@ -53,28 +53,36 @@ class AboutWindow(QtWidgets.QWidget):
layout = QtWidgets.QVBoxLayout()
top_layout.addLayout(layout)
layout.addWidget(QtWidgets.QLabel(
f"NanoVNASaver version {self.app.version}"))
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(
"\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}")
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.")
"NanoVNA Firmware Version: Not connected."
)
layout.addWidget(self.versionLabel)
layout.addStretch()
@ -106,14 +114,15 @@ class AboutWindow(QtWidgets.QWidget):
with contextlib.suppress(IOError, AttributeError):
self.versionLabel.setText(
f"NanoVNA Firmware Version: {self.app.vna.name} "
f"v{self.app.vna.version}")
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}')
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 ="):
@ -122,17 +131,20 @@ class AboutWindow(QtWidgets.QWidget):
latest_url = line[13:].strip(" \"'")
except error.HTTPError as e:
logger.exception(
"Checking for updates produced an HTTP exception: %s", e)
"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)
"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)
"Checking for updates produced a URL exception: %s", e
)
self.updateLabel.setText("Connection error.")
return
@ -147,13 +159,17 @@ class AboutWindow(QtWidgets.QWidget):
"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.')
f'Press "About" to find the update.',
)
else:
QtWidgets.QMessageBox.information(
self, "Updates available",
"There is a new update for NanoVNA-Saver available!")
self,
"Updates available",
"There is a new update for NanoVNA-Saver available!",
)
self.updateLabel.setText(
f'<a href="{latest_url}">New version available</a>.')
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?
@ -161,5 +177,6 @@ class AboutWindow(QtWidgets.QWidget):
#
self.updateLabel.setText(
f"Last checked: "
f"{strftime('%Y-%m-%d %H:%M:%S', localtime())}")
f"{strftime('%Y-%m-%d %H:%M:%S', localtime())}"
)
return

Wyświetl plik

@ -29,7 +29,9 @@ from NanoVNASaver.Analysis.HighPassAnalysis import HighPassAnalysis
from NanoVNASaver.Analysis.LowPassAnalysis import LowPassAnalysis
from NanoVNASaver.Analysis.PeakSearchAnalysis import PeakSearchAnalysis
from NanoVNASaver.Analysis.ResonanceAnalysis import ResonanceAnalysis
from NanoVNASaver.Analysis.SimplePeakSearchAnalysis import SimplePeakSearchAnalysis
from NanoVNASaver.Analysis.SimplePeakSearchAnalysis import (
SimplePeakSearchAnalysis,
)
from NanoVNASaver.Analysis.VSWRAnalysis import VSWRAnalysis
from NanoVNASaver.Windows.Defaults import make_scrollable
@ -55,25 +57,28 @@ class AnalysisWindow(QtWidgets.QWidget):
select_analysis_box = QtWidgets.QGroupBox("Select analysis")
select_analysis_layout = QtWidgets.QFormLayout(select_analysis_box)
self.analysis_list = QtWidgets.QComboBox()
self.analysis_list.addItem("Low-pass filter", LowPassAnalysis(self.app))
self.analysis_list.addItem(
"Low-pass filter", LowPassAnalysis(self.app))
"Band-pass filter", BandPassAnalysis(self.app)
)
self.analysis_list.addItem(
"Band-pass filter", BandPassAnalysis(self.app))
"High-pass filter", HighPassAnalysis(self.app)
)
self.analysis_list.addItem(
"High-pass filter", HighPassAnalysis(self.app))
"Band-stop filter", BandStopAnalysis(self.app)
)
self.analysis_list.addItem(
"Band-stop filter", BandStopAnalysis(self.app))
self.analysis_list.addItem(
"Simple Peak search", SimplePeakSearchAnalysis(self.app))
self.analysis_list.addItem(
"Peak search", PeakSearchAnalysis(self.app))
"Simple Peak search", SimplePeakSearchAnalysis(self.app)
)
self.analysis_list.addItem("Peak search", PeakSearchAnalysis(self.app))
self.analysis_list.addItem("VSWR analysis", VSWRAnalysis(self.app))
self.analysis_list.addItem(
"Resonance analysis", ResonanceAnalysis(self.app))
"Resonance analysis", ResonanceAnalysis(self.app)
)
self.analysis_list.addItem("HWEF analysis", EFHWAnalysis(self.app))
self.analysis_list.addItem(
"HWEF analysis", EFHWAnalysis(self.app))
self.analysis_list.addItem(
"MagLoop analysis", MagLoopAnalysis(self.app))
"MagLoop analysis", MagLoopAnalysis(self.app)
)
select_analysis_layout.addRow("Analysis type", self.analysis_list)
self.analysis_list.currentIndexChanged.connect(self.updateSelection)
@ -82,15 +87,18 @@ class AnalysisWindow(QtWidgets.QWidget):
select_analysis_layout.addRow(btn_run_analysis)
self.checkbox_run_automatically = QtWidgets.QCheckBox(
"Run automatically")
"Run automatically"
)
self.checkbox_run_automatically.stateChanged.connect(
self.toggleAutomaticRun)
self.toggleAutomaticRun
)
select_analysis_layout.addRow(self.checkbox_run_automatically)
analysis_box = QtWidgets.QGroupBox("Analysis")
analysis_box.setSizePolicy(
QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding)
QtWidgets.QSizePolicy.MinimumExpanding,
)
self.analysis_layout = QtWidgets.QVBoxLayout(analysis_box)
self.analysis_layout.setContentsMargins(0, 0, 0, 0)
@ -110,7 +118,8 @@ class AnalysisWindow(QtWidgets.QWidget):
if old_item is not None:
old_widget = self.analysis_layout.itemAt(0).widget()
self.analysis_layout.replaceWidget(
old_widget, self.analysis.widget())
old_widget, self.analysis.widget()
)
old_widget.hide()
else:
self.analysis_layout.addWidget(self.analysis.widget())

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