72 Commits

Author SHA1 Message Date
c5dc320989 2-port SOLT cal
Some checks failed
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Failing after -56s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-07-08 00:02:03 -06:00
452dddc19c functional 1 port calibration 2025-07-07 23:22:36 -06:00
3c02a4b388 allow downselecting measurements 2025-07-07 22:49:49 -06:00
339dbe255e smash some functions together
Some checks failed
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Failing after -1m4s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-07-07 22:28:21 -06:00
81143a72c4 bettah port handling
Some checks failed
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Failing after -58s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-07-07 22:15:48 -06:00
f021780971 working 2-port capture 2025-07-07 20:13:59 -06:00
6f947a28fa add really basic usage file for developing vna class outside of gui 2025-07-07 20:04:54 -06:00
581131f1e0 document and rearrange some stuff 2025-07-07 20:04:16 -06:00
adf6e40752 change default IP to what I have configured on my pluto (revert later) 2025-07-07 19:43:15 -06:00
411f96dd87 only define default ip once 2025-07-07 19:39:16 -06:00
b4e4b689ea Add basic switch control for white-wired pluto io shield
Some checks failed
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Failing after -53s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-07-07 19:36:59 -06:00
2012c37ccb add image of my janky cal standard
Some checks failed
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Failing after -1m6s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-06-26 22:00:08 -06:00
5184c05bb5 README formatting
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -1m0s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-06-26 21:37:41 -06:00
8d7f87c9e6 add links to sister projects
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -1m2s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-06-26 21:08:56 -06:00
776c9cc491 typing and defaults
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -48s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-06-26 21:06:15 -06:00
994080e574 start a pickle-less cal read/write 2025-06-26 21:06:15 -06:00
b6cb1ecde7 DAC updates 2025-06-26 21:06:15 -06:00
383fe3ceea slight power cal improvement 2025-06-26 21:06:15 -06:00
9912e318a8 add help links 2025-06-26 21:06:15 -06:00
d8b1a56c99 rename config path 2025-06-26 21:06:15 -06:00
505b374e8f Update README.md
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -1m0s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-06-26 08:35:30 -06:00
c73533b156 Update README.md
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -41s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-06-26 08:13:33 -06:00
594c72f2bf Update README.md
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -1m1s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-06-04 14:14:59 -06:00
43d9486edf Update README.md
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -1m1s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-06-04 14:10:29 -06:00
27f7277c45 Update README.md
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -32s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-06-04 13:55:14 -06:00
1a76c4e7ab IO control on pluto appears to be working
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in 1m27s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-03-09 01:54:05 -07:00
67ddbb0f90 use tabs for plots 2025-03-08 23:11:01 -07:00
97497e640e disable some plots that are annoying me 2025-03-08 23:05:35 -07:00
e78cf5595a fix import issue when running gui entry point 2025-03-08 23:02:02 -07:00
086f348084 add pypi link
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in 16s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-01-17 00:37:30 -07:00
8f61cd85d0 fix vswr plots for S11 > 0dB
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in 21s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Successful in 25s
2025-01-17 00:34:29 -07:00
80fd25e7e4 add really rudimentary 1-port calibration support 2025-01-17 00:34:01 -07:00
30fb1190bb accept a raw IP
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in 19s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-01-16 23:44:04 -07:00
fa80af8447 adjustable ip address at runtime 2025-01-16 23:39:19 -07:00
926a6abf1f split plots into separate file 2025-01-16 23:19:41 -07:00
2285bb78c1 minor config stuf 2025-01-16 23:17:40 -07:00
894d980a64 update README.md 2025-01-16 23:12:29 -07:00
9a922762fa basic config file stuff 2025-01-16 23:12:18 -07:00
26682f1741 remove simulation
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in 18s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-01-16 22:12:31 -07:00
f1f6c0d45b rearrange readme 2025-01-16 21:55:46 -07:00
e0c42766bf flake8
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in 20s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-01-16 21:53:55 -07:00
0202163f08 gui working with real data
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in 17s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-01-16 21:50:35 -07:00
18a1b2faa3 better tx tone
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in 40s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2025-01-15 23:01:10 -07:00
c034cdcb95 document firmware versions 2025-01-15 22:59:29 -07:00
0c152c337e make vna.py importable 2025-01-15 22:23:54 -07:00
22a33c2b84 some config stuff 2025-01-15 22:14:27 -07:00
3369bb290a remove debugging print
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in 37s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2024-12-21 13:17:16 -07:00
322b5ebc9b don't reformat all the lines on every re-draw 2024-12-21 13:15:15 -07:00
014c742e0a smith too 2024-12-21 13:11:06 -07:00
6d920c6809 plotting with random data 2024-12-21 13:09:56 -07:00
2f7b9fc1eb urls
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -41s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2024-12-19 00:55:27 -07:00
134f5b45d7 fix homepage url
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -42s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2024-12-19 00:46:43 -07:00
f35d9a85a6 nothing important
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -42s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2024-12-19 00:28:20 -07:00
5650e7b03b start cli
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -42s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2024-12-19 00:25:47 -07:00
76886011bb partially fix formatting
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -43s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2024-12-19 00:07:05 -07:00
528de3b4cc auto-formatting tools 2024-12-19 00:04:51 -07:00
1256c71fac remove unused imports 2024-12-19 00:03:56 -07:00
d637fb73ee more better shortcuts
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -40s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2024-12-18 23:59:45 -07:00
5c4f7e5c97 fix entry point
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -41s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2024-12-18 23:55:06 -07:00
919101b339 Oops. I broke typing
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -42s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2024-12-18 23:50:05 -07:00
67a1864837 util improvements
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -41s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2024-12-18 23:38:45 -07:00
eca933ccef update readme 2024-12-18 23:34:56 -07:00
a5b391e757 untested: add some plot update stuff
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -38s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2024-12-18 23:31:34 -07:00
34a4532a02 more gui stuff
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -42s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2024-12-18 22:52:12 -07:00
98f40179e1 menu stuff
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -41s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2024-12-18 22:35:16 -07:00
5a7bc985b4 start making gui
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -31s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2024-12-18 21:37:57 -07:00
f4c099c1d2 update README.md
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -14s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2024-12-08 13:53:31 -07:00
78d0034e34 use an old release of pypa/gh-action-pypi-publish that actually works
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -16s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2024-12-08 13:48:24 -07:00
a56b2e30e2 remove unrelated stuff from python_publish 2024-12-08 13:35:58 -07:00
ec010af947 calibrated measurements look a little funky
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -15s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2024-12-06 23:12:11 -07:00
7e0df9e643 vna_capture() working through class 2024-12-06 23:03:43 -07:00
a20217967f sweep_b_over_a working
All checks were successful
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Successful in -11s
Publish Python 🐍 distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been skipped
2024-12-04 19:26:44 -07:00
16 changed files with 2212 additions and 493 deletions

View File

@ -54,13 +54,7 @@ jobs:
with: with:
name: python-package-distributions name: python-package-distributions
path: dist/ path: dist/
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Publish distribution 📦 to PyPI - name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@0ab0b79471669eb3a4d647e625009c62f9f3b241
with: with:
password: ${{ secrets.PYPI_API_TOKEN }} password: ${{ secrets.PYPI_API_TOKEN }}
verbose: true
print-hash: true

View File

@ -9,6 +9,9 @@ charon-vna = {file = ".", editable = true}
[dev-packages] [dev-packages]
ipykernel = "*" ipykernel = "*"
ipywidgets = "*" ipywidgets = "*"
black = "*"
flake8 = "*"
isort = "*"
[requires] [requires]
python_version = "3.10" python_version = "3.10"

633
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "8ca47bc9e64405da418625530ed3861fbf72ff2f615aa4053f0da928dd09a494" "sha256": "470252a74774400955ef60d73f7e697760609be00b7733aa7a51b2f192e37fe5"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -36,74 +36,63 @@
}, },
"contourpy": { "contourpy": {
"hashes": [ "hashes": [
"sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0", "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1",
"sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda",
"sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", "sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d",
"sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", "sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509",
"sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843", "sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6",
"sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8", "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f",
"sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e",
"sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751",
"sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294", "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86",
"sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84", "sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b",
"sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927", "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc",
"sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8", "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546",
"sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", "sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec",
"sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7", "sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f",
"sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82",
"sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", "sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c",
"sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b", "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b",
"sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3", "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c",
"sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c",
"sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973", "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53",
"sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087", "sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80",
"sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242",
"sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc", "sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85",
"sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18", "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124",
"sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5",
"sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2",
"sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2", "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3",
"sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", "sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d",
"sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", "sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc",
"sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6", "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342",
"sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b", "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1",
"sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2", "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1",
"sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595",
"sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30",
"sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab",
"sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3",
"sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5", "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2",
"sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd",
"sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7",
"sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277",
"sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102", "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453",
"sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697",
"sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b",
"sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e", "sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454",
"sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", "sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9",
"sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8", "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1",
"sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6",
"sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", "sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291",
"sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14", "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750",
"sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699",
"sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", "sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e",
"sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81",
"sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0", "sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9",
"sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da", "sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375"
"sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800",
"sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6",
"sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66",
"sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca",
"sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb",
"sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c",
"sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06",
"sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779",
"sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8",
"sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f",
"sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c"
], ],
"markers": "python_version >= '3.9'", "markers": "python_version >= '3.10'",
"version": "==1.3.0" "version": "==1.3.1"
}, },
"cycler": { "cycler": {
"hashes": [ "hashes": [
@ -123,57 +112,59 @@
}, },
"fonttools": { "fonttools": {
"hashes": [ "hashes": [
"sha256:07e005dc454eee1cc60105d6a29593459a06321c21897f769a281ff2d08939f6", "sha256:07f8288aacf0a38d174445fc78377a97fb0b83cfe352a90c9d9c1400571963c7",
"sha256:0a911591200114969befa7f2cb74ac148bce5a91df5645443371aba6d222e263", "sha256:11e5de1ee0d95af4ae23c1a138b184b7f06e0b6abacabf1d0db41c90b03d834b",
"sha256:0d1d353ef198c422515a3e974a1e8d5b304cd54a4c2eebcae708e37cd9eeffb1", "sha256:1bc7ad24ff98846282eef1cbeac05d013c2154f977a79886bb943015d2b1b261",
"sha256:0e88e3018ac809b9662615072dcd6b84dca4c2d991c6d66e1970a112503bba7e", "sha256:1dcc07934a2165ccdc3a5a608db56fb3c24b609658a5b340aee4ecf3ba679dc0",
"sha256:1d152d1be65652fc65e695e5619e0aa0982295a95a9b29b52b85775243c06556", "sha256:22f38464daa6cdb7b6aebd14ab06609328fe1e9705bb0fcc7d1e69de7109ee02",
"sha256:262705b1663f18c04250bd1242b0515d3bbae177bee7752be67c979b7d47f43d", "sha256:27e4ae3592e62eba83cd2c4ccd9462dcfa603ff78e09110680a5444c6925d841",
"sha256:278913a168f90d53378c20c23b80f4e599dca62fbffae4cc620c8eed476b723e", "sha256:3983313c2a04d6cc1fe9251f8fc647754cf49a61dac6cb1e7249ae67afaafc45",
"sha256:301540e89cf4ce89d462eb23a89464fef50915255ece765d10eee8b2bf9d75b2", "sha256:529cef2ce91dc44f8e407cc567fae6e49a1786f2fefefa73a294704c415322a4",
"sha256:31c32d7d4b0958600eac75eaf524b7b7cb68d3a8c196635252b7a2c30d80e986", "sha256:5323a22eabddf4b24f66d26894f1229261021dacd9d29e89f7872dd8c63f0b8b",
"sha256:357cacb988a18aace66e5e55fe1247f2ee706e01debc4b1a20d77400354cddeb", "sha256:54153c49913f45065c8d9e6d0c101396725c5621c8aee744719300f79771d75a",
"sha256:37cddd62d83dc4f72f7c3f3c2bcf2697e89a30efb152079896544a93907733bd", "sha256:546565028e244a701f73df6d8dd6be489d01617863ec0c6a42fa25bf45d43048",
"sha256:41bb0b250c8132b2fcac148e2e9198e62ff06f3cc472065dff839327945c5882", "sha256:5480673f599ad410695ca2ddef2dfefe9df779a9a5cda89503881e503c9c7d90",
"sha256:4aa4817f0031206e637d1e685251ac61be64d1adef111060df84fdcbc6ab6c44", "sha256:5e8d657cd7326eeaba27de2740e847c6b39dde2f8d7cd7cc56f6aad404ddf0bd",
"sha256:4e10d2e0a12e18f4e2dd031e1bf7c3d7017be5c8dbe524d07706179f355c5dac", "sha256:62d65a3022c35e404d19ca14f291c89cc5890032ff04f6c17af0bd1927299674",
"sha256:5419771b64248484299fa77689d4f3aeed643ea6630b2ea750eeab219588ba20", "sha256:6314bf82c54c53c71805318fcf6786d986461622dd926d92a465199ff54b1b72",
"sha256:54471032f7cb5fca694b5f1a0aaeba4af6e10ae989df408e0216f7fd6cdc405d", "sha256:7a8aa2c5e5b8b3bcb2e4538d929f6589a5c6bdb84fd16e2ed92649fb5454f11c",
"sha256:58974b4987b2a71ee08ade1e7f47f410c367cdfc5a94fabd599c88165f56213a", "sha256:827e95fdbbd3e51f8b459af5ea10ecb4e30af50221ca103bea68218e9615de07",
"sha256:58d29b9a294573d8319f16f2f79e42428ba9b6480442fa1836e4eb89c4d9d61c", "sha256:859c358ebf41db18fb72342d3080bce67c02b39e86b9fbcf1610cca14984841b",
"sha256:5eb2474a7c5be8a5331146758debb2669bf5635c021aee00fd7c353558fc659d", "sha256:86721fbc389ef5cc1e2f477019e5069e8e4421e8d9576e9c26f840dbb04678de",
"sha256:6e37561751b017cf5c40fce0d90fd9e8274716de327ec4ffb0df957160be3bff", "sha256:89bdc5d88bdeec1b15af790810e267e8332d92561dce4f0748c2b95c9bdf3926",
"sha256:76ae5091547e74e7efecc3cbf8e75200bc92daaeb88e5433c5e3e95ea8ce5aa7", "sha256:8c4491699bad88efe95772543cd49870cf756b019ad56294f6498982408ab03e",
"sha256:7965af9b67dd546e52afcf2e38641b5be956d68c425bef2158e95af11d229f10", "sha256:8c5ec45428edaa7022f1c949a632a6f298edc7b481312fc7dc258921e9399628",
"sha256:7e3b7d44e18c085fd8c16dcc6f1ad6c61b71ff463636fcb13df7b1b818bd0c02", "sha256:8e75f12c82127486fac2d8bfbf5bf058202f54bf4f158d367e41647b972342ca",
"sha256:7ed7ee041ff7b34cc62f07545e55e1468808691dddfd315d51dd82a6b37ddef2", "sha256:a430178ad3e650e695167cb53242dae3477b35c95bef6525b074d87493c4bf29",
"sha256:82834962b3d7c5ca98cb56001c33cf20eb110ecf442725dc5fdf36d16ed1ab07", "sha256:a8c2794ded89399cc2169c4d0bf7941247b8d5932b2659e09834adfbb01589aa",
"sha256:8583e563df41fdecef31b793b4dd3af8a9caa03397be648945ad32717a92885b", "sha256:aca318b77f23523309eec4475d1fbbb00a6b133eb766a8bdc401faba91261abe",
"sha256:8fa92cb248e573daab8d032919623cc309c005086d743afb014c836636166f08", "sha256:ae3b6600565b2d80b7c05acb8e24d2b26ac407b27a3f2e078229721ba5698427",
"sha256:93d458c8a6a354dc8b48fc78d66d2a8a90b941f7fec30e94c7ad9982b1fa6bab", "sha256:aedbeb1db64496d098e6be92b2e63b5fac4e53b1b92032dfc6988e1ea9134a4d",
"sha256:957f669d4922f92c171ba01bef7f29410668db09f6c02111e22b2bce446f3285", "sha256:aee3b57643827e237ff6ec6d28d9ff9766bd8b21e08cd13bff479e13d4b14765",
"sha256:9dc080e5a1c3b2656caff2ac2633d009b3a9ff7b5e93d0452f40cd76d3da3b3c", "sha256:b54baf65c52952db65df39fcd4820668d0ef4766c0ccdf32879b77f7c804d5c5",
"sha256:9ef1b167e22709b46bf8168368b7b5d3efeaaa746c6d39661c1b4405b6352e58", "sha256:b586ab5b15b6097f2fb71cafa3c98edfd0dba1ad8027229e7b1e204a58b0e09d",
"sha256:a7a310c6e0471602fe3bf8efaf193d396ea561486aeaa7adc1f132e02d30c4b9", "sha256:b8d5e8916c0970fbc0f6f1bece0063363bb5857a7f170121a4493e31c3db3314",
"sha256:ab774fa225238986218a463f3fe151e04d8c25d7de09df7f0f5fce27b1243dbc", "sha256:bc5dbb4685e51235ef487e4bd501ddfc49be5aede5e40f4cefcccabc6e60fb4b",
"sha256:ada215fd079e23e060157aab12eba0d66704316547f334eee9ff26f8c0d7b8ab", "sha256:bdcc9f04b36c6c20978d3f060e5323a43f6222accc4e7fcbef3f428e216d96af",
"sha256:c39287f5c8f4a0c5a55daf9eaf9ccd223ea59eed3f6d467133cc727d7b943a55", "sha256:c3ca99e0d460eff46e033cd3992a969658c3169ffcd533e0a39c63a38beb6831",
"sha256:c9c563351ddc230725c4bdf7d9e1e92cbe6ae8553942bd1fb2b2ff0884e8b714", "sha256:caf8230f3e10f8f5d7593eb6d252a37caf58c480b19a17e250a63dad63834cf3",
"sha256:d26732ae002cc3d2ecab04897bb02ae3f11f06dd7575d1df46acd2f7c012a8d8", "sha256:cd70de1a52a8ee2d1877b6293af8a2484ac82514f10b1c67c1c5762d38073e56",
"sha256:d3b659d1029946f4ff9b6183984578041b520ce0f8fb7078bb37ec7445806b33", "sha256:cf4fe7c124aa3f4e4c1940880156e13f2f4d98170d35c749e6b4f119a872551e",
"sha256:dd9cc95b8d6e27d01e1e1f1fae8559ef3c02c76317da650a19047f249acd519d", "sha256:d342e88764fb201286d185093781bf6628bbe380a913c24adf772d901baa8276",
"sha256:e4564cf40cebcb53f3dc825e85910bf54835e8a8b6880d59e5159f0f325e637e", "sha256:da9da6d65cd7aa6b0f806556f4985bcbf603bf0c5c590e61b43aa3e5a0f822d0",
"sha256:e7d82b9e56716ed32574ee106cabca80992e6bbdcf25a88d97d21f73a0aae664", "sha256:dc5294a3d5c84226e3dbba1b6f61d7ad813a8c0238fceea4e09aa04848c3d851",
"sha256:e8a4b261c1ef91e7188a30571be6ad98d1c6d9fa2427244c545e2fa0a2494dd7", "sha256:dd68c87a2bfe37c5b33bcda0fba39b65a353876d3b9006fde3adae31f97b3ef5",
"sha256:e96bc94c8cda58f577277d4a71f51c8e2129b8b36fd05adece6320dd3d57de8a", "sha256:e6e8766eeeb2de759e862004aa11a9ea3d6f6d5ec710551a88b476192b64fd54",
"sha256:ed2f80ca07025551636c555dec2b755dd005e2ea8fbeb99fc5cdff319b70b23b", "sha256:e894b5bd60d9f473bed7a8f506515549cc194de08064d829464088d23097331b",
"sha256:f5b8a096e649768c2f4233f947cf9737f8dbf8728b90e2771e2497c6e3d21d13", "sha256:eb6ca911c4c17eb51853143624d8dc87cdcdf12a711fc38bf5bd21521e79715f",
"sha256:f8e953cc0bddc2beaf3a3c3b5dd9ab7554677da72dfaf46951e193c9653e515a", "sha256:ed63959d00b61959b035c7d47f9313c2c1ece090ff63afea702fe86de00dbed4",
"sha256:fda582236fee135d4daeca056c8c88ec5f6f6d88a004a79b84a02547c8f57386", "sha256:f412604ccbeee81b091b420272841e5ec5ef68967a9790e80bffd0e30b8e2977",
"sha256:fdb062893fd6d47b527d39346e0c5578b7957dcea6d6a3b6794569370013d9ac" "sha256:f7d66c15ba875432a2d2fb419523f5d3d347f91f48f57b8b08a2dfc3c39b8a3f",
"sha256:f9e736f60f4911061235603a6119e72053073a12c6d7904011df2d8fad2c0e35",
"sha256:fb594b5a99943042c702c550d5494bdd7577f6ef19b0bc73877c948a63184a32"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==4.54.1" "version": "==4.55.3"
}, },
"kiwisolver": { "kiwisolver": {
"hashes": [ "hashes": [
@ -297,49 +288,43 @@
}, },
"matplotlib": { "matplotlib": {
"hashes": [ "hashes": [
"sha256:039082812cacd6c6bec8e17a9c1e6baca230d4116d522e81e1f63a74d01d2e21", "sha256:01d2b19f13aeec2e759414d3bfe19ddfb16b13a1250add08d46d5ff6f9be83c6",
"sha256:03ba9c1299c920964e8d3857ba27173b4dbb51ca4bab47ffc2c2ba0eb5e2cbc5", "sha256:12eaf48463b472c3c0f8dbacdbf906e573013df81a0ab82f0616ea4b11281908",
"sha256:050598c2b29e0b9832cde72bcf97627bf00262adbc4a54e2b856426bb2ef0697", "sha256:2c5829a5a1dd5a71f0e31e6e8bb449bc0ee9dbfb05ad28fc0c6b55101b3a4be6",
"sha256:18128cc08f0d3cfff10b76baa2f296fc28c4607368a8402de61bb3f2eb33c7d9", "sha256:2fbbabc82fde51391c4da5006f965e36d86d95f6ee83fb594b279564a4c5d0d2",
"sha256:1cd93b91ab47a3616b4d3c42b52f8363b88ca021e340804c6ab2536344fad9ca", "sha256:3547d153d70233a8496859097ef0312212e2689cdf8d7ed764441c77604095ae",
"sha256:1d94ff717eb2bd0b58fe66380bd8b14ac35f48a98e7c6765117fe67fb7684e64", "sha256:359f87baedb1f836ce307f0e850d12bb5f1936f70d035561f90d41d305fdacea",
"sha256:306c8dfc73239f0e72ac50e5a9cf19cc4e8e331dd0c54f5e69ca8758550f1e1e", "sha256:3b427392354d10975c1d0f4ee18aa5844640b512d5311ef32efd4dd7db106ede",
"sha256:37e51dd1c2db16ede9cfd7b5cabdfc818b2c6397c83f8b10e0e797501c963a03", "sha256:4659665bc7c9b58f8c00317c3c2a299f7f258eeae5a5d56b4c64226fca2f7c59",
"sha256:3fd595f34aa8a55b7fc8bf9ebea8aa665a84c82d275190a61118d33fbc82ccae", "sha256:4673ff67a36152c48ddeaf1135e74ce0d4bce1bbf836ae40ed39c29edf7e2765",
"sha256:4876d7d40219e8ae8bb70f9263bcbe5714415acfdf781086601211335e24f8aa", "sha256:503feb23bd8c8acc75541548a1d709c059b7184cde26314896e10a9f14df5f12",
"sha256:5413401594cfaff0052f9d8b1aafc6d305b4bd7c4331dccd18f561ff7e1d3bd3", "sha256:5439f4c5a3e2e8eab18e2f8c3ef929772fd5641876db71f08127eed95ab64683",
"sha256:5816b1e1fe8c192cbc013f8f3e3368ac56fbecf02fb41b8f8559303f24c5015e", "sha256:5cdbaf909887373c3e094b0318d7ff230b2ad9dcb64da7ade654182872ab2593",
"sha256:65aacf95b62272d568044531e41de26285d54aec8cb859031f511f84bd8b495a", "sha256:5e6c6461e1fc63df30bf6f80f0b93f5b6784299f721bc28530477acd51bfc3d1",
"sha256:6758baae2ed64f2331d4fd19be38b7b4eae3ecec210049a26b6a4f3ae1c85dcc", "sha256:5fd41b0ec7ee45cd960a8e71aea7c946a28a0b8a4dcee47d2856b2af051f334c",
"sha256:6d1ce5ed2aefcdce11904fc5bbea7d9c21fff3d5f543841edf3dea84451a09ea", "sha256:607b16c8a73943df110f99ee2e940b8a1cbf9714b65307c040d422558397dac5",
"sha256:6d9f07a80deab4bb0b82858a9e9ad53d1382fd122be8cde11080f4e7dfedb38b", "sha256:7e8632baebb058555ac0cde75db885c61f1212e47723d63921879806b40bec6a",
"sha256:7741f26a58a240f43bee74965c4882b6c93df3e7eb3de160126d8c8f53a6ae6e", "sha256:81713dd0d103b379de4516b861d964b1d789a144103277769238c732229d7f03",
"sha256:8912ef7c2362f7193b5819d17dae8629b34a95c58603d781329712ada83f9447", "sha256:845d96568ec873be63f25fa80e9e7fae4be854a66a7e2f0c8ccc99e94a8bd4ef",
"sha256:909645cce2dc28b735674ce0931a4ac94e12f5b13f6bb0b5a5e65e7cea2c192b", "sha256:95b710fea129c76d30be72c3b38f330269363fbc6e570a5dd43580487380b5ff",
"sha256:96ab43906269ca64a6366934106fa01534454a69e471b7bf3d79083981aaab92", "sha256:96f2886f5c1e466f21cc41b70c5a0cd47bfa0015eb2d5793c88ebce658600e25",
"sha256:9d78bbc0cbc891ad55b4f39a48c22182e9bdaea7fc0e5dbd364f49f729ca1bbb", "sha256:994c07b9d9fe8d25951e3202a68c17900679274dadfc1248738dcfa1bd40d7f3",
"sha256:ab68d50c06938ef28681073327795c5db99bb4666214d2d5f880ed11aeaded66", "sha256:9ade1003376731a971e398cc4ef38bb83ee8caf0aee46ac6daa4b0506db1fd06",
"sha256:ac43031375a65c3196bee99f6001e7fa5bdfb00ddf43379d3c0609bdca042df9", "sha256:9b0558bae37f154fffda54d779a592bc97ca8b4701f1c710055b609a3bac44c8",
"sha256:ae82a14dab96fbfad7965403c643cafe6515e386de723e498cf3eeb1e0b70cc7", "sha256:a2a43cbefe22d653ab34bb55d42384ed30f611bcbdea1f8d7f431011a2e1c62e",
"sha256:b2696efdc08648536efd4e1601b5fd491fd47f4db97a5fbfd175549a7365c1b2", "sha256:a994f29e968ca002b50982b27168addfd65f0105610b6be7fa515ca4b5307c95",
"sha256:b82c5045cebcecd8496a4d694d43f9cc84aeeb49fe2133e036b207abe73f4d30", "sha256:ad2e15300530c1a94c63cfa546e3b7864bd18ea2901317bae8bbf06a5ade6dcf",
"sha256:be0fc24a5e4531ae4d8e858a1a548c1fe33b176bb13eff7f9d0d38ce5112a27d", "sha256:ae80dc3a4add4665cf2faa90138384a7ffe2a4e37c58d83e115b54287c4f06ef",
"sha256:bf81de2926c2db243c9b2cbc3917619a0fc85796c6ba4e58f541df814bbf83c7", "sha256:b886d02a581b96704c9d1ffe55709e49b4d2d52709ccebc4be42db856e511278",
"sha256:c375cc72229614632c87355366bdf2570c2dac01ac66b8ad048d2dabadf2d0d4", "sha256:c40ba2eb08b3f5de88152c2333c58cee7edcead0a2a0d60fcafa116b17117adc",
"sha256:c797dac8bb9c7a3fd3382b16fe8f215b4cf0f22adccea36f1545a6d7be310b41", "sha256:c55b20591ced744aa04e8c3e4b7543ea4d650b6c3c4b208c08a05b4010e8b442",
"sha256:cef2a73d06601437be399908cf13aee74e86932a5ccc6ccdf173408ebc5f6bb2", "sha256:c58a9622d5dbeb668f407f35f4e6bfac34bb9ecdcc81680c04d0258169747997",
"sha256:d52a3b618cb1cbb769ce2ee1dcdb333c3ab6e823944e9a2d36e37253815f9556", "sha256:d44cb942af1693cced2604c33a9abcef6205601c445f6d0dc531d813af8a2f5a",
"sha256:d719465db13267bcef19ea8954a971db03b9f48b4647e3860e4bc8e6ed86610f", "sha256:d907fddb39f923d011875452ff1eca29a9e7f21722b873e90db32e5d8ddff12e",
"sha256:d8dd059447824eec055e829258ab092b56bb0579fc3164fa09c64f3acd478772", "sha256:fd44fc75522f58612ec4a33958a7e5552562b7705b42ef1b4f8c0818e304a363"
"sha256:dbe196377a8248972f5cede786d4c5508ed5f5ca4a1e09b44bda889958b33f8c",
"sha256:e0830e188029c14e891fadd99702fd90d317df294c3298aad682739c5533721a",
"sha256:f053c40f94bc51bc03832a41b4f153d83f2062d88c72b5e79997072594e97e51",
"sha256:f32c7410c7f246838a77d6d1eff0c0f87f3cb0e7c4247aebea71a6d5a68cab49",
"sha256:f6ee45bc4245533111ced13f1f2cace1e7f89d1c793390392a80c139d6cf0e6c",
"sha256:f7c0410f181a531ec4e93bbc27692f2c71a15c2da16766f5ba9761e7ae518413"
], ],
"markers": "python_version >= '3.9'", "markers": "python_version >= '3.10'",
"version": "==3.9.2" "version": "==3.10.0"
}, },
"numcodecs": { "numcodecs": {
"hashes": [ "hashes": [
@ -366,11 +351,64 @@
}, },
"numpy": { "numpy": {
"hashes": [ "hashes": [
"sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098", "sha256:0557eebc699c1c34cccdd8c3778c9294e8196df27d713706895edc6f57d29608",
"sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761" "sha256:0798b138c291d792f8ea40fe3768610f3c7dd2574389e37c3f26573757c8f7ef",
"sha256:0da8495970f6b101ddd0c38ace92edea30e7e12b9a926b57f5fabb1ecc25bb90",
"sha256:0f0986e917aca18f7a567b812ef7ca9391288e2acb7a4308aa9d265bd724bdae",
"sha256:122fd2fcfafdefc889c64ad99c228d5a1f9692c3a83f56c292618a59aa60ae83",
"sha256:140dd80ff8981a583a60980be1a655068f8adebf7a45a06a6858c873fcdcd4a0",
"sha256:16757cf28621e43e252c560d25b15f18a2f11da94fea344bf26c599b9cf54b73",
"sha256:18142b497d70a34b01642b9feabb70156311b326fdddd875a9981f34a369b671",
"sha256:1c92113619f7b272838b8d6702a7f8ebe5edea0df48166c47929611d0b4dea69",
"sha256:1e25507d85da11ff5066269d0bd25d06e0a0f2e908415534f3e603d2a78e4ffa",
"sha256:30bf971c12e4365153afb31fc73f441d4da157153f3400b82db32d04de1e4066",
"sha256:3579eaeb5e07f3ded59298ce22b65f877a86ba8e9fe701f5576c99bb17c283da",
"sha256:36b2b43146f646642b425dd2027730f99bac962618ec2052932157e213a040e9",
"sha256:3905a5fffcc23e597ee4d9fb3fcd209bd658c352657548db7316e810ca80458e",
"sha256:3a4199f519e57d517ebd48cb76b36c82da0360781c6a0353e64c0cac30ecaad3",
"sha256:3f2f5cddeaa4424a0a118924b988746db6ffa8565e5829b1841a8a3bd73eb59a",
"sha256:40deb10198bbaa531509aad0cd2f9fadb26c8b94070831e2208e7df543562b74",
"sha256:440cfb3db4c5029775803794f8638fbdbf71ec702caf32735f53b008e1eaece3",
"sha256:4723a50e1523e1de4fccd1b9a6dcea750c2102461e9a02b2ac55ffeae09a4410",
"sha256:4bddbaa30d78c86329b26bd6aaaea06b1e47444da99eddac7bf1e2fab717bd72",
"sha256:4e58666988605e251d42c2818c7d3d8991555381be26399303053b58a5bbf30d",
"sha256:54dc1d6d66f8d37843ed281773c7174f03bf7ad826523f73435deb88ba60d2d4",
"sha256:57fcc997ffc0bef234b8875a54d4058afa92b0b0c4223fc1f62f24b3b5e86038",
"sha256:58b92a5828bd4d9aa0952492b7de803135038de47343b2aa3cc23f3b71a3dc4e",
"sha256:5a145e956b374e72ad1dff82779177d4a3c62bc8248f41b80cb5122e68f22d13",
"sha256:6ab153263a7c5ccaf6dfe7e53447b74f77789f28ecb278c3b5d49db7ece10d6d",
"sha256:7832f9e8eb00be32f15fdfb9a981d6955ea9adc8574c521d48710171b6c55e95",
"sha256:7fe4bb0695fe986a9e4deec3b6857003b4cfe5c5e4aac0b95f6a658c14635e31",
"sha256:7fe8f3583e0607ad4e43a954e35c1748b553bfe9fdac8635c02058023277d1b3",
"sha256:85ad7d11b309bd132d74397fcf2920933c9d1dc865487128f5c03d580f2c3d03",
"sha256:9874bc2ff574c40ab7a5cbb7464bf9b045d617e36754a7bc93f933d52bd9ffc6",
"sha256:a184288538e6ad699cbe6b24859206e38ce5fba28f3bcfa51c90d0502c1582b2",
"sha256:a222d764352c773aa5ebde02dd84dba3279c81c6db2e482d62a3fa54e5ece69b",
"sha256:a50aeff71d0f97b6450d33940c7181b08be1441c6c193e678211bff11aa725e7",
"sha256:a55dc7a7f0b6198b07ec0cd445fbb98b05234e8b00c5ac4874a63372ba98d4ab",
"sha256:a62eb442011776e4036af5c8b1a00b706c5bc02dc15eb5344b0c750428c94219",
"sha256:a7d41d1612c1a82b64697e894b75db6758d4f21c3ec069d841e60ebe54b5b571",
"sha256:a98f6f20465e7618c83252c02041517bd2f7ea29be5378f09667a8f654a5918d",
"sha256:afe8fb968743d40435c3827632fd36c5fbde633b0423da7692e426529b1759b1",
"sha256:b0b227dcff8cdc3efbce66d4e50891f04d0a387cce282fe1e66199146a6a8fca",
"sha256:b30042fe92dbd79f1ba7f6898fada10bdaad1847c44f2dff9a16147e00a93661",
"sha256:b606b1aaf802e6468c2608c65ff7ece53eae1a6874b3765f69b8ceb20c5fa78e",
"sha256:b6207dc8fb3c8cb5668e885cef9ec7f70189bec4e276f0ff70d5aa078d32c88e",
"sha256:c2aed8fcf8abc3020d6a9ccb31dbc9e7d7819c56a348cc88fd44be269b37427e",
"sha256:cb24cca1968b21355cc6f3da1a20cd1cebd8a023e3c5b09b432444617949085a",
"sha256:cff210198bb4cae3f3c100444c5eaa573a823f05c253e7188e1362a5555235b3",
"sha256:d35717333b39d1b6bb8433fa758a55f1081543de527171543a2b710551d40881",
"sha256:df12a1f99b99f569a7c2ae59aa2d31724e8d835fc7f33e14f4792e3071d11221",
"sha256:e09d40edfdb4e260cb1567d8ae770ccf3b8b7e9f0d9b5c2a9992696b30ce2742",
"sha256:e12c6c1ce84628c52d6367863773f7c8c8241be554e8b79686e91a43f1733773",
"sha256:e2b8cd48a9942ed3f85b95ca4105c45758438c7ed28fff1e4ce3e57c3b589d8e",
"sha256:e500aba968a48e9019e42c0c199b7ec0696a97fa69037bea163b55398e390529",
"sha256:ebe5e59545401fbb1b24da76f006ab19734ae71e703cdb4a8b347e84a0cece67",
"sha256:f0dd071b95bbca244f4cb7f70b77d2ff3aaaba7fa16dc41f58d14854a6204e6c",
"sha256:f8c8b141ef9699ae777c6278b52c706b653bf15d135d302754f6b2e90eb30367"
], ],
"markers": "python_version >= '3.10'", "markers": "python_version >= '3.10'",
"version": "==2.1.3" "version": "==2.2.0"
}, },
"packaging": { "packaging": {
"hashes": [ "hashes": [
@ -532,6 +570,36 @@
"markers": "python_version >= '3.9'", "markers": "python_version >= '3.9'",
"version": "==3.2.0" "version": "==3.2.0"
}, },
"pyside6": {
"hashes": [
"sha256:6d1fd95651cdbdea741af21e155350986eca31ff015fc4c721ce01c2a110a4cc",
"sha256:7d6adc5d53313249bbe02edb673877c1d437e215d71e88da78412520653f5c9f",
"sha256:866eeaca3ffead6b9d30fa3ed395d5624da0246d7586c8b8207e77ac65d82458",
"sha256:ddeeaeca8ebd0ddb1ded30dd33e9240a40f330cc91832de346ba6c9d0cd1253e"
],
"markers": "python_version < '3.14' and python_version >= '3.9'",
"version": "==6.8.1"
},
"pyside6-addons": {
"hashes": [
"sha256:570a25016d80046274f454ed0bb06734f478ce6c21be5dec62b624773fc7504e",
"sha256:879c12346b4b76f5d5ee6499d8ca53b5666c0c998b8fdf8780f08f69ea95d6f9",
"sha256:d7c8c1e89ee0db84631d5b8fdb9129d9d2a0ffb3b4cb2f5192dc8367dd980db4",
"sha256:f80cc03c1ac54132c6f800aa461dced64acd7d1646898db164ccb56fe3c23dd4"
],
"markers": "python_version < '3.14' and python_version >= '3.9'",
"version": "==6.8.1"
},
"pyside6-essentials": {
"hashes": [
"sha256:2f600b149e65b57acd6a444edb17615adc42cc2491548ae443ccb574036d86b1",
"sha256:bd05155245e3cd1572e68d72772e78fadfd713575bbfdd2c5e060d5278e390e9",
"sha256:bf8a3c9ee0b997eb18fb00cb09aacaa28b8a51ce3c295a252cc594c5530aba56",
"sha256:d5ed4ddb149f36d65bc49ae4260b2d213ee88b2d9a309012ae27f38158c2d1b6"
],
"markers": "python_version < '3.14' and python_version >= '3.9'",
"version": "==6.8.1"
},
"python-dateutil": { "python-dateutil": {
"hashes": [ "hashes": [
"sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
@ -549,11 +617,11 @@
}, },
"scikit-rf": { "scikit-rf": {
"hashes": [ "hashes": [
"sha256:722def5442bb7033d608e8d3f717b38a3315846496925e85e8015cf006c36bb7", "sha256:796758dcd79650d8c66585583ac8ef13afde0d65d2da324ec60a4557eae52a9a",
"sha256:d7e1de31842d8922d1c741de7cc09aecf083a94ddc408ee2fe3f36a284858a09" "sha256:9864653abf0761049ba95deb37741581fff5cf8d43f4acf6e46576e34e04ad43"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.4.1" "version": "==1.5.0"
}, },
"scipy": { "scipy": {
"hashes": [ "hashes": [
@ -594,13 +662,23 @@
"markers": "python_version >= '3.10'", "markers": "python_version >= '3.10'",
"version": "==1.14.1" "version": "==1.14.1"
}, },
"shiboken6": {
"hashes": [
"sha256:1dc4c1976809b0e68872bb98474cccd590455bdcd015f0e0639907e94af27b6a",
"sha256:3ea127fd72be113b73cacd70e06687ad6f83c1c888047833c7dcdd5cf8e7f586",
"sha256:9a2f51d1ddd3b6d193a0f0fdc09f8d41f2092bc664723c9b9efc1056660d0608",
"sha256:ab5b60602ca6227103138aae89c4f5df3b1b8e249cbc8ec9e6e2a57f20ad9a91"
],
"markers": "python_version < '3.14' and python_version >= '3.9'",
"version": "==6.8.1"
},
"six": { "six": {
"hashes": [ "hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0" "version": "==1.17.0"
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
@ -620,11 +698,11 @@
}, },
"xarray": { "xarray": {
"hashes": [ "hashes": [
"sha256:ae1d38cb44a0324dfb61e492394158ae22389bf7de9f3c174309c17376df63a0", "sha256:1ccace44573ddb862e210ad3ec204210654d2c750bec11bbe7d842dfc298591f",
"sha256:e369e2bac430e418c2448e5b96f07da4635f98c1319aa23cfeb3fbcb9a01d2e0" "sha256:6ee94f63ddcbdd0cf3909d1177f78cdac756640279c0e32ae36819a89cdaba37"
], ],
"markers": "python_version >= '3.10'", "markers": "python_version >= '3.10'",
"version": "==2024.10.0" "version": "==2024.11.0"
}, },
"zarr": { "zarr": {
"hashes": [ "hashes": [
@ -638,10 +716,48 @@
"develop": { "develop": {
"asttokens": { "asttokens": {
"hashes": [ "hashes": [
"sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24", "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7",
"sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0" "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"
], ],
"version": "==2.4.1" "markers": "python_version >= '3.8'",
"version": "==3.0.0"
},
"black": {
"hashes": [
"sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f",
"sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd",
"sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea",
"sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981",
"sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b",
"sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7",
"sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8",
"sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175",
"sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d",
"sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392",
"sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad",
"sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f",
"sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f",
"sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b",
"sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875",
"sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3",
"sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800",
"sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65",
"sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2",
"sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812",
"sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50",
"sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==24.10.0"
},
"click": {
"hashes": [
"sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
"sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
],
"markers": "python_version >= '3.7'",
"version": "==8.1.7"
}, },
"comm": { "comm": {
"hashes": [ "hashes": [
@ -653,12 +769,35 @@
}, },
"debugpy": { "debugpy": {
"hashes": [ "hashes": [
"sha256:a6531d952b565b7cb2fbd1ef5df3d333cf160b44f37547a4e7cf73666aca5d8d", "sha256:0e22f846f4211383e6a416d04b4c13ed174d24cc5d43f5fd52e7821d0ebc8920",
"sha256:e6355385db85cbd666be703a96ab7351bc9e6c61d694893206f8001e22aee091", "sha256:116bf8342062246ca749013df4f6ea106f23bc159305843491f64672a55af2e5",
"sha256:ec684553aba5b4066d4de510859922419febc710df7bba04fe9e7ef3de15d34f" "sha256:189058d03a40103a57144752652b3ab08ff02b7595d0ce1f651b9acc3a3a35a0",
"sha256:23dc34c5e03b0212fa3c49a874df2b8b1b8fda95160bd79c01eb3ab51ea8d851",
"sha256:28e45b3f827d3bf2592f3cf7ae63282e859f3259db44ed2b129093ca0ac7940b",
"sha256:2b26fefc4e31ff85593d68b9022e35e8925714a10ab4858fb1b577a8a48cb8cd",
"sha256:32db46ba45849daed7ccf3f2e26f7a386867b077f39b2a974bb5c4c2c3b0a280",
"sha256:40499a9979c55f72f4eb2fc38695419546b62594f8af194b879d2a18439c97a9",
"sha256:44b1b8e6253bceada11f714acf4309ffb98bfa9ac55e4fce14f9e5d4484287a1",
"sha256:52c3cf9ecda273a19cc092961ee34eb9ba8687d67ba34cc7b79a521c1c64c4c0",
"sha256:52d8a3166c9f2815bfae05f386114b0b2d274456980d41f320299a8d9a5615a7",
"sha256:61bc8b3b265e6949855300e84dc93d02d7a3a637f2aec6d382afd4ceb9120c9f",
"sha256:654130ca6ad5de73d978057eaf9e582244ff72d4574b3e106fb8d3d2a0d32458",
"sha256:6ad2688b69235c43b020e04fecccdf6a96c8943ca9c2fb340b8adc103c655e57",
"sha256:6c1f6a173d1140e557347419767d2b14ac1c9cd847e0b4c5444c7f3144697e4e",
"sha256:84e511a7545d11683d32cdb8f809ef63fc17ea2a00455cc62d0a4dbb4ed1c308",
"sha256:85de8474ad53ad546ff1c7c7c89230db215b9b8a02754d41cb5a76f70d0be296",
"sha256:8988f7163e4381b0da7696f37eec7aca19deb02e500245df68a7159739bbd0d3",
"sha256:8da1db4ca4f22583e834dcabdc7832e56fe16275253ee53ba66627b86e304da1",
"sha256:8ffc382e4afa4aee367bf413f55ed17bd91b191dcaf979890af239dda435f2a1",
"sha256:987bce16e86efa86f747d5151c54e91b3c1e36acc03ce1ddb50f9d09d16ded0e",
"sha256:ad7efe588c8f5cf940f40c3de0cd683cc5b76819446abaa50dc0829a30c094db",
"sha256:bb3b15e25891f38da3ca0740271e63ab9db61f41d4d8541745cfc1824252cb28",
"sha256:c928bbf47f65288574b78518449edaa46c82572d340e2750889bbf8cd92f3737",
"sha256:ce291a5aca4985d82875d6779f61375e959208cdf09fcec40001e65fb0a54768",
"sha256:d8768edcbeb34da9e11bcb8b5c2e0958d25218df7a6e56adf415ef262cd7b6d1"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.8.8" "version": "==1.8.11"
}, },
"decorator": { "decorator": {
"hashes": [ "hashes": [
@ -684,6 +823,15 @@
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==2.1.0" "version": "==2.1.0"
}, },
"flake8": {
"hashes": [
"sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38",
"sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"
],
"index": "pypi",
"markers": "python_full_version >= '3.8.1'",
"version": "==7.1.1"
},
"ipykernel": { "ipykernel": {
"hashes": [ "hashes": [
"sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5", "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5",
@ -695,11 +843,11 @@
}, },
"ipython": { "ipython": {
"hashes": [ "hashes": [
"sha256:0188a1bd83267192123ccea7f4a8ed0a78910535dbaa3f37671dca76ebd429c8", "sha256:85ec56a7e20f6c38fce7727dcca699ae4ffc85985aa7b23635a8008f918ae321",
"sha256:40b60e15b22591450eef73e40a027cf77bd652e757523eebc5bd7c7c498290eb" "sha256:cb0a405a306d2995a5cbb9901894d240784a9f341394c6ba3f4fe8c6eb89ff6e"
], ],
"markers": "python_version >= '3.10'", "markers": "python_version >= '3.10'",
"version": "==8.29.0" "version": "==8.30.0"
}, },
"ipywidgets": { "ipywidgets": {
"hashes": [ "hashes": [
@ -710,13 +858,22 @@
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==8.1.5" "version": "==8.1.5"
}, },
"isort": {
"hashes": [
"sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109",
"sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"
],
"index": "pypi",
"markers": "python_full_version >= '3.8.0'",
"version": "==5.13.2"
},
"jedi": { "jedi": {
"hashes": [ "hashes": [
"sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd", "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0",
"sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0" "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==0.19.1" "version": "==0.19.2"
}, },
"jupyter-client": { "jupyter-client": {
"hashes": [ "hashes": [
@ -750,6 +907,22 @@
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==0.1.7" "version": "==0.1.7"
}, },
"mccabe": {
"hashes": [
"sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
"sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
],
"markers": "python_version >= '3.6'",
"version": "==0.7.0"
},
"mypy-extensions": {
"hashes": [
"sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d",
"sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"
],
"markers": "python_version >= '3.5'",
"version": "==1.0.0"
},
"nest-asyncio": { "nest-asyncio": {
"hashes": [ "hashes": [
"sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe",
@ -774,6 +947,14 @@
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==0.8.4" "version": "==0.8.4"
}, },
"pathspec": {
"hashes": [
"sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08",
"sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"
],
"markers": "python_version >= '3.8'",
"version": "==0.12.1"
},
"pexpect": { "pexpect": {
"hashes": [ "hashes": [
"sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523",
@ -835,6 +1016,22 @@
], ],
"version": "==0.2.3" "version": "==0.2.3"
}, },
"pycodestyle": {
"hashes": [
"sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3",
"sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"
],
"markers": "python_version >= '3.8'",
"version": "==2.12.1"
},
"pyflakes": {
"hashes": [
"sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f",
"sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"
],
"markers": "python_version >= '3.8'",
"version": "==3.2.0"
},
"pygments": { "pygments": {
"hashes": [ "hashes": [
"sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199",
@ -968,11 +1165,11 @@
}, },
"six": { "six": {
"hashes": [ "hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0" "version": "==1.17.0"
}, },
"stack-data": { "stack-data": {
"hashes": [ "hashes": [
@ -981,22 +1178,60 @@
], ],
"version": "==0.6.3" "version": "==0.6.3"
}, },
"tomli": {
"hashes": [
"sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6",
"sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd",
"sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c",
"sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b",
"sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8",
"sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6",
"sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77",
"sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff",
"sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea",
"sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192",
"sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249",
"sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee",
"sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4",
"sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98",
"sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8",
"sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4",
"sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281",
"sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744",
"sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69",
"sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13",
"sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140",
"sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e",
"sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e",
"sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc",
"sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff",
"sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec",
"sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2",
"sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222",
"sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106",
"sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272",
"sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a",
"sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"
],
"markers": "python_version < '3.11'",
"version": "==2.2.1"
},
"tornado": { "tornado": {
"hashes": [ "hashes": [
"sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8", "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803",
"sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f", "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec",
"sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4", "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482",
"sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3", "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634",
"sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14", "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38",
"sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842", "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b",
"sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9", "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c",
"sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698", "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf",
"sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7", "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946",
"sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d", "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73",
"sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4" "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==6.4.1" "version": "==6.4.2"
}, },
"traitlets": { "traitlets": {
"hashes": [ "hashes": [

View File

@ -1,13 +1,48 @@
# Charon # Charon VNA
Named after [Pluto's moon](https://en.wikipedia.org/wiki/Charon_(moon)), Charon uses the [ADI Pluto SDR]() as a vector network analyzer. The basic usage is as a 1 port VNA but this can be extended to arbitrarily many ports with the addition of a couple RF switches. <!-- ![PyPi Downloads](https://img.shields.io/pypi/dm/charon-vna) -->
<!-- ![Last Commit](https://img.shields.io/gitea/last-commit/brendanhaines/charon-vna?gitea_url=https%3A%2F%2Fgit.brendanhaines.com) -->
<!-- ![Workflow Status](https://git.brendanhaines.com/brendanhaines/charon-vna/actions/workflows/python_publish.yml/badge.svg) -->
Named after [Pluto's moon](https://en.wikipedia.org/wiki/Charon_(moon)), Charon uses the [ADI Pluto SDR](https://www.analog.com/en/resources/evaluation-hardware-and-software/evaluation-boards-kits/adalm-pluto.html) as a vector network analyzer. The basic usage is as a 1 port VNA but this can be extended to arbitrarily many ports with the addition of a couple RF switches.
## Installation ## Installation
1. Install LibIIO. This is a dependency of [PyADI-IIO](https://wiki.analog.com/resources/tools-software/linux-software/pyadi-iio). 1. Install LibIIO. This is a dependency of [PyADI-IIO](https://wiki.analog.com/resources/tools-software/linux-software/pyadi-iio).
On Ubuntu 22.04 just run `sudo apt-get install -y libiio-dev` On Ubuntu 22.04 just run `sudo apt-get install -y libiio-dev`
2. `pip install charon-vna` 2. Charon releases are published on [PyPi](https://pypi.org/project/charon-vna/). Install using pip:
`pip install charon-vna`
## Hardware Setup
You need a few things:
- [Analog Devices Pluto SDR](https://www.analog.com/en/resources/evaluation-hardware-and-software/evaluation-boards-kits/adalm-pluto.html)
- Any variant of the Pluto *should* work too such as the [Pluto+](https://github.com/plutoplus/plutoplus?tab=readme-ov-file) however I have only tested with the basic flavor
- Note that you _must_ have two receive ports which means revision C or later of the basic Pluto
- Directional couplers (1 per port up to 4 ports)
- I have been using [AAMCS-UDC-0.5G-18G-10dB-Sf](http://www.aa-mcs.com/wp-content/uploads/documents/AAMCS-UDC-0.5G-18G-10dB-Sf.pdf)
- [Cerberus RF switch](https://git.brendanhaines.com/brendanhaines/cerberus_sp4t) + [Pluto IO Shield](https://git.brendanhaines.com/brendanhaines/pluto_io_shield)
- Optional. Without this you'll be limited to S11 and uncalibrated S21 measurements (with required re-cabling)
- There's nothing special about this particular board, if you want more than 4 ports you can make your own pretty easily. You just need 3 SPxT switches. Note that these switches will see tons of cycles so avoid mechanical switches
- SMA cables
- Calibration standard
- Ideally something with s-parameters measured on a better VNA
- I have used a basic SMA load and two modified SMA jacks with decent results
![calibration standard](img/calibration_standard.jpg)
### Pluto Configuration
Most of my testing is with Pluto firmware [v0.39](https://github.com/analogdevicesinc/plutosdr-fw/releases/tag/v0.39) though this may work with other firmware versions. I had issues with the Pluto sometimes seeing no signal which resolved when I upgraded from v0.35. Instructions for upgrading firmware are on the [Analog Devices wiki](https://wiki.analog.com/university/tools/pluto/users/firmware).
We need two receive channels on the SDR. If you have a Pluto+ that should already be configured and you can skip this step.
Analog devices has a [guide](https://wiki.analog.com/university/tools/pluto/users/customizing#updating_to_the_ad9364) for enabling the second channel. Ideally this should be set as `ad9361` to enable a wider band of operation in addition to the second channel, however the critical setting is enabling 2r2t. SSH into the Pluto and run the following:
```bash
fw_setenv attr_name compatible
fw_setenv attr_val ad9361
fw_setenv mode 2r2t
```
## Usage ## Usage
@ -19,6 +54,7 @@ It will also be accessible over a socket to enable test automation with external
TBD TBD
### Power Calibration ### Power Calibration
I include a default output power lookup table. This is derived from two TX channels of two Pluto SDRs and does not include any of the loss of a coupler or Charon switch board. I include a default output power lookup table. This is derived from two TX channels of two Pluto SDRs and does not include any of the loss of a coupler or Charon switch board.
Absolute output power is generally not well calibrated for VNAs anyway and has negligible impact on most measurements so this is probably sufficient for most users. If you're trying to run a power sweep this may be insufficient. Absolute output power is generally not well calibrated for VNAs anyway and has negligible impact on most measurements so this is probably sufficient for most users. If you're trying to run a power sweep this may be insufficient.
@ -27,20 +63,16 @@ If you have an RF power meter you can generate your own power calibration.
Note that unlike the main calibration, power calibration frequencies do not need to match the measurement frequencies. Values are interpolated. Note that unlike the main calibration, power calibration frequencies do not need to match the measurement frequencies. Values are interpolated.
## Hardware ## References
You need a few things: #### Pluto Default Connection Settings
- [Analog Devices Pluto SDR](https://www.analog.com/en/resources/evaluation-hardware-and-software/evaluation-boards-kits/adalm-pluto.html).
Any variant of the Pluto *should* work too such as the [Pluto+](https://github.com/plutoplus/plutoplus?tab=readme-ov-file)
- Directional couplers (1 per port up to 4 ports).
I have been using [AAMCS-UDC-0.5G-18G-10dB-Sf](http://www.aa-mcs.com/wp-content/uploads/documents/AAMCS-UDC-0.5G-18G-10dB-Sf.pdf)
- Charon switch board - coming soon.
Without this, you'll be limited to S11 and uncalibrated S21 measurements (with required re-cabling).
There's nothing special about this particular board, if you want more than 4 ports you can make your own pretty easily. You just need 3 SPxT switches. Note that these switches will see tons of cycles so avoid mechanical switches.
- SMA cables
### Pluto Modification - user: `root`
- password: `analog`
- ip: `192.168.2.1`
We need two receive channels on the SDR. If you have a Pluto+ that should already be configured and you can skip this step. ## Alternatives
Analog devices has a [guide](https://wiki.analog.com/university/tools/pluto/users/customizing#updating_to_the_ad9364) for enabling the second channel. Ideally this should be set as `ad9361` to enable a wider band of operation in addition to the second channel, however the critical setting is enabling 2r2t. - [NanoVNA](https://nanovna.com/). 2-ports. 50 kHz - 2.7 GHz. Degraded performance above 1.5 GHz. S11 and S21 only.
- [pluto-network-analyzer](https://github.com/fromconcepttocircuit/pluto-network-analyzer). 2-ports. 100 MHz - 3 GHz. S11 and S21 only. Uses a [wideband RF bridge](https://www.60dbm.com/product/rf-bridge-1-3000-mhz/) instead of a coupler
- [LibreVNA](https://github.com/jankae/LibreVNA). 2-ports. 100 KHz - 6 GHz. I've never used this but it is almost certainly faster than Charon. Not sure how the performance compares. $700 on [AliExpress](https://www.aliexpress.us/item/3256802242049773.html?gatewayAdapt=glo2usa4itemAdapt)

26
charon_vna/cli.py Normal file
View File

@ -0,0 +1,26 @@
# %% imports
from pathlib import Path
import click
# from charon_vna.vna import Charon
@click.command()
@click.argument("start", type=float)
@click.argument("stop", type=float)
@click.argument("pts", type=int)
@click.option("--ports", "-n", type=int, default=1, help="Number of ports.")
@click.option("--power", "-p", type=float, default=-5, help="Port output power [dBm].")
@click.option("--snp", "-o", type=click.Path(), help="Path for output Touchstone file.")
def capture(start: float, stop: float, pts: int, power: float, snp: Path, ports: int):
raise NotImplementedError
# %%
def main():
capture()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,505 @@
{
"frequency": [
80000000.0,
80841683.36673346,
81683366.73346694,
82525050.1002004,
83366733.46693386,
84208416.83366734,
85050100.2004008,
85891783.56713426,
86733466.93386774,
87575150.3006012,
88416833.66733468,
89258517.03406814,
90100200.4008016,
90941883.76753508,
91783567.13426854,
92625250.501002,
93466933.86773548,
94308617.23446894,
95150300.6012024,
95991983.96793588,
96833667.33466934,
97675350.70140281,
98517034.06813627,
99358717.43486974,
100200400.8016032,
101042084.16833667,
101883767.53507014,
102725450.90180361,
103567134.26853707,
104408817.63527054,
105250501.00200401,
106092184.36873747,
106933867.73547095,
107775551.10220441,
108617234.46893787,
109458917.83567134,
110300601.20240481,
111142284.56913827,
111983967.93587175,
112825651.30260521,
113667334.66933867,
114509018.03607213,
115350701.40280561,
116192384.76953909,
117034068.13627255,
117875751.50300601,
118717434.86973947,
119559118.23647295,
120400801.60320641,
121242484.96993989,
122084168.33667335,
122925851.70340681,
123767535.07014027,
124609218.43687375,
125450901.80360723,
126292585.17034069,
127134268.53707415,
127975951.90380761,
128817635.27054109,
129659318.63727455,
130501002.00400802,
131342685.37074149,
132184368.73747495,
133026052.10420841,
133867735.47094189,
134709418.83767536,
135551102.20440882,
136392785.5711423,
137234468.93787575,
138076152.30460924,
138917835.67134267,
139759519.03807616,
140601202.40480962,
141442885.7715431,
142284569.13827655,
143126252.50501,
143967935.8717435,
144809619.23847696,
145651302.60521042,
146492985.97194389,
147334669.33867735,
148176352.70541084,
149018036.07214427,
149859719.43887776,
150701402.80561122,
151543086.17234468,
152384769.53907818,
153226452.9058116,
154068136.2725451,
154909819.63927856,
155751503.00601202,
156593186.3727455,
157434869.73947895,
158276553.10621244,
159118236.4729459,
159959919.83967936,
160801603.20641282,
161643286.57314628,
162484969.93987978,
163326653.30661324,
164168336.6733467,
165010020.04008016,
165851703.40681362,
166693386.7735471,
167535070.14028054,
168376753.50701404,
169218436.8737475,
170060120.24048096,
170901803.60721445,
171743486.97394788,
172585170.34068137,
173426853.70741484,
174268537.0741483,
175110220.4408818,
175951903.80761522,
176793587.1743487,
177635270.54108217,
178476953.90781564,
179318637.2745491,
180160320.64128256,
181002004.00801605,
181843687.37474948,
182685370.74148297,
183527054.10821643,
184368737.4749499,
185210420.8416834,
186052104.20841682,
186893787.5751503,
187735470.94188377,
188577154.30861723,
189418837.67535073,
190260521.04208416,
191102204.40881765,
191943887.7755511,
192785571.14228457,
193627254.50901806,
194468937.8757515,
195310621.242485,
196152304.60921845,
196993987.9759519,
197835671.34268537,
198677354.70941883,
199519038.07615232,
200360721.44288576,
201202404.80961925,
202044088.1763527,
202885771.54308617,
203727454.90981966,
204569138.2765531,
205410821.6432866,
206252505.01002005,
207094188.3767535,
207935871.743487,
208777555.11022043,
209619238.47695392,
210460921.8436874,
211302605.21042085,
212144288.5771543,
212985971.94388777,
213827655.31062126,
214669338.67735472,
215511022.04408818,
216352705.41082165,
217194388.7775551,
218036072.14428857,
218877755.51102206,
219719438.87775552,
220561122.24448898,
221402805.61122245,
222244488.9779559,
223086172.3446894,
223927855.71142286,
224769539.07815632,
225611222.44488978,
226452905.81162325,
227294589.1783567,
228136272.5450902,
228977955.91182366,
229819639.27855712,
230661322.64529058,
231503006.01202404,
232344689.37875754,
233186372.745491,
234028056.11222446,
234869739.47895792,
235711422.84569138,
236553106.21242484,
237394789.57915834,
238236472.9458918,
239078156.31262526,
239919839.67935872,
240761523.04609218,
241603206.41282564,
242444889.77955914,
243286573.1462926,
244128256.51302606,
244969939.87975952,
245811623.24649298,
246653306.61322647,
247494989.97995993,
248336673.3466934,
249178356.71342686,
250020040.08016032,
250861723.44689378,
251703406.81362727,
252545090.18036073,
253386773.5470942,
254228456.91382766,
255070140.28056112,
255911823.6472946,
256753507.01402807,
257595190.38076153,
258436873.747495,
259278557.11422846,
260120240.48096192,
260961923.8476954,
261803607.21442887,
262645290.58116233,
263486973.9478958,
264328657.31462926,
265170340.68136275,
266012024.0480962,
266853707.41482967,
267695390.78156313,
268537074.1482966,
269378757.51503,
270220440.8817636,
271062124.248497,
271903807.61523044,
272745490.98196393,
273587174.3486974,
274428857.71543086,
275270541.08216435,
276112224.44889784,
276953907.8156313,
277795591.1823647,
278637274.5490982,
279478957.9158317,
280320641.2825651,
281162324.6492986,
282004008.0160321,
282845691.38276553,
283687374.74949896,
284529058.1162325,
285370741.48296595,
286212424.8496994,
287054108.21643287,
287895791.58316636,
288737474.9498998,
289579158.3166333,
290420841.6833668,
291262525.0501002,
292104208.41683364,
292945891.78356713,
293787575.1503006,
294629258.51703405,
295470941.88376755,
296312625.25050104,
297154308.61723447,
297995991.98396796,
298837675.35070145,
299679358.7174349,
300521042.0841683,
301362725.4509018,
302204408.8176353,
303046092.1843687,
303887775.5511022,
304729458.9178357,
305571142.28456914,
306412825.6513026,
307254509.0180361,
308096192.38476956,
308937875.751503,
309779559.1182365,
310621242.48497,
311462925.8517034,
312304609.2184369,
313146292.5851704,
313987975.9519038,
314829659.31863725,
315671342.68537074,
316513026.05210423,
317354709.41883767,
318196392.78557116,
319038076.15230465,
319879759.5190381,
320721442.8857715,
321563126.25250506,
322404809.6192385,
323246492.9859719,
324088176.3527054,
324929859.7194389,
325771543.08617234,
326613226.45290583,
327454909.8196393,
328296593.18637276,
329138276.5531062,
329979959.9198397,
330821643.2865732,
331663326.6533066,
332505010.0200401,
333346693.3867736,
334188376.753507,
335030060.12024045,
335871743.486974,
336713426.85370743,
337555110.22044086,
338396793.58717436,
339238476.95390785,
340080160.3206413,
340921843.6873748,
341763527.05410826,
342605210.4208417,
343446893.7875751,
344288577.1543086,
345130260.5210421,
345971943.88777554,
346813627.25450903,
347655310.6212425,
348496993.98797596,
349338677.35470945,
350180360.7214429,
351022044.08817637,
351863727.45490986,
352705410.8216433,
353547094.1883768,
354388777.5551102,
355230460.9218437,
356072144.28857714,
356913827.65531063,
357755511.0220441,
358597194.38877755,
359438877.75551105,
360280561.1222445,
361122244.48897797,
361963927.85571146,
362805611.2224449,
363647294.5891784,
364488977.9559118,
365330661.3226453,
366172344.6893788,
367014028.05611223,
367855711.4228457,
368697394.78957915,
369539078.15631264,
370380761.5230461,
371222444.88977957,
372064128.25651306,
372905811.6232465,
373747494.98998,
374589178.3567134,
375430861.7234469,
376272545.0901804,
377114228.4569138,
377955911.8236473,
378797595.19038075,
379639278.55711424,
380480961.92384773,
381322645.29058117,
382164328.65731466,
383006012.0240481,
383847695.3907816,
384689378.7575151,
385531062.1242485,
386372745.490982,
387214428.8577154,
388056112.2244489,
388897795.59118235,
389739478.95791584,
390581162.32464933,
391422845.69138277,
392264529.05811626,
393106212.4248497,
393947895.7915832,
394789579.1583167,
395631262.5250501,
396472945.8917836,
397314629.258517,
398156312.6252505,
398997995.991984,
399839679.35871744,
400681362.72545093,
401523046.09218436,
402364729.45891786,
403206412.8256513,
404048096.1923848,
404889779.5591183,
405731462.9258517,
406573146.2925852,
407414829.6593186,
408256513.0260521,
409098196.3927856,
409939879.75951904,
410781563.12625253,
411623246.49298596,
412464929.85971946,
413306613.22645295,
414148296.5931864,
414989979.95991987,
415831663.3266533,
416673346.6933868,
417515030.0601203,
418356713.4268537,
419198396.7935872,
420040080.16032064,
420881763.52705413,
421723446.89378756,
422565130.26052105,
423406813.62725455,
424248496.993988,
425090180.36072147,
425931863.7274549,
426773547.0941884,
427615230.4609219,
428456913.8276553,
429298597.1943888,
430140280.56112224,
430981963.92785573,
431823647.2945892,
432665330.66132265,
433507014.02805614,
434348697.3947896,
435190380.76152307,
436032064.12825656,
436873747.49499,
437715430.8617235,
438557114.2284569,
439398797.5951904,
440240480.96192384,
441082164.3286573,
441923847.6953908,
442765531.06212425,
443607214.42885774,
444448897.7955912,
445290581.16232467,
446132264.52905816,
446973947.8957916,
447815631.2625251,
448657314.6292585,
449498997.995992,
450340681.3627255,
451182364.7294589,
452024048.0961924,
452865731.46292585,
453707414.82965934,
454549098.1963928,
455390781.56312627,
456232464.92985976,
457074148.2965932,
457915831.6633267,
458757515.0300601,
459599198.3967936,
460440881.7635271,
461282565.1302605,
462124248.496994,
462965931.86372745,
463807615.23046094,
464649298.59719443,
465490981.96392787,
466332665.33066136,
467174348.6973948,
468016032.0641283,
468857715.4308618,
469699398.7975952,
470541082.1643287,
471382765.5310621,
472224448.8977956,
473066132.26452905,
473907815.63126254,
474749498.99799603,
475591182.36472946,
476432865.73146296,
477274549.0981964,
478116232.4649299,
478957915.83166337,
479799599.1983968,
480641282.5651303,
481482965.9318637,
482324649.2985972,
483166332.6653307,
484008016.03206414,
484849699.39879763,
485691382.76553106,
486533066.13226455,
487374749.498998,
488216432.8657315,
489058116.23246497,
489899799.5991984,
490741482.9659319,
491583166.3326653,
492424849.6993988,
493266533.0661323,
494108216.43286574,
494949899.79959923,
495791583.16633266,
496633266.53306615,
497474949.89979964,
498316633.2665331,
499158316.63326657,
500000000.0
],
"power": -5
}

View File

@ -0,0 +1,25 @@
import json
import subprocess
import numpy as np
from charon_vna.gui import PATH_CONFIG_DEFAULT
config = dict(
frequency=np.linspace(80e6, 500e6, 500).tolist(),
power=-5,
)
with open(PATH_CONFIG_DEFAULT, "w") as f:
json.dump(config, f)
# autoformat
subprocess.run(
[
"python",
"-m",
"json.tool",
PATH_CONFIG_DEFAULT.resolve().as_posix(),
PATH_CONFIG_DEFAULT.resolve().as_posix(),
]
)

293
charon_vna/gui.py Normal file
View File

@ -0,0 +1,293 @@
# %% imports
import json
import pickle
import re
import sys
import webbrowser
from pathlib import Path
from typing import List
import matplotlib as mpl
import numpy as np
import skrf as rf
import xarray as xr
from numpy import typing as npt
from PySide6.QtGui import QAction, QKeySequence
from PySide6.QtWidgets import (
QApplication,
QFileDialog,
QInputDialog,
QLineEdit,
QMainWindow,
QMenu,
QProgressBar,
QTabWidget,
QVBoxLayout,
QWidget,
)
from charon_vna.plots import PlotWidget
from charon_vna.util import net2s, s2net
from charon_vna.vna import Charon
# %%
PATH_CONFIG_DEFAULT = Path(__file__).parent / "config_default.json"
CONFIG_SUFFIX = ".json"
class MainWindow(QMainWindow):
config_path: Path | None
# device: Charon
plots: List[PlotWidget]
def __init__(self, ip: str | None = None):
super().__init__()
self.config_path = PATH_CONFIG_DEFAULT
with open(self.config_path, "r") as f:
config = json.load(f)
self._frequency = config["frequency"]
vna_kwargs = dict(
frequency=self._frequency,
)
if ip is not None:
vna_kwargs["ip"] = ip
self.vna = Charon(**vna_kwargs)
self.active_port = 0
self.vna.set_switches(a=self.active_port, b=self.active_port)
mpl.use("QtAgg")
self.setWindowTitle("Charon VNA")
# Menu
menubar = self.menuBar()
menu_file = QMenu("&File")
menubar.addMenu(menu_file)
action_open_config = QAction("&Open Configuration", self)
menu_file.addAction(action_open_config)
action_open_config.triggered.connect(self.open_config)
action_open_config.setShortcut(QKeySequence("Ctrl+O"))
action_save_config = QAction("&Save Configuration", self)
menu_file.addAction(action_save_config)
action_save_config.triggered.connect(self.save_config)
action_save_config.setShortcut(QKeySequence("Ctrl+S"))
action_saveas_config = QAction("Save Configuration &As", self)
menu_file.addAction(action_saveas_config)
action_saveas_config.triggered.connect(self.saveas_config)
action_saveas_config.setShortcut(QKeySequence("Ctrl+Shift+S"))
menu_stimulus = QMenu("&Stimulus")
menubar.addMenu(menu_stimulus)
action_set_frequency = QAction("&Frequency", self)
menu_stimulus.addAction(action_set_frequency)
action_set_frequency.triggered.connect(self.set_frequency)
action_set_power = QAction("&Power", self)
menu_stimulus.addAction(action_set_power)
# action_set_power.triggered.connect(self.set_power)
action_trigger = QAction("&Trigger", self)
action_trigger.triggered.connect(self.capture)
action_trigger.setShortcut("Ctrl+T")
menu_stimulus.addAction(action_trigger)
action_p0 = QAction("Switch &Port", self)
action_p0.triggered.connect(self.toggle_port)
menu_stimulus.addAction(action_p0)
menu_calibration = QMenu("&Calibration")
menubar.addMenu(menu_calibration)
action_cal_solt = QAction("&SOLT", self)
action_cal_solt.triggered.connect(self.calibrate_solt)
menu_calibration.addAction(action_cal_solt)
menu_help = QMenu("&Help")
menubar.addMenu(menu_help)
action_open_homepage = QAction("&Documentation", self)
action_open_homepage.triggered.connect(
lambda: webbrowser.open("https://git.brendanhaines.com/brendanhaines/charon_vna")
)
menu_help.addAction(action_open_homepage)
action_report_issue = QAction("&Report an Issue", self)
action_report_issue.triggered.connect(
lambda: webbrowser.open("https://git.brendanhaines.com/brendanhaines/charon_vna/issues")
)
menu_help.addAction(action_report_issue)
# Content
window_layout = QVBoxLayout()
prog_sweep = QProgressBar()
prog_sweep.setMinimum(0)
prog_sweep.setMaximum(100)
prog_sweep.setFormat("%v / %m")
# prog_sweep.setTextVisible(False)
prog_sweep.setValue(0)
window_layout.addWidget(prog_sweep)
self.prog_sweep = prog_sweep
plot_widget = QTabWidget()
self.plots = []
for type_ in [
"logmag",
"phase",
"vswr",
"smith",
]:
self.plots.append(PlotWidget(type_=type_))
plot_widget.addTab(self.plots[-1], type_)
window_layout.addWidget(plot_widget)
# Set the central widget of the Window.
widget = QWidget()
widget.setLayout(window_layout)
self.setCentralWidget(widget)
def toggle_port(self):
self.active_port = int(not self.active_port)
print(f"Activating port {self.active_port}")
self.vna.set_switches(a=self.active_port, b=self.active_port)
def saveas_config(self) -> None:
print("Prompting for save path...")
dialog = QFileDialog(self)
dialog.setNameFilter(f"*{CONFIG_SUFFIX}")
dialog.setDefaultSuffix(CONFIG_SUFFIX)
dialog.setAcceptMode(QFileDialog.AcceptMode.AcceptSave)
if dialog.exec():
config_path = Path(dialog.selectedFiles()[0])
if config_path.suffix != CONFIG_SUFFIX:
raise ValueError(
f"{config_path.name} is not a valid configuration file. Must have extension {CONFIG_SUFFIX}"
)
if config_path == PATH_CONFIG_DEFAULT:
raise ValueError(f"Cannot overwrite default configuration file at {PATH_CONFIG_DEFAULT}")
self.config_path = config_path
print(f"Config path is now {self.config_path.resolve()}")
self.save_config()
def open_config(self) -> None:
print("Prompting for load path...")
dialog = QFileDialog(self)
dialog.setNameFilter(f"*{CONFIG_SUFFIX}")
dialog.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen)
if dialog.exec():
config_path = Path(dialog.selectedFiles()[0])
print(config_path)
if config_path.suffix != CONFIG_SUFFIX:
raise ValueError(
f"{config_path.name} is not a valid configuration file. Must have extension {CONFIG_SUFFIX}"
)
self.config_path = config_path
print(f"Config path is now {self.config_path.resolve()}")
self.load_config(self.config_path)
def save_config(self) -> None:
if self.config_path == PATH_CONFIG_DEFAULT:
self.saveas_config()
else:
print(f"Saving config to {self.config_path.resolve()}")
# TODO: save config
def load_config(self, path: Path) -> None:
print(f"Loading config from {path}...")
# TODO: load config
def progress_callback(self, done: int, total: int):
self.prog_sweep.setMaximum(total)
self.prog_sweep.setValue(done)
def capture(self) -> None:
s = self.vna.vna_capture(self._frequency, self.progress_callback)
if self.vna.calibration is not None:
s_calibrated = self.vna.calibration.apply_cal(s2net(s))
data = net2s(s_calibrated)
else:
data = xr.DataArray(
[[s]],
dims=["m", "n", "frequency"],
coords=dict(
frequency=s.coords["frequency"],
m=[1],
n=[1],
),
)
for plot in self.plots:
plot.update_plot(data)
def set_frequency(self, *, frequency: npt.ArrayLike | None = None):
print(frequency)
if frequency is None:
start, ok = QInputDialog.getDouble(
self, "Start Frequency", "Start Frequency", minValue=30e6, maxValue=6e9, value=1e9
)
stop, ok = QInputDialog.getDouble(
self, "Stop Frequency", "Stop Frequency", minValue=30e6, maxValue=6e9, value=2e9
)
points, ok = QInputDialog.getInt(self, "Points", "Points", minValue=2, value=101)
frequency = np.linspace(start, stop, points)
# Currently does not support zero span
self._frequency = frequency
def calibrate_solt(self):
if len(self.vna.ports) > 1:
raise NotImplementedError
calfile = Path(__file__).parent / "cal.pkl"
if calfile.exists():
# don't re-cal while debugging because that's slooooooow
with open(calfile, "rb") as f:
calibration = pickle.load(f)
else:
s = dict()
for net in ["short", "open", "load"]:
input(f"Connect {net} standard and press ENTER...")
s[net] = self.vna.vna_capture(self._frequency, self.progress_callback)
ideal = rf.media.DefinedGammaZ0(frequency=rf.media.Frequency.from_f(self._frequency, unit="Hz"))
calibration = rf.calibration.OnePort(
[s2net(s["short"]), s2net(s["open"]), s2net(s["load"])],
[ideal.short(), ideal.open(), ideal.load(0)],
)
# TODO: don't use pickles for calibration. They're fragile
with open(calfile, "wb") as f:
pickle.dump(calibration, f)
self.vna.calibration = calibration
def main() -> None:
app = QApplication(sys.argv)
try:
window = MainWindow()
except Exception as e:
if e.args[0] == "No device found":
dialog = QInputDialog()
text, ok = dialog.getText(
None,
"Pluto IP Address",
"Enter Pluto IP Address",
QLineEdit.Normal,
Charon.DEFAULT_IP,
)
match = re.match(r"(\d{1,3}\.){3}\d{1,3}", text)
if not match:
raise ValueError(f"Invalid IP address: {text}")
window = MainWindow(ip=text)
else:
raise e
window.show()
app.exec()
# %%
if __name__ == "__main__":
main()

90
charon_vna/gui_helpers.py Normal file
View File

@ -0,0 +1,90 @@
from PySide6.QtCore import QMargins, QPoint, QRect, QSize, Qt
from PySide6.QtWidgets import QLayout, QSizePolicy
class FlowLayout(QLayout):
def __init__(self, parent=None):
super().__init__(parent)
if parent is not None:
self.setContentsMargins(QMargins(0, 0, 0, 0))
self._item_list = []
def __del__(self):
item = self.takeAt(0)
while item:
item = self.takeAt(0)
def addItem(self, item):
self._item_list.append(item)
def count(self):
return len(self._item_list)
def itemAt(self, index):
if 0 <= index < len(self._item_list):
return self._item_list[index]
return None
def takeAt(self, index):
if 0 <= index < len(self._item_list):
return self._item_list.pop(index)
return None
def expandingDirections(self):
return Qt.Orientation(0)
def hasHeightForWidth(self):
return True
def heightForWidth(self, width):
height = self._do_layout(QRect(0, 0, width, 0), True)
return height
def setGeometry(self, rect):
super(FlowLayout, self).setGeometry(rect)
self._do_layout(rect, False)
def sizeHint(self):
return self.minimumSize()
def minimumSize(self):
size = QSize()
for item in self._item_list:
size = size.expandedTo(item.minimumSize())
size += QSize(2 * self.contentsMargins().top(), 2 * self.contentsMargins().top())
return size
def _do_layout(self, rect, test_only):
x = rect.x()
y = rect.y()
line_height = 0
spacing = self.spacing()
for item in self._item_list:
style = item.widget().style()
layout_spacing_x = style.layoutSpacing(
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Orientation.Horizontal
)
layout_spacing_y = style.layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)
space_x = spacing + layout_spacing_x
space_y = spacing + layout_spacing_y
next_x = x + item.sizeHint().width() + space_x
if next_x - space_x > rect.right() and line_height > 0:
x = rect.x()
y = y + line_height + space_y
next_x = x + item.sizeHint().width() + space_x
line_height = 0
if not test_only:
item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
x = next_x
line_height = max(line_height, item.sizeHint().height())
return y + line_height - rect.y()

View File

@ -1,12 +1,94 @@
import json
import shutil
import zipfile
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory
from typing import List
import skrf as rf import skrf as rf
import xarray as xr
from util import net2s
# scikit-rf has no way to save files aside from touchstone and pickle # scikit-rf has no way to save Calibration objects aside from pickle
def cal2zarr(cal: rf.calibration.Calibration, outpath: Path): def cal2zip(cal: rf.calibration.Calibration, path: Path | str) -> None:
ideals = [net2s(net) for net in cal.ideals] path = Path(path)
measured = [net2s(net) for net in cal.measured] cal_type = cal.__class__
# s.to_zarr(outpath) measured: List[rf.network.Network] = cal.measured
ideals: List[rf.network.Network] = cal.ideals
if cal_type not in [rf.calibration.OnePort]:
raise NotImplementedError(f"Calibration {cal_type.__name__} serialization not implemented")
assert len(ideals) == len(measured) # this should have already been asserted when cal was instantiated
with TemporaryDirectory() as temp:
dir_temp = Path(temp)
with zipfile.ZipFile(dir_temp / "archive.zip", "w") as archive:
# create a configuration file
filename_config = dir_temp / "config.json"
with open(filename_config, "w") as f:
json.dump(
dict(
cal_type=cal_type.__name__,
num_standards=len(measured),
),
f,
)
archive.write(filename_config, str(filename_config.relative_to(dir_temp)))
# add standard data
dir_ideals = dir_temp / "ideals"
dir_ideals.mkdir()
for ii, ideal in enumerate(ideals):
filename = dir_ideals / f"{ii}.s2p"
ideal.write_touchstone(filename)
archive.write(filename, str(filename.relative_to(dir_temp)))
# add test data
dir_measured = dir_temp / "measured"
dir_measured.mkdir()
for ii, meas in enumerate(measured):
filename = dir_measured / f"{ii}.s2p"
meas.write_touchstone(filename)
archive.write(filename, str(filename.relative_to(dir_temp)))
print("Wrote calibration to file")
archive.printdir()
shutil.move(dir_temp / "archive.zip", path)
def zip2cal(path: Path | str):
path = Path(path)
if not path.exists():
raise FileNotFoundError(f"Calibration file {path} does not exist")
with zipfile.ZipFile(path) as archive:
archive.printdir()
config = json.loads(archive.read("config.json"))
print(config)
ideals = list()
measured = list()
with TemporaryDirectory() as temp:
dir_temp = Path(temp)
for ii in range(config["num_standards"]):
with open(dir_temp / f"{ii}.s2p", "wb") as f:
f.write(archive.read(f"ideals/{ii}.s2p"))
ideals.append(rf.network.Network(dir_temp / f"{ii}.s2p"))
with open(dir_temp / f"{ii}.s2p", "wb") as f:
f.write(archive.read(f"measured/{ii}.s2p"))
measured.append(rf.network.Network(dir_temp / f"{ii}.s2p"))
cal_type = config["cal_type"]
CalClass = getattr(rf.calibration, cal_type)
if not issubclass(CalClass, rf.calibration.Calibration):
raise ValueError()
calibration = CalClass(measured=measured, ideals=ideals)
return calibration

129
charon_vna/plots.py Normal file
View File

@ -0,0 +1,129 @@
# %% imports
from typing import Callable, List, Literal, Tuple
import numpy as np
import xarray as xr
from matplotlib import pyplot as plt
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
from matplotlib.lines import Line2D
from matplotlib.ticker import EngFormatter
from numpy import typing as npt
from PySide6.QtWidgets import QVBoxLayout, QWidget
from skrf import plotting as rf_plt
from charon_vna.util import db20, s2vswr
__all__ = ("PlotWidget",)
# %%
class PlotWidget(QWidget):
traces: List[Tuple[int | str]]
lines: List[Line2D]
def __init__(self, type_: str = "logmag"):
super().__init__()
self.traces = [(1, 1)]
layout = QVBoxLayout()
self.setLayout(layout)
self.fig = plt.Figure(figsize=(5, 4), dpi=100, tight_layout=True)
self.ax = self.fig.add_subplot(111)
self.set_plot_type(type_)
self.lines = [
self.ax.plot([np.nan], [np.nan], label="$S_{" + str(m) + str(n) + "}$")[0] for m, n in self.traces
]
self.ax.legend(loc="upper right")
canvas = FigureCanvasQTAgg(self.fig)
layout.addWidget(canvas)
# toolbar = QToolBar("Toolbar")
# toolbar.addAction("blah")
# self.addToolBar(toolbar)
def set_plot_type(
self,
type_: Literal["logmag", "phase", "vswr", "smith"],
sweep_type: Literal["frequency", "time"] = "frequency",
) -> None:
if sweep_type != "frequency":
raise NotImplementedError("Only frequency sweeps are currently supported")
if type_ == "logmag":
self.setup_logmag()
elif type_ == "phase":
self.setup_phase()
elif type_ == "vswr":
self.setup_vswr()
elif type_ == "smith":
self.setup_smith()
else:
raise ValueError(f"Unknown plot type: {type_}")
self._plot_type = type_
def update_plot(self, data: xr.DataArray):
if self._plot_type == "logmag":
self.update_logmag(data)
elif self._plot_type == "phase":
self.update_phase(data)
elif self._plot_type == "vswr":
self.update_vswr(data)
elif self._plot_type == "smith":
self.update_smith(data)
def setup_rect(self) -> None:
self.ax.grid(True)
self.ax.xaxis.set_major_formatter(EngFormatter())
self.ax.set_xlabel("Frequency [Hz]")
def update_rect(self, data: xr.DataArray, func: Callable[[npt.ArrayLike], npt.ArrayLike]) -> None:
self.ax.set_xlim(data["frequency"].min().data, data["frequency"].max().data)
for ii, (m, n) in enumerate(self.traces):
self.lines[ii].set_xdata(data["frequency"])
self.lines[ii].set_ydata(func(data.sel(m=m, n=n)))
self.fig.canvas.draw()
def setup_logmag(self, ylim: List[float] = [-30, 30]) -> None:
self.setup_rect()
self.ax.set_ylim(ylim)
self.ax.set_ylabel("Amplitude [dB]")
def update_logmag(self, data: xr.DataArray) -> None:
self.update_rect(data, db20)
def setup_phase(self) -> None:
self.setup_rect()
self.ax.set_ylim(-200, 200)
self.ax.set_ylabel("Phase [deg]")
def update_phase(self, data: xr.DataArray):
self.update_rect(data, lambda s: np.angle(s, deg=True))
def setup_vswr(self) -> None:
self.setup_rect()
self.ax.set_yticks(np.arange(1, 11))
self.ax.set_ylim(1, 10)
self.ax.set_ylabel("VSWR")
def update_vswr(self, data: xr.DataArray) -> None:
self.update_rect(data, s2vswr)
def setup_smith(self) -> None:
self.ax.grid(False)
self.ax.set_xlim(-1, 1)
self.ax.set_ylim(-1, 1)
self.ax.set_aspect("equal")
rf_plt.smith(ax=self.ax, smithR=1, chart_type="z", draw_vswr=None)
def update_smith(self, data: xr.DataArray) -> None:
for ii, (m, n) in enumerate(self.traces):
sel = data.sel(m=m, n=n)
self.lines[ii].set_xdata(sel.real)
self.lines[ii].set_ydata(sel.imag)
self.fig.canvas.draw()

View File

@ -1,6 +1,7 @@
import numpy as np import numpy as np
import skrf as rf import skrf as rf
import xarray as xr import xarray as xr
from numpy import typing as npt
HAM_BANDS = [ HAM_BANDS = [
[135.7e3, 137.8e3], [135.7e3, 137.8e3],
@ -37,12 +38,16 @@ HAM_BANDS = [
] ]
def db10(p): def db10(p: npt.ArrayLike) -> npt.ArrayLike:
return 10 * np.log10(np.abs(p)) return 10 * np.log10(np.abs(p))
def db20(p): def db20(v: npt.ArrayLike) -> npt.ArrayLike:
return 20 * np.log10(np.abs(p)) return 20 * np.log10(np.abs(v))
def s2vswr(s: npt.ArrayLike) -> npt.ArrayLike:
return np.abs((1 + np.abs(s)) / (1 - np.abs(s)))
def minmax(x): def minmax(x):

View File

@ -1,7 +1,9 @@
# %% imports # %% imports
import copy import copy
import pickle
from enum import IntEnum, unique
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional, Tuple from typing import Any, Callable, Dict, List, Literal, Tuple
import adi import adi
import iio import iio
@ -12,25 +14,93 @@ from matplotlib import pyplot as plt
from matplotlib.ticker import EngFormatter from matplotlib.ticker import EngFormatter
from numpy import typing as npt from numpy import typing as npt
from scipy import signal from scipy import signal
from util import HAM_BANDS, db20, net2s, s2net
from charon_vna.util import HAM_BANDS, db20, net2s, s2net
dir_ = Path(__file__).parent dir_ = Path(__file__).parent
# %% connection # %% connection
def generate_tone(f: float, fs: float, N: int = 1024, scale: int = 2**14):
fs = int(fs)
fc = int(f / (fs / N)) * (fs / N)
ts = 1 / float(fs)
t = np.arange(0, N * ts, ts)
i = np.cos(2 * np.pi * t * fc) * scale
q = np.sin(2 * np.pi * t * fc) * scale
iq = i + 1j * q
return iq
@unique
class AD9361Register(IntEnum):
AUXDAC1_WORD = 0x018
AUXDAC2_WORD = 0x019
AUXDAC1_CONFIG = 0x01A
AUXDAC2_CONFIG = 0x01B
AUXADC_CLOCK_DIVIDER = 0x01C
AUXADC_CONFIG = 0x01D
AUXADC_WORD_MSB = 0x01E
AUXADC_WORD_LSB = 0x01F
AUTO_GPIO = 0x020
AGC_GAIN_LOCK_DELAY = 0x021
AGC_ATTACK_DELAY = 0x022
AUXDAC_ENABLE_CONTROL = 0x023
RX_LOAD_SYNTH_DELAY = 0x024
TX_LOAD_SYNTH_DELAY = 0x025
EXTERNAL_LNA_CONTROL = 0x026
GPO_FORCE_AND_INIT = 0x027
GPO0_RX_DELAY = 0x028
GPO1_RX_DELAY = 0x029
GPO2_RX_DELAY = 0x02A
GPO3_RX_DELAY = 0x02B
GPO0_TX_DELAY = 0x02C
GPO1_TX_DELAY = 0x02D
GPO2_TX_DELAY = 0x02E
GPO3_TX_DELAY = 0x02F
AUXDAC1_RX_DELAY = 0x030
AUXDAC1_TX_DELAY = 0x031
AUXDAC2_RX_DELAY = 0x032
AUXDAC2_TX_DELAY = 0x033
@unique
class AD9361DacVref(IntEnum):
VREF_1V0 = 0b00
VREF_1V5 = 0b01
VREF_2V0 = 0b10
VREF_2V5 = 0b11
@unique
class AD9361DacStepFactor(IntEnum):
FACTOR_2 = 0b0
FACTOR_1 = 0b1
class Charon: class Charon:
FREQUENCY_OFFSET = 1e6 FREQUENCY_OFFSET = 1e6
DEFAULT_IP = "192.168.3.1"
calibration: rf.calibration.Calibration | None = None
def __init__( def __init__(
self, self,
uri: str, ip: str = DEFAULT_IP,
frequency: npt.ArrayLike = np.linspace(1e9, 2e9, 3), frequency: npt.ArrayLike = np.linspace(1e9, 2e9, 3),
ports: Tuple[int] = (1,), ports: Tuple[int] | int = 1,
): ):
if isinstance(ports, int):
ports = (np.arange(ports) + 1).tolist()
ports = tuple(ports)
self.ports = ports self.ports = ports
self.frequency = frequency self.frequency = frequency
# everything RF # everything RF
uri = f"ip:{ip}"
self.sdr = adi.ad9361(uri=uri) self.sdr = adi.ad9361(uri=uri)
for attr, expected in [ for attr, expected in [
("adi,2rx-2tx-mode-enable", True), ("adi,2rx-2tx-mode-enable", True),
@ -58,8 +128,8 @@ class Charon:
self.sdr.loopback = 0 self.sdr.loopback = 0
self.sdr.gain_control_mode_chan0 = "manual" self.sdr.gain_control_mode_chan0 = "manual"
self.sdr.gain_control_mode_chan1 = "manual" self.sdr.gain_control_mode_chan1 = "manual"
self.sdr.rx_hardwaregain_chan0 = 40 self.sdr.rx_hardwaregain_chan0 = 10
self.sdr.rx_hardwaregain_chan1 = 40 self.sdr.rx_hardwaregain_chan1 = 10
self.sdr.tx_hardwaregain_chan0 = -10 self.sdr.tx_hardwaregain_chan0 = -10
# switch control # switch control
@ -67,10 +137,17 @@ class Charon:
self.ctrl = ctx.find_device("ad9361-phy") self.ctrl = ctx.find_device("ad9361-phy")
# raw ad9361 register accesss: # raw ad9361 register accesss:
# https://ez.analog.com/linux-software-drivers/f/q-a/120853/control-fmcomms3-s-gpo-with-python # https://ez.analog.com/linux-software-drivers/f/q-a/120853/control-fmcomms3-s-gpo-with-python
# https://www.analog.com/media/cn/technical-documentation/user-guides/ad9364_register_map_reference_manual_ug-672.pdf # https://www.analog.com/media/cn/technical-documentation/user-guides/ad9364_register_map_reference_manual_ug-672.pdf # noqa: E501
self.ctrl.reg_write(0x26, 0x90) # bit 7: AuxDAC Manual, bit 4: GPO Manual self.ctrl.reg_write(AD9361Register.EXTERNAL_LNA_CONTROL, 0x90) # bit 7: AuxDAC Manual, bit 4: GPO Manual
self._set_gpo(self.ports[0] - 1) self.ctrl.reg_write(AD9361Register.AUXDAC_ENABLE_CONTROL, 0x3F)
# TODO: init AuxDAC
# initialize switch control outputs
self._set_gpo(0b0000)
self._set_dac_code(value=0, channel=1)
self._set_dac_code(value=0, channel=2)
# set default switch state
self.set_switches(a=self.ports[0] - 1, b=self.ports[0] - 1)
def get_config(self) -> Dict[str, Any]: def get_config(self) -> Dict[str, Any]:
config = dict() config = dict()
@ -94,33 +171,83 @@ class Charon:
return config return config
def _get_gpo(self) -> int:
return (self.ctrl.reg_read(AD9361Register.GPO_FORCE_AND_INIT) >> 4) & 0x0F
def _set_gpo(self, value: int) -> None: def _set_gpo(self, value: int) -> None:
self.ctrl.reg_write(0x27, (value & 0x0F) << 4) # bits 7-4: GPO3-0 self.ctrl.reg_write(AD9361Register.GPO_FORCE_AND_INIT, (value & 0x0F) << 4) # bits 7-4: GPO3-0
def _get_dac_code(self, channel: Literal[1, 2]) -> Tuple[float, AD9361DacVref, AD9361DacStepFactor]:
word = self.ctrl.reg_read(AD9361Register.__getitem__(f"AUXDAC{channel}_WORD"))
config = self.ctrl.reg_read(AD9361Register.__getitem__(f"AUXDAC{channel}_CONFIG"))
value = (word << 2) + (config & 0x3)
vref = AD9361DacVref((config >> 2) & 0x3)
step_factor = AD9361DacStepFactor((config >> 4) & 0x1)
return (value, vref, step_factor)
def _set_dac_code(
self,
value: int,
channel: Literal[1, 2],
vref: AD9361DacVref = AD9361DacVref.VREF_1V0,
step_factor: AD9361DacStepFactor = AD9361DacStepFactor.FACTOR_2,
) -> None:
if channel not in [1, 2]:
raise ValueError(f"Invalid channel {channel}. Must be 1 or 2")
if value > 0x3FF or value < 0:
raise ValueError("Invalid value for 10 bit DAC. Must be between 0 and 0x3FF (inclusive)")
vref = AD9361DacVref(vref)
step_factor = AD9361DacStepFactor(step_factor)
# https://www.analog.com/media/cn/technical-documentation/user-guides/ad9364_register_map_reference_manual_ug-672.pdf
# page 13
# vout = 0.97 * vref + (0.000738 + 9e-6 * (vref * 1.6 - 2)) * auxdac_word[9:0] * step_factor - 0.3572 * step_factor + 0.05
# vout ~= (vref - 0.3572 * step_factor) + 0.000738 * auxdac_word[9:0] * step_factor
# which gives a 1.5V swing with step_factor == 2 and 0.75V swing with step_factor == 1
# vref basically just changes the minimum voltage with negligible impact on output scaling
self.ctrl.reg_write(
AD9361Register.__getitem__(f"AUXDAC{channel}_WORD"),
(value >> 2) & 0xFF,
)
self.ctrl.reg_write(
AD9361Register.__getitem__(f"AUXDAC{channel}_CONFIG"),
(value & 0x3) | (vref.value << 2) | (step_factor << 4),
)
def set_switches(self, b: int, a: int, excitation: int | None = None):
if excitation is None:
excitation = a
val = 0
val |= int(bool(excitation)) << 0 # exc = GPO0
val |= int(bool(a)) << 2 # a = GPO2
val |= int(bool(b)) << 1 # b = GPO1
self._set_gpo(val)
def set_output_power(self, power: float): def set_output_power(self, power: float):
if power == -5: pout = xr.DataArray(
# FIXME: this is a hack because I don't want to go through re-calibration [-15, -10, -5, 0, 5],
tx_gain = -8 dims=["tx_gain"],
else: coords=dict(
raise NotImplementedError() # TODO: correct over frequency
# # TODO: correct over frequency frequency=1e9, # FIXME: I'm not sure at what frequency I generated this table
# tx_gain_idx = np.abs(pout.sel(tx_channel=0) - power).argmin(dim="tx_gain") tx_channel=0,
# tx_gain = pout.coords["tx_gain"][tx_gain_idx] tx_gain=[-22, -17, -12, -7, -1],
),
)
tx_gain_idx = np.abs(pout - power).argmin(dim="tx_gain")
tx_gain = pout.coords["tx_gain"][tx_gain_idx]
self.sdr.tx_hardwaregain_chan0 = float(tx_gain) self.sdr.tx_hardwaregain_chan0 = float(tx_gain)
def generate_tone(self, f: float, N: int = 1024, fs: Optional[float] = None):
if fs is None:
fs = self.sdr.sample_rate
fs = int(fs)
fc = int(f / (fs / N)) * (fs / N)
ts = 1 / float(fs)
t = np.arange(0, N * ts, ts)
i = np.cos(2 * np.pi * t * fc) * 2**14
q = np.sin(2 * np.pi * t * fc) * 2**14
iq = i + 1j * q
return iq
def set_output(self, frequency: float, power: float): def set_output(self, frequency: float, power: float):
# TODO: switch to DDS in Pluto # TODO: switch to DDS in Pluto
@ -128,185 +255,286 @@ class Charon:
self.set_output_power(power) self.set_output_power(power)
self.sdr.tx_lo = int(frequency - self.FREQUENCY_OFFSET) self.sdr.tx_lo = int(frequency - self.FREQUENCY_OFFSET)
self.sdr.tx_cyclic_buffer = True self.sdr.tx_cyclic_buffer = True
self.sdr.tx(self.generate_tone(self.FREQUENCY_OFFSET)) # For some reason the pluto's DDS has truly horrendous phase noise to the point where it looks modulated
self.sdr.tx(generate_tone(f=self.FREQUENCY_OFFSET, fs=self.sdr.sample_rate))
# self.sdr.dds_single_tone(self.FREQUENCY_OFFSET, scale=0.9, channel=0)
def _rx(self, count: int = 1) -> npt.ArrayLike: def _rx(self, count: int = 1, fc: float | None = None) -> npt.ArrayLike:
if count < 1: if count < 1:
raise ValueError raise ValueError
self.sdr.rx_destroy_buffer() self.sdr.rx_destroy_buffer()
if fc is not None:
self.sdr.rx_lo = int(fc)
self.sdr.rx_enabled_channels = [0, 1]
self.sdr.gain_control_mode_chan0 = "manual"
self.sdr.gain_control_mode_chan1 = "manual"
self.sdr.rx_hardwaregain_chan0 = 30
self.sdr.rx_hardwaregain_chan1 = 30
return np.concatenate([np.array(self.sdr.rx()) for _ in range(count)], axis=-1) return np.concatenate([np.array(self.sdr.rx()) for _ in range(count)], axis=-1)
def get_b_over_a(self, frequency: float):
self.set_output(frequency=frequency, power=-5)
# %% data = self._rx(1, fc=frequency - self.FREQUENCY_OFFSET)
sdr = Charon("ip:192.168.3.1") ddc_tone = generate_tone(f=-self.FREQUENCY_OFFSET, fs=self.sdr.sample_rate, scale=1)
ddc_data = data * ddc_tone
# %% initialization ddc_rel = ddc_data[1] / ddc_data[0]
config = sdr.get_config()
config
# %% generate tone # plt.figure()
sdr.set_output(frequency=1e9 + sdr.FREQUENCY_OFFSET, power=-5) # plt.plot(
# np.fft.fftshift(np.fft.fftfreq(ddc_data.shape[-1], 1 / self.sdr.sample_rate)),
# np.abs(np.fft.fftshift(np.fft.fft(ddc_data, axis=-1))).T,
# )
# plt.show()
# %% capture data # TODO: calculate sos only once
data = sdr._rx(1) n, wn = signal.buttord(
# %%
fig, axs = plt.subplots(2, 2, sharex=True, tight_layout=True)
# ddc_tone = np.exp(
# -1j * 2 * np.pi * (sdr.FREQUENCY_OFFSET / sdr.sdr.sample_rate) * np.arange(data.shape[-1]), dtype=np.complex128
# )
ddc_tone = sdr.generate_tone(-sdr.FREQUENCY_OFFSET) * 2**-14
ddc_data = data * ddc_tone
axs[0][0].plot(np.real(ddc_data).T, label="DDC")
axs[1][0].plot(np.imag(ddc_data).T, label="DDC")
ddc_rel = ddc_data[1] / ddc_data[0]
axs[0][1].plot(np.real(ddc_rel).T, label="DDC")
axs[1][1].plot(np.imag(ddc_rel).T, label="DDC")
n, wn = signal.buttord(
wp=0.3 * sdr.FREQUENCY_OFFSET, wp=0.3 * sdr.FREQUENCY_OFFSET,
ws=0.8 * sdr.FREQUENCY_OFFSET, ws=0.8 * sdr.FREQUENCY_OFFSET,
gpass=1, gpass=1,
gstop=40, gstop=40,
analog=False, analog=False,
fs=sdr.sdr.sample_rate, fs=self.sdr.sample_rate,
)
sos = signal.butter(n, wn, "lowpass", analog=False, output="sos", fs=sdr.sdr.sample_rate)
filt_data = signal.sosfiltfilt(sos, ddc_data, axis=-1)
axs[0][0].plot(np.real(filt_data).T, label="FILT")
axs[1][0].plot(np.imag(filt_data).T, label="FILT")
filt_rel = filt_data[1] / filt_data[0]
axs[0][1].plot(np.real(filt_rel).T, label="FILT")
axs[1][1].plot(np.imag(filt_rel).T, label="FILT")
s = np.mean(filt_rel)
axs[0][1].axhline(np.real(s), color="k")
axs[1][1].axhline(np.imag(s), color="k")
axs[0][0].grid(True)
axs[1][0].grid(True)
axs[0][1].grid(True)
axs[1][1].grid(True)
axs[0][0].legend(loc="lower right")
axs[1][0].legend(loc="lower right")
axs[0][1].legend(loc="lower right")
axs[1][1].legend(loc="lower right")
axs[0][0].set_ylabel("Real")
axs[1][0].set_ylabel("Imag")
axs[0][0].set_title("By Channel")
axs[0][1].set_title("Relative")
# %% Plot in time
fig, axs = plt.subplots(2, 1, sharex=True, tight_layout=True)
axs[0].plot(np.real(data).T)
axs[1].plot(np.imag(data).T)
axs[0].set_ylabel("Real")
axs[1].set_ylabel("Imag")
axs[0].grid(True)
axs[1].grid(True)
axs[-1].set_xlabel("Sample")
axs[-1].set_xlim(0, data.shape[-1])
fig.show()
# %%
fig, ax = plt.subplots(1, 1, tight_layout=True)
ax.plot(np.real(data).T, np.imag(data).T)
ax.grid(True)
ax.set_aspect("equal")
ax.set_xlabel("Real")
ax.set_ylabel("Imag")
ax.set_xlim(np.array([-1, 1]) * (2 ** (12 - 1) - 1))
ax.set_ylim(ax.get_xlim())
fig.show()
# %% Plot in frequency
f = np.fft.fftfreq(data.shape[-1], 1 / sdr.sdr.sample_rate)
RX_BITS = 10
Pxx_den = np.fft.fft(data, axis=-1) / (len(data) * 2 ** (2 * RX_BITS))
Pxx_den_ddc = np.fft.fft(ddc_data, axis=-1) / (len(ddc_data) * 2 ** (2 * RX_BITS))
Pxx_den_filt = np.fft.fft(filt_data, axis=-1) / (len(filt_data) * 2 ** (2 * RX_BITS))
fft_ddc_tone = np.fft.fft(ddc_tone, axis=-1) / (len(ddc_tone))
plt.figure()
for cc, chan in enumerate(sdr.sdr.rx_enabled_channels):
# plt.plot(
# np.fft.fftshift(f),
# db20(np.fft.fftshift(Pxx_den[cc])),
# label=f"Channel {chan}",
# )
plt.plot(
np.fft.fftshift(f),
db20(np.fft.fftshift(Pxx_den_ddc[cc])),
label=f"Channel {chan}",
) )
plt.plot( sos = signal.butter(n, wn, "lowpass", analog=False, output="sos", fs=self.sdr.sample_rate)
np.fft.fftshift(f), # TODO: figure out why filt sucks. Introduces SO much phase noise (out to several MHz)
db20(np.fft.fftshift(Pxx_den_filt[cc])), filt_data = signal.sosfiltfilt(sos, ddc_data, axis=-1)
label=f"Channel {chan}",
)
# plt.plot(
# np.fft.fftshift(f),
# db20(np.fft.fftshift(fft_ddc_tone)),
# label="DDC Tone",
# )
plt.legend()
# plt.ylim(1e-7, 1e2)
plt.ylim(-100, 0)
plt.xlabel("Frequency [Hz]")
plt.ylabel("Power [dBfs]")
plt.title(f"Fc = {sdr.sdr.rx_lo / 1e9} GHz")
plt.gca().xaxis.set_major_formatter(EngFormatter())
plt.grid(True)
plt.show()
filt_rel = filt_data[1] / filt_data[0]
return np.mean(data[1] / data[0])
def capture(
self,
callback: Callable[int, int] | None = None,
*,
measurements: List[Tuple[int, int]] = None,
):
if measurements is None:
measurements = [(m, n) for n in self.ports for m in self.ports]
measurements = list(measurements)
# %%
def vna_capture(frequency: npt.ArrayLike):
s = xr.DataArray( s = xr.DataArray(
np.empty(len(frequency), dtype=np.complex128), np.zeros(
dims=["frequency"], [len(self.frequency), len(self.ports), len(self.ports)],
dtype=np.complex128,
),
dims=["frequency", "m", "n"],
coords=dict( coords=dict(
frequency=frequency, frequency=self.frequency,
m=list(self.ports),
n=list(self.ports),
), ),
) )
for freq in s.frequency.data:
set_output(frequency=freq, power=-5) total_count = len(measurements) * len(s.frequency)
sdr.rx_destroy_buffer() count = 0
sdr.rx_lo = int(freq)
sdr.rx_enabled_channels = [0, 1] for m in s.m.data:
sdr.gain_control_mode_chan0 = "manual" for n in s.n.data:
sdr.gain_control_mode_chan1 = "manual" if (m, n) in measurements:
sdr.rx_hardwaregain_chan0 = 40 self.set_switches(b=m - 1, a=n - 1)
sdr.rx_hardwaregain_chan1 = 40
rx = sdr.rx() for ff, freq in enumerate(s.frequency.data):
s.loc[dict(frequency=freq)] = np.mean(rx[1] / rx[0]) if callback is not None:
# report progress during sweep
callback(count, total_count)
self.set_output(frequency=freq, power=-5)
self.sdr.rx_destroy_buffer()
self.sdr.rx_lo = int(freq)
self.sdr.rx_enabled_channels = [0, 1]
self.sdr.gain_control_mode_chan0 = "manual"
self.sdr.gain_control_mode_chan1 = "manual"
self.sdr.rx_hardwaregain_chan0 = 40
self.sdr.rx_hardwaregain_chan1 = 40
rx = self.sdr.rx()
s.loc[dict(frequency=freq, m=m, n=n)] = np.mean(rx[1] / rx[0])
count += 1
if callback is not None:
# mark capture as complete
callback(total_count, total_count)
return s return s
def calibrate_sol(self, prompt: Callable[[str], None] | None = None, **kwargs) -> None:
if len(self.ports) != 1:
raise ValueError(
f"SOL calibration needs only one port but {len(self.ports)} ports are enabled. "
"Did you mean to use SOLT?"
)
if prompt is None:
prompt = lambda s: input(f"{s}\nENTER to continue...")
ideal = rf.media.DefinedGammaZ0(frequency=rf.media.Frequency.from_f(self.frequency, unit="Hz"))
ideals = [ideal.short(), ideal.open(), ideal.load(0)]
names = ["short", "open", "load"]
measured = list()
for name in names:
prompt(f"Connect standard {name} to port {self.ports[0]}")
measured.append(self.capture(**kwargs))
cal = rf.OnePort(measured=[s2net(m) for m in measured], ideals=ideals)
self.calibration = cal
def calibrate_solt(self, prompt: Callable[[str], None] | None = None, **kwargs) -> None:
if len(self.ports) < 2:
raise ValueError(
f"SOLT calibration needs at least two ports but {len(self.ports)} ports are enabled. "
"Did you mean to use SOL?"
)
if len(self.ports) > 2:
raise NotImplementedError("SOLT calibration with more than two ports not yet supported")
if prompt is None:
prompt = lambda s: input(f"{s}\nENTER to continue...")
ideal = rf.media.DefinedGammaZ0(frequency=rf.media.Frequency.from_f(self.frequency, unit="Hz"))
ideals = [ideal.short(), ideal.open(), ideal.load(0)]
ideals = [rf.two_port_reflect(id, id) for id in ideals]
thru = np.zeros((len(self.frequency), 2, 2), dtype=np.complex128)
thru[:, 0, 1] = 1
thru[:, 1, 0] = 1
thru = rf.Network(frequency=self.frequency, f_unit="Hz", s=thru)
ideals.append(thru)
names_1p = ["short", "open", "load"]
names_2p = ["thru"]
measured = list()
for name in names_1p:
measured_param = list()
for port in self.ports:
prompt(f"Connect standard {name} to port {port}")
measured_param.append(self.capture(measurements=[(port, port)], **kwargs).sel(m=port, n=port))
measured.append(rf.two_port_reflect(*[s2net(m) for m in measured_param]))
for name in names_2p:
prompt(f"Connect standard {name} between ports {self.ports[0]} and {self.ports[1]}")
measured.append(s2net(self.capture(**kwargs)))
cal = rf.SOLT(measured=measured, ideals=ideals)
self.calibration = cal
def save_calibration(self, path: Path | str):
path = Path(path)
if path.suffix.lower() == ".pkl":
with open(str(path), "wb") as f:
pickle.dump(self.calibration, f)
else:
raise NotImplementedError(f"Unknown calibration file extension: {path.suffix}")
def load_calibration(self, path: Path | str):
path = Path(path)
if path.suffix.lower() == ".pkl":
with open(str(path), "rb") as f:
cal = pickle.load(f)
if not isinstance(cal, rf.calibration.Calibration):
raise ValueError(f"Expected {rf.calibration.Calibration}, got {type(cal)}")
self.calibration = cal
else:
raise NotImplementedError(f"Unknown calibration file extension: {path.suffix}")
# %% # %%
s = vna_capture(frequency=np.linspace(70e6, 200e6, 101)) if __name__ == "__main__":
pass
# %% Plot Logmag # %%
fig, axs = plt.subplots(2, 1, sharex=True, tight_layout=True) sdr = Charon("ip:192.168.3.1", frequency=np.linspace(1e9, 1.1e9, 11))
axs[0].plot(s.frequency, db20(s), label="Measured") # %% initialization
axs[1].plot(s.frequency, np.rad2deg(np.angle((s))), label="Measured") config = sdr.get_config()
# print(sdr.ctrl.debug_attrs["adi,rx-rf-port-input-select"].value)
# print(sdr.ctrl.debug_attrs["adi,tx-rf-port-input-select"].value)
config
axs[0].grid(True) # %% generate tone
axs[1].grid(True) fc = 1e9
sdr.set_output(frequency=fc + sdr.FREQUENCY_OFFSET, power=-5)
axs[0].set_ylim(-80, 0) # %% capture data
axs[1].set_ylim(-200, 200) data = sdr._rx(1, fc=fc)
axs[1].set_xlim(np.min(s.frequency), np.max(s.frequency))
axs[1].xaxis.set_major_formatter(EngFormatter(places=1))
axs[1].set_xlabel("Frequency")
axs[0].set_ylabel("|S11| [dB]") # %% Plot in time
axs[1].set_ylabel("∠S11 [deg]") fig, axs = plt.subplots(2, 1, sharex=True, tight_layout=True)
axs[0].plot(np.real(data).T)
axs[1].plot(np.imag(data).T)
axs[0].set_ylabel("Real")
axs[1].set_ylabel("Imag")
axs[0].grid(True)
axs[1].grid(True)
axs[-1].set_xlabel("Sample")
axs[-1].set_xlim(0, data.shape[-1])
fig.show()
reference_sparams = None # %%
reference_sparams = dir_ / "RBP-135+_Plus25degC.s2p" fig, ax = plt.subplots(1, 1, tight_layout=True)
if reference_sparams is not None: ax.plot(np.real(data).T, np.imag(data).T)
ax.grid(True)
ax.set_aspect("equal")
ax.set_xlabel("Real")
ax.set_ylabel("Imag")
ax.set_xlim(np.array([-1, 1]) * (2 ** (12 - 1) - 1))
ax.set_ylim(ax.get_xlim())
fig.show()
# %% Plot in frequency
f = np.fft.fftfreq(data.shape[-1], 1 / sdr.sdr.sample_rate)
RX_BITS = 12 # for each of i, q (including sign bit)
fft_data = np.fft.fft(data, axis=-1, norm="forward") / (2 ** (RX_BITS - 1))
plt.figure()
for cc, chan in enumerate(sdr.sdr.rx_enabled_channels):
plt.plot(
np.fft.fftshift(f),
db20(np.fft.fftshift(fft_data[cc])),
label=f"Channel {chan}",
)
plt.legend()
plt.ylim(-100, 0)
plt.xlabel("Frequency [Hz]")
plt.ylabel("Power [dBfs]")
plt.title(f"Fc = {sdr.sdr.rx_lo / 1e9} GHz")
plt.gca().xaxis.set_major_formatter(EngFormatter())
plt.grid(True)
plt.show()
# %%
s = sdr.vna_capture(frequency=np.linspace(70e6, 200e6, 101))
# %% Plot Logmag
fig, axs = plt.subplots(2, 1, sharex=True, tight_layout=True)
axs[0].plot(s.frequency, db20(s), label="Measured")
axs[1].plot(s.frequency, np.rad2deg(np.angle((s))), label="Measured")
axs[0].grid(True)
axs[1].grid(True)
axs[0].set_ylim(-80, 0)
axs[1].set_ylim(-200, 200)
axs[1].set_xlim(np.min(s.frequency), np.max(s.frequency))
axs[1].xaxis.set_major_formatter(EngFormatter(places=1))
axs[1].set_xlabel("Frequency")
axs[0].set_ylabel("|S11| [dB]")
axs[1].set_ylabel("∠S11 [deg]")
reference_sparams = None
reference_sparams = dir_ / "RBP-135+_Plus25degC.s2p"
if reference_sparams is not None:
ref = rf.Network(reference_sparams) ref = rf.Network(reference_sparams)
rbp135 = net2s(ref) rbp135 = net2s(ref)
@ -315,67 +543,65 @@ if reference_sparams is not None:
axs[0].legend() axs[0].legend()
axs[1].legend() axs[1].legend()
plt.show() plt.show()
# %% SOL calibration
cal_frequency = np.linspace(70e6, 600e6, 101)
ideal_cal_frequency = rf.Frequency(np.min(cal_frequency), np.max(cal_frequency), len(cal_frequency))
input("Connect SHORT and press ENTER...")
short = sdr.vna_capture(frequency=cal_frequency)
input("Connect OPEN and press ENTER...")
open = sdr.vna_capture(frequency=cal_frequency)
input("Connect LOAD and press ENTER...")
load = sdr.vna_capture(frequency=cal_frequency)
# %% SOL calibration short_net = s2net(short)
cal_frequency = np.linspace(70e6, 600e6, 101) open_net = s2net(open)
ideal_cal_frequency = rf.Frequency(np.min(cal_frequency), np.max(cal_frequency), len(cal_frequency)) load_net = s2net(load)
input("Connect SHORT and press ENTER...")
short = vna_capture(frequency=cal_frequency)
input("Connect OPEN and press ENTER...")
open = vna_capture(frequency=cal_frequency)
input("Connect LOAD and press ENTER...")
load = vna_capture(frequency=cal_frequency)
short_net = s2net(short) cal_ideal = rf.media.DefinedGammaZ0(frequency=ideal_cal_frequency)
open_net = s2net(open) calibration = rf.calibration.OnePort(
load_net = s2net(load)
cal_ideal = rf.media.DefinedGammaZ0(frequency=ideal_cal_frequency)
calibration = rf.calibration.OnePort(
[short_net, open_net, load_net], [short_net, open_net, load_net],
[cal_ideal.short(), cal_ideal.open(), cal_ideal.load(0)], [cal_ideal.short(), cal_ideal.open(), cal_ideal.load(0)],
) )
# %%
s = sdr.vna_capture(frequency=cal_frequency)
# %% # %%
s = vna_capture(frequency=cal_frequency) s_calibrated = calibration.apply_cal(s2net(s))
# %% plt.figure()
s_calibrated = calibration.apply_cal(s2net(s)) s_calibrated.plot_s_smith()
# ref.plot_s_smith(m=1, n=1)
plt.show()
plt.figure() plt.figure()
s_calibrated.plot_s_smith() for start, stop in HAM_BANDS:
# ref.plot_s_smith(m=1, n=1)
plt.show()
plt.figure()
for start, stop in HAM_BANDS:
plt.axvspan(start, stop, alpha=0.1, color="k") plt.axvspan(start, stop, alpha=0.1, color="k")
s_calibrated.plot_s_db() s_calibrated.plot_s_db()
# ref.plot_s_db(m=1, n=1) # ref.plot_s_db(m=1, n=1)
plt.gca().xaxis.set_major_formatter(EngFormatter()) plt.gca().xaxis.set_major_formatter(EngFormatter())
plt.grid(True) plt.grid(True)
plt.xlim(s_calibrated.f[0], s_calibrated.f[-1]) plt.xlim(s_calibrated.f[0], s_calibrated.f[-1])
plt.show() plt.show()
plt.figure() plt.figure()
for start, stop in HAM_BANDS: for start, stop in HAM_BANDS:
plt.axvspan(start, stop, alpha=0.1, color="k") plt.axvspan(start, stop, alpha=0.1, color="k")
# s_calibrated.plot_s_vswr() # s_calibrated.plot_s_vswr()
# drop invalid points # drop invalid points
vswr = copy.deepcopy(s_calibrated.s_vswr[:, 0, 0]) vswr = copy.deepcopy(s_calibrated.s_vswr[:, 0, 0])
vswr[vswr < 1] = np.nan vswr[vswr < 1] = np.nan
plt.plot(s_calibrated.f, vswr) plt.plot(s_calibrated.f, vswr)
plt.axhline(1, color="k", linestyle="--") plt.axhline(1, color="k", linestyle="--")
plt.ylabel("VSWR") plt.ylabel("VSWR")
plt.xlabel("Frequency [Hz]") plt.xlabel("Frequency [Hz]")
# ref.plot_s_vswr(m=1, n=1) # ref.plot_s_vswr(m=1, n=1)
plt.gca().xaxis.set_major_formatter(EngFormatter()) plt.gca().xaxis.set_major_formatter(EngFormatter())
plt.grid(True) plt.grid(True)
plt.ylim(0, 10) plt.ylim(0, 10)
plt.xlim(s_calibrated.f[0], s_calibrated.f[-1]) plt.xlim(s_calibrated.f[0], s_calibrated.f[-1])
plt.show() plt.show()
# %% # %%

71
charon_vna/vna_dev.py Normal file
View File

@ -0,0 +1,71 @@
# %% imports
import numpy as np
from matplotlib import pyplot as plt
from charon_vna.util import db20, net2s, s2net
from charon_vna.vna import Charon
# %%
frequency = np.linspace(80e6, 280e6, 301)
# %%
vna = Charon(frequency=frequency, ports=2)
# %%
s = vna.capture()
# %%
for m in s.m.data:
for n in s.n.data:
plt.plot(s.frequency, db20(s.sel(m=m, n=n)), label="$S_{" + str(m) + str(n) + "}$")
plt.grid(True)
plt.legend()
plt.show()
# %%
vna.calibrate_sol()
# %%
vna.calibrate_solt()
# %%
vna.save_calibration("./calibration.pkl")
# %%
vna.load_calibration("./calibration.pkl")
# %%
s2 = net2s(vna.calibration.apply_cal(s2net(s)))
# s2.coords["m"] = s.m
# s2.coords["n"] = s.n
for m in s.m.data:
for n in s.n.data:
plt.plot(s.frequency, db20(s.sel(m=m, n=n)), label="$S_{" + str(m) + str(n) + "}$ (uncalibrated)")
plt.plot(s2.frequency, db20(s2.sel(m=m, n=n)), label="$S_{" + str(m) + str(n) + "}$ (calibrated)")
plt.grid(True)
plt.legend()
plt.ylabel("Magnitude [dB]")
# plt.ylim(-30, 5)
plt.show()
for m in s.m.data:
for n in s.n.data:
if m != n:
plt.plot(
s.frequency,
np.angle(s.sel(m=m, n=n), deg=True),
label="$S_{" + str(m) + str(n) + "}$ (uncalibrated)",
)
plt.plot(
s2.frequency,
np.angle(s2.sel(m=m, n=n), deg=True),
label="$S_{" + str(m) + str(n) + "}$ (calibrated)",
)
plt.grid(True)
plt.legend()
plt.ylabel("Phase [deg]")
plt.show()
# %%

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 KiB

View File

@ -21,14 +21,17 @@ dependencies = [
"pyadi-iio", "pyadi-iio",
"click", "click",
"matplotlib", "matplotlib",
"pyside6",
] ]
dynamic = ["version"] dynamic = ["version"]
[project.urls] [project.urls]
homepage = "https://git.brendanhaines.com/brendanhaines/charon" homepage = "https://pypi.org/project/charon-vna/"
repository = "https://git.brendanhaines.com/brendanhaines/charon_vna"
[project.scripts] [project.scripts]
# charon = "charon.charon:main" charon-cli = "charon_vna.cli:main"
charon-gui = "charon_vna.gui:main"
[tool.setuptools_scm] [tool.setuptools_scm]
version_file = "charon_vna/_version.py" version_file = "charon_vna/_version.py"