QPolargraph
A Qt-based instrument interface for a two-dimensional polargraph scanner. Built on QInstrument.
A polargraph translates a payload to any position within its scan area using two stepper motors and a timing belt. Originally developed by Jürg Lehni and Uli Franke as a drawing machine, the design scales easily to large areas, delivers millimeter-scale positioning accuracy, and is very cost-effective to build.
Hardware
| Component | Description |
|---|---|
| 2× Nema-17 stepper motor | Mounted above and to either side of the scan area |
| GT2 timing belt | Spans both motors; payload hangs from its midpoint |
| GT2 drive gear (25-tooth) | One per motor; engages the belt |
| Adafruit Motor Shield v2 | Drives both steppers from a single Arduino |
| Arduino (Uno or compatible) | Runs the acam3 firmware; connects to the host via USB |
The belt forms a V-shape between the two motors. Each motor changes the length of one side of the V, moving the payload in two dimensions.
Firmware
The Arduino must be flashed with hardware/arduino/acam3/acam3.ino before first use.
The easiest way is to use the built-in installer:
qpolargraph-flash
This opens a dialog that detects the attached Arduino, installs any missing
Arduino libraries (Adafruit Motor Shield V2 Library, AccelStepper),
compiles, and uploads the firmware — all without opening the Arduino IDE.
It requires arduino-cli to be
installed and on PATH.
Alternatively, open hardware/arduino/acam3/acam3.ino in the
Arduino IDE and upload manually.
The firmware and package versions are coupled: Motors.identify() checks
that the connected Arduino reports the expected firmware version and that
the Adafruit Motor Shield is detected at I2C address 0x60, refusing to
open the port if either check fails.
Installation
Clone the repository and install in editable mode:
git clone https://github.com/davidgrier/QPolargraph
cd QPolargraph
python -m venv .qp
source .qp/bin/activate # Windows: .qp\Scripts\activate
pip install -e ".[dev]"
pip install PyQt6 # or PyQt5, PySide2, PySide6
QInstrument is installed automatically as a dependency.
Calibration
Five geometric parameters describe the polargraph. Set them in the
QPolargraphWidget control panel or pass them directly to Polargraph():
| Parameter | Default | Description |
|---|---|---|
ell |
1.0 m | Horizontal distance between the two motor pulley centres |
y0 |
0.1 m | Vertical distance from the pulleys to the home position |
pitch |
2.0 mm | GT2 belt tooth pitch |
circumference |
25 | Number of belt teeth on the drive gear |
steps |
200 | Motor steps per revolution |
Measure ell and y0 with a ruler after mounting the motors.
pitch, circumference, and steps are determined by the belt and gear
specifications and normally do not need to change.
Settings are saved to ~/.QScanner/ automatically when the application
closes, so calibration only needs to be done once.
Quick start
Scanner application
python -m QPolargraph
# or, after installation:
qpolargraph
The application opens a live plot showing the belt geometry and the scan trajectory. Use the Scan button to start a polar-arc sweep, or Home / Center to move the payload to the home or centre position.
Embedding in your own application
Subclass QScanner to add experiment-specific data acquisition:
from qtpy.QtWidgets import QApplication
from qtpy import QtCore
from QPolargraph.QScanner import QScanner
import sys
class MyScanner(QScanner):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.scanner.dataReady.connect(self.acquire)
@QtCore.Slot(object)
def acquire(self, position):
x, y = position
# measure something at (x, y) and call self.plotData(x, y, hue)
if __name__ == '__main__':
app = QApplication(sys.argv)
scanner = MyScanner()
scanner.show()
sys.exit(app.exec())
Controlling the hardware directly
from qtpy.QtCore import QCoreApplication
from QPolargraph.Polargraph import Polargraph
import sys
app = QCoreApplication(sys.argv)
pg = Polargraph(ell=0.8, y0=0.15).find() # auto-detects USB port
print(pg.position) # (x, y, running) in metres
pg.moveTo(0.0, 0.3)
while pg.running():
pass
pg.release()
Architecture
QInstrument.QSerialInstrument
└── Motors # acam3 serial protocol: goto, speed, position, …
└── Polargraph # geometry: step indexes ↔ Cartesian coordinates
QInstrument.QInstrumentWidget
└── QRasterScanWidget # scan-pattern parameter controls
QInstrument.QInstrumentWidget
└── QPolargraphWidget # polargraph hardware controls
QtCore.QObject
└── QScanPattern # base: rectangular perimeter trajectory
├── RasterScan # row-by-row zigzag raster
└── PolarScan # arc-by-arc sweep centred on the left pulley
QtWidgets.QMainWindow
└── QScanner # full application: live plot + scan controls
Development
Run the test suite:
source .qp/bin/activate
pytest tests/
Tests run automatically before every git push. To install the pre-push
hook in a fresh clone:
git config core.hooksPath .githooks
Acknowledgements
Work on this project at New York University is supported by the National Science Foundation of the United States under award number DMR-2438983.
References
H. W. Gao, K. I. Mishra, A. Winters, S. Wolin, and D. G. Grier, "Flexible wide-field high-resolution scanning camera for continuous-wave acoustic holography," Review of Scientific Instruments 89, 114901 (2018). https://doi.org/10.1063/1.5053666
Metadata
Release files for QPolargraph 1.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| qpolargraph-1.5.0.tar.gz | 45.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| qpolargraph-1.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 88.4 kB
Release files / qpolargraph-1.5.0.tar.gz
| Download URL | qpolargraph-1.5.0.tar.gz |
|---|---|
| Size | 45.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5294b825dbd21dc5c3436ae7c6de962c58c4d26028a09d7f147e3d3b9338897e
|
|
BLAKE2b-256 checksum How to use checksums |
930a5de9bec3bef9f0a716d516c4967a1865dc6566583f908f1215b8439c3efe
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 3, 2026.
Transparency logRelease files / qpolargraph-1.5.0-py3-none-any.whl
| Download URL | qpolargraph-1.5.0-py3-none-any.whl |
|---|---|
| Size | 42.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ed6989b363e8c7e47c95b5d6c226f47df6e6b460ca2c17b3892c8e49103d83ce
|
|
BLAKE2b-256 checksum How to use checksums |
887c921ae3e9e3fa0df2c2211f2ca8dd37b09fc3185fcf06cfeee8f913363aaf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 3, 2026.
Transparency log