316 lines
15 KiB
Python
316 lines
15 KiB
Python
# This is a component of AXIS, a front-end for emc
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
"""The reference consumer of ``gcode.parse``'s move-batch protocol.
|
|
|
|
The protocol is opt-in and lives in ``src/emc/rs274ngc/gcodemodule.cc`` (class
|
|
``MoveBatch``): a canon that sets ``use_move_batches = True`` (the bool itself -
|
|
anything else, including a merely truthy value, stays on the legacy protocol)
|
|
and provides a callable ``move_batch`` stops receiving ``straight_feed``/``straight_traverse``/
|
|
``straight_probe``/``rigid_tap``/``dwell``/``user_defined_function``/
|
|
``change_tool``/``tool_offset`` as one Python call per event, and receives them
|
|
instead as blocks of fixed-width float64 rows. On a million-move file that is
|
|
the difference between a million Python frames and about sixteen.
|
|
|
|
Each row is 13 float64s::
|
|
|
|
[kind, line_number, x, y, z, a, b, c, u, v, w, feedrate, spindle_speed]
|
|
|
|
``kind`` is one of the ``KIND_*`` constants below. Rows of kinds 0-3 are moves
|
|
and carry exactly the axis values the legacy per-event call would have passed -
|
|
after the interpreter's position update and after the metric conversion - which
|
|
is what lets this module reproduce ``GLCanon``'s geometry bit for bit rather
|
|
than merely closely. Rows of kinds 4-7 are the numeric non-move events, which
|
|
are batched rather than forwarded so that a canned drilling cycle's one dwell
|
|
per hole does not shred the moves around it into four-row batches; they carry
|
|
their payload in the axis slots (dwell seconds in x; M1xx index/P/Q in x/y/z;
|
|
tool number in x; the nine tool offsets in x..w) and zeros elsewhere.
|
|
|
|
``feedrate`` is the last ``SET_FEED_RATE`` value, so no correlation with a
|
|
separate rate callback is needed. ``spindle_speed`` is the last S word for
|
|
spindle 0 - data the legacy protocol never delivered at all, since that canon
|
|
call is an empty stub. This consumer ignores it; it is there for the renderer
|
|
rewrite to read without another C change.
|
|
|
|
**The view is on loan.** ``move_batch`` is handed a read-only memoryview over a
|
|
buffer the C module reuses. Copy or fully consume it before returning; a
|
|
retained view sees whatever the next batch overwrote it with.
|
|
|
|
Ordering is guaranteed by the C side: rows are delivered before any callback
|
|
that is still forwarded (comments, offsets, rotation, plane, messages, arcs,
|
|
``next_line``), when the buffer fills, once a second, and at end of parse. So
|
|
within one batch the suppression state, the g92/g5x offsets, the XY rotation and
|
|
the plane are all constant - which is what lets a whole run of moves be
|
|
transformed in one vectorized pass. The feed rate is the deliberate exception:
|
|
``set_feed_rate`` does *not* flush, because the rate is in the row, so
|
|
``self.feedrate`` may already have moved on by the time a batch holding rows
|
|
made at the old rate arrives. **Read the column, never the attribute.** It is
|
|
also forwarded only when the rate actually changes, since the interpreter
|
|
reports an ``F`` word whether or not it changed anything; a consumer that
|
|
counted ``set_feed_rate`` calls would see fewer of them than in legacy mode.
|
|
|
|
Rows are staged into the canon's own staging chunk rather than filled from here.
|
|
That is not about splitting a batch up - a whole batch is filled in one call,
|
|
and ``ProgramGeometry.add_moves_raw`` costs the same per row anywhere above
|
|
about a thousand of them. It is about putting a *floor* under the piece size:
|
|
filled straight from here, a fill would be exactly as big as whatever the C
|
|
side happened to deliver, and a batch can be a handful of rows. At 8 rows a
|
|
fill costs about 9x per row what it costs at 4096. Staging accumulates until
|
|
``FILL_BATCH`` instead, and as a bonus arc segments staged between batches keep
|
|
their place without a flush.
|
|
|
|
No GUI opts in yet; doing that is a separate change.
|
|
"""
|
|
|
|
import numpy as np
|
|
|
|
import rs274.glcanon
|
|
from rs274.glcanon import GLCanon
|
|
from rs274 import glcanon_bake
|
|
from rs274.glcanon_bake import CAT_TRAVERSE, CAT_FEED
|
|
|
|
#: Row width and columns, matching ``MoveBatch::ROW`` and the layout above.
|
|
ROW_WIDTH = 13
|
|
COL_KIND = 0
|
|
COL_LINE = 1
|
|
COL_AXES = slice(2, 11)
|
|
COL_FEEDRATE = 11
|
|
COL_SPINDLE = 12
|
|
|
|
# Column 0. Must match ``MoveBatch::Kind`` in gcodemodule.cc.
|
|
KIND_TRAVERSE = 0
|
|
KIND_FEED = 1
|
|
KIND_PROBE = 2
|
|
KIND_RIGID_TAP = 3
|
|
KIND_DWELL = 4
|
|
KIND_M1XX = 5
|
|
KIND_CHANGE_TOOL = 6
|
|
KIND_TOOL_OFFSET = 7
|
|
#: Kinds at or above this are events, not moves: they end a vectorized run.
|
|
FIRST_EVENT_KIND = KIND_DWELL
|
|
|
|
#: The nine axis letters, in row order.
|
|
_AXES = "xyzabcuvw"
|
|
|
|
#: ``CANON_PLANE`` (canon.hh) to the 0/1/2 index ``GLCanon._record_dwell``
|
|
#: derives from ``self.state.plane``: XY/UV -> 0, XZ/UW -> 1, YZ/VW -> 2.
|
|
_PLANE_INDEX = {1: 0, 2: 2, 3: 1, 4: 0, 5: 2, 6: 1}
|
|
|
|
|
|
class MoveBatchMixin:
|
|
"""Turns delivered batches into exactly what per-event ``GLCanon`` builds.
|
|
|
|
Mix in *before* ``GLCanon`` so the ``_record_dwell`` override below wins.
|
|
"""
|
|
|
|
#: What the C side reads to choose the protocol, once, at parse start.
|
|
#: Must be the bool ``True``; the C side ignores any other value.
|
|
use_move_batches = True
|
|
|
|
def batch_progress(self, lineno):
|
|
"""Called once per non-empty batch with the last line number in it.
|
|
|
|
The hook a GUI overrides to drive a progress bar. The C side flushes
|
|
before its once-a-second abort check, so this fires at least about that
|
|
often on a long parse - which is what replaces the per-line ``next_line``
|
|
a GUI used to count.
|
|
"""
|
|
|
|
# -- the protocol ------------------------------------------------------
|
|
|
|
def move_batch(self, view):
|
|
rows = np.frombuffer(view, dtype=np.float64).reshape(-1, ROW_WIDTH)
|
|
if len(rows) == 0:
|
|
return
|
|
# No flush here. Whatever the per-call path staged since the last batch
|
|
# - arc segments, which are deliberately not batched - is sitting in the
|
|
# canon's staging chunk, and _batch_run appends to that same chunk, so
|
|
# emission order is kept by construction. Flushing first would be worse
|
|
# than redundant: it would fill once per batch, and a file with an F
|
|
# word every few moves delivers batches of a few rows.
|
|
kinds = rows[:, COL_KIND].astype(np.int64)
|
|
events = np.nonzero(kinds >= FIRST_EVENT_KIND)[0]
|
|
start = 0
|
|
for at in events:
|
|
if at > start:
|
|
self._batch_run(rows[start:at], kinds[start:at])
|
|
self._batch_event(rows[at], int(kinds[at]))
|
|
start = int(at) + 1
|
|
if start < len(rows):
|
|
self._batch_run(rows[start:], kinds[start:])
|
|
self.batch_progress(int(rows[-1, COL_LINE]))
|
|
|
|
# -- a run of move rows ------------------------------------------------
|
|
|
|
def _batch_run(self, rows, kinds):
|
|
"""Fill one uninterrupted run of move rows, vectorized.
|
|
|
|
Everything a move depends on - suppression, the offsets, the rotation,
|
|
the tool-offset triple - is constant across a run, because every event
|
|
that could change one either flushes the batch (comments, offsets,
|
|
rotation) or ends the run (the tool rows). That is the whole reason the
|
|
run is the unit of work.
|
|
"""
|
|
if self.suppress > 0:
|
|
# Legacy straight_traverse/feed/probe/rigid_tap all return before
|
|
# touching any state while hidden, so a suppressed run leaves even
|
|
# `lo` and `first_move` alone.
|
|
return
|
|
|
|
n = len(rows)
|
|
# rotate_and_translate (rs274.interpret.Translated), op for op, on the
|
|
# whole run at once. The order matters and is not incidental: the same
|
|
# sequence of IEEE adds in the same order is what makes these results
|
|
# bit-identical to the per-move canon's, so an "equivalent" rearrangement
|
|
# here would turn an exact-equality test into an approximate one.
|
|
pts = rows[:, COL_AXES].copy()
|
|
pts += self._offsets("g92_offset_")
|
|
if self.rotation_xy:
|
|
rotx = pts[:, 0] * self.rotation_cos - pts[:, 1] * self.rotation_sin
|
|
pts[:, 1] = pts[:, 0] * self.rotation_sin + pts[:, 1] * self.rotation_cos
|
|
pts[:, 0] = rotx
|
|
pts += self._offsets("g5x_offset_")
|
|
|
|
# Where each move starts. Rigid taps do not advance the chain (the
|
|
# legacy rigid_tap never assigns self.lo), so consecutive taps all hang
|
|
# off the same point.
|
|
advances = kinds != KIND_RIGID_TAP
|
|
before = np.cumsum(advances) - advances # advancing rows before i
|
|
chain = np.empty((n, 9), dtype=np.float64)
|
|
chained = before > 0
|
|
chain[~chained] = self.lo
|
|
if chained.any():
|
|
source = np.nonzero(advances)[0]
|
|
chain[chained] = pts[source[before[chained] - 1]]
|
|
|
|
# A tap's endpoint is its transformed x,y,z joined to the chain point's
|
|
# a..w - the legacy rigid_tap transforms x,y,z with zeros and then
|
|
# splices self.lo's rotary and UVW components back in.
|
|
ends = pts.copy()
|
|
taps = ~advances
|
|
if taps.any():
|
|
ends[taps, 3:] = chain[taps, 3:]
|
|
|
|
# Leading traverses while first_move is set move the tool without
|
|
# drawing; the first move that is not a traverse clears the flag.
|
|
keep_from = 0
|
|
if self.first_move:
|
|
drawn = np.nonzero(kinds != KIND_TRAVERSE)[0]
|
|
if len(drawn) == 0:
|
|
keep_from = n
|
|
else:
|
|
keep_from = int(drawn[0])
|
|
self.first_move = False
|
|
|
|
# Before the early return: a dropped leading traverse still moves the
|
|
# tool, and the move after it starts where it left off.
|
|
if advances.any():
|
|
self.lo = tuple(pts[np.nonzero(advances)[0][-1]].tolist())
|
|
if keep_from >= n:
|
|
return
|
|
|
|
# Each surviving row becomes one staging row, except a tap, which
|
|
# becomes two - down, and back up the way it came.
|
|
index = np.arange(keep_from, n)
|
|
repeats = np.where(kinds[keep_from:] == KIND_RIGID_TAP, 2, 1)
|
|
index = np.repeat(index, repeats)
|
|
total = len(index)
|
|
offset_in_move = (np.arange(total)
|
|
- np.repeat(np.cumsum(repeats) - repeats, repeats))
|
|
returning = (offset_in_move == 1)[:, np.newaxis]
|
|
|
|
# Written straight into the canon's own staging chunk, in bulk, rather
|
|
# than filled from here. Not to break the run up - a run that is already
|
|
# big is filled in one call, and add_moves_raw costs the same per row
|
|
# anywhere above ~1024 - but to stop a *small* one reaching the fill on
|
|
# its own. Filled from here, the piece size would be the batch size, and
|
|
# a file with an F word every few moves arrives a few rows at a time:
|
|
# measured, a fill of 8 rows costs ~9x per row what a fill of 4096 does,
|
|
# which is enough to make batch mode slower than the per-move canon it
|
|
# replaces. Staging leaves the flush policy exactly where GLCanon has
|
|
# it, and reuses one buffer instead of allocating a chunk per run.
|
|
traversing = kinds[index] == KIND_TRAVERSE
|
|
self._reserve_staging(total)
|
|
at = self._staged
|
|
chunk = self._staging[at:at + total]
|
|
chunk[:, glcanon_bake.STAGE_LINE] = rows[index, COL_LINE]
|
|
chunk[:, glcanon_bake.STAGE_P1] = np.where(returning, ends[index],
|
|
chain[index])
|
|
chunk[:, glcanon_bake.STAGE_P2] = np.where(returning, chain[index],
|
|
ends[index])
|
|
chunk[:, glcanon_bake.STAGE_FEEDRATE] = np.where(
|
|
traversing, 0.0, rows[index, COL_FEEDRATE] / 60.)
|
|
chunk[:, glcanon_bake.STAGE_OFFSET] = (self.xo, self.yo, self.zo)
|
|
self._pending_kinds.extend(
|
|
np.where(traversing, CAT_TRAVERSE, CAT_FEED).astype(np.uint8)
|
|
.tobytes())
|
|
self._staged = at + total
|
|
if self._staged >= rs274.glcanon.FILL_BATCH:
|
|
self._flush_moves()
|
|
|
|
def _offsets(self, prefix):
|
|
return np.array([getattr(self, prefix + axis) for axis in _AXES],
|
|
dtype=np.float64)
|
|
|
|
# -- one non-move row --------------------------------------------------
|
|
|
|
def _batch_event(self, row, kind):
|
|
"""Replay one batched event exactly where it sits in the stream.
|
|
|
|
Delegates to the canon's own methods rather than reimplementing them:
|
|
these are the same calls the C module would have made, and going through
|
|
them keeps the suppression rules (dwell and M1xx are dropped while
|
|
hidden, tool changes and tool offsets are not) as the one statement of
|
|
those rules rather than two. ``lineno`` is taken from the row, since a
|
|
batched event delivers no ``next_line``.
|
|
"""
|
|
self.lineno = int(row[COL_LINE])
|
|
if kind == KIND_DWELL:
|
|
self.dwell(float(row[2]))
|
|
elif kind == KIND_M1XX:
|
|
self.user_defined_function(int(row[2]), float(row[3]),
|
|
float(row[4]))
|
|
elif kind == KIND_CHANGE_TOOL:
|
|
self.change_tool(int(row[2]))
|
|
elif kind == KIND_TOOL_OFFSET:
|
|
self.tool_offset(*row[COL_AXES].tolist())
|
|
else:
|
|
raise ValueError("move_batch: unknown row kind %d" % kind)
|
|
|
|
def _record_dwell(self, color):
|
|
"""As ``GLCanon._record_dwell``, but the plane comes from ``set_plane``.
|
|
|
|
The per-move canon reads ``self.state.plane`` - the snapshot the
|
|
``next_line`` for the dwell's own line carried. A batched dwell delivers
|
|
no ``next_line``, so that snapshot can predate the dwell by several
|
|
lines and hold the wrong plane. ``self.plane`` is what ``set_plane``
|
|
last set, and ``set_plane`` is still forwarded (and flushes first), so
|
|
it is current for every row in the batch.
|
|
"""
|
|
plane = _PLANE_INDEX.get(self.plane, 0)
|
|
self.dwells.append((self.lineno, color, self.lo[0], self.lo[1],
|
|
self.lo[2], plane))
|
|
self._flush_moves()
|
|
self._program_geometry.mark_dwell(self.lineno, self.lo, color, plane)
|
|
|
|
|
|
class BatchGLCanon(MoveBatchMixin, GLCanon):
|
|
"""``GLCanon`` that opts into the move-batch protocol.
|
|
|
|
Produces the same program record, dwell table, tool list and final state as
|
|
``GLCanon`` on the same file - see the equality test - so a GUI can swap one
|
|
for the other wherever numpy is importable.
|
|
"""
|