From 119c95dfbf48c45aba1dd4ce528abbea5c7cc09e Mon Sep 17 00:00:00 2001 From: NI-velmurugan Date: Thu, 22 Oct 2026 13:05:53 +0000 Subject: [PATCH 1/8] Adding Digital Filtering in NI-Scope --- CHANGELOG.md | 1 + build/templates/tox-system_tests.ini.mako | 3 + docs/niscope/examples.rst | 9 ++ generated/niscope/tox-system_tests.ini | 1 + .../examples/niscope_digital_filtering.py | 145 ++++++++++++++++++ 5 files changed, 159 insertions(+) create mode 100644 src/niscope/examples/niscope_digital_filtering.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 86a8d9eb38..6b36767dc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1895,6 +1895,7 @@ #### [niscope] Unreleased - Added - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. + - Added the `niscope_digital_filtering.py` example. - Changed - Removed diff --git a/build/templates/tox-system_tests.ini.mako b/build/templates/tox-system_tests.ini.mako index c2b3d9131d..9c7f07cb3e 100644 --- a/build/templates/tox-system_tests.ini.mako +++ b/build/templates/tox-system_tests.ini.mako @@ -76,6 +76,9 @@ deps = % endif % if module_name == 'nidigital': ${module_name}-system_tests: nisync +% endif +% if module_name == 'niscope': + ${module_name}-system_tests: matplotlib; sys_platform != "win32" % endif ${module_name}-system_tests: pytest ${module_name}-system_tests: coverage diff --git a/docs/niscope/examples.rst b/docs/niscope/examples.rst index 71f84c469c..68b5402487 100644 --- a/docs/niscope/examples.rst +++ b/docs/niscope/examples.rst @@ -3,6 +3,15 @@ Examples `You can download all niscope examples for latest version here `_ +niscope_digital_filtering.py +---------------------------- + +.. literalinclude:: ../../src/niscope/examples/niscope_digital_filtering.py + :language: python + :linenos: + :encoding: utf8 + :caption: `(niscope_digital_filtering.py) `_ + niscope_fetch.py ---------------- diff --git a/generated/niscope/tox-system_tests.ini b/generated/niscope/tox-system_tests.ini index 2f232d27ab..afadc34815 100644 --- a/generated/niscope/tox-system_tests.ini +++ b/generated/niscope/tox-system_tests.ini @@ -39,6 +39,7 @@ commands = deps = niscope-wheel_dep: build + niscope-system_tests: matplotlib; sys_platform != "win32" niscope-system_tests: pytest niscope-system_tests: coverage niscope-system_tests: numpy diff --git a/src/niscope/examples/niscope_digital_filtering.py b/src/niscope/examples/niscope_digital_filtering.py new file mode 100644 index 0000000000..241a08da08 --- /dev/null +++ b/src/niscope/examples/niscope_digital_filtering.py @@ -0,0 +1,145 @@ +#!/usr/bin/python + +import argparse +import niscope +import pytest +import sys + +FILTERS = { + 'NONE': niscope.ArrayMeasurement.NO_MEASUREMENT, + 'FIR': niscope.ArrayMeasurement.WINDOWED_FIR_FILTER, + 'BESSEL': niscope.ArrayMeasurement.BESSEL_FILTER, + 'BUTTERWORTH': niscope.ArrayMeasurement.BUTTERWORTH_FILTER, + 'CHEBYSHEV': niscope.ArrayMeasurement.CHEBYSHEV_FILTER, +} +FFT_FUNCTIONS = { + 'VOLTS_RMS': niscope.ArrayMeasurement.FFT_AMP_SPECTRUM_VOLTS_RMS, + 'DB': niscope.ArrayMeasurement.FFT_AMP_SPECTRUM_DB, +} + + +def _create_chart(plt): + figure, (waveform_axes, spectrum_axes) = plt.subplots(2, 1) + (waveform_line,) = waveform_axes.plot([]) + (spectrum_line,) = spectrum_axes.plot([]) + waveform_axes.set_title('Filtered Waveform') + waveform_axes.set_xlabel('Time (s)') + waveform_axes.set_ylabel('Amplitude') + spectrum_axes.set_title('Spectrum') + spectrum_axes.set_xlabel('Frequency (Hz)') + spectrum_axes.set_ylabel('Amplitude') + for axes in (waveform_axes, spectrum_axes): + axes.grid(True) + figure.tight_layout() + return figure, waveform_axes, waveform_line, spectrum_axes, spectrum_line + + +def _update_chart(waveform_axes, waveform_line, filtered, spectrum_axes, spectrum_line, spectrum): + for axes, line, waveforms in ((waveform_axes, waveform_line, filtered), (spectrum_axes, spectrum_line, spectrum)): + for waveform in waveforms: + x = [waveform.relative_initial_x + i * waveform.x_increment for i in range(len(waveform.samples))] + line.set_data(x, waveform.samples) + axes.relim() + axes.autoscale_view() + + +def example( + resource_name, channels, options, min_sample_rate, min_record_length, + filter_name, filter_type, cutoff_frequency, center_frequency, width, + fir_taps, fir_window, iir_order, fft_function, total_iterations, +): + # Imported lazily because matplotlib is unavailable on some platforms (e.g. 32-bit Python) + import matplotlib.pyplot as plt + + with niscope.Session(resource_name=resource_name, options=options) as session: + session.configure_vertical(range=2.0, coupling=niscope.VerticalCoupling.DC) + session.configure_horizontal_timing(min_sample_rate=min_sample_rate, min_num_pts=min_record_length, ref_position=50.0, num_records=1, enforce_realtime=True) + + channel_session = session.channels[channels] + channel_session.meas_filter_type = niscope.FilterType[filter_type] + channel_session.meas_filter_cutoff_freq = cutoff_frequency + channel_session.meas_filter_center_freq = center_frequency + channel_session.meas_filter_width = width + channel_session.meas_filter_taps = fir_taps + channel_session.meas_fir_filter_window = niscope.FIRFilterWindow[fir_window] + channel_session.meas_filter_order = iir_order + channel_session.meas_filter_transient_waveform_percent = 20.0 + channel_session.meas_filter_ripple = 0.5 + + figure, waveform_axes, waveform_line, spectrum_axes, spectrum_line = _create_chart(plt) + + iterations = 0 + try: + # A total_iterations of None runs until the chart window is closed or Ctrl+C is pressed + while plt.fignum_exists(figure.number) and (total_iterations is None or iterations < total_iterations): + # The filter runs as a processing step before any other measurement; it is added and cleared on every acquisition + channel_session.add_waveform_processing(FILTERS[filter_name]) + try: + with session.initiate(): + filtered = channel_session.fetch_array_measurement(niscope.ArrayMeasurement.NO_MEASUREMENT) + spectrum = channel_session.fetch_array_measurement(FFT_FUNCTIONS[fft_function]) + finally: + channel_session.clear_waveform_processing() + + _update_chart(waveform_axes, waveform_line, filtered, spectrum_axes, spectrum_line, spectrum) + iterations += 1 + plt.pause(0.02) + + # The acquisition has stopped; keep the final chart on screen until its window is closed + if total_iterations is not None and plt.fignum_exists(figure.number): + plt.show() + except KeyboardInterrupt: + pass + finally: + plt.close(figure) + + +def _main(argsv): + parser = argparse.ArgumentParser(description='Continuously applies a digital filter to acquired waveforms and plots the filtered waveform and its FFT amplitude spectrum.', formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('-n', '--resource-name', default='PXI1Slot3', help='Specifies the resource name of the device to initialize.') + parser.add_argument('-c', '--channels', default='0', help='Channel to plot') + parser.add_argument('-sr', '--min-sample-rate', default=10000000.0, type=float, help='Specify the sampling rate for the acquisition in Samples per second.') + parser.add_argument('-l', '--min-record-length', default=4096, type=int, help='Specifies the minimum number of points you require in the waveform record for each channel.') + parser.add_argument('-f', '--filter', default='CHEBYSHEV', choices=FILTERS.keys(), type=str.upper, help='Filter to apply') + parser.add_argument('-ft', '--filter-type', default='LOWPASS', choices=niscope.FilterType.__members__.keys(), type=str.upper, help='Specifies the type of filter, for both IIR and FIR filters.') + parser.add_argument('-cf', '--cutoff-frequency', default=2500000.0, type=float, help='Specifies the cutoff frequency in hertz for filters of type lowpass and highpass.') + parser.add_argument('-cc', '--center-frequency', default=500000.0, type=float, help='The center frequency in hertz for filters of type bandpass and bandstop.') + parser.add_argument('-w', '--width', default=100000.0, type=float, help='Specifies the width of bandpass and bandstop type filters in hertz.') + parser.add_argument('-t', '--fir-taps', default=25, type=int, help='Defines the number of taps (coefficients) for an FIR filter.') + parser.add_argument('-fw', '--fir-window', default='HANNING', choices=niscope.FIRFilterWindow.__members__.keys(), type=str.upper, help='Specifies the FIR window type.') + parser.add_argument('-o', '--iir-order', default=2, type=int, help='Specifies the order of an IIR filter.') + parser.add_argument('-fft', '--fft-function', default='DB', choices=FFT_FUNCTIONS.keys(), type=str.upper, help='FFT amplitude spectrum function') + parser.add_argument('--total-iterations', default=None, type=int, help='Stops after this many acquisitions and keeps the chart open until its window is closed; omit to run until the chart window is closed or Ctrl+C is pressed.') + parser.add_argument('-op', '--option-string', default='', type=str, help='Specifies initialization commands.') + args = parser.parse_args(argsv) + example( + args.resource_name, args.channels, args.option_string, args.min_sample_rate, args.min_record_length, + args.filter, args.filter_type, args.cutoff_frequency, args.center_frequency, args.width, + args.fir_taps, args.fir_window, args.iir_order, args.fft_function, args.total_iterations, + ) + + +def main(): + _main(sys.argv[1:]) + + +def test_example(): + plt = pytest.importorskip('matplotlib.pyplot') + plt.switch_backend('Agg') # Avoid opening a chart window during the test + options = {'simulate': True, 'driver_setup': {'Model': '5164', 'BoardType': 'PXIe', }, } + example( + 'PXI1Slot2', '0', options, 10000000.0, 4096, + 'CHEBYSHEV', 'LOWPASS', 2500000.0, 500000.0, 100000.0, + 25, 'HANNING', 2, 'DB', 2, + ) + + +def test_main(): + plt = pytest.importorskip('matplotlib.pyplot') + plt.switch_backend('Agg') # Avoid opening a chart window during the test + cmd_line = ['--option-string', 'Simulate=1, DriverSetup=Model:5164; BoardType:PXIe', '--total-iterations', '2', ] + _main(cmd_line) + + +if __name__ == '__main__': + main() From 45bc8264bc9834272b9235202667fe6133fb2bb9 Mon Sep 17 00:00:00 2001 From: NI-velmurugan Date: Fri, 23 Oct 2026 12:46:20 +0000 Subject: [PATCH 2/8] Fixing PR comments --- .../examples/niscope_digital_filtering.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/niscope/examples/niscope_digital_filtering.py b/src/niscope/examples/niscope_digital_filtering.py index 241a08da08..ee18b9f5ef 100644 --- a/src/niscope/examples/niscope_digital_filtering.py +++ b/src/niscope/examples/niscope_digital_filtering.py @@ -20,8 +20,6 @@ def _create_chart(plt): figure, (waveform_axes, spectrum_axes) = plt.subplots(2, 1) - (waveform_line,) = waveform_axes.plot([]) - (spectrum_line,) = spectrum_axes.plot([]) waveform_axes.set_title('Filtered Waveform') waveform_axes.set_xlabel('Time (s)') waveform_axes.set_ylabel('Amplitude') @@ -31,12 +29,17 @@ def _create_chart(plt): for axes in (waveform_axes, spectrum_axes): axes.grid(True) figure.tight_layout() - return figure, waveform_axes, waveform_line, spectrum_axes, spectrum_line + return figure, waveform_axes, spectrum_axes -def _update_chart(waveform_axes, waveform_line, filtered, spectrum_axes, spectrum_line, spectrum): - for axes, line, waveforms in ((waveform_axes, waveform_line, filtered), (spectrum_axes, spectrum_line, spectrum)): - for waveform in waveforms: +def _update_chart(waveform_axes, filtered, spectrum_axes, spectrum): + for axes, waveforms in ((waveform_axes, filtered), (spectrum_axes, spectrum)): + # One line per channel is created on the first acquisition and updated afterwards + if len(axes.lines) < len(waveforms): + while len(axes.lines) < len(waveforms): + axes.plot([], [], label=waveforms[len(axes.lines)].channel) + axes.legend(loc='upper right') + for line, waveform in zip(axes.lines, waveforms): x = [waveform.relative_initial_x + i * waveform.x_increment for i in range(len(waveform.samples))] line.set_data(x, waveform.samples) axes.relim() @@ -66,7 +69,7 @@ def example( channel_session.meas_filter_transient_waveform_percent = 20.0 channel_session.meas_filter_ripple = 0.5 - figure, waveform_axes, waveform_line, spectrum_axes, spectrum_line = _create_chart(plt) + figure, waveform_axes, spectrum_axes = _create_chart(plt) iterations = 0 try: @@ -81,7 +84,7 @@ def example( finally: channel_session.clear_waveform_processing() - _update_chart(waveform_axes, waveform_line, filtered, spectrum_axes, spectrum_line, spectrum) + _update_chart(waveform_axes, filtered, spectrum_axes, spectrum) iterations += 1 plt.pause(0.02) @@ -97,10 +100,11 @@ def example( def _main(argsv): parser = argparse.ArgumentParser(description='Continuously applies a digital filter to acquired waveforms and plots the filtered waveform and its FFT amplitude spectrum.', formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('-n', '--resource-name', default='PXI1Slot3', help='Specifies the resource name of the device to initialize.') - parser.add_argument('-c', '--channels', default='0', help='Channel to plot') - parser.add_argument('-sr', '--min-sample-rate', default=10000000.0, type=float, help='Specify the sampling rate for the acquisition in Samples per second.') + parser.add_argument('-c', '--channels', default='0', help='Comma-separated list of channels to plot, one line per channel') parser.add_argument('-l', '--min-record-length', default=4096, type=int, help='Specifies the minimum number of points you require in the waveform record for each channel.') + parser.add_argument('-sr', '--min-sample-rate', default=10000000.0, type=float, help='Specify the sampling rate for the acquisition in Samples per second.') parser.add_argument('-f', '--filter', default='CHEBYSHEV', choices=FILTERS.keys(), type=str.upper, help='Filter to apply') + parser.add_argument('-fft', '--fft-function', default='DB', choices=FFT_FUNCTIONS.keys(), type=str.upper, help='FFT amplitude spectrum function') parser.add_argument('-ft', '--filter-type', default='LOWPASS', choices=niscope.FilterType.__members__.keys(), type=str.upper, help='Specifies the type of filter, for both IIR and FIR filters.') parser.add_argument('-cf', '--cutoff-frequency', default=2500000.0, type=float, help='Specifies the cutoff frequency in hertz for filters of type lowpass and highpass.') parser.add_argument('-cc', '--center-frequency', default=500000.0, type=float, help='The center frequency in hertz for filters of type bandpass and bandstop.') @@ -108,7 +112,6 @@ def _main(argsv): parser.add_argument('-t', '--fir-taps', default=25, type=int, help='Defines the number of taps (coefficients) for an FIR filter.') parser.add_argument('-fw', '--fir-window', default='HANNING', choices=niscope.FIRFilterWindow.__members__.keys(), type=str.upper, help='Specifies the FIR window type.') parser.add_argument('-o', '--iir-order', default=2, type=int, help='Specifies the order of an IIR filter.') - parser.add_argument('-fft', '--fft-function', default='DB', choices=FFT_FUNCTIONS.keys(), type=str.upper, help='FFT amplitude spectrum function') parser.add_argument('--total-iterations', default=None, type=int, help='Stops after this many acquisitions and keeps the chart open until its window is closed; omit to run until the chart window is closed or Ctrl+C is pressed.') parser.add_argument('-op', '--option-string', default='', type=str, help='Specifies initialization commands.') args = parser.parse_args(argsv) From 6cbab9f654b7584bb4fadd524bce9dd874634bb3 Mon Sep 17 00:00:00 2001 From: NI-velmurugan Date: Mon, 26 Oct 2026 05:19:11 +0000 Subject: [PATCH 3/8] Trigger CI build From 005888be6f93f29ed3e0d8e1b6c7549ebdc74338 Mon Sep 17 00:00:00 2001 From: NI-velmurugan Date: Mon, 26 Oct 2026 07:02:14 +0000 Subject: [PATCH 4/8] Updating the graph label and order --- src/niscope/examples/niscope_digital_filtering.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/niscope/examples/niscope_digital_filtering.py b/src/niscope/examples/niscope_digital_filtering.py index ee18b9f5ef..1d9f4f9323 100644 --- a/src/niscope/examples/niscope_digital_filtering.py +++ b/src/niscope/examples/niscope_digital_filtering.py @@ -19,13 +19,13 @@ def _create_chart(plt): - figure, (waveform_axes, spectrum_axes) = plt.subplots(2, 1) - waveform_axes.set_title('Filtered Waveform') - waveform_axes.set_xlabel('Time (s)') - waveform_axes.set_ylabel('Amplitude') + figure, (spectrum_axes, waveform_axes) = plt.subplots(2, 1) spectrum_axes.set_title('Spectrum') spectrum_axes.set_xlabel('Frequency (Hz)') - spectrum_axes.set_ylabel('Amplitude') + spectrum_axes.set_ylabel('Amplitude (dB)') + waveform_axes.set_title('Filtered Waveform') + waveform_axes.set_xlabel('Time (s)') + waveform_axes.set_ylabel('Amplitude (V)') for axes in (waveform_axes, spectrum_axes): axes.grid(True) figure.tight_layout() From 33f169020a15be94f96d2b1ad5de097735d44e40 Mon Sep 17 00:00:00 2001 From: NI-velmurugan Date: Mon, 26 Oct 2026 07:14:10 +0000 Subject: [PATCH 5/8] Trigger CI build From 31cef6d7b5ab2cdb4b7b945093317d64b5529909 Mon Sep 17 00:00:00 2001 From: NI-velmurugan Date: Mon, 26 Oct 2026 09:05:08 +0000 Subject: [PATCH 6/8] Trigger CI build From b1831f44a4301eca856b55692bf7e3ad9ae0bfb6 Mon Sep 17 00:00:00 2001 From: NI-velmurugan Date: Mon, 26 Oct 2026 10:12:40 +0000 Subject: [PATCH 7/8] Trigger CI build From f09fa96255c3ff98f9ccea6232259711c8661a15 Mon Sep 17 00:00:00 2001 From: NI-velmurugan Date: Mon, 26 Oct 2026 12:36:45 +0000 Subject: [PATCH 8/8] Trigger CI build