Source code for gcode_reader.analysis

import logging
import time

import numpy as np
import pandas as pd

from .emulate import tag_literals
from .constants import ZERO_TOLERANCE
from .emulate.operations import AdditiveOperation


def _df_column_name_validation(
    df: pd.DataFrame,
    required_cols: list,
):
    """
    Validates the presence of required columns by name in a DataFrame.

    Args:
        df: The DataFrame to validate.
        required_cols: A list of column names that must be present.

    Raises:
        KeyError: If one or more required columns are missing.
    """
    required_cols = set(required_cols)
    if not required_cols.issubset(df.columns):
        missing = required_cols - set(df.columns)
        raise KeyError(
            f"Column validation failed. Missing required columns: {list(missing)}"
        )


def _identify_additive_process_start_end_indices(
    df: pd.DataFrame,
):
    """
    Identifies the start and end indices of the primary additive process.
    This is defined as one index before the first extrusion event and one
    index after the last extrusion event.

    Args:
        df: The DataFrame to search, containing a 'tags' column.

    Returns:
        A tuple containing the start and end integer indices.

    Raises:
        ValueError: If no extrusion tags are found in the DataFrame.
    """

    _df_column_name_validation(df, ["tags"])

    extrude_pattern = f"{tag_literals.EXTRUDE}|{tag_literals.MOVE_EXTRUDE}"
    extrude_indices = df.index[
        df["tags"].str.contains(extrude_pattern, case=False, na=False)
    ]

    # NOTE: This will force extrusion tags to exist in order to construct an Event Series
    # TODO: whole range of indices
    if extrude_indices.empty:
        raise ValueError(
            f"No extrusion tags ({extrude_pattern}) found to identify process start/end."
        )

    i_start = df.index.get_loc(extrude_indices[0])
    i_end = df.index.get_loc(extrude_indices[-1])
    return i_start - 1, i_end + 1


def _build_abaqus_event_series(
    event_series_df: pd.DataFrame,
    spatial_scale_factor: float = 1e-3,
    extrusion_col: str = "deposited_volume",
    time_col: str = "elapsed_time",
):
    """Constructs an Abaqus Event Series from a DataFrame with process data

    Args:
        event_series_df: DataFrame with process data.
        spatial_scale_factor: Factor to scale spatial coordinates (x, y, z).
        extrusion_col: Name of the column representing extruded volume or rate.
        time_col: Name of the column with elapsed time per step.

    Returns:
        A DataFrame formatted for Abaqus event series input.
    """
    t0 = time.perf_counter()
    logging.debug(f"\tBuilding Abaqus Event Series: (_build_abaqus_event_series)")
    logging.debug(f"\t\tFrom {event_series_df.index.size} process data instances")

    _df_column_name_validation(
        df=event_series_df,
        required_cols=[
            "x",
            "y",
            "z",
            "dx",
            "dy",
            "dz",
            extrusion_col,
            time_col,
            "tags",
        ],
    )

    event_series_df = event_series_df.reset_index()

    # create a new column for boolean extrusion values
    logging.debug(f"\t\tIdentifying extrusion values")
    event_series_df["e"] = (event_series_df[extrusion_col] > 0).astype(int)

    # set prime commands to 0 so they are ignored
    event_series_df.loc[
        event_series_df.tags.str.contains(r"prime", case=False), "e"
    ] = 0

    # set the first extrusion entry to 0 so it doesn't mess up the beginning of the abaqus activation
    # shift the extrusion column up by one so it complies with the abaqus format
    # Ensure the last value is 0
    event_series_df["e"] = event_series_df.e.shift(-1)
    event_series_df.loc[0, "e"] = 0
    event_series_df.loc[event_series_df.index[-1], "e"] = 0

    logging.debug(f"\t\tEvaluating time component")
    event_series_df["time"] = event_series_df[time_col].cumsum()
    event_series_df["time"] -= event_series_df["time"].iloc[0]

    # scale locations to analysis unit system
    if spatial_scale_factor is not None:
        logging.debug(f"\t\tScaling by factor {spatial_scale_factor}")
        event_series_df.loc[:, ["x", "y", "z"]] *= spatial_scale_factor

    # Abaqus Torch Error:
    # perturb the Y-coordinates of vertical movements
    logging.debug(f"\t\tAddressing Torch Error")
    vertical_movements = (
        (np.abs(event_series_df["dx"]) < ZERO_TOLERANCE)
        & (np.abs(event_series_df["dy"]) < ZERO_TOLERANCE)
        & (np.abs(event_series_df["dz"]) > ZERO_TOLERANCE)
    )
    event_series_df.loc[vertical_movements, "y"] += 1e-4

    dt = time.perf_counter() - t0
    logging.debug(f"\t...Complete in {dt:0.3} (s) (_build_abaqus_event_series)")
    return event_series_df[["time", "x", "y", "z", "e"]]


[docs] def operation_to_event_series( operation: AdditiveOperation, spatial_scale_factor=1e-3 ) -> pd.DataFrame: """ Converts an 'operation' DataFrame to an Abaqus event series. """ t0 = time.perf_counter() logging.debug( f"operation_to_event_series: Building Event Series from Operation {operation.name}. " ) logging.debug(f"\tPreparing ProcessData DataFrame:") data = [data.to_dict() for data in operation.process_data] operation_df = pd.DataFrame(data) operation_df["tags"] = [command.TAG for command in operation.commands] required_cols = [ "location", "relative_movement", "deposited_volume", "elapsed_time", "tags", ] _df_column_name_validation(operation_df, required_cols) i_start, i_end = _identify_additive_process_start_end_indices(operation_df) # truncate to only the extrusion portion # pythonic indexing is [inclusive : exclusive] # remove the comments, config entries, and empty lines since they inturrupt activiation with imposed ffill # NOTE: Use the values that are derived from the process data! event_series_df = ( operation_df.iloc[i_start:i_end] .dropna(subset=["relative_movement"])[required_cols] .copy() ) location_array = np.stack(event_series_df["location"].values) event_series_df["x"] = location_array[:, 0] event_series_df["y"] = location_array[:, 1] event_series_df["z"] = location_array[:, 2] relative_move_array = np.stack(event_series_df["relative_movement"].values) event_series_df["dx"] = relative_move_array[:, 0] event_series_df["dy"] = relative_move_array[:, 1] event_series_df["dz"] = relative_move_array[:, 2] logging.debug(f"\t...Complete in {time.perf_counter() - t0:0.3f} (s)") event_series = _build_abaqus_event_series(event_series_df, spatial_scale_factor) logging.debug( f"operation_to_event_series: Complete in {time.perf_counter() - t0:0.3f} (s)" ) return event_series
[docs] def gcode_dataframe_to_event_series( gcode_df: pd.DataFrame, spatial_scale_factor=1e-3 ) -> pd.DataFrame: """ Converts a 'G-code' DataFrame to an Abaqus event series. """ required_cols = [ "x", "y", "z", "dx", "dy", "dz", "E", "time", "tags", ] _df_column_name_validation(gcode_df, required_cols) i_start, i_end = _identify_additive_process_start_end_indices(gcode_df) event_series_df = ( gcode_df[required_cols] .iloc[i_start:i_end] .dropna(subset=["dx", "dy", "dz"]) .copy() ) return _build_abaqus_event_series( event_series_df, spatial_scale_factor, extrusion_col="E", time_col="time" )