import re
import pandas as pd
import numpy as np
from typing import Iterable
from ..syntax import flavors, ABS_POSITION
from .tag_literals import MOVE_EXTRUDE
from ._dataframe import (
_check_loc_dist_time_gcode_dataframe,
)
[docs]
def gcode_statistics(
gcode: pd.DataFrame,
flavor: dict = flavors["default"],
words: Iterable = None,
) -> dict:
"""Quick statistics for any G-code DataFrame. For AM-specific stats (layer count, deposition volume, etc.), use ``AdditivePart`` instead.
Args:
gcode (pd.DataFrame): Cleaned and tagged G-code DataFrame.
flavor (dict, optional): Syntax definition for G-code. Defaults to flavors["default"].
words (Iterable, optional): Words (e.g. ``"G1"``, ``("G", 1)``) to count occurrences of. Defaults to ``[]``.
Returns:
dict: Statistics including line/word/command counts, positioning mode, units, bounding box, and elapsed time.
"""
# Cleans the gcode by inserting location and distance columns if missing
# Forward fills missing values
gcode = _check_loc_dist_time_gcode_dataframe(gcode, flavor, True)
n_rows = gcode.shape[0]
# We can estimate the number of words as shape[0] * word_map present in the dataframe column list
word_map = []
for value in flavor["word_map"].values():
if isinstance(value, list):
word_map.extend(value)
else:
word_map.append(value)
word_map = set(word_map).intersection(gcode.columns)
#
# Count the number of G and M commands with values to estimate the number of commands
#
cmd_columns = []
if "G" in gcode.columns:
cmd_columns.append("G")
if "M" in gcode.columns:
cmd_columns.append("M")
n_irrelevant_rows = gcode[cmd_columns].isnull().any(axis=1).sum()
stats = {
"n_lines": n_rows,
"n_words": len(word_map) * n_rows,
"n_commands": n_rows - n_irrelevant_rows,
"n_word_use": _word_appearance_gcode_df(gcode, flavor, words),
}
# Absolute or relative positioning
# TODO: Relative as the default? Probably syntax specific
# for now omitting without a G command
if gcode[gcode["G"] == 90].shape[0] != 0:
stats["positioning"] = "absolute"
elif gcode[gcode["G"] == 91].shape[0] != 0:
stats["positioning"] = "relative"
#
# Unit system
# TODO: This isn't going to work if the system switches back and forth between units
# user will receive whichever unit system was last in flavor["units"]["commands"]
#
if flavor["units"]["commands"]:
for key, val in flavor["units"]["commands"].items():
if key in gcode.columns and (gcode[key] == val).any():
stats["units"] = flavor["units"]["commands"][key]
#
# Contruct a bounding box around the entire dataset and try bounding the deposition
#
stats["bounds"] = bounds_of_gcode_df(gcode)
#
# Time estimate, same as event series
#
stats["elapsed_time_s"] = gcode["time"].sum()
return stats
[docs]
def bounds_of_gcode_df(gcode: pd.DataFrame, tag=None) -> tuple | None:
"""Compute the XYZ bounding box of a G-code DataFrame. For AM-specific bounds, use ``AdditivePart`` instead.
Args:
gcode (pd.DataFrame): G-code DataFrame with absolute position columns.
tag (str, optional): If provided, filter to rows whose ``tags`` string contains this tag before computing bounds.
Returns:
tuple: ``((x_min, y_min, z_min), (x_max, y_max, z_max))``, or ``None`` if position columns are missing.
"""
X, Y, Z = ABS_POSITION
try:
assert all(col in gcode.columns for col in ABS_POSITION)
except:
print(
f"\nFailed to calculate bounds.\n\tColumns {ABS_POSITION} missing from DataFrame with columns {gcode.columns}"
)
return
if tag:
gcode = gcode[gcode["tags"].str.contains(tag)]
return (
(gcode[X].min(), gcode[Y].min(), gcode[Z].min()),
(gcode[X].max(), gcode[Y].max(), gcode[Z].max()),
)
def _word_appearance_gcode_df(
gcode: pd.DataFrame, flavor: dict = flavors["default"], words: Iterable = None
) -> dict:
"""Count occurrences of specific G-code words in a DataFrame.
Args:
gcode (pd.DataFrame): G-code DataFrame to search.
flavor (dict, optional): Syntax definition used to parse word strings. Defaults to flavors["default"].
words (Iterable, optional): Words to count (strings like ``"G1"`` or tuples like ``("G", 1)``). Defaults to ``[]``.
Returns:
dict: Mapping of word to occurrence count.
"""
if words is None:
words = []
# if a word is a string, we assume that the user only wanted 1 word
elif isinstance(words, str):
words = [words]
# words variable allows us to check number of times that a specific word was used
n_word_use = dict()
for word in words:
n_word_use[word] = 0
# Simple case, where word is whole. i.e. "G1"
if isinstance(word, str):
split_word = re.findall(flavor["word_re"], word)
if split_word:
key, val = split_word[0]
val = float(val)
if key not in gcode.columns:
continue
n_word_use[word] = gcode[gcode[key] == val].shape[0]
# Assuming that the word has been split. i.e. ("G", 1), ["G", 1]
# Here we assume that the type of word[1] is correct
elif isinstance(word, (tuple, list, np.ndarray)) and len(word) == 2:
key, val = word
if key not in gcode.columns:
continue
n_word_use[f"{key}{val}"] = gcode[gcode[key] == val].shape[0]
return n_word_use