From 94f3976915d64a77135a1fdc8983085ee8d2e1f9 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 11 Jun 2015 11:20:56 -0400 Subject: Add keys to statements for linking to primitives. Add some API features to ExcellonFile, such as getting a tool path length and changing tool parameters. Excellonfiles write method generates statements based on the drill hits in the hits member, so drill hits in a generated file can be re-ordered by re-ordering the drill hits in ExcellonFile.hits. see #30 --- gerber/cam.py | 3 +- gerber/excellon.py | 120 ++++++++++++++++++++++++++++++++--------- gerber/excellon_statements.py | 121 ++++++++++++++++++++++++------------------ gerber/primitives.py | 4 +- gerber/tests/test_excellon.py | 17 +++++- 5 files changed, 185 insertions(+), 80 deletions(-) diff --git a/gerber/cam.py b/gerber/cam.py index 31b6d2f..23d8214 100644 --- a/gerber/cam.py +++ b/gerber/cam.py @@ -220,7 +220,8 @@ class CamFile(object): self.zeros = 'leading' self.format = (2, 5) self.statements = statements if statements is not None else [] - self.primitives = primitives + if primitives is not None: + self.primitives = primitives self.filename = filename self.layer_name = layer_name diff --git a/gerber/excellon.py b/gerber/excellon.py index f994b67..1f0c570 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -24,6 +24,7 @@ This module provides Excellon file classes and parsing utilities """ import math +import operator from .excellon_statements import * from .cam import CamFile, FileSettings @@ -49,6 +50,22 @@ def read(filename): return ExcellonParser(settings).parse(filename) +class DrillHit(object): + def __init__(self, tool, position): + self.tool = tool + self.position = position + + def to_inch(self): + if self.tool.units == 'metric': + self.tool.to_inch() + self.position = tuple(map(inch, self.position)) + + def to_metric(self): + if self.tool.units == 'inch': + self.tool.to_metric() + self.position = tuple(map(metric, self.position)) + + class ExcellonFile(CamFile): """ A class representing a single excellon file @@ -81,17 +98,19 @@ class ExcellonFile(CamFile): filename=filename) self.tools = tools self.hits = hits - self.primitives = [Drill(position, tool.diameter, units=settings.units) - for tool, position in self.hits] + + @property + def primitives(self): + return [Drill(hit.position, hit.tool.diameter,units=self.settings.units) for hit in self.hits] + @property def bounds(self): xmin = ymin = 100000000000 xmax = ymax = -100000000000 - for tool, position in self.hits: - radius = tool.diameter / 2. - x = position[0] - y = position[1] + for hit in self.hits: + radius = hit.tool.diameter / 2. + x, y = hit.position xmin = min(x - radius, xmin) xmax = max(x + radius, xmax) ymin = min(y - radius, ymin) @@ -101,20 +120,23 @@ class ExcellonFile(CamFile): def report(self, filename=None): """ Print or save drill report """ - toolfmt = ' T%%02d %%%d.%df %%d\n' % self.settings.format - rprt = 'Excellon Drill Report\n\n' + if self.settings.units == 'inch': + toolfmt = ' T{:0>2d} {:%d.%df} {: >3d} {:f}in.\n' % self.settings.format + else: + toolfmt = ' T{:0>2d} {:%d.%df} {: >3d} {:f}mm\n' % self.settings.format + rprt = '=====================\nExcellon Drill Report\n=====================\n' if self.filename is not None: rprt += 'NC Drill File: %s\n\n' % self.filename - rprt += 'Drill File Info:\n\n' + rprt += 'Drill File Info:\n----------------\n' rprt += (' Data Mode %s\n' % 'Absolute' if self.settings.notation == 'absolute' else 'Incremental') rprt += (' Units %s\n' % 'Inches' if self.settings.units == 'inch' else 'Millimeters') - rprt += '\nTool List:\n\n' - rprt += ' Code Size Hits\n' - rprt += ' --------------------------\n' + rprt += '\nTool List:\n----------\n\n' + rprt += ' Code Size Hits Path Length\n' + rprt += ' --------------------------------------\n' for tool in self.tools.itervalues(): - rprt += toolfmt % (tool.number, tool.diameter, tool.hit_count) + rprt += toolfmt.format(tool.number, tool.diameter, tool.hit_count, self.tool_path_length(tool.number)) if filename is not None: with open(filename, 'w') as f: f.write(rprt) @@ -122,9 +144,22 @@ class ExcellonFile(CamFile): def write(self, filename): with open(filename, 'w') as f: + # Copy the header verbatim for statement in self.statements: - f.write(statement.to_excellon(self.settings) + '\n') - + print(statement) + if not isinstance(statement, ToolSelectionStmt): + f.write(statement.to_excellon(self.settings) + '\n') + else: + break + + # Write out coordinates for drill hits by tool + for tool in self.tools.itervalues(): + f.write(ToolSelectionStmt(tool.number).to_excellon(self.settings) + '\n') + for hit in self.hits: + if hit.tool.number == tool.number: + f.write(CoordinateStmt(*hit.position).to_excellon(self.settings) + '\n') + f.write(EndOfProgramStmt().to_excellon() + '\n') + def to_inch(self): """ Convert units to inches @@ -137,8 +172,8 @@ class ExcellonFile(CamFile): tool.to_inch() for primitive in self.primitives: primitive.to_inch() - self.hits = [(tool, tuple(map(inch, pos))) - for tool, pos in self.hits] + for hit in self.hits: + hit.position = tuple(map(inch, hit,position)) def to_metric(self): @@ -152,17 +187,52 @@ class ExcellonFile(CamFile): tool.to_metric() for primitive in self.primitives: primitive.to_metric() - self.hits = [(tool, tuple(map(metric, pos))) - for tool, pos in self.hits] + for hit in self.hits: + hit.position = tuple(map(metric, hit.position)) def offset(self, x_offset=0, y_offset=0): for statement in self.statements: statement.offset(x_offset, y_offset) for primitive in self.primitives: primitive.offset(x_offset, y_offset) - self.hits = [(tool, (pos[0] + x_offset, pos[1] + y_offset)) - for tool, pos in self.hits] + for hit in self. hits: + hit.position = tuple(map(operator.add, hit.position, (x_offset, y_offset))) + def tool_path_length(self, tool_number): + """ Return the path length for a given tool + """ + length = 0.0 + pos = (0, 0) + for hit in self.hits: + tool = hit.tool + if tool.number == tool_number: + length = length + math.hypot(*tuple(map(operator.sub, pos, hit.position))) + pos = hit.position + return length + + + def update_tool(self, tool_number, **kwargs): + """ Change parameters of a tool + """ + if kwargs.get('feed_rate') is not None: + self.tools[tool_number].feed_rate = kwargs.get('feed_rate') + if kwargs.get('retract_rate') is not None: + self.tools[tool_number].retract_rate = kwargs.get('retract_rate') + if kwargs.get('rpm') is not None: + self.tools[tool_number].rpm = kwargs.get('rpm') + if kwargs.get('diameter') is not None: + self.tools[tool_number].diameter = kwargs.get('diameter') + if kwargs.get('max_hit_count') is not None: + self.tools[tool_number].max_hit_count = kwargs.get('max_hit_count') + if kwargs.get('depth_offset') is not None: + self.tools[tool_number].depth_offset = kwargs.get('depth_offset') + # Update drill hits + newtool = self.tools[tool_number] + for hit in self.hits: + if hit.tool.number == newtool.number: + hit.tool = newtool + + class ExcellonParser(object): """ Excellon File Parser @@ -248,6 +318,8 @@ class ExcellonParser(object): self.statements.append(RewindStopStmt()) if self.state == 'HEADER': self.state = 'DRILL' + elif self.state == 'INIT': + self.state = 'HEADER' elif line[:3] == 'M95': self.statements.append(HeaderEndStmt()) @@ -312,7 +384,7 @@ class ExcellonParser(object): for i in range(stmt.count): self.pos[0] += stmt.xdelta if stmt.xdelta is not None else 0 self.pos[1] += stmt.ydelta if stmt.ydelta is not None else 0 - self.hits.append((self.active_tool, tuple(self.pos))) + self.hits.append(DrillHit(self.active_tool, tuple(self.pos))) self.active_tool._hit() elif line[0] in ['X', 'Y']: @@ -331,7 +403,7 @@ class ExcellonParser(object): if y is not None: self.pos[1] += y if self.state == 'DRILL': - self.hits.append((self.active_tool, tuple(self.pos))) + self.hits.append(DrillHit(self.active_tool, tuple(self.pos))) self.active_tool._hit() else: self.statements.append(UnknownStmt.from_excellon(line)) @@ -402,7 +474,7 @@ def detect_excellon_format(filename): size = tuple([t[1] - t[0] for t in p.bounds]) hole_area = 0.0 for hit in p.hits: - tool = hit[0] + tool = hit.tool hole_area += math.pow(math.pi * tool.diameter / 2., 2) results[key] = (size, p.hole_count, hole_area) except: diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py index 31a3c72..fa05e53 100644 --- a/gerber/excellon_statements.py +++ b/gerber/excellon_statements.py @@ -22,7 +22,7 @@ Excellon Statements """ import re - +import uuid from .utils import (parse_gerber_value, write_gerber_value, decimal_string, inch, metric) @@ -40,13 +40,15 @@ class ExcellonStatement(object): """ Excellon Statement abstract base class """ - units = 'inch' - @classmethod def from_excellon(cls, line): raise NotImplementedError('from_excellon must be implemented in a ' 'subclass') - + + def __init__(self, unit='inch', id=None): + self.units = unit + self.id = uuid.uuid4().int if id is None else id + def to_excellon(self, settings=None): raise NotImplementedError('to_excellon must be implemented in a ' 'subclass') @@ -107,7 +109,7 @@ class ExcellonTool(ExcellonStatement): """ @classmethod - def from_excellon(cls, line, settings): + def from_excellon(cls, line, settings, id=None): """ Create a Tool from an excellon file tool definition line. Parameters @@ -126,6 +128,7 @@ class ExcellonTool(ExcellonStatement): commands = re.split('([BCFHSTZ])', line)[1:] commands = [(command, value) for command, value in pairwise(commands)] args = {} + args['id'] = id nformat = settings.format zero_suppression = settings.zero_suppression for cmd, val in commands: @@ -165,6 +168,8 @@ class ExcellonTool(ExcellonStatement): return cls(settings, **tool_dict) def __init__(self, settings, **kwargs): + if kwargs.get('id') is not None: + super(ExcellonTool, self).__init__(id=kwargs.get('id')) self.settings = settings self.number = kwargs.get('number') self.feed_rate = kwargs.get('feed_rate') @@ -221,7 +226,7 @@ class ExcellonTool(ExcellonStatement): class ToolSelectionStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line): + def from_excellon(cls, line, **kwargs): """ Create a ToolSelectionStmt from an excellon file line. Parameters @@ -244,9 +249,10 @@ class ToolSelectionStmt(ExcellonStatement): tool = int(line[:2]) compensation_index = int(line[2:]) - return cls(tool, compensation_index) + return cls(tool, compensation_index, **kwargs) - def __init__(self, tool, compensation_index=None): + def __init__(self, tool, compensation_index=None, **kwargs): + super(ToolSelectionStmt, self).__init__(**kwargs) tool = int(tool) compensation_index = (int(compensation_index) if compensation_index is not None else None) @@ -263,7 +269,7 @@ class ToolSelectionStmt(ExcellonStatement): class CoordinateStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line, settings): + def from_excellon(cls, line, settings, **kwargs): x_coord = None y_coord = None if line[0] == 'X': @@ -276,11 +282,12 @@ class CoordinateStmt(ExcellonStatement): else: y_coord = parse_gerber_value(line.strip(' Y'), settings.format, settings.zero_suppression) - c = cls(x_coord, y_coord) + c = cls(x_coord, y_coord, **kwargs) c.units = settings.units return c - def __init__(self, x=None, y=None): + def __init__(self, x=None, y=None, **kwargs): + super(CoordinateStmt, self).__init__(**kwargs) self.x = x self.y = y @@ -329,7 +336,7 @@ class CoordinateStmt(ExcellonStatement): class RepeatHoleStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line, settings): + def from_excellon(cls, line, settings, **kwargs): match = re.compile(r'R(?P[0-9]*)X?(?P[+\-]?\d*\.?\d*)?Y?' '(?P[+\-]?\d*\.?\d*)?').match(line) stmt = match.groupdict() @@ -340,11 +347,12 @@ class RepeatHoleStmt(ExcellonStatement): ydelta = (parse_gerber_value(stmt['ydelta'], settings.format, settings.zero_suppression) if stmt['ydelta'] is not '' else None) - c = cls(count, xdelta, ydelta) + c = cls(count, xdelta, ydelta, **kwargs) c.units = settings.units return c - def __init__(self, count, xdelta=0.0, ydelta=0.0): + def __init__(self, count, xdelta=0.0, ydelta=0.0, **kwargs): + super(RepeatHoleStmt, self).__init__(**kwargs) self.count = count self.xdelta = xdelta self.ydelta = ydelta @@ -385,10 +393,11 @@ class RepeatHoleStmt(ExcellonStatement): class CommentStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line): + def from_excellon(cls, line, **kwargs): return cls(line.lstrip(';')) - def __init__(self, comment): + def __init__(self, comment, **kwargs): + super(CommentStmt, self).__init__(**kwargs) self.comment = comment def to_excellon(self, settings=None): @@ -397,8 +406,8 @@ class CommentStmt(ExcellonStatement): class HeaderBeginStmt(ExcellonStatement): - def __init__(self): - pass + def __init__(self, **kwargs): + super(HeaderBeginStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return 'M48' @@ -406,8 +415,8 @@ class HeaderBeginStmt(ExcellonStatement): class HeaderEndStmt(ExcellonStatement): - def __init__(self): - pass + def __init__(self, **kwargs): + super(HeaderEndStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return 'M95' @@ -415,8 +424,8 @@ class HeaderEndStmt(ExcellonStatement): class RewindStopStmt(ExcellonStatement): - def __init__(self): - pass + def __init__(self, **kwargs): + super(RewindStopStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return '%' @@ -425,7 +434,7 @@ class RewindStopStmt(ExcellonStatement): class EndOfProgramStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line, settings): + def from_excellon(cls, line, settings, **kwargs): match = re.compile(r'M30X?(?P\d*\.?\d*)?Y?' '(?P\d*\.?\d*)?').match(line) stmt = match.groupdict() @@ -435,11 +444,12 @@ class EndOfProgramStmt(ExcellonStatement): y = (parse_gerber_value(stmt['y'], settings.format, settings.zero_suppression) if stmt['y'] is not '' else None) - c = cls(x, y) + c = cls(x, y, **kwargs) c.units = settings.units return c - def __init__(self, x=None, y=None): + def __init__(self, x=None, y=None, **kwargs): + super(EndOfProgramStmt, self).__init__(**kwargs) self.x = x self.y = y @@ -476,12 +486,13 @@ class EndOfProgramStmt(ExcellonStatement): class UnitStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line): + def from_excellon(cls, line, **kwargs): units = 'inch' if 'INCH' in line else 'metric' zeros = 'leading' if 'LZ' in line else 'trailing' - return cls(units, zeros) + return cls(units, zeros, **kwargs) - def __init__(self, units='inch', zeros='leading'): + def __init__(self, units='inch', zeros='leading', **kwargs): + super(UnitStmt, self).__init__(**kwargs) self.units = units.lower() self.zeros = zeros @@ -500,10 +511,11 @@ class UnitStmt(ExcellonStatement): class IncrementalModeStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line): - return cls('off') if 'OFF' in line else cls('on') + def from_excellon(cls, line, **kwargs): + return cls('off', **kwargs) if 'OFF' in line else cls('on', **kwargs) - def __init__(self, mode='off'): + def __init__(self, mode='off', **kwargs): + super(IncrementalModeStmt, self).__init__(**kwargs) if mode.lower() not in ['on', 'off']: raise ValueError('Mode may be "on" or "off"') self.mode = mode @@ -515,11 +527,12 @@ class IncrementalModeStmt(ExcellonStatement): class VersionStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line): + def from_excellon(cls, line, **kwargs): version = int(line.split(',')[1]) - return cls(version) + return cls(version, **kwargs) - def __init__(self, version=1): + def __init__(self, version=1, **kwargs): + super(VersionStmt, self).__init__(**kwargs) version = int(version) if version not in [1, 2]: raise ValueError('Valid versions are 1 or 2') @@ -532,11 +545,12 @@ class VersionStmt(ExcellonStatement): class FormatStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line): + def from_excellon(cls, line, **kwargs): fmt = int(line.split(',')[1]) - return cls(fmt) + return cls(fmt, **kwargs) - def __init__(self, format=1): + def __init__(self, format=1, **kwargs): + super(FormatStmt, self).__init__(**kwargs) format = int(format) if format not in [1, 2]: raise ValueError('Valid formats are 1 or 2') @@ -549,11 +563,12 @@ class FormatStmt(ExcellonStatement): class LinkToolStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line): + def from_excellon(cls, line, **kwargs): linked = [int(tool) for tool in line.split('/')] - return cls(linked) + return cls(linked, **kwargs) - def __init__(self, linked_tools): + def __init__(self, linked_tools, **kwargs): + super(LinkToolStmt, self).__init__(**kwargs) self.linked_tools = [int(x) for x in linked_tools] def to_excellon(self, settings=None): @@ -563,12 +578,13 @@ class LinkToolStmt(ExcellonStatement): class MeasuringModeStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line): + def from_excellon(cls, line, **kwargs): if not ('M71' in line or 'M72' in line): raise ValueError('Not a measuring mode statement') - return cls('inch') if 'M72' in line else cls('metric') + return cls('inch', **kwargs) if 'M72' in line else cls('metric', **kwargs) - def __init__(self, units='inch'): + def __init__(self, units='inch', **kwargs): + super(MeasuringModeStmt, self).__init__(**kwargs) units = units.lower() if units not in ['inch', 'metric']: raise ValueError('units must be "inch" or "metric"') @@ -585,8 +601,8 @@ class MeasuringModeStmt(ExcellonStatement): class RouteModeStmt(ExcellonStatement): - def __init__(self): - pass + def __init__(self, **kwargs): + super(RouteModeStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return 'G00' @@ -594,8 +610,8 @@ class RouteModeStmt(ExcellonStatement): class DrillModeStmt(ExcellonStatement): - def __init__(self): - pass + def __init__(self, **kwargs): + super(DrillModeStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return 'G05' @@ -603,8 +619,8 @@ class DrillModeStmt(ExcellonStatement): class AbsoluteModeStmt(ExcellonStatement): - def __init__(self): - pass + def __init__(self, **kwargs): + super(AbsoluteModeStmt, self).__init__(**kwargs) def to_excellon(self, settings=None): return 'G90' @@ -613,10 +629,11 @@ class AbsoluteModeStmt(ExcellonStatement): class UnknownStmt(ExcellonStatement): @classmethod - def from_excellon(cls, line): - return cls(line) + def from_excellon(cls, line, **kwargs): + return cls(line, **kwargs) - def __init__(self, stmt): + def __init__(self, stmt, **kwargs): + super(UnknownStmt, self).__init__(**kwargs) self.stmt = stmt def to_excellon(self, settings=None): diff --git a/gerber/primitives.py b/gerber/primitives.py index bdd49f7..00ecb12 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -36,11 +36,13 @@ class Primitive(object): Rotation of a primitive about its origin in degrees. Positive rotation is counter-clockwise as viewed from the board top. """ - def __init__(self, level_polarity='dark', rotation=0, units=None): + def __init__(self, level_polarity='dark', rotation=0, units=None, id=None, statement_id=None): self.level_polarity = level_polarity self.rotation = rotation self.units = units self._to_convert = list() + self.id = id + self.statement_id = statement_id def bounding_box(self): """ Calculate bounding box diff --git a/gerber/tests/test_excellon.py b/gerber/tests/test_excellon.py index d47ad6a..006277d 100644 --- a/gerber/tests/test_excellon.py +++ b/gerber/tests/test_excellon.py @@ -24,6 +24,17 @@ def test_read(): ncdrill = read(NCDRILL_FILE) assert(isinstance(ncdrill, ExcellonFile)) +def test_write(): + ncdrill = read(NCDRILL_FILE) + ncdrill.write('test.ncd') + with open(NCDRILL_FILE) as src: + srclines = src.readlines() + + with open('test.ncd') as res: + for idx, line in enumerate(res): + assert_equal(line.strip(), srclines[idx].strip()) + os.remove('test.ncd') + def test_read_settings(): ncdrill = read(NCDRILL_FILE) assert_equal(ncdrill.settings['format'], (2, 4)) @@ -47,9 +58,11 @@ def test_conversion(): ncdrill.to_metric() assert_equal(ncdrill.settings.units, 'metric') + inch_primitives = ncdrill_inch.primitives + for tool in iter(ncdrill_inch.tools.values()): tool.to_metric() - for primitive in ncdrill_inch.primitives: + for primitive in inch_primitives: primitive.to_metric() for statement in ncdrill_inch.statements: statement.to_metric() @@ -57,7 +70,7 @@ def test_conversion(): for m_tool, i_tool in zip(iter(ncdrill.tools.values()), iter(ncdrill_inch.tools.values())): assert_equal(i_tool, m_tool) - for m, i in zip(ncdrill.primitives,ncdrill_inch.primitives): + for m, i in zip(ncdrill.primitives,inch_primitives): assert_equal(m, i) -- cgit From ec2ca92da6e3dac1d56bfba28d4c2cadc35a9811 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 11 Jun 2015 14:00:40 -0400 Subject: Python 3 fix remove dict itervalues() calls --- gerber/excellon.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gerber/excellon.py b/gerber/excellon.py index 1f0c570..65d014f 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -135,7 +135,7 @@ class ExcellonFile(CamFile): rprt += '\nTool List:\n----------\n\n' rprt += ' Code Size Hits Path Length\n' rprt += ' --------------------------------------\n' - for tool in self.tools.itervalues(): + for tool in iter(self.tools.values()): rprt += toolfmt.format(tool.number, tool.diameter, tool.hit_count, self.tool_path_length(tool.number)) if filename is not None: with open(filename, 'w') as f: @@ -153,7 +153,7 @@ class ExcellonFile(CamFile): break # Write out coordinates for drill hits by tool - for tool in self.tools.itervalues(): + for tool in iter(self.tools.values()): f.write(ToolSelectionStmt(tool.number).to_excellon(self.settings) + '\n') for hit in self.hits: if hit.tool.number == tool.number: @@ -168,7 +168,7 @@ class ExcellonFile(CamFile): self.units = 'inch' for statement in self.statements: statement.to_inch() - for tool in self.tools.itervalues(): + for tool in iter(self.tools.values()): tool.to_inch() for primitive in self.primitives: primitive.to_inch() -- cgit From 15254a5bb7ad866e09374c5a99e9be4468e4d3c7 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 6 Jul 2015 12:13:59 -0400 Subject: Add tool path optimization example Add example demonstrating use of tsp-solver with pcb-tools to optimize tool paths in an excellon file. This is based on @koppi's script in #30 --- examples/excellon_optimize.py | 90 +++++++++ examples/excellon_optimize_after.PNG | Bin 0 -> 33753 bytes examples/excellon_optimize_before.PNG | Bin 0 -> 90206 bytes examples/gerbers/shld.drd | 354 ++++++++++++++++++++++++++++++++++ gerber/excellon.py | 35 +++- 5 files changed, 469 insertions(+), 10 deletions(-) create mode 100644 examples/excellon_optimize.py create mode 100644 examples/excellon_optimize_after.PNG create mode 100644 examples/excellon_optimize_before.PNG create mode 100644 examples/gerbers/shld.drd diff --git a/examples/excellon_optimize.py b/examples/excellon_optimize.py new file mode 100644 index 0000000..5f0adbc --- /dev/null +++ b/examples/excellon_optimize.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Example using pcb-tools with tsp-solver (github.com/dmishin/tsp-solver) to +# optimize tool paths in an Excellon file. +# +# +# Copyright 2015 Hamilton Kibbe +# Based on a script by https://github.com/koppi +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +import sys +import math +import gerber +from operator import sub +from gerber.excellon import DrillHit + +try: + from tsp_solver.greedy import solve_tsp +except ImportError: + print('\n=================================================================\n' + 'This example requires tsp-solver be installed in order to run.\n\n' + 'tsp-solver can be downloaded from:\n' + ' http://github.com/dmishin/tsp-solver.\n' + '=================================================================') + sys.exit(0) + + +if __name__ == '__main__': + + # Get file name to open + if len(sys.argv) < 2: + fname = 'gerbers/shld.drd' + else: + fname = sys.argv[1] + + # Read the excellon file + f = gerber.read(fname) + + positions = {} + tools = {} + hit_counts = f.hit_count() + oldpath = sum(f.path_length().values()) + + #Get hit positions + for hit in f.hits: + tool_num = hit.tool.number + if tool_num not in positions.keys(): + positions[tool_num] = [] + positions[tool_num].append(hit.position) + + hits = [] + + # Optimize tool path for each tool + for tool, count in iter(hit_counts.items()): + + # Calculate distance matrix + distance_matrix = [[math.hypot(*tuple(map(sub, + positions[tool][i], + positions[tool][j]))) + for j in iter(range(count))] + for i in iter(range(count))] + + # Calculate new path + path = solve_tsp(distance_matrix, 50) + + # Create new hits list + hits += [DrillHit(f.tools[tool], positions[tool][p]) for p in path] + + # Update the file + f.hits = hits + f.filename = f.filename + '.optimized' + f.write() + + # Print drill report + print(f.report()) + print('Original path length: %1.4f' % oldpath) + print('Optimized path length: %1.4f' % sum(f.path_length().values())) + diff --git a/examples/excellon_optimize_after.PNG b/examples/excellon_optimize_after.PNG new file mode 100644 index 0000000..0f16387 Binary files /dev/null and b/examples/excellon_optimize_after.PNG differ diff --git a/examples/excellon_optimize_before.PNG b/examples/excellon_optimize_before.PNG new file mode 100644 index 0000000..26a36ab Binary files /dev/null and b/examples/excellon_optimize_before.PNG differ diff --git a/examples/gerbers/shld.drd b/examples/gerbers/shld.drd new file mode 100644 index 0000000..a919b5b --- /dev/null +++ b/examples/gerbers/shld.drd @@ -0,0 +1,354 @@ +% +M48 +M72 +T01C0.03200 +T02C0.03543 +T03C0.04000 +% +T01 +X11212Y16343 +X80212Y16343 +X21212Y16343 +X99212Y22143 +X99212Y12143 +X40212Y16343 +T02 +X10812Y191043 +X70812Y111043 +X130812Y111043 +X80812Y141043 +X110812Y71043 +X160812Y51043 +X20812Y171043 +X30812Y91043 +X50812Y111043 +X50812Y121043 +X20812Y161043 +X90812Y111043 +X70812Y61043 +X40812Y171043 +X50812Y81043 +X160812Y61043 +X40812Y191043 +X30812Y31043 +X90812Y131043 +X10812Y31043 +X150812Y111043 +X170812Y51043 +X110812Y151043 +X10812Y51043 +X150812Y51043 +X140812Y121043 +X170812Y61043 +X30812Y61043 +X70812Y91043 +X70812Y101043 +X160812Y161043 +X40812Y81043 +X220812Y151043 +X180812Y71043 +X30812Y151043 +X50812Y161043 +X150812Y131043 +X40812Y61043 +X130812Y91043 +X90812Y61043 +X80812Y101043 +X30812Y191043 +X130812Y151043 +X60812Y31043 +X50812Y91043 +X40812Y111043 +X220812Y141043 +X30812Y81043 +X140812Y81043 +X60812Y61043 +X210812Y131043 +X160812Y71043 +X90812Y41043 +X120812Y151043 +X10812Y161043 +X80812Y151043 +X50812Y71043 +X160812Y151043 +X110812Y111043 +X30812Y121043 +X10812Y41043 +X20812Y41043 +X40812Y51043 +X10812Y151043 +X200812Y101043 +X70812Y41043 +X120812Y51043 +X40812Y41043 +X80812Y91043 +X170812Y161043 +X100812Y71043 +X40812Y31043 +X30812Y141043 +X180812Y131043 +X10812Y61043 +X120812Y141043 +X200812Y151043 +X90812Y121043 +X50812Y31043 +X170812Y121043 +X170812Y111043 +X60812Y121043 +X40812Y101043 +X120812Y121043 +X100812Y161043 +X10812Y81043 +X130812Y131043 +X60812Y81043 +X200812Y111043 +X140812Y51043 +X150812Y71043 +X160812Y111043 +X120812Y111043 +X130812Y101043 +X20812Y51043 +X20812Y201043 +X90812Y71043 +X190812Y61043 +X170812Y81043 +X70812Y71043 +X50812Y101043 +X150812Y81043 +X60812Y131043 +X190812Y121043 +X170812Y131043 +X130812Y121043 +X20812Y91043 +X70812Y151043 +X70812Y141043 +X180812Y111043 +X10812Y181043 +X40812Y131043 +X80812Y121043 +X120812Y61043 +X160812Y101043 +X90812Y31043 +X10812Y91043 +X80812Y71043 +X100812Y121043 +X100812Y51043 +X160812Y121043 +X40812Y71043 +X50812Y51043 +X180812Y81043 +X90812Y51043 +X60812Y71043 +X40812Y161043 +X190812Y141043 +X20812Y31043 +X100812Y151043 +X200812Y141043 +X180812Y151043 +X60812Y51043 +X120812Y131043 +X150812Y141043 +X180812Y51043 +X150812Y101043 +X170812Y101043 +X150812Y151043 +X30812Y111043 +X90812Y151043 +X80812Y131043 +X170812Y151043 +X80812Y51043 +X10812Y201043 +X60812Y151043 +X140812Y111043 +X100812Y91043 +X90812Y161043 +X130812Y81043 +X190812Y111043 +X140812Y101043 +X20812Y71043 +X150812Y121043 +X90812Y141043 +X60812Y111043 +X110812Y121043 +X30812Y71043 +X30812Y51043 +X210812Y141043 +X50812Y61043 +X140812Y131043 +X30812Y201043 +X190812Y101043 +X70812Y81043 +X20812Y121043 +X20812Y191043 +X80812Y161043 +X80812Y81043 +X20812Y151043 +X40812Y121043 +X80812Y31043 +X80812Y111043 +X190812Y151043 +X30812Y181043 +X60812Y91043 +X110812Y61043 +X180812Y61043 +X10812Y141043 +X50812Y131043 +X130812Y51043 +X50812Y151043 +X110812Y51043 +X70812Y131043 +X60812Y41043 +X200812Y161043 +X80812Y61043 +X140812Y161043 +X190812Y81043 +X20812Y141043 +X70812Y161043 +X140812Y151043 +X20812Y61043 +X20812Y81043 +X100812Y131043 +X200812Y131043 +X140812Y141043 +X40812Y151043 +X40812Y91043 +X60812Y101043 +X160812Y81043 +X130812Y71043 +X30812Y41043 +X10812Y71043 +X180812Y141043 +X170812Y141043 +X180812Y91043 +X180812Y101043 +X150812Y61043 +X120812Y161043 +X90812Y101043 +X200812Y121043 +X190812Y91043 +X160812Y141043 +X130812Y161043 +X20812Y101043 +X90812Y81043 +X190812Y161043 +X30812Y171043 +X40812Y181043 +X70812Y51043 +X110812Y101043 +X60812Y141043 +X120812Y101043 +X30812Y161043 +X100812Y141043 +X220812Y131043 +X50812Y141043 +X30812Y101043 +X60812Y161043 +X150812Y161043 +X20812Y131043 +X150812Y91043 +X100812Y61043 +X10812Y131043 +X30812Y131043 +X100812Y41043 +X140812Y61043 +X210812Y151043 +X70812Y121043 +X100812Y101043 +X180812Y121043 +X40812Y201043 +X190812Y71043 +X10812Y171043 +X110812Y141043 +X130812Y61043 +X110812Y81043 +X80812Y41043 +X50812Y41043 +X110812Y131043 +X190812Y131043 +X130812Y141043 +X140812Y91043 +X20812Y111043 +X140812Y71043 +X170812Y91043 +X120812Y91043 +X190812Y51043 +X120812Y81043 +X160812Y91043 +X100812Y81043 +X120812Y71043 +X10812Y121043 +X170812Y71043 +X110812Y91043 +X100812Y111043 +X110812Y161043 +X70812Y31043 +X90812Y91043 +X40812Y141043 +X20812Y181043 +X210812Y161043 +X180812Y161043 +X160812Y131043 +T03 +X86712Y189043 +X213012Y23043 +X126732Y201114 +X96712Y189043 +X86732Y201114 +X56732Y201114 +X142812Y23443 +X106712Y189043 +X112754Y11450 +X182720Y200950 +X106732Y201114 +X207259Y55639 +X207259Y81239 +X203131Y11150 +X76732Y201114 +X192720Y200950 +X66712Y189043 +X96732Y201114 +X193131Y11150 +X66732Y201114 +X203012Y23043 +X122754Y11450 +X76712Y189043 +X173131Y11150 +X192712Y188843 +X116712Y189043 +X116732Y201114 +X213131Y11150 +X162720Y200950 +X225059Y55639 +X183131Y11150 +X126712Y189043 +X183012Y23043 +X212712Y188843 +X163131Y11150 +X213563Y110846 +X122812Y23443 +X132812Y23443 +X182712Y188843 +X212720Y200950 +X202720Y200950 +X193012Y23043 +X213563Y120846 +X172720Y200950 +X225059Y81239 +X223563Y120846 +X56712Y189043 +X172712Y188843 +X213563Y100846 +X142720Y200950 +X163012Y23043 +X142754Y11450 +X223563Y110846 +X132754Y11450 +X142712Y188843 +X162712Y188843 +X152712Y188843 +X223563Y100846 +X202712Y188843 +X112812Y23443 +X173012Y23043 +X152720Y200950 +M30 diff --git a/gerber/excellon.py b/gerber/excellon.py index 65d014f..d89b349 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -136,17 +136,18 @@ class ExcellonFile(CamFile): rprt += ' Code Size Hits Path Length\n' rprt += ' --------------------------------------\n' for tool in iter(self.tools.values()): - rprt += toolfmt.format(tool.number, tool.diameter, tool.hit_count, self.tool_path_length(tool.number)) + rprt += toolfmt.format(tool.number, tool.diameter, tool.hit_count, self.path_length(tool.number)) if filename is not None: with open(filename, 'w') as f: f.write(rprt) return rprt - def write(self, filename): + def write(self, filename=None): + filename = filename if filename is not None else self.filename with open(filename, 'w') as f: + # Copy the header verbatim for statement in self.statements: - print(statement) if not isinstance(statement, ToolSelectionStmt): f.write(statement.to_excellon(self.settings) + '\n') else: @@ -198,18 +199,32 @@ class ExcellonFile(CamFile): for hit in self. hits: hit.position = tuple(map(operator.add, hit.position, (x_offset, y_offset))) - def tool_path_length(self, tool_number): + def path_length(self, tool_number=None): """ Return the path length for a given tool """ - length = 0.0 - pos = (0, 0) + lengths = {} + positions = {} for hit in self.hits: tool = hit.tool - if tool.number == tool_number: - length = length + math.hypot(*tuple(map(operator.sub, pos, hit.position))) - pos = hit.position - return length + num = tool.number + positions[num] = (0, 0) if positions.get(num) is None else positions[num] + lengths[num] = 0.0 if lengths.get(num) is None else lengths[num] + lengths[num] = lengths[num] + math.hypot(*tuple(map(operator.sub, positions[num], hit.position))) + positions[num] = hit.position + + if tool_number is None: + return lengths + else: + return lengths.get(tool_number) + def hit_count(self, tool_number=None): + counts = {} + for tool in iter(self.tools.values()): + counts[tool.number] = tool.hit_count + if tool_number is None: + return counts + else: + return counts.get(tool_number) def update_tool(self, tool_number, **kwargs): """ Change parameters of a tool -- cgit