Skip to main content

Control library for Genmitsu CNC machines used as low-cost lab automation platforms (liquid handling, fraction collection, vision-based capping, etc.).

Project description

CNC MACHINE CODE

Authors: Owen Melville, Kelvin Chow

Last Updated: 2026-03-18

Overall description

This package can be used to control Genmitsu CNC machines. This is useful for accelerated discovery because you can put your tools onto the CNC machine.

Install from PyPI:

pip install cnc-4-science

Then import cnc_machine_core and use its methods to intuitively and seamlessly move the CNC machine with whatever scientific tools you want to incorporate. See examples/liquid_handling/picus_pipette/ for a full worked example.

Basic Functions:

  • Home CNC machine

  • Move to absolute points (x,y,z)

  • Move to locations defined in a structured way (Eg move to Vial Position 0)

  • Control of the spindle output

  • Handles all gcode and CNC communication so you don't have to

  • Makes sure you don't move the CNC machine to a position it can't go

  • Automatic alarm detection and recovery (re-homes if a limit switch is triggered)

  • Orthogonal waypoint moves for collision avoidance between deck slots

API Reference

Method Description
home() Homes the robot and parks at the origin
origin() Moves the robot to the origin
connect() / close() Open and close serial connection to the CNC
move_to_point(x, y, z) Move to absolute coordinates
move_to_point_safe(x, y, z) Raises Z to clearance first, moves XY, then lowers Z. Prevents collisions with labware
move_to_point_safe_orthogonal(x, y, z, waypoint, axis_order) Moves one axis at a time through waypoints for collision avoidance. Axis orders: yxy, xyx, xyxy, yxyx
move_to_location(location, index) Move to a named location at the given index
spindle_on(speed) Turn on spindle at given RPM (M3)
spindle_off() Turn off spindle (M5)
is_alarm() Returns True if GRBL is in alarm state (e.g. limit switch triggered)
recover_if_alarm() Checks for alarm and auto-homes to recover. Called internally before every move

Deck and Labware

The cnc_deck module provides Well, Labware, and Deck objects for coordinate resolution:

from cnc_machine_core import Deck

deck = Deck()                                        # standard 4-slot deck
plate = deck.load_labware("1", "labware/my_labware.json")  # returns Labware

well = plate["A1"]                                   # Well object
x, y, z = well.position()                            # absolute CNC coordinates
x, y, z = well.position(offset={"x": 6.75, "y": -4.0})  # with tool offset

for well in plate.wells():                           # iterate in ordering
    print(well.name, well.position())

Alternative deck layouts are in deck/ (pass by name or path):

  • cnc_4_slot_deck.json — standard 4-slot (2×2) [default]
  • cnc_1_slot_deck.json — single slot at origin (open deck, no labware required)

Labware definitions are created using the Opentrons Labware Creator. Only the X and Y well coordinates from the Opentrons JSON are used. Z heights are defined per-protocol as calibrated constants, since Z depends on the specific tool and labware combination rather than the labware geometry alone.

Custom labware JSON files go in labware/. See the existing files there for reference.

Direct Positioning (No Labware)

For simpler setups that don't need labware definitions, use the open deck and move directly to absolute coordinates:

from cnc_machine_core import Deck

deck = Deck("cnc_1_slot_deck")           # built-in name; or pass a path to custom JSON
# No labware needed — move to raw coordinates
cnc.move_to_point_safe(x=100, y=50, z=-20)

Alternatively, position arrays can be defined in a YAML file and addressed by index using move_to_location(). This is useful for regular grids where you don't need named wells. Positions are defined in location_status.yaml:

vial_rack:
  num_x: 2          # columns
  num_y: 4          # rows
  x_origin: 166.5   # first position X
  y_origin: 125     # first position Y
  z_origin: 0
  x_offset: 36      # spacing between columns
  y_offset: -36     # spacing between rows

The location index moves through a full column before advancing to the next. Index 0 is at the origin.

image

Z Calibration Helper

A Z calibration script is included at examples/liquid_handling/picus_pipette/z_helper.py. It moves the CNC to a selected slot/well (with tool offset applied), then lets the user step Z up and down at three granularity levels (coarse, medium, fine) to find the correct working height. This is much faster than manually jogging and reading coordinates. Copy it into your own application and update the deck/labware/tool configuration for your setup.

Deck State

The deck_state module tracks per-well status across all deck slots with YAML persistence:

from cnc_machine_core import DeckState

ds = DeckState()
ds.init_wells_from_labware("1", plate)              # from Labware object
ds.init_from_preset({"1": {"A1": "sample"}})         # override specific wells
ds.set_status("1", "A1", "processed")               # update (auto-saves)
loc = ds.find_next(["1", "2"], "sample")             # first match -> ("1", "A2")
ds.count(["1"], "processed")                         # count by status
ds.summary()                                         # print slot breakdown

Status strings are application-defined — use whatever makes sense for your workflow. A sample preset is in examples/liquid_handling/picus_pipette/deck_preset.yaml.

Starting a New Application

After physically setting up the CNC machine, run examples/hello_cnc/hardware_check.py as a sanity check to verify the serial connection, homing, movement, and spindle all work correctly. Edit examples/hello_cnc/cnc_config.yaml to match your COM port and bounds before running:

python examples/hello_cnc/hardware_check.py

Once the hardware check passes, create your application from the starter example at examples/liquid_handling/picus_pipette/ (a full worked example using a Sartorius Picus 2 pipette for serial dilution).

  1. Copy the example — copy examples/liquid_handling/picus_pipette/ to a new repository or directory

  2. Install cnc-4-science — install as a dependency in your project's virtual environment:

    Linux / macOS / Raspberry Pi:

    python3 -m venv .venv
    source .venv/bin/activate
    pip install cnc-4-science
    

    Windows:

    python -m venv .venv
    .\.venv\Scripts\Activate.ps1
    pip install cnc-4-science
    

    Or for local editable development against an unreleased core:

    pip install -e path/to/cnc-machine
    
  3. Configure your CNC — edit tools/cnc_config.yaml (COM port, baud, bounds). Then load it in your script with CNC_Machine.from_config("tools/cnc_config.yaml")

  4. Add your labware — place custom labware JSON files in custom_labware/, or load standard Opentrons labware via opentrons_shared_data.labware.load_definition(...)

  5. Configure each tool — create tools/<tool>_config.yaml per pipette/effector with COM port, mounting offset, Z heights, and any tool-specific parameters

  6. Calibrate Z heights — run python z_helper.py to find working Z for each tool + labware combo; copy results into the tool's <tool>_config.yaml

  7. Validate — set virtual: true in tools/cnc_config.yaml for a dry run, then on hardware

  8. Implement tools — use tools/picus_pipette.py as a pattern for wrapping new tools

Architecture

cnc-4-science (library, on PyPI)         Your Application (copied example)
├── src/cnc_machine_core/                  ├── protocols/
│   ├── cnc_machine.py  (motion)            │   └── serial_dilution_demo.py
│   ├── cnc_deck.py     (deck/wells)        ├── z_helper.py
│   ├── deck_state.py   (state)             ├── deck_preset.yaml
│   ├── deck/           (definitions)       ├── custom_labware/
│   └── labware/        (labware JSON)      ├── tools/
├── examples/                                │   ├── cnc_config.yaml          (CNC + deck layout + Z heights)
│   ├── hello_cnc/                          │   ├── picus_config.yaml        (tool: port, offset, volumes)
│   └── liquid_handling/                     │   ├── picus_pipette.py         (tool wrapper)
└── pyproject.toml                            │   └── picus_driver.py          (vendored low-level driver)
                                              └── requirements.txt  (`cnc-4-science`)
  • Library owns: machine control, deck/labware primitives, standard definitions
  • App owns: concrete tool implementations, calibrated configs, workflow protocols

Tool Contract

Every tool class must follow this interface:

class MyTool:
    def __init__(self, cnc_machine, tool_config):
        self.cnc = cnc_machine
        self.offset = tool_config.get("offset", {"x": 0, "y": 0, "z": 0})
        # extract parameters from tool_config["parameters"]

See examples/liquid_handling/picus_pipette/tools/picus_pipette.py for a working reference implementation.

Advice on Integration with Scientific Instruments

  • Create a separate python file for each tool (camera, force sensor, syringe pump, etc.)

  • Create an instrument class that imports cnc_machine along with the python files for each tool (eg fraction_collector.py)

  • In your instrument class make methods that intuitively describe the general actions of your instrument (eg dispense_fraction)

  • Make your workflows in seperate python files or Jupyter notebook files that create an instance of your instrument class

  • This will make your workflows as clean and simple as possible while hard to mess up!

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cnc_4_science-0.8.0.tar.gz (20.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cnc_4_science-0.8.0-py3-none-any.whl (21.7 kB view details)

Uploaded Python 3

File details

Details for the file cnc_4_science-0.8.0.tar.gz.

File metadata

  • Download URL: cnc_4_science-0.8.0.tar.gz
  • Upload date:
  • Size: 20.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cnc_4_science-0.8.0.tar.gz
Algorithm Hash digest
SHA256 18f146e24e1c6607508d7dee28c10625d149a1d8c76e147a233b41f964293554
MD5 236693e4163f6dc2d73472b023707318
BLAKE2b-256 b6b38969c38603862e14e8c37dcdda7f46c236ef2b4c16c3e79f7c9f19081ed6

See more details on using hashes here.

Provenance

The following attestation bundles were made for cnc_4_science-0.8.0.tar.gz:

Publisher: publish.yml on AccelerationConsortium/cnc-4-science

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cnc_4_science-0.8.0-py3-none-any.whl.

File metadata

  • Download URL: cnc_4_science-0.8.0-py3-none-any.whl
  • Upload date:
  • Size: 21.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cnc_4_science-0.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0bd7cd064aeb3e32467c5bec094d04506859047f94474f421bf0172ad978d512
MD5 0fc221ef6340ec4d6a830bae4f7de565
BLAKE2b-256 bd00fb9f284285e9f2ea008bd809fdbf3ac4480b3a47647b9e658d72070130de

See more details on using hashes here.

Provenance

The following attestation bundles were made for cnc_4_science-0.8.0-py3-none-any.whl:

Publisher: publish.yml on AccelerationConsortium/cnc-4-science

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page