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
"""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,
G43length comp, retract,M30. A human wrote them once; the script can't get them wrong on a Tuesday. - Coordinates are emitted with
:.3fso you never seeX39.99999999999999— formatting discipline is half of trustworthy generation. G81/G83are modal: only the first hole carries the cycle definition, the rest are bareX Ypositions — output reads like a machinist wrote it.--start-anglerotates the pattern so hole #1 lands where your print says, not where the math likes.> O1001.ncpipes 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
HEADERto match your shop's reality before first use — that's the point of it being a template. G98returns to initial Z between holes (safe over clamps); switch toG99for 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.