SCPI-Instrument-Control 1.0.0__tar.gz → 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/CHANGELOG.md +9 -0
- {scpi_instrument_control-1.0.0/SCPI_Instrument_Control.egg-info → scpi_instrument_control-1.0.1}/PKG-INFO +1 -1
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1/SCPI_Instrument_Control.egg-info}/PKG-INFO +1 -1
- scpi_instrument_control-1.0.1/SCPI_Instrument_Control.egg-info/SOURCES.txt +142 -0
- scpi_instrument_control-1.0.1/SCPI_Instrument_Control.egg-info/entry_points.txt +3 -0
- scpi_instrument_control-1.0.1/SCPI_Instrument_Control.egg-info/top_level.txt +2 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/pyproject.toml +6 -5
- scpi_instrument_control-1.0.1/scpi_control/__init__.py +49 -0
- scpi_instrument_control-1.0.1/scpi_control/analysis.py +401 -0
- scpi_instrument_control-1.0.1/scpi_control/automation.py +557 -0
- scpi_instrument_control-1.0.1/scpi_control/awg_models.py +277 -0
- scpi_instrument_control-1.0.1/scpi_control/awg_output.py +503 -0
- scpi_instrument_control-1.0.1/scpi_control/awg_scpi_commands.py +183 -0
- scpi_instrument_control-1.0.1/scpi_control/channel.py +269 -0
- scpi_instrument_control-1.0.1/scpi_control/connection/__init__.py +40 -0
- scpi_instrument_control-1.0.1/scpi_control/connection/base.py +115 -0
- scpi_instrument_control-1.0.1/scpi_control/connection/mock.py +613 -0
- scpi_instrument_control-1.0.1/scpi_control/connection/socket.py +211 -0
- scpi_instrument_control-1.0.1/scpi_control/connection/visa_connection.py +395 -0
- scpi_instrument_control-1.0.1/scpi_control/exceptions.py +37 -0
- scpi_instrument_control-1.0.1/scpi_control/function_generator.py +381 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/__init__.py +3 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/app.py +118 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/connection_manager.py +204 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/live_view_worker.py +179 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/main_window.py +1605 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/utils/validators.py +177 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/vnc_window.py +251 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/waveform_capture_worker.py +132 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/__init__.py +21 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/channel_control.py +387 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/cursor_panel.py +319 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/error_dialog.py +222 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/fft_display.py +353 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/math_panel.py +327 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/measurement_marker.py +460 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/measurement_markers/__init__.py +32 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/measurement_markers/frequency_marker.py +251 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/measurement_markers/timing_marker.py +411 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/measurement_markers/voltage_marker.py +271 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/measurement_panel.py +320 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/protocol_decode_panel.py +417 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/psu_control.py +323 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/reference_panel.py +345 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/scope_web_view.py +160 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/terminal_widget.py +311 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/timebase_control.py +294 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/trigger_control.py +297 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/vector_graphics_panel.py +530 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/visual_measurement_panel.py +617 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/waveform_display.py +1120 -0
- scpi_instrument_control-1.0.1/scpi_control/gui/widgets/waveform_display_pg.py +713 -0
- scpi_instrument_control-1.0.1/scpi_control/math_channel.py +449 -0
- scpi_instrument_control-1.0.1/scpi_control/measurement.py +350 -0
- scpi_instrument_control-1.0.1/scpi_control/measurement_config.py +245 -0
- scpi_instrument_control-1.0.1/scpi_control/models.py +300 -0
- scpi_instrument_control-1.0.1/scpi_control/oscilloscope.py +461 -0
- scpi_instrument_control-1.0.1/scpi_control/power_supply.py +424 -0
- scpi_instrument_control-1.0.1/scpi_control/power_supply_output.py +410 -0
- scpi_instrument_control-1.0.1/scpi_control/protocol_decode.py +210 -0
- scpi_instrument_control-1.0.1/scpi_control/protocol_decoders/__init__.py +7 -0
- scpi_instrument_control-1.0.1/scpi_control/protocol_decoders/i2c_decoder.py +325 -0
- scpi_instrument_control-1.0.1/scpi_control/protocol_decoders/spi_decoder.py +264 -0
- scpi_instrument_control-1.0.1/scpi_control/protocol_decoders/uart_decoder.py +225 -0
- scpi_instrument_control-1.0.1/scpi_control/psu_data_logger.py +293 -0
- scpi_instrument_control-1.0.1/scpi_control/psu_models.py +236 -0
- scpi_instrument_control-1.0.1/scpi_control/psu_scpi_commands.py +162 -0
- scpi_instrument_control-1.0.1/scpi_control/py.typed +0 -0
- scpi_instrument_control-1.0.1/scpi_control/reference_waveform.py +412 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/__init__.py +23 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/app.py +38 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/generators/__init__.py +12 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/generators/base.py +53 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/generators/markdown_generator.py +546 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/generators/pdf_generator.py +1205 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/llm/__init__.py +7 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/llm/analyzer.py +287 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/llm/client.py +446 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/llm/context_builder.py +292 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/llm/prompts.py +108 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/main_window.py +862 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/models/__init__.py +16 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/models/app_settings.py +85 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/models/criteria.py +219 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/models/plot_style.py +82 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/models/report_data.py +549 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/models/report_options.py +63 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/models/template.py +493 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/models/test_types.py +284 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/utils/__init__.py +7 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/utils/image_handler.py +194 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/utils/waveform_analyzer.py +1138 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/utils/waveform_loader.py +316 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/widgets/__init__.py +7 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/widgets/ai_analysis_panel.py +567 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/widgets/chat_sidebar.py +325 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/widgets/llm_settings_dialog.py +585 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/widgets/metadata_panel.py +299 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/widgets/pdf_preview_dialog.py +356 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/widgets/report_options_dialog.py +557 -0
- scpi_instrument_control-1.0.1/scpi_control/report_generator/widgets/template_manager_dialog.py +390 -0
- scpi_instrument_control-1.0.1/scpi_control/scpi_commands.py +206 -0
- scpi_instrument_control-1.0.1/scpi_control/screen_capture.py +261 -0
- scpi_instrument_control-1.0.1/scpi_control/trigger.py +325 -0
- scpi_instrument_control-1.0.1/scpi_control/vector_graphics.py +537 -0
- scpi_instrument_control-1.0.1/scpi_control/waveform.py +634 -0
- scpi_instrument_control-1.0.0/SCPI_Instrument_Control.egg-info/SOURCES.txt +0 -43
- scpi_instrument_control-1.0.0/SCPI_Instrument_Control.egg-info/entry_points.txt +0 -3
- scpi_instrument_control-1.0.0/SCPI_Instrument_Control.egg-info/top_level.txt +0 -1
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/LICENSE +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/MANIFEST.in +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/README.md +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/SCPI_Instrument_Control.egg-info/dependency_links.txt +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/SCPI_Instrument_Control.egg-info/requires.txt +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/docs/SDS800XHD_Series_ProgrammingGuide_EN11G.pdf +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/docs/VISUAL_MEASUREMENTS.md +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/docs/images/01_main_window.png +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/docs/images/channel_controls.png +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/docs/images/cursors.png +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/docs/images/fft_analysis.png +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/docs/images/main_window.png +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/docs/images/measurements_panel.png +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/docs/images/visual_measurements.png +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/examples/README.md +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/examples/advanced_analysis.py +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/examples/basic_usage.py +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/examples/batch_capture.py +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/examples/continuous_capture.py +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/examples/function_generator_basic.py +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/examples/live_plot.py +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/examples/measurements.py +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/examples/probe_calibration_analysis.py +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/examples/psu_advanced_features.py +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/examples/psu_basic_control.py +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/examples/psu_gui_test.py +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/examples/psu_usb_connection.py +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/examples/report_generation_example.py +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/examples/simple_capture.py +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/examples/trigger_based_capture.py +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/examples/vector_graphics_xy_mode.py +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/examples/waveform_capture.py +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/resources/Test Equipment Wide.png +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/resources/Test Equipment.icns +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/resources/Test Equipment.ico +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/resources/Test Equipment.png +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/setup.cfg +0 -0
- {scpi_instrument_control-1.0.0 → scpi_instrument_control-1.0.1}/siglent/__init__.py +0 -0
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
|
|
11
|
+
## [1.0.1] - 2026-01-07
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
10
19
|
## [1.0.0] - 2026-01-06
|
|
11
20
|
|
|
12
21
|
### ⚠️ MAJOR RELEASE - Package Renamed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SCPI-Instrument-Control
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Universal Python library for controlling SCPI-compatible test equipment via Ethernet/LAN. Supports oscilloscopes (Siglent SDS series, generic SCPI scopes), function generators/AWGs (Siglent SDG series, generic SCPI AWGs), and power supplies (Siglent SPD series, generic SCPI PSUs). Features include waveform capture, measurements, FFT analysis, PyQt6 GUI, automated report generation with AI analysis, and comprehensive SCPI command abstraction.
|
|
5
5
|
Author: Robin Mumm
|
|
6
6
|
Maintainer: little-did-I-know
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SCPI-Instrument-Control
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Universal Python library for controlling SCPI-compatible test equipment via Ethernet/LAN. Supports oscilloscopes (Siglent SDS series, generic SCPI scopes), function generators/AWGs (Siglent SDG series, generic SCPI AWGs), and power supplies (Siglent SPD series, generic SCPI PSUs). Features include waveform capture, measurements, FFT analysis, PyQt6 GUI, automated report generation with AI analysis, and comprehensive SCPI command abstraction.
|
|
5
5
|
Author: Robin Mumm
|
|
6
6
|
Maintainer: little-did-I-know
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
CHANGELOG.md
|
|
2
|
+
LICENSE
|
|
3
|
+
MANIFEST.in
|
|
4
|
+
README.md
|
|
5
|
+
pyproject.toml
|
|
6
|
+
SCPI_Instrument_Control.egg-info/PKG-INFO
|
|
7
|
+
SCPI_Instrument_Control.egg-info/SOURCES.txt
|
|
8
|
+
SCPI_Instrument_Control.egg-info/dependency_links.txt
|
|
9
|
+
SCPI_Instrument_Control.egg-info/entry_points.txt
|
|
10
|
+
SCPI_Instrument_Control.egg-info/requires.txt
|
|
11
|
+
SCPI_Instrument_Control.egg-info/top_level.txt
|
|
12
|
+
docs/SDS800XHD_Series_ProgrammingGuide_EN11G.pdf
|
|
13
|
+
docs/VISUAL_MEASUREMENTS.md
|
|
14
|
+
docs/images/01_main_window.png
|
|
15
|
+
docs/images/channel_controls.png
|
|
16
|
+
docs/images/cursors.png
|
|
17
|
+
docs/images/fft_analysis.png
|
|
18
|
+
docs/images/main_window.png
|
|
19
|
+
docs/images/measurements_panel.png
|
|
20
|
+
docs/images/visual_measurements.png
|
|
21
|
+
examples/README.md
|
|
22
|
+
examples/advanced_analysis.py
|
|
23
|
+
examples/basic_usage.py
|
|
24
|
+
examples/batch_capture.py
|
|
25
|
+
examples/continuous_capture.py
|
|
26
|
+
examples/function_generator_basic.py
|
|
27
|
+
examples/live_plot.py
|
|
28
|
+
examples/measurements.py
|
|
29
|
+
examples/probe_calibration_analysis.py
|
|
30
|
+
examples/psu_advanced_features.py
|
|
31
|
+
examples/psu_basic_control.py
|
|
32
|
+
examples/psu_gui_test.py
|
|
33
|
+
examples/psu_usb_connection.py
|
|
34
|
+
examples/report_generation_example.py
|
|
35
|
+
examples/simple_capture.py
|
|
36
|
+
examples/trigger_based_capture.py
|
|
37
|
+
examples/vector_graphics_xy_mode.py
|
|
38
|
+
examples/waveform_capture.py
|
|
39
|
+
resources/Test Equipment Wide.png
|
|
40
|
+
resources/Test Equipment.icns
|
|
41
|
+
resources/Test Equipment.ico
|
|
42
|
+
resources/Test Equipment.png
|
|
43
|
+
scpi_control/__init__.py
|
|
44
|
+
scpi_control/analysis.py
|
|
45
|
+
scpi_control/automation.py
|
|
46
|
+
scpi_control/awg_models.py
|
|
47
|
+
scpi_control/awg_output.py
|
|
48
|
+
scpi_control/awg_scpi_commands.py
|
|
49
|
+
scpi_control/channel.py
|
|
50
|
+
scpi_control/exceptions.py
|
|
51
|
+
scpi_control/function_generator.py
|
|
52
|
+
scpi_control/math_channel.py
|
|
53
|
+
scpi_control/measurement.py
|
|
54
|
+
scpi_control/measurement_config.py
|
|
55
|
+
scpi_control/models.py
|
|
56
|
+
scpi_control/oscilloscope.py
|
|
57
|
+
scpi_control/power_supply.py
|
|
58
|
+
scpi_control/power_supply_output.py
|
|
59
|
+
scpi_control/protocol_decode.py
|
|
60
|
+
scpi_control/psu_data_logger.py
|
|
61
|
+
scpi_control/psu_models.py
|
|
62
|
+
scpi_control/psu_scpi_commands.py
|
|
63
|
+
scpi_control/py.typed
|
|
64
|
+
scpi_control/reference_waveform.py
|
|
65
|
+
scpi_control/scpi_commands.py
|
|
66
|
+
scpi_control/screen_capture.py
|
|
67
|
+
scpi_control/trigger.py
|
|
68
|
+
scpi_control/vector_graphics.py
|
|
69
|
+
scpi_control/waveform.py
|
|
70
|
+
scpi_control/connection/__init__.py
|
|
71
|
+
scpi_control/connection/base.py
|
|
72
|
+
scpi_control/connection/mock.py
|
|
73
|
+
scpi_control/connection/socket.py
|
|
74
|
+
scpi_control/connection/visa_connection.py
|
|
75
|
+
scpi_control/gui/__init__.py
|
|
76
|
+
scpi_control/gui/app.py
|
|
77
|
+
scpi_control/gui/connection_manager.py
|
|
78
|
+
scpi_control/gui/live_view_worker.py
|
|
79
|
+
scpi_control/gui/main_window.py
|
|
80
|
+
scpi_control/gui/vnc_window.py
|
|
81
|
+
scpi_control/gui/waveform_capture_worker.py
|
|
82
|
+
scpi_control/gui/utils/validators.py
|
|
83
|
+
scpi_control/gui/widgets/__init__.py
|
|
84
|
+
scpi_control/gui/widgets/channel_control.py
|
|
85
|
+
scpi_control/gui/widgets/cursor_panel.py
|
|
86
|
+
scpi_control/gui/widgets/error_dialog.py
|
|
87
|
+
scpi_control/gui/widgets/fft_display.py
|
|
88
|
+
scpi_control/gui/widgets/math_panel.py
|
|
89
|
+
scpi_control/gui/widgets/measurement_marker.py
|
|
90
|
+
scpi_control/gui/widgets/measurement_panel.py
|
|
91
|
+
scpi_control/gui/widgets/protocol_decode_panel.py
|
|
92
|
+
scpi_control/gui/widgets/psu_control.py
|
|
93
|
+
scpi_control/gui/widgets/reference_panel.py
|
|
94
|
+
scpi_control/gui/widgets/scope_web_view.py
|
|
95
|
+
scpi_control/gui/widgets/terminal_widget.py
|
|
96
|
+
scpi_control/gui/widgets/timebase_control.py
|
|
97
|
+
scpi_control/gui/widgets/trigger_control.py
|
|
98
|
+
scpi_control/gui/widgets/vector_graphics_panel.py
|
|
99
|
+
scpi_control/gui/widgets/visual_measurement_panel.py
|
|
100
|
+
scpi_control/gui/widgets/waveform_display.py
|
|
101
|
+
scpi_control/gui/widgets/waveform_display_pg.py
|
|
102
|
+
scpi_control/gui/widgets/measurement_markers/__init__.py
|
|
103
|
+
scpi_control/gui/widgets/measurement_markers/frequency_marker.py
|
|
104
|
+
scpi_control/gui/widgets/measurement_markers/timing_marker.py
|
|
105
|
+
scpi_control/gui/widgets/measurement_markers/voltage_marker.py
|
|
106
|
+
scpi_control/protocol_decoders/__init__.py
|
|
107
|
+
scpi_control/protocol_decoders/i2c_decoder.py
|
|
108
|
+
scpi_control/protocol_decoders/spi_decoder.py
|
|
109
|
+
scpi_control/protocol_decoders/uart_decoder.py
|
|
110
|
+
scpi_control/report_generator/__init__.py
|
|
111
|
+
scpi_control/report_generator/app.py
|
|
112
|
+
scpi_control/report_generator/main_window.py
|
|
113
|
+
scpi_control/report_generator/generators/__init__.py
|
|
114
|
+
scpi_control/report_generator/generators/base.py
|
|
115
|
+
scpi_control/report_generator/generators/markdown_generator.py
|
|
116
|
+
scpi_control/report_generator/generators/pdf_generator.py
|
|
117
|
+
scpi_control/report_generator/llm/__init__.py
|
|
118
|
+
scpi_control/report_generator/llm/analyzer.py
|
|
119
|
+
scpi_control/report_generator/llm/client.py
|
|
120
|
+
scpi_control/report_generator/llm/context_builder.py
|
|
121
|
+
scpi_control/report_generator/llm/prompts.py
|
|
122
|
+
scpi_control/report_generator/models/__init__.py
|
|
123
|
+
scpi_control/report_generator/models/app_settings.py
|
|
124
|
+
scpi_control/report_generator/models/criteria.py
|
|
125
|
+
scpi_control/report_generator/models/plot_style.py
|
|
126
|
+
scpi_control/report_generator/models/report_data.py
|
|
127
|
+
scpi_control/report_generator/models/report_options.py
|
|
128
|
+
scpi_control/report_generator/models/template.py
|
|
129
|
+
scpi_control/report_generator/models/test_types.py
|
|
130
|
+
scpi_control/report_generator/utils/__init__.py
|
|
131
|
+
scpi_control/report_generator/utils/image_handler.py
|
|
132
|
+
scpi_control/report_generator/utils/waveform_analyzer.py
|
|
133
|
+
scpi_control/report_generator/utils/waveform_loader.py
|
|
134
|
+
scpi_control/report_generator/widgets/__init__.py
|
|
135
|
+
scpi_control/report_generator/widgets/ai_analysis_panel.py
|
|
136
|
+
scpi_control/report_generator/widgets/chat_sidebar.py
|
|
137
|
+
scpi_control/report_generator/widgets/llm_settings_dialog.py
|
|
138
|
+
scpi_control/report_generator/widgets/metadata_panel.py
|
|
139
|
+
scpi_control/report_generator/widgets/pdf_preview_dialog.py
|
|
140
|
+
scpi_control/report_generator/widgets/report_options_dialog.py
|
|
141
|
+
scpi_control/report_generator/widgets/template_manager_dialog.py
|
|
142
|
+
siglent/__init__.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "SCPI-Instrument-Control"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.1"
|
|
8
8
|
description = "Universal Python library for controlling SCPI-compatible test equipment via Ethernet/LAN. Supports oscilloscopes (Siglent SDS series, generic SCPI scopes), function generators/AWGs (Siglent SDG series, generic SCPI AWGs), and power supplies (Siglent SPD series, generic SCPI PSUs). Features include waveform capture, measurements, FFT analysis, PyQt6 GUI, automated report generation with AI analysis, and comprehensive SCPI command abstraction."
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -122,18 +122,19 @@ all = [
|
|
|
122
122
|
]
|
|
123
123
|
|
|
124
124
|
[project.scripts]
|
|
125
|
-
siglent-gui = "
|
|
126
|
-
siglent-report-generator = "
|
|
125
|
+
siglent-gui = "scpi_control.gui.app:main"
|
|
126
|
+
siglent-report-generator = "scpi_control.report_generator.app:main"
|
|
127
127
|
|
|
128
128
|
[tool.setuptools]
|
|
129
129
|
include-package-data = true
|
|
130
130
|
|
|
131
131
|
[tool.setuptools.package-data]
|
|
132
132
|
siglent = ["py.typed"]
|
|
133
|
+
scpi_control = ["py.typed"]
|
|
133
134
|
|
|
134
135
|
[tool.setuptools.packages.find]
|
|
135
136
|
where = ["."]
|
|
136
|
-
include = ["siglent*"]
|
|
137
|
+
include = ["siglent*", "scpi_control*"]
|
|
137
138
|
|
|
138
139
|
[tool.black]
|
|
139
140
|
line-length = 100
|
|
@@ -158,7 +159,7 @@ filterwarnings = [
|
|
|
158
159
|
]
|
|
159
160
|
|
|
160
161
|
[tool.coverage.run]
|
|
161
|
-
source = ["siglent"]
|
|
162
|
+
source = ["siglent", "scpi_control"]
|
|
162
163
|
omit = [
|
|
163
164
|
"*/tests/*",
|
|
164
165
|
"*/test_*.py",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""SCPI Instrument Control Package
|
|
2
|
+
|
|
3
|
+
This package provides programmatic control for SCPI-compatible test equipment
|
|
4
|
+
via Ethernet/LAN connection.
|
|
5
|
+
|
|
6
|
+
Supported equipment:
|
|
7
|
+
- Oscilloscopes (Siglent SDS series and SCPI-compatible models)
|
|
8
|
+
- Function Generators / AWGs (Siglent SDG series and SCPI-compatible models)
|
|
9
|
+
- Power Supplies (Siglent SPD series and SCPI-compatible models)
|
|
10
|
+
|
|
11
|
+
For high-level automation and data collection, see the automation module:
|
|
12
|
+
from scpi_control.automation import DataCollector, TriggerWaitCollector
|
|
13
|
+
|
|
14
|
+
For power supply control:
|
|
15
|
+
from scpi_control import PowerSupply
|
|
16
|
+
|
|
17
|
+
For function generator control:
|
|
18
|
+
from scpi_control import FunctionGenerator
|
|
19
|
+
|
|
20
|
+
For automated test report generation:
|
|
21
|
+
from scpi_control.report_generator import ReportGenerator
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__version__ = "1.0.1"
|
|
25
|
+
|
|
26
|
+
from scpi_control.exceptions import CommandError, SiglentConnectionError, SiglentError, SiglentTimeoutError
|
|
27
|
+
from scpi_control.oscilloscope import Oscilloscope
|
|
28
|
+
|
|
29
|
+
# Power supply support (stable as of v0.5.0)
|
|
30
|
+
from scpi_control.power_supply import PowerSupply
|
|
31
|
+
from scpi_control.psu_data_logger import PSUDataLogger, TimedPSULogger
|
|
32
|
+
|
|
33
|
+
# Function generator support (new in v0.6.0, stable in v1.0.0)
|
|
34
|
+
from scpi_control.function_generator import FunctionGenerator
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
# Core features
|
|
38
|
+
"Oscilloscope",
|
|
39
|
+
"SiglentError",
|
|
40
|
+
"SiglentConnectionError",
|
|
41
|
+
"SiglentTimeoutError",
|
|
42
|
+
"CommandError",
|
|
43
|
+
# Power supply features
|
|
44
|
+
"PowerSupply",
|
|
45
|
+
"PSUDataLogger",
|
|
46
|
+
"TimedPSULogger",
|
|
47
|
+
# Function generator features
|
|
48
|
+
"FunctionGenerator",
|
|
49
|
+
]
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""Advanced analysis tools for waveform data."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Optional, Tuple
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from scipy import signal
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class FFTResult:
|
|
15
|
+
"""Result of FFT analysis.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
frequency: Frequency array (Hz)
|
|
19
|
+
magnitude: Magnitude array (linear or dB)
|
|
20
|
+
phase: Phase array (radians)
|
|
21
|
+
window: Window function used
|
|
22
|
+
sample_rate: Sample rate (Hz)
|
|
23
|
+
magnitude_db: Whether magnitude is in dB
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
frequency: np.ndarray
|
|
27
|
+
magnitude: np.ndarray
|
|
28
|
+
phase: np.ndarray
|
|
29
|
+
window: str
|
|
30
|
+
sample_rate: float
|
|
31
|
+
magnitude_db: bool = True
|
|
32
|
+
|
|
33
|
+
def get_peak_frequency(self, num_peaks: int = 1) -> list:
|
|
34
|
+
"""Get the peak frequencies.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
num_peaks: Number of peaks to find
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
List of (frequency, magnitude) tuples
|
|
41
|
+
"""
|
|
42
|
+
# Find peaks
|
|
43
|
+
peaks, _ = signal.find_peaks(self.magnitude, height=0)
|
|
44
|
+
|
|
45
|
+
# Sort by magnitude (descending)
|
|
46
|
+
if len(peaks) > 0:
|
|
47
|
+
peak_magnitudes = self.magnitude[peaks]
|
|
48
|
+
sorted_indices = np.argsort(peak_magnitudes)[::-1]
|
|
49
|
+
top_peaks = peaks[sorted_indices[:num_peaks]]
|
|
50
|
+
|
|
51
|
+
results = []
|
|
52
|
+
for peak_idx in top_peaks:
|
|
53
|
+
freq = self.frequency[peak_idx]
|
|
54
|
+
mag = self.magnitude[peak_idx]
|
|
55
|
+
results.append((freq, mag))
|
|
56
|
+
|
|
57
|
+
return results
|
|
58
|
+
else:
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FFTAnalyzer:
|
|
63
|
+
"""FFT analyzer for frequency domain analysis of waveforms."""
|
|
64
|
+
|
|
65
|
+
WINDOW_FUNCTIONS = {
|
|
66
|
+
"rectangular": np.ones,
|
|
67
|
+
"hanning": np.hanning,
|
|
68
|
+
"hamming": np.hamming,
|
|
69
|
+
"blackman": np.blackman,
|
|
70
|
+
"bartlett": np.bartlett,
|
|
71
|
+
"flattop": signal.windows.flattop,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def __init__(self):
|
|
75
|
+
"""Initialize FFT analyzer."""
|
|
76
|
+
logger.info("FFT analyzer initialized")
|
|
77
|
+
|
|
78
|
+
def compute_fft(self, waveform, window: str = "hanning", output_db: bool = True, detrend: bool = True) -> Optional[FFTResult]:
|
|
79
|
+
"""Compute FFT of a waveform.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
waveform: Input waveform
|
|
83
|
+
window: Window function name ('rectangular', 'hanning', 'hamming', 'blackman', 'bartlett', 'flattop')
|
|
84
|
+
output_db: If True, output magnitude in dB; otherwise linear
|
|
85
|
+
detrend: If True, remove DC component before FFT
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
FFTResult or None if error
|
|
89
|
+
"""
|
|
90
|
+
if waveform is None:
|
|
91
|
+
logger.warning("Cannot compute FFT on None waveform")
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
# Get voltage data
|
|
96
|
+
voltage = waveform.voltage
|
|
97
|
+
time = waveform.time
|
|
98
|
+
|
|
99
|
+
if len(voltage) < 2:
|
|
100
|
+
logger.warning("Waveform too short for FFT")
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
# Calculate sample rate
|
|
104
|
+
dt = np.mean(np.diff(time))
|
|
105
|
+
sample_rate = 1.0 / dt
|
|
106
|
+
|
|
107
|
+
# Detrend if requested (remove DC component)
|
|
108
|
+
if detrend:
|
|
109
|
+
voltage = voltage - np.mean(voltage)
|
|
110
|
+
|
|
111
|
+
# Apply window function
|
|
112
|
+
if window.lower() in self.WINDOW_FUNCTIONS:
|
|
113
|
+
window_func = self.WINDOW_FUNCTIONS[window.lower()]
|
|
114
|
+
windowed_voltage = voltage * window_func(len(voltage))
|
|
115
|
+
else:
|
|
116
|
+
logger.warning(f"Unknown window function '{window}', using rectangular")
|
|
117
|
+
windowed_voltage = voltage
|
|
118
|
+
|
|
119
|
+
# Compute FFT
|
|
120
|
+
fft_result = np.fft.rfft(windowed_voltage)
|
|
121
|
+
frequencies = np.fft.rfftfreq(len(voltage), dt)
|
|
122
|
+
|
|
123
|
+
# Calculate magnitude and phase
|
|
124
|
+
magnitude_linear = np.abs(fft_result)
|
|
125
|
+
phase = np.angle(fft_result)
|
|
126
|
+
|
|
127
|
+
# Convert to dB if requested
|
|
128
|
+
if output_db:
|
|
129
|
+
# Avoid log(0) by using a small epsilon
|
|
130
|
+
epsilon = 1e-12
|
|
131
|
+
magnitude = 20 * np.log10(magnitude_linear + epsilon)
|
|
132
|
+
else:
|
|
133
|
+
magnitude = magnitude_linear
|
|
134
|
+
|
|
135
|
+
result = FFTResult(
|
|
136
|
+
frequency=frequencies,
|
|
137
|
+
magnitude=magnitude,
|
|
138
|
+
phase=phase,
|
|
139
|
+
window=window,
|
|
140
|
+
sample_rate=sample_rate,
|
|
141
|
+
magnitude_db=output_db,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
logger.info(f"FFT computed: {len(frequencies)} frequency bins, " f"sample rate {sample_rate/1e6:.3f} MHz, window={window}")
|
|
145
|
+
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.error(f"FFT computation error: {e}")
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
def compute_power_spectrum(self, waveform, window: str = "hanning", nperseg: Optional[int] = None) -> Optional[Tuple[np.ndarray, np.ndarray]]:
|
|
153
|
+
"""Compute power spectral density using Welch's method.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
waveform: Input waveform
|
|
157
|
+
window: Window function name
|
|
158
|
+
nperseg: Length of each segment (default: 256)
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Tuple of (frequencies, power_spectrum) or None if error
|
|
162
|
+
"""
|
|
163
|
+
if waveform is None:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
voltage = waveform.voltage
|
|
168
|
+
time = waveform.time
|
|
169
|
+
dt = np.mean(np.diff(time))
|
|
170
|
+
sample_rate = 1.0 / dt
|
|
171
|
+
|
|
172
|
+
if nperseg is None:
|
|
173
|
+
nperseg = min(256, len(voltage) // 4)
|
|
174
|
+
|
|
175
|
+
# Compute power spectral density using Welch's method
|
|
176
|
+
frequencies, psd = signal.welch(voltage, fs=sample_rate, window=window, nperseg=nperseg, scaling="density")
|
|
177
|
+
|
|
178
|
+
logger.info(f"Power spectrum computed using Welch's method")
|
|
179
|
+
|
|
180
|
+
return frequencies, psd
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.error(f"Power spectrum computation error: {e}")
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
def compute_spectrogram(self, waveform, window: str = "hanning", nperseg: Optional[int] = None) -> Optional[Tuple[np.ndarray, np.ndarray, np.ndarray]]:
|
|
187
|
+
"""Compute spectrogram (time-frequency representation).
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
waveform: Input waveform
|
|
191
|
+
window: Window function name
|
|
192
|
+
nperseg: Length of each segment (default: 256)
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Tuple of (frequencies, times, spectrogram) or None if error
|
|
196
|
+
"""
|
|
197
|
+
if waveform is None:
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
voltage = waveform.voltage
|
|
202
|
+
time = waveform.time
|
|
203
|
+
dt = np.mean(np.diff(time))
|
|
204
|
+
sample_rate = 1.0 / dt
|
|
205
|
+
|
|
206
|
+
if nperseg is None:
|
|
207
|
+
nperseg = min(256, len(voltage) // 4)
|
|
208
|
+
|
|
209
|
+
# Compute spectrogram
|
|
210
|
+
frequencies, times, Sxx = signal.spectrogram(voltage, fs=sample_rate, window=window, nperseg=nperseg)
|
|
211
|
+
|
|
212
|
+
logger.info(f"Spectrogram computed: {len(frequencies)}x{len(times)} matrix")
|
|
213
|
+
|
|
214
|
+
return frequencies, times, Sxx
|
|
215
|
+
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.error(f"Spectrogram computation error: {e}")
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
def apply_bandpass_filter(self, waveform, lowcut: float, highcut: float, order: int = 5):
|
|
221
|
+
"""Apply bandpass filter to waveform.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
waveform: Input waveform
|
|
225
|
+
lowcut: Low frequency cutoff (Hz)
|
|
226
|
+
highcut: High frequency cutoff (Hz)
|
|
227
|
+
order: Filter order
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Filtered waveform or None if error
|
|
231
|
+
"""
|
|
232
|
+
if waveform is None:
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
voltage = waveform.voltage
|
|
237
|
+
time = waveform.time
|
|
238
|
+
dt = np.mean(np.diff(time))
|
|
239
|
+
sample_rate = 1.0 / dt
|
|
240
|
+
|
|
241
|
+
# Design Butterworth bandpass filter
|
|
242
|
+
nyquist = sample_rate / 2.0
|
|
243
|
+
low = lowcut / nyquist
|
|
244
|
+
high = highcut / nyquist
|
|
245
|
+
|
|
246
|
+
if low <= 0 or high >= 1:
|
|
247
|
+
logger.error("Filter frequencies out of valid range")
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
sos = signal.butter(order, [low, high], btype="band", output="sos")
|
|
251
|
+
|
|
252
|
+
# Apply filter
|
|
253
|
+
filtered_voltage = signal.sosfilt(sos, voltage)
|
|
254
|
+
|
|
255
|
+
# Create filtered waveform
|
|
256
|
+
result = type(waveform)(time=time, voltage=filtered_voltage, channel=f"{waveform.channel}_FILTERED")
|
|
257
|
+
|
|
258
|
+
logger.info(f"Bandpass filter applied: {lowcut:.2f}-{highcut:.2f} Hz, order={order}")
|
|
259
|
+
|
|
260
|
+
return result
|
|
261
|
+
|
|
262
|
+
except Exception as e:
|
|
263
|
+
logger.error(f"Bandpass filter error: {e}")
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
def apply_lowpass_filter(self, waveform, cutoff: float, order: int = 5):
|
|
267
|
+
"""Apply lowpass filter to waveform.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
waveform: Input waveform
|
|
271
|
+
cutoff: Cutoff frequency (Hz)
|
|
272
|
+
order: Filter order
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Filtered waveform or None if error
|
|
276
|
+
"""
|
|
277
|
+
if waveform is None:
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
voltage = waveform.voltage
|
|
282
|
+
time = waveform.time
|
|
283
|
+
dt = np.mean(np.diff(time))
|
|
284
|
+
sample_rate = 1.0 / dt
|
|
285
|
+
|
|
286
|
+
# Design Butterworth lowpass filter
|
|
287
|
+
nyquist = sample_rate / 2.0
|
|
288
|
+
normal_cutoff = cutoff / nyquist
|
|
289
|
+
|
|
290
|
+
if normal_cutoff <= 0 or normal_cutoff >= 1:
|
|
291
|
+
logger.error("Filter cutoff out of valid range")
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
sos = signal.butter(order, normal_cutoff, btype="low", output="sos")
|
|
295
|
+
|
|
296
|
+
# Apply filter
|
|
297
|
+
filtered_voltage = signal.sosfilt(sos, voltage)
|
|
298
|
+
|
|
299
|
+
# Create filtered waveform
|
|
300
|
+
result = type(waveform)(time=time, voltage=filtered_voltage, channel=f"{waveform.channel}_FILTERED")
|
|
301
|
+
|
|
302
|
+
logger.info(f"Lowpass filter applied: {cutoff:.2f} Hz, order={order}")
|
|
303
|
+
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
except Exception as e:
|
|
307
|
+
logger.error(f"Lowpass filter error: {e}")
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
def apply_highpass_filter(self, waveform, cutoff: float, order: int = 5):
|
|
311
|
+
"""Apply highpass filter to waveform.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
waveform: Input waveform
|
|
315
|
+
cutoff: Cutoff frequency (Hz)
|
|
316
|
+
order: Filter order
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Filtered waveform or None if error
|
|
320
|
+
"""
|
|
321
|
+
if waveform is None:
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
voltage = waveform.voltage
|
|
326
|
+
time = waveform.time
|
|
327
|
+
dt = np.mean(np.diff(time))
|
|
328
|
+
sample_rate = 1.0 / dt
|
|
329
|
+
|
|
330
|
+
# Design Butterworth highpass filter
|
|
331
|
+
nyquist = sample_rate / 2.0
|
|
332
|
+
normal_cutoff = cutoff / nyquist
|
|
333
|
+
|
|
334
|
+
if normal_cutoff <= 0 or normal_cutoff >= 1:
|
|
335
|
+
logger.error("Filter cutoff out of valid range")
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
sos = signal.butter(order, normal_cutoff, btype="high", output="sos")
|
|
339
|
+
|
|
340
|
+
# Apply filter
|
|
341
|
+
filtered_voltage = signal.sosfilt(sos, voltage)
|
|
342
|
+
|
|
343
|
+
# Create filtered waveform
|
|
344
|
+
result = type(waveform)(time=time, voltage=filtered_voltage, channel=f"{waveform.channel}_FILTERED")
|
|
345
|
+
|
|
346
|
+
logger.info(f"Highpass filter applied: {cutoff:.2f} Hz, order={order}")
|
|
347
|
+
|
|
348
|
+
return result
|
|
349
|
+
|
|
350
|
+
except Exception as e:
|
|
351
|
+
logger.error(f"Highpass filter error: {e}")
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
@staticmethod
|
|
355
|
+
def calculate_thd(fft_result: FFTResult, fundamental_freq: float, num_harmonics: int = 5) -> Optional[float]:
|
|
356
|
+
"""Calculate Total Harmonic Distortion (THD).
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
fft_result: FFT result
|
|
360
|
+
fundamental_freq: Fundamental frequency (Hz)
|
|
361
|
+
num_harmonics: Number of harmonics to include
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
THD in percent or None if error
|
|
365
|
+
"""
|
|
366
|
+
try:
|
|
367
|
+
# Find fundamental frequency bin
|
|
368
|
+
freq_resolution = fft_result.frequency[1] - fft_result.frequency[0]
|
|
369
|
+
fund_idx = int(round(fundamental_freq / freq_resolution))
|
|
370
|
+
|
|
371
|
+
if fund_idx >= len(fft_result.magnitude):
|
|
372
|
+
logger.error("Fundamental frequency out of range")
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
# Get fundamental magnitude (linear)
|
|
376
|
+
if fft_result.magnitude_db:
|
|
377
|
+
fund_mag = 10 ** (fft_result.magnitude[fund_idx] / 20.0)
|
|
378
|
+
else:
|
|
379
|
+
fund_mag = fft_result.magnitude[fund_idx]
|
|
380
|
+
|
|
381
|
+
# Calculate harmonic magnitudes
|
|
382
|
+
harmonic_power = 0.0
|
|
383
|
+
for n in range(2, num_harmonics + 2):
|
|
384
|
+
harmonic_idx = n * fund_idx
|
|
385
|
+
if harmonic_idx < len(fft_result.magnitude):
|
|
386
|
+
if fft_result.magnitude_db:
|
|
387
|
+
harm_mag = 10 ** (fft_result.magnitude[harmonic_idx] / 20.0)
|
|
388
|
+
else:
|
|
389
|
+
harm_mag = fft_result.magnitude[harmonic_idx]
|
|
390
|
+
harmonic_power += harm_mag**2
|
|
391
|
+
|
|
392
|
+
# Calculate THD
|
|
393
|
+
thd = 100.0 * np.sqrt(harmonic_power) / fund_mag
|
|
394
|
+
|
|
395
|
+
logger.info(f"THD calculated: {thd:.2f}% (fundamental={fundamental_freq:.2f} Hz)")
|
|
396
|
+
|
|
397
|
+
return thd
|
|
398
|
+
|
|
399
|
+
except Exception as e:
|
|
400
|
+
logger.error(f"THD calculation error: {e}")
|
|
401
|
+
return None
|