# VirtualScope

## class VirtualScope( num_channels: int = 4, signals: Mapping[int, SineWave | SquareWave] | None = None, )[source]

Bases: [`ScopeDriverBase`](https://dev.nominal.io/instro/ref/instruments/instro.scope.ScopeDriverBase.md#instro.scope.ScopeDriverBase "instro.scope.scope.ScopeDriverBase")

Oscilloscope simulated in-process: no transport, server, or hardware.

Every channel sees a signal from the start: a 1 kHz, 1 V peak [`SineWave`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.SineWave.md#instro.scope.drivers.virtual.SineWave "instro.scope.drivers.virtual.SineWave") on odd channels
(channel 1, 3, …) and a 1 kHz, 1 V peak [`SquareWave`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.SquareWave.md#instro.scope.drivers.virtual.SquareWave "instro.scope.drivers.virtual.SquareWave") on even channels (2, 4, …). A
capture is 1000 points across 10 horizontal divisions centered on the trigger, where the
trigger source crosses the trigger level on the configured slope. A sine whose peaks miss the
level aligns to its nearest peak; a square triggers on its edges at any level. Gaussian noise
of 0.01 division is added, reduced by the square root of the average count in `AVERAGE`
mode, and AC coupling removes the signal’s offset. Like an ADC, a capture clips at the screen
edges: the vertical offset is the voltage at screen center, and the screen spans 4 divisions
of the vertical scale above and below it.

While running, every fetch and measurement reads a fresh capture. `stop`, `single`, and
`digitize` capture every channel at once, as a real acquisition does, and later fetches and
measurements read that capture until the scope runs again, whatever settings or signals
change meanwhile. Voltage measurements come from the same acquisition, clipped the same way,
but sampled at 100 points per cycle over at most 10 cycles, so a slow timebase reads the
signal’s true levels, as a real scope’s full-resolution record does, even when the 1000
fetched points alias. Frequency, period, and duty cycle come from the signal the capture was
taken from. Every setting reads back as set.

It simulates driving a scope from a script or notebook, so it has no screen or storage:
[`save_screenshot`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.save_screenshot "instro.scope.drivers.virtual.VirtualScope.save_screenshot"), [`save_settings`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.save_settings "instro.scope.drivers.virtual.VirtualScope.save_settings"), and [`load_settings`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.load_settings "instro.scope.drivers.virtual.VirtualScope.load_settings") raise `NotImplementedError`.

Create a virtual scope.

Parameters:
:   - **num_channels** ([*int*](https://docs.python.org/3/builtins/functions.html#int "(in Python v3.15)"))

      Number of analog-input channels, numbered from 1. Pass the same
      count to `InstroScope`; a channel above it raises `ValueError`.
    - **signals** (*Mapping**[*[*int*](https://docs.python.org/3/builtins/functions.html#int "(in Python v3.15)")*,* [*SineWave*](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.SineWave.md#instro.scope.drivers.virtual.SineWave "instro.scope.drivers.virtual.SineWave") *|* [*SquareWave*](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.SquareWave.md#instro.scope.drivers.virtual.SquareWave "instro.scope.drivers.virtual.SquareWave")*]* *|* *None*)

      The signal on each listed
      channel. Channels left out see the default: a sine on odd channels, a square on
      even ones. Change a channel’s signal at any time with [`set_signal`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.set_signal "instro.scope.drivers.virtual.VirtualScope.set_signal").

Raises:
:   [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError "(in Python v3.15)") – `num_channels` is less than 1, or `signals` names a channel that does
    not exist.

Example:

```
from instro.scope import InstroScope, ScopeMeasurementType
from instro.scope.drivers import VirtualScope
from instro.scope.drivers.virtual import SineWave

driver = VirtualScope(num_channels=4, signals={3: SineWave(frequency=5e3, amplitude=0.5)})
with InstroScope(name="scope", driver=driver, num_channels=4) as scope:
    scope.single()
    print(scope.measure(ScopeMeasurementType.VRMS, channel=1).latest)  # about 0.71: sine
    print(scope.measure(ScopeMeasurementType.VRMS, channel=2).latest)  # about 1.0: square
    print(scope.measure(ScopeMeasurementType.FREQUENCY, channel=3).latest)  # 5000.0
```

Methods

| [`check_errors`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.check_errors "instro.scope.drivers.virtual.VirtualScope.check_errors") | No error queue; invalid settings, including NaN or infinite values, raise `ValueError` when set. |
| --- | --- |
| [`close`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.close "instro.scope.drivers.virtual.VirtualScope.close") | Nothing to disconnect. |
| [`digitize`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.digitize "instro.scope.drivers.virtual.VirtualScope.digitize") | Take one capture and stop; never times out, since the simulated signal always triggers. |
| [`fetch_waveform`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.fetch_waveform "instro.scope.drivers.virtual.VirtualScope.fetch_waveform") | A copy of the capture, so editing the returned samples leaves later reads unchanged. |
| [`force_trigger`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.force_trigger "instro.scope.drivers.virtual.VirtualScope.force_trigger") | Nothing to force: the simulated signal always triggers. |
| [`get_acquisition_mode`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.get_acquisition_mode "instro.scope.drivers.virtual.VirtualScope.get_acquisition_mode") | Read back the current acquisition mode. |
| [`get_acquisition_state`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.get_acquisition_state "instro.scope.drivers.virtual.VirtualScope.get_acquisition_state") | Read back the acquisition run state (RUNNING / STOPPED). |
| [`get_average_count`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.get_average_count "instro.scope.drivers.virtual.VirtualScope.get_average_count") | Read back the waveforms-to-average count. |
| [`get_coupling`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.get_coupling "instro.scope.drivers.virtual.VirtualScope.get_coupling") | Read back input coupling on `channel`. |
| [`get_horizontal_scale`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.get_horizontal_scale "instro.scope.drivers.virtual.VirtualScope.get_horizontal_scale") | Read back the timebase (seconds/div). |
| [`get_probe_attenuation`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.get_probe_attenuation "instro.scope.drivers.virtual.VirtualScope.get_probe_attenuation") | Read back `channel`'s probe attenuation ratio. |
| [`get_sample_rate`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.get_sample_rate "instro.scope.drivers.virtual.VirtualScope.get_sample_rate") | 1000 points across the 10-division window. |
| [`get_signal`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.get_signal "instro.scope.drivers.virtual.VirtualScope.get_signal") | Return the signal `channel` sees. |
| [`get_trigger_status`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.get_trigger_status "instro.scope.drivers.virtual.VirtualScope.get_trigger_status") | Read back the trigger status (ARMED / READY / TRIGGERED / …). |
| [`get_vertical_offset`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.get_vertical_offset "instro.scope.drivers.virtual.VirtualScope.get_vertical_offset") | Read back `channel`'s vertical offset (volts). |
| [`get_vertical_scale`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.get_vertical_scale "instro.scope.drivers.virtual.VirtualScope.get_vertical_scale") | Read back `channel`'s vertical scale (V/div). |
| [`load_settings`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.load_settings "instro.scope.drivers.virtual.VirtualScope.load_settings") | Not supported: the virtual scope has no setup storage. |
| [`measure`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.measure "instro.scope.drivers.virtual.VirtualScope.measure") | Voltages from the capture; `NaN` for frequency, period, and duty cycle on a signal with no edges. |
| [`open`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.open "instro.scope.drivers.virtual.VirtualScope.open") | Nothing to connect to. |
| [`run`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.run "instro.scope.drivers.virtual.VirtualScope.run") | Free-run: every fetch and measurement reads a fresh capture. |
| [`save_screenshot`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.save_screenshot "instro.scope.drivers.virtual.VirtualScope.save_screenshot") | Not supported: the virtual scope has no screen to capture. |
| [`save_settings`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.save_settings "instro.scope.drivers.virtual.VirtualScope.save_settings") | Not supported: the virtual scope has no setup storage. |
| [`set_acquisition_mode`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.set_acquisition_mode "instro.scope.drivers.virtual.VirtualScope.set_acquisition_mode") | Accept any mode; only `AVERAGE` changes the capture, by reducing its noise. |
| [`set_average_count`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.set_average_count "instro.scope.drivers.virtual.VirtualScope.set_average_count") | Set the number of waveforms to average. |
| [`set_coupling`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.set_coupling "instro.scope.drivers.virtual.VirtualScope.set_coupling") | Set AC/DC input coupling on `channel`. |
| [`set_horizontal_scale`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.set_horizontal_scale "instro.scope.drivers.virtual.VirtualScope.set_horizontal_scale") | Set the timebase to `seconds_per_div`. |
| [`set_probe_attenuation`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.set_probe_attenuation "instro.scope.drivers.virtual.VirtualScope.set_probe_attenuation") | Accepted and read back; waveforms are already referred to the probe tip. |
| [`set_signal`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.set_signal "instro.scope.drivers.virtual.VirtualScope.set_signal") | Set the signal `channel` sees; a stopped scope keeps showing its last capture. |
| [`set_trigger_level`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.set_trigger_level "instro.scope.drivers.virtual.VirtualScope.set_trigger_level") | Set the trigger threshold to `level` (volts). |
| [`set_trigger_mode`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.set_trigger_mode "instro.scope.drivers.virtual.VirtualScope.set_trigger_mode") | Accept either mode; the simulated signal always triggers. |
| [`set_trigger_slope`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.set_trigger_slope "instro.scope.drivers.virtual.VirtualScope.set_trigger_slope") | `EITHER` aligns on the rising edge. |
| [`set_trigger_source`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.set_trigger_source "instro.scope.drivers.virtual.VirtualScope.set_trigger_source") | Set the trigger source to analog `channel`. |
| [`set_trigger_type`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.set_trigger_type "instro.scope.drivers.virtual.VirtualScope.set_trigger_type") | Accept any type; captures align as an edge trigger. |
| [`set_vertical_offset`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.set_vertical_offset "instro.scope.drivers.virtual.VirtualScope.set_vertical_offset") | Set the voltage at screen center; captures clip 4 divisions above and below it. |
| [`set_vertical_scale`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.set_vertical_scale "instro.scope.drivers.virtual.VirtualScope.set_vertical_scale") | Set `channel`'s vertical scale to `volts_per_div` (V/div). |
| [`setup_measurement`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.setup_measurement "instro.scope.drivers.virtual.VirtualScope.setup_measurement") | Ensure a measurement slot exists for `measurement_type`/`channel` before the scope triggers. |
| [`single`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.single "instro.scope.drivers.virtual.VirtualScope.single") | Capture every channel at once and stop; the simulated signal always triggers. |
| [`stop`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.stop "instro.scope.drivers.virtual.VirtualScope.stop") | Freeze every channel's capture for later fetches and measurements; a stopped scope keeps its own. |

### set_signal( signal: SineWave | SquareWave, channel: int, ) → None[source]

Set the signal `channel` sees; a stopped scope keeps showing its last capture.

Parameters:
:   - **signal** ([*SineWave*](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.SineWave.md#instro.scope.drivers.virtual.SineWave "instro.scope.drivers.virtual.SineWave") *|* [*SquareWave*](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.SquareWave.md#instro.scope.drivers.virtual.SquareWave "instro.scope.drivers.virtual.SquareWave"))

      The signal at the probe tip.
    - **channel** ([*int*](https://docs.python.org/3/builtins/functions.html#int "(in Python v3.15)"))

      Target channel (1-indexed).

Raises:
:   [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError "(in Python v3.15)") – `channel` does not exist.

### get_signal( channel: int, ) → SineWave | SquareWave[source]

Return the signal `channel` sees.

### open() → None[source]

Nothing to connect to.

### close() → None[source]

Nothing to disconnect.

### check_errors() → None[source]

No error queue; invalid settings, including NaN or infinite values, raise `ValueError` when set.

### set_vertical_scale( volts_per_div: float, channel: int, ) → None[source]

Set `channel`’s vertical scale to `volts_per_div` (V/div).

Drivers may snap to the nearest hardware-supported step. Callers that
need the actual applied value should `get_vertical_scale()` afterward.

### get_vertical_scale(channel: int) → float[source]

Read back `channel`’s vertical scale (V/div).

### set_vertical_offset(offset: float, channel: int) → None[source]

Set the voltage at screen center; captures clip 4 divisions above and below it.

### get_vertical_offset(channel: int) → float[source]

Read back `channel`’s vertical offset (volts).

### set_coupling( coupling: Coupling, channel: int, ) → None[source]

Set AC/DC input coupling on `channel`.

### get_coupling(channel: int) → Coupling[source]

Read back input coupling on `channel`.

### set_probe_attenuation(factor: float, channel: int) → None[source]

Accepted and read back; waveforms are already referred to the probe tip.

### get_probe_attenuation(channel: int) → float[source]

Read back `channel`’s probe attenuation ratio.

### set_horizontal_scale(seconds_per_div: float) → None[source]

Set the timebase to `seconds_per_div`. Applies globally to all channels.

### get_horizontal_scale() → float[source]

Read back the timebase (seconds/div).

### get_sample_rate() → float[source]

1000 points across the 10-division window.

### set_acquisition_mode( mode: AcquisitionMode, ) → None[source]

Accept any mode; only `AVERAGE` changes the capture, by reducing its noise.

### get_acquisition_mode() → AcquisitionMode[source]

Read back the current acquisition mode.

### set_average_count(count: int) → None[source]

Set the number of waveforms to average. Only takes effect in `AcquisitionMode.AVERAGE`.

### get_average_count() → int[source]

Read back the waveforms-to-average count.

### run() → None[source]

Free-run: every fetch and measurement reads a fresh capture.

### stop() → None[source]

Freeze every channel’s capture for later fetches and measurements; a stopped scope keeps its own.

### single() → None[source]

Capture every channel at once and stop; the simulated signal always triggers.

### digitize(timeout: float) → None[source]

Take one capture and stop; never times out, since the simulated signal always triggers.

Right after [`single`](https://dev.nominal.io/instro/ref/instruments/instro.scope.drivers.virtual.VirtualScope.md#instro.scope.drivers.virtual.VirtualScope.single "instro.scope.drivers.virtual.VirtualScope.single"), keeps that shot instead of taking another, to work around an
`InstroScope` bug (#716).

### get_acquisition_state() → AcquisitionState[source]

Read back the acquisition run state (RUNNING / STOPPED).

### fetch_waveform( channel: int, ) → WaveformData[source]

A copy of the capture, so editing the returned samples leaves later reads unchanged.

### measure( measurement_type: ScopeMeasurementType, channel: int, ) → float[source]

Voltages from the capture; `NaN` for frequency, period, and duty cycle on a signal with no edges.

### set_trigger_source(channel: int) → None[source]

Set the trigger source to analog `channel`.

Drivers typically cache this value because the trigger-level SCPI on
some scopes requires the source channel in the same command.

### set_trigger_type( trigger_type: TriggerType, ) → None[source]

Accept any type; captures align as an edge trigger.

### set_trigger_level(level: float) → None[source]

Set the trigger threshold to `level` (volts). Applies to the configured trigger source.

### set_trigger_slope( slope: TriggerSlope, ) → None[source]

`EITHER` aligns on the rising edge.

### set_trigger_mode( mode: TriggerMode, ) → None[source]

Accept either mode; the simulated signal always triggers.

### force_trigger() → None[source]

Nothing to force: the simulated signal always triggers.

### get_trigger_status() → TriggerStatus[source]

Read back the trigger status (ARMED / READY / TRIGGERED / …).

### save_screenshot( filepath: str, to_instrument: bool = False, ) → bytes[source]

Not supported: the virtual scope has no screen to capture.

### save_settings( name: str, to_instrument: bool = False, ) → bytes[source]

Not supported: the virtual scope has no setup storage.

### load_settings( name: str, from_instrument: bool = False, ) → None[source]

Not supported: the virtual scope has no setup storage.

### setup_measurement( measurement_type: ScopeMeasurementType, channel: int, ) → None

Ensure a measurement slot exists for `measurement_type`/`channel` before the scope triggers.

Required for instruments (e.g. Tektronix) that compute measurements
during acquisition — the slot must be present at trigger time or the
first `measure()` returns stale/invalid data. Default is a no-op for
instruments (e.g. Keysight 1200X) that compute on demand.
