Automated Domain Writing#
\(_{Yongtao}\) \(_{Liu,}\) \(_{liuy3@ornl.gov,}\) \(_{youngtaoliu@gmail.com}\)
\(_{July}\) \(_{2026}\)
Note
aecroscopywave version 0.1.20.
Overview#
This notebook demonstrates the Domain Writing workflow introduced in this chapter, in three variations. All three share one connection to Igor Pro / PyScanner (pycy, pyvi, set up once in Setup below); each section then configures its own DAQ settings and waveform before pulsing.
Pulse at Center with Backswitch — pulses of varying amplitude and duration are applied at a single, fixed tip location, with a backswitch pulse and a re-centering move between each one.
Grid Pulsing — the same pulse is applied across a grid of tip locations, moved between with pixel-based coordinates.
Grid Pulsing with Cypher Image — grid pulsing driven from a full Cypher image: pixel coordinates are converted to piezo voltages, and each pulsed location is imaged individually before returning to the full frame.
Setup#
Import#
import numpy as np
import matplotlib.pyplot as plt
import time
from aecroscopywave.wavebuilding import WaveGenerator
from aecroscopywave.interfaces import WaveVI, Cypher
Initialize an instance of PyCypher#
Connects to the running Igor Pro session via COM — Igor Pro and the Cypher AFM software must already be open. This connection (pycy) is reused by all three sections below.
pycy = Cypher.PyCypher()
# Test executing Igor Pro commands; you should see messages in Igor Pro's command window
pycy.igor.Execute("Print \"Igor Execute: Hello from Igor Pro!\"")
pycy.Execute("Print \"Execute: Hello from Igor Pro!\"")
Initialize an instance of PyWaveVI#
Connects to the LabVIEW-based PyScanner application that drives the NI-6124 DAQ card. Update _path to match the PyScanner executable’s location on your machine. Like pycy above, this connection (pyvi) is reused by all three sections — each section only reconfigures the DAQ settings it needs.
_path = r'C:\Users\PyScanner_FPGA_6124_01\PyScanner_FPGA_6124_01.exe'
pyvi = WaveVI.PyWaveVI(vi_path = _path)
1. Pulse at Center with Backswitch#
Configure DAQ#
This section’s DAQ output uses amplifier stage 0.
# Set DAQ settings for PyWaveVI. It takes the default settings in LabView if no argument is provided
pyvi.set_IO_settings(IO_setting_dict ={"trigger_type": 0, "AO_amplifier": 0, "IO_timeout": 5, "AI_ch01": 1, "AI_ch02": 1, "AI_ch03": 1})
Set a wave#
Before every write pulse in this workflow, a fixed +7 V “backswitch” pulse is applied first to reset the sample to a known polarization state — so each subsequent switching pulse starts from the same baseline rather than whatever state the previous pulse left behind.
t, backswitchwave = WaveGenerator.square_pulse(amplitude = 7, pulse_duration = [0.1, 0.1, 0])
plt.plot(t, backswitchwave)
Upload wave to VI#
Uploads the backswitch waveform to the LabVIEW VI buffer, ready to be sent to the DAQ card.
pyvi.set_AO_waveforms(waveform=backswitchwave, zero_tail=False)
Check uploaded wave#
Reads back the waveform currently sitting in the VI buffer, as a sanity check before executing it.
uploadedwave = pyvi.get_AO_waveforms()
plt.plot(uploadedwave)
Execute wave#
Clears, uploads, and triggers the waveform on the NI-6124 card in one call — this is what actually outputs the backswitch pulse to the sample.
pyvi.set_IO_control(clear = True, upload = True, do_IO = True, fetch_result = False)
Set zero wave#
Defines a 0 V wave and immediately executes it, returning the output to a neutral baseline between pulses.
t, setzero = WaveGenerator.square_pulse(amplitude = 0, pulse_duration = [0.1, 0.1, 0.1])
plt.plot(t, setzero)
pyvi.set_AO_waveforms(waveform=setzero, zero_tail=False)
pyvi.set_IO_control(clear = True, upload = True, do_IO = True, fetch_result = False)
Move tip#
Moves the tip to pixel (255, 255) — a corner of the current scan frame — as a safe starting position before pulsing begins.
# Initialize tip movement
pycy.initialize_move_tip()
# Move tip
pycy.move_tip(coordinates = [255, 255], coordinates_type="pixel", transit_time = 0.5)
Pulsing#
Rather than pulsing at different spatial locations, this experiment sweeps two pulse parameters together: amplitude (pulse_v, -3 to -9 V) and duration (pulse_d, 0.05 to 1 s). np.meshgrid builds every amplitude/duration combination (num_x × num_y = 36 total), so the effect of each pulse condition can be compared side by side.
# Pulse
num_x = 6
num_y = 6
start_pulse_v = -3 # Define location array parameters
end_pulse_v = -9
start_pulse_d = 0.05
end_pulse_d = 1
# Generate location array
pulse_v = np.linspace(start_pulse_v, end_pulse_v, num_x, dtype = float)
pulse_d = np.linspace(start_pulse_d, end_pulse_d, num_y, dtype = float)
pulse = np.meshgrid(pulse_v, pulse_d)
pulse_v = pulse[0].reshape(-1)
pulse_d = pulse[1].reshape(-1) # pulse_pos_x and pulse_pos_y are the coordinates of all locations
print (pulse_v)
print(pulse_d)
Records the current frame center (CenterX, CenterY) so the final full-frame image can be re-centered on it after all the pulsing is done.
metadata = pycy.get_MasterVariables()
metadata
CenterX, CenterY = metadata['XOffset'], metadata['YOffset']
Start grid pulsing#
For each of the 36 amplitude/duration pairs: upload and execute the backswitch pulse while imaging at zero drive amplitude, zero the output, apply that pair’s write pulse, then re-center the tip and image the result at full drive amplitude. Repeating this for every combination builds a set of switching events under different pulse conditions, side by side in one frame.
# Initialize tip movement
pycy.initialize_move_tip()
pycy.engage()
for i in range(len(pulse_v)):
# Upload backswitchwave and execute
pyvi.set_AO_waveforms(waveform=backswitchwave, zero_tail=False)
pyvi.set_IO_control(clear = True, upload = True, do_IO = True, fetch_result = False)
# Backswitch Scan
pycy.set_Master_Panel(parameters={ 'DriveAmplitude': 0, "ScanSize": 5e-7}, do_scan="Frame Up")
time.sleep(1)
# Upload setzero and execute
pyvi.set_AO_waveforms(waveform=setzero, zero_tail=False)
pyvi.set_IO_control(clear = True, upload = True, do_IO = True, fetch_result = False)
time.sleep(1)
#####################---Move tip to the pulse location---#####################
_, square_wave = WaveGenerator.square_pulse(amplitude = pulse_v[i], pulse_duration = [0.2, pulse_d[i], 0.2])
pyvi.set_AO_waveforms(waveform=square_wave, zero_tail=False)
time.sleep(1)
pycy.initialize_move_tip()
time.sleep(1)
pycy.engage()
time.sleep(0.5)
# Loc center
pycy.move_tip(coordinates = [32, 32], coordinates_type="pixel", transit_time = 0.5)
time.sleep(1)
pyvi.set_IO_control(clear = True, upload = True, do_IO = True, fetch_result = False)
time.sleep(1)
#####################---Set image parameter, particular center offset---#####################
pycy.set_Master_Panel(parameters={ 'DriveAmplitude': 1, "ScanSize": 5e-7}, do_scan="Frame Up")
With pulsing complete, image the whole written region at higher resolution (512×512), centered back on the original frame.
params = { 'DriveAmplitude': 1, "ScanSize": 1e-6, "ScanPoints": 512, "ScanLines": 512, "ScanRate": 1,
'XOffset': CenterX, 'YOffset': CenterY}
pycy.set_Master_Panel(parameters=params, do_scan="Frame Up")
Save Data#
Save the pulse parameter sweep and the final AFM state for later analysis.
np.savez('pulse.npz', pulse_v = pulse_v, pulse_d = pulse_d)
import json
metadata = pycy.get_MasterVariables()
# Save
with open("metadata.json", "w") as f:
json.dump(metadata, f)
2. Grid Pulsing#
Configure DAQ#
This run uses a different amplifier stage (AO_amplifier: 2) than Sections 1 and 3.
# Set DAQ settings for PyWaveVI. It takes the default settings in LabView if no argument is provided
pyvi.set_IO_settings(IO_setting_dict ={"trigger_type": 0, "AO_amplifier": 2, "IO_timeout": 5, "AI_ch01": 1, "AI_ch02": 1, "AI_ch03": 1})
Set a wave#
A preliminary -1 V pulse used to verify the upload/check/execute pipeline works end-to-end. It’s superseded by the actual experiment pulse defined in “Set wave” below, under Grid Pulsing.
t, square_wave = WaveGenerator.square_pulse(amplitude = -1, pulse_duration = [0.2, 5, 0.2])
plt.plot(t, square_wave)
Upload wave to VI#
Uploads the preliminary test pulse to the VI buffer.
pyvi.set_AO_waveforms(waveform=square_wave, zero_tail=False)
Check uploaded wave#
Reads it back as a sanity check.
uploadedwave = pyvi.get_AO_waveforms()
plt.plot(uploadedwave)
Execute wave#
Fires the preliminary test pulse to confirm the pipeline works end-to-end.
pyvi.set_IO_control(clear = True, upload = True, do_IO = True, fetch_result = False)
Move tip#
Moves the tip to pixel (255, 255) as a starting position before grid pulsing begins.
# Initialize tip movement
pycy.initialize_move_tip()
# Move tip
pycy.move_tip(coordinates = [255, 255], coordinates_type="pixel", transit_time = 0.5)
# Engage tip or withdraw
pycy.engage()
# pycy.withdraw()
Grid Pulsing#
Set grid locations#
Builds a 20×20 grid of pixel coordinates spanning the full frame (0–255 in both axes) — 400 tip locations to visit, one pulse per location.
# All locations span across [start_point_x, end_point_x] in x-direction and [start_point_y, end_point_y] in y-direction.
# There are num_x rows and num_y columns in the locations array
start_point_x = 0 # Define location array parameters
end_point_x = 255
start_point_y = 0
end_point_y = 255
num_x = 20
num_y = 20
# Generate location array
pos_x = np.linspace(start_point_x, end_point_x, num_x, dtype = int)
pos_y = np.linspace(start_point_y, end_point_y, num_y, dtype = int)
pulse_pos = np.meshgrid(pos_x, pos_y)
pulse_pos_x = pulse_pos[0].reshape(-1)
pulse_pos_y = pulse_pos[1].reshape(-1) # pulse_pos_x and pulse_pos_y are the coordinates of all locations
Set wave#
This -3.5 V, 5 s pulse is the one actually applied at every grid location below.
t, square_wave = WaveGenerator.square_pulse(amplitude = -3.5, pulse_duration = [0.2, 5, 0.2])
plt.plot(t, square_wave)
Upload wave to VI#
pyvi.set_AO_waveforms(waveform=square_wave, zero_tail=False)
Upload wave to DAQ#
Uploads and clears in preparation, but doesn’t trigger yet (do_IO=False) — the pulse fires once per grid location inside the loop below instead.
pyvi.set_IO_control(clear = True, upload = True, do_IO = False, fetch_result = False)
Engage tip#
pycy.engage()
Start grid pulsing#
Moves the tip to each of the 400 grid locations in turn and re-triggers the already-uploaded pulse at each one (do_IO=True, upload=False) — since the waveform doesn’t change between locations, it only needs to be uploaded once, above.
for i in range(len(pulse_pos_x)):
#####################---Move tip to the pulse location---#####################
pycy.move_tip(coordinates = [pulse_pos_x[i], pulse_pos_y[i]], coordinates_type="pixel", transit_time = 0.5)
time.sleep(0.5)
#####################---Apply pulse---#####################
pyvi.set_IO_control(clear = False, upload = False, do_IO = True, fetch_result = False)
time.sleep(1)
3. Grid Pulsing with Cypher Image#
Configure DAQ#
Back to amplifier stage 0, as in Section 1.
# Set DAQ settings for PyWaveVI. It takes the default settings in LabView if no argument is provided
pyvi.set_IO_settings(IO_setting_dict ={"trigger_type": 0, "AO_amplifier": 0, "IO_timeout": 5, "AI_ch01": 1, "AI_ch02": 1, "AI_ch03": 1})
Set a wave#
A +7 V backswitch pulse, executed once below to reset the whole sample to a known state before the grid starts — unlike Section 1, this section does not repeat the backswitch between individual pulses.
t, backswitchwave = WaveGenerator.square_pulse(amplitude = 7, pulse_duration = [0.1, 0.1, 0])
plt.plot(t, backswitchwave)
Upload wave to VI#
Uploads the backswitch waveform, ready to execute.
pyvi.set_AO_waveforms(waveform=backswitchwave, zero_tail=False)
Check uploaded wave#
Reads back the buffered waveform as a sanity check.
uploadedwave = pyvi.get_AO_waveforms()
plt.plot(uploadedwave)
Execute wave#
Fires the one-time backswitch pulse.
pyvi.set_IO_control(clear = True, upload = True, do_IO = True, fetch_result = False)
Set zero wave#
Defines and immediately executes a 0 V wave, returning the output to baseline after the backswitch pulse.
_, setzero = WaveGenerator.square_pulse(amplitude = 0, pulse_duration = [0.1, 0.1, 0.1])
pyvi.set_AO_waveforms(waveform=setzero, zero_tail=False)
pyvi.set_IO_control(clear = True, upload = True, do_IO = True, fetch_result = False)
Move tip#
Moves the tip to pixel (255, 255) as a starting position before the grid pulsing sequence begins.
# Initialize tip movement
pycy.initialize_move_tip()
# Move tip
pycy.move_tip(coordinates = [255, 255], coordinates_type="pixel", transit_time = 0.5)
Grid Pulsing#
Set grid locations#
An 11×11 grid (121 locations) this time — coarser than Section 2’s 20×20, since each location here will also be individually imaged, which takes considerably longer per point.
# All locations span across [start_point_x, end_point_x] in x-direction and [start_point_y, end_point_y] in y-direction.
# There are num_x rows and num_y columns in the locations array
start_point_x = 0 # Define location array parameters
end_point_x = 255
start_point_y = 0
end_point_y = 255
num_x = 11
num_y = 11
# Generate location array
pos_x = np.linspace(start_point_x, end_point_x, num_x, dtype = int)
pos_y = np.linspace(start_point_y, end_point_y, num_y, dtype = int)
pulse_pos = np.meshgrid(pos_x, pos_y)
pulse_pos_x = pulse_pos[0].reshape(-1)
pulse_pos_y = pulse_pos[1].reshape(-1) # pulse_pos_x and pulse_pos_y are the coordinates of all locations
Set pulse parameters#
As in Section 1, amplitude and duration are swept together over the same range — but here indexed by the same 11×11 grid used for tip positions, so each grid location gets its own distinct pulse condition rather than every location sharing one fixed pulse.
# Pulse
start_pulse_v = -3 # Define location array parameters
end_pulse_v = -9
start_pulse_d = 0.05
end_pulse_d = 1.0
# Generate location array
pulse_v = np.linspace(start_pulse_v, end_pulse_v, num_x, dtype = float)
pulse_d = np.linspace(start_pulse_d, end_pulse_d, num_y, dtype = float)
pulse = np.meshgrid(pulse_v, pulse_d)
pulse_v = pulse[0].reshape(-1)
pulse_d = pulse[1].reshape(-1) # pulse_pos_x and pulse_pos_y are the coordinates of all locations
print (pulse_v)
print(pulse_d)
Convert pixel coordinates to voltages#
Unlike Sections 1–2, tip moves in this section are specified directly in piezo voltage rather than pixel coordinates (see the loop below) — pixel_to_voltage() converts each grid location once, up front, so the loop doesn’t have to redo this conversion on every iteration.
pulse_pos_voltages = [] # Convert pixel coordinates to piezo voltages
for i in range(len(pulse_pos_x)):
pulse_pos_voltages.append(pycy.pixel_to_voltage(pixel_coordinates=[pulse_pos_x[i], pulse_pos_y[i]]))
pulse_pos_voltages
Generate grid points#
Defines a helper that lays out physical (voltage-unit) imaging positions for the per-location scans below — independent of, though matched in shape to, the pulse grid above.
from typing import Tuple
def generate_grid_points(
center_offset: Tuple[float, float],
img_size: float,
grid_shape: Tuple[int, int] = (10, 10),
margin: float = 0.0001
) -> np.ndarray:
"""
Generate a uniform grid of points within the current image area.
Parameters
----------
center_offset : Tuple[float, float]
(x, y) offset of the image center in voltage units.
img_size : float
Side length of the square image area in voltage units.
grid_shape : Tuple[int, int], optional
Number of grid points as (rows, cols). Default is (10, 10).
margin : float, optional
Fractional margin to inset from image edges (0.0 = edge-to-edge,
0.1 = 10% inset on each side). Default is 0.0.
Returns
-------
np.ndarray, shape (N, 2)
Array of (x, y) voltage coordinates for each grid point,
ordered row by row (left-to-right, bottom-to-top).
Examples
--------
>>> pts = generate_grid_points(center_offset=(1.0, -0.5), img_size=2.0)
>>> pts.shape
(100, 2)
"""
cx, cy = center_offset
half = (img_size / 2) * (1.0 - margin)
x_coords = np.linspace(cx - half, cx + half, grid_shape[0])
y_coords = np.linspace(cy - half, cy + half, grid_shape[1])
xx, yy = np.meshgrid(x_coords, y_coords)
# Flatten to (N, 2) array of (x, y) pairs
grid_points = np.column_stack([xx.ravel(), yy.ravel()])
return grid_points
Records the current frame center and uses it to build the physical grid of per-location imaging positions (pts).
metadata = pycy.get_MasterVariables()
metadata
CenterX, CenterY = metadata['XOffset'], metadata['YOffset']
pts = generate_grid_points(center_offset=(metadata['XOffset'], metadata['YOffset']), grid_shape=(num_x, num_y), img_size=4e-6)
pts.shape
Start grid pulsing#
For each of the 121 grid locations: drop the drive amplitude to zero and move the tip there in voltage units, re-engage and apply that location’s pulse, then capture a small, fast image (64×64, 350 nm) centered exactly on that spot. Unlike Sections 1–2, this builds up one image per pulsed location rather than a single full-frame image at the end.
# Initialize tip movement
pycy.initialize_move_tip()
pycy.engage()
for i in range(len(pulse_pos_x)):
#####################---Move tip to the pulse location---#####################
pycy.PV(parameters = {'DriveAmplitude': 0})
time.sleep(2)
pycy.initialize_move_tip()
time.sleep(1)
pycy.move_tip(coordinates = [pulse_pos_voltages[i][0], pulse_pos_voltages[i][1]], coordinates_type="voltage", transit_time = 0.5)
time.sleep(0.5)
#####################---Apply pulse---#####################
pycy.engage()
_, square_wave = WaveGenerator.square_pulse(amplitude = pulse_v[i], pulse_duration = [0.2, pulse_d[i], 0.2])
pyvi.set_AO_waveforms(waveform=square_wave, zero_tail=False)
time.sleep(1)
pyvi.set_IO_control(clear = True, upload = True, do_IO = True, fetch_result = False)
time.sleep(1)
#####################---Set image parameter, particular center offset---#####################
params = { 'DriveAmplitude': 1, "ScanSize": 3.5e-7, "ScanPoints": 64, "ScanLines": 64, "ScanRate": 1,
'XOffset': pts[i,0], 'YOffset': pts[i,1]}
pycy.set_Master_Panel(parameters=params, do_scan="Frame Up")
Finally, image the complete written region at the original scan size, re-centered on the frame’s starting position (do_scan="Frame Down" this time, rather than “Frame Up”).
params = { 'DriveAmplitude': 1, "ScanSize": 5e-6, "ScanPoints": 512, "ScanLines": 512, "ScanRate": 1,
'XOffset': CenterX, 'YOffset': CenterY}
pycy.set_Master_Panel(parameters=params, do_scan="Frame Down")
Save Data#
As in Section 1, save the pulse parameter sweep and the final AFM state for later analysis.
np.savez('pulse.npz', pulse_v = pulse_v, pulse_d = pulse_d)
import json
metadata = pycy.get_MasterVariables()
# Save
with open("metadata.json", "w") as f:
json.dump(metadata, f)