Code Library
CNC ProgrammingPythonPython 3.8+ · no dependenciesMIT license

Bolt-circle G-code generator with peck drilling (Python)

The complete version of the generator from the blog post: bolt-circle drilling as a command-line tool. Give it a center, radius, hole count, depth, feed, and RPM and it prints a FANUC-style program — plain G81, or G83 peck drilling when you pass a peck increment. The header and footer are a fixed, human-reviewed template; the script only generates the middle, which is exactly the discipline that keeps generated G-code trustworthy.

Before you run it

  • Python 3.8+ (standard library only)
  • A dry run above the part before the first real cut — every time, no exceptions

The code

GitHub
"""Generate a FANUC-style bolt-circle drilling program (G81, or G83 with --peck).

Examples:
  python bolt_circle.py --radius 40 --holes 8 --depth 12 --feed 120 --rpm 2400
  python bolt_circle.py --radius 40 --holes 8 --depth 30 --feed 100 --rpm 1800 \
      --peck 4 > O1001.nc

All dimensions are mm. REVIEW THE OUTPUT AND DRY-RUN IT ABOVE THE PART -
generated code fails with perfect confidence.
"""

import argparse
import math

HEADER = """O1001 (BOLT CIRCLE - GENERATED)
(REVIEW BEFORE RUNNING - DRY RUN FIRST)
G90 G94 G17 G21
G54
T1 M6
S{rpm} M3
G43 H01 Z25.0
"""

FOOTER = """G80
G0 Z25.0
M5
M30
"""


def bolt_circle(cx, cy, radius, holes, depth, feed, rpm, peck, start_angle):
    lines = [HEADER.format(rpm=rpm)]
    for i in range(holes):
        a = math.radians(start_angle + i * 360.0 / holes)
        x = cx + radius * math.cos(a)
        y = cy + radius * math.sin(a)
        if i == 0:
            cycle = (
                f"G98 G83 X{x:.3f} Y{y:.3f} Z{-depth:.3f} R2.0 "
                f"Q{peck:.3f} F{feed:.0f}"
                if peck > 0
                else f"G98 G81 X{x:.3f} Y{y:.3f} Z{-depth:.3f} R2.0 F{feed:.0f}"
            )
            lines.append(cycle)
        else:
            lines.append(f"X{x:.3f} Y{y:.3f}")  # the canned cycle is modal
    lines.append(FOOTER)
    return "\n".join(lines)


def main():
    ap = argparse.ArgumentParser(
        description="FANUC-style bolt-circle drilling generator")
    ap.add_argument("--cx", type=float, default=0.0, help="circle center X")
    ap.add_argument("--cy", type=float, default=0.0, help="circle center Y")
    ap.add_argument("--radius", type=float, required=True)
    ap.add_argument("--holes", type=int, required=True)
    ap.add_argument("--depth", type=float, required=True, help="drill depth, positive")
    ap.add_argument("--feed", type=float, required=True, help="mm/min")
    ap.add_argument("--rpm", type=int, required=True)
    ap.add_argument("--peck", type=float, default=0.0,
                    help="peck increment for G83; omit for plain G81")
    ap.add_argument("--start-angle", type=float, default=0.0,
                    help="angle of hole #1, degrees CCW from 3 o'clock")
    args = ap.parse_args()

    if args.holes < 1 or args.radius <= 0 or args.depth <= 0:
        ap.error("holes, radius, and depth must be positive")

    print(bolt_circle(args.cx, args.cy, args.radius, args.holes, args.depth,
                      args.feed, args.rpm, args.peck, args.start_angle))


if __name__ == "__main__":
    main()

How it works

  • The header/footer are string constants, not generated — safe start-up line, tool change, G43 length comp, retract, M30. A human wrote them once; the script can't get them wrong on a Tuesday.
  • Coordinates are emitted with :.3f so you never see X39.99999999999999 — formatting discipline is half of trustworthy generation.
  • G81/G83 are modal: only the first hole carries the cycle definition, the rest are bare X Y positions — output reads like a machinist wrote it.
  • --start-angle rotates the pattern so hole #1 lands where your print says, not where the math likes.
  • > O1001.nc pipes the program straight to a file; the script prints to stdout on purpose.

Gotchas & honest limits

  • The template assumes T1 with a valid H01 length offset and G54 set. Edit HEADER to match your shop's reality before first use — that's the point of it being a template.
  • G98 returns to initial Z between holes (safe over clamps); switch to G99 for speed only when the path between holes is provably clear.
  • Dialect is FANUC-ish: Haas takes it nearly as-is, but always run it through your control's graphics/dry-run. Sinumerik and Heidenhain need different cycles entirely.
  • Depth is entered positive and emitted as Z-; the sign convention is the generator's, so read the output once before trusting it.

Goes deeper

Want this adapted to your shop — or built into a real tool?

Samples are the free 80%. The last 20% is the part I do for a living.

Get in touch
Home
Blog
Tools
Code
Email
LinkedIn
Résumé