From 39637cf342bf3aab6bd538ccfe7f4a1af1b81339 Mon Sep 17 00:00:00 2001 From: usmfi Date: Tue, 28 Jan 2025 13:56:54 +0100 Subject: [PATCH 01/68] impl volumetric extrusion --- .pre-commit-config.yaml | 2 +- .../data/default_printer_presets.yaml | 3 ++- pyGCodeDecode/gcode_interpreter.py | 13 ++++----- pyGCodeDecode/state_generator.py | 27 ++++++++++++++++--- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3ab8ee5..974470a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - id: black-jupyter # isort import sorting - repo: https://github.com/timothycrosley/isort - rev: "5.13.2" + rev: "6.0.0" hooks: - id: isort args: ["--profile", "black"] diff --git a/pyGCodeDecode/data/default_printer_presets.yaml b/pyGCodeDecode/data/default_printer_presets.yaml index f2e4e68..fd98083 100644 --- a/pyGCodeDecode/data/default_printer_presets.yaml +++ b/pyGCodeDecode/data/default_printer_presets.yaml @@ -48,7 +48,8 @@ prusa_mini_klipper: debugging: # general properties nozzle_diam: 0.4 - filament_diam: 1.75 + filament_diam: 2.85 + extrusion_volumetric: True # true: mm3 with UltiGCode, else mm length # default settings p_vel: 85 p_acc: 100 diff --git a/pyGCodeDecode/gcode_interpreter.py b/pyGCodeDecode/gcode_interpreter.py index 9554529..18ccd74 100644 --- a/pyGCodeDecode/gcode_interpreter.py +++ b/pyGCodeDecode/gcode_interpreter.py @@ -604,7 +604,7 @@ def get_values(self, t: float, output_unit_system: str = None) -> Tuple[List[flo return tmp_vel, tmp_pos - def get_width(self, t: float, extrusion_h: float, filament_dia: float): + def get_width(self, t: float, extrusion_h: float, filament_dia: float = None) -> float: """Return the extrusion width for a certain extrusion height at time. Args: @@ -615,13 +615,14 @@ def get_width(self, t: float, extrusion_h: float, filament_dia: float): Returns: float: width """ + filament_dia = self.initial_machine_setup["filament_diam"] if filament_dia is None else filament_dia + curr_val = self.get_values(t=t) feed_rate = np.linalg.norm(curr_val[0][:3]) # calculate feed rate at current time flow_rate = curr_val[0][3] # get extrusion rate at current time - filament_cross_sec = (np.pi * filament_dia**2) / 4 # calculate cross area of filament - + filament_cross_sec = np.pi * (filament_dia / 2) ** 2 # calculate cross area of filament width = ( (flow_rate * filament_cross_sec) / (extrusion_h * feed_rate) if feed_rate > 0 else 0 ) # calculate width, zero if no movement. @@ -649,11 +650,7 @@ def check_initial_setup(self, initial_machine_setup): "printer_name", "firmware", ] - optional_keys = [ - "layer_cue", - "nozzle_diam", - "filament_diam", - ] + optional_keys = ["layer_cue", "nozzle_diam", "filament_diam", "extrusion_volumetric"] valid_keys = req_keys + optional_keys diff --git a/pyGCodeDecode/state_generator.py b/pyGCodeDecode/state_generator.py index 7acc298..0499626 100644 --- a/pyGCodeDecode/state_generator.py +++ b/pyGCodeDecode/state_generator.py @@ -207,6 +207,26 @@ def dict_list_traveler(line_dict_list: List[dict], initial_machine_setup: dict) state_list: (list[state]) all states in a list """ + + def apply_extrusion(line_dict: dict, virtual_machine: dict, command: str, initial_machine_setup: dict) -> dict: + import math + + e_value = line_dict[command]["E"] + + # volumetric to length conversion + # (1) V = (d/2)^2 * pi * E + # (2) E = V / ((d/2)^2 * pi) + if initial_machine_setup.get("extrusion_volumetric", False): + # volumetric extrusion + e_value = e_value / (math.pi * (initial_machine_setup["filament_diam"] / 2) ** 2) + + if virtual_machine["absolute_extrusion"] is True: + virtual_machine["E"] = e_value + if virtual_machine["absolute_extrusion"] is False: # redundant + virtual_machine["E"] = virtual_machine["E"] + e_value + + return virtual_machine + state_list: List[state] = list() virtual_machine = { @@ -298,10 +318,9 @@ def dict_list_traveler(line_dict_list: List[dict], initial_machine_setup: dict) # look for extrusion commands and apply abs/rel if "E" in line_dict[command]: - if virtual_machine["absolute_extrusion"] is True: - virtual_machine["E"] = line_dict[command]["E"] - if virtual_machine["absolute_extrusion"] is False: # redundant - virtual_machine["E"] = virtual_machine["E"] + line_dict[command]["E"] + virtual_machine = apply_extrusion(line_dict, virtual_machine, command, initial_machine_setup) + + # feed rates in unit/min to unit/sec if "F" in line_dict[command]: virtual_machine["p_vel"] = line_dict[command]["F"] / 60 From 4ed58744824ca4597793c9a6afa6a3af675a4a75 Mon Sep 17 00:00:00 2001 From: usmfi Date: Sun, 16 Mar 2025 16:52:30 +0100 Subject: [PATCH 02/68] added verbosity control and specified lvl for prints --- pyGCodeDecode/cli.py | 14 ++-- pyGCodeDecode/gcode_interpreter.py | 121 +++++++++++++---------------- pyGCodeDecode/helpers.py | 77 +++++++++++++++++- pyGCodeDecode/planner_block.py | 4 +- pyGCodeDecode/state_generator.py | 4 +- pyGCodeDecode/tools.py | 4 +- 6 files changed, 141 insertions(+), 83 deletions(-) diff --git a/pyGCodeDecode/cli.py b/pyGCodeDecode/cli.py index 7b4e484..99a8e49 100644 --- a/pyGCodeDecode/cli.py +++ b/pyGCodeDecode/cli.py @@ -29,22 +29,23 @@ def _find_gcode_file(specified_path: pathlib.Path | None) -> pathlib.Path: g_code_file = specified_path elif specified_path is not None: custom_print( - f"❌ The specified G-code:\n{specified_path.resolve()}\nis not valid.\n" "🛑 Exiting the program." + f"❌ The specified G-code:\n{specified_path.resolve()}\nis not valid.\n" "🛑 Exiting the program.", + lvl=1, ) exit() else: custom_print("⚠️ No G-code file specified. Looking for a G-code file in the current directory... 👀") files_list = list(pathlib.Path.cwd().glob("*.gcode")) if files_list.__len__() == 0: - custom_print("❌ No G-code file found in the current directory.\n" "🛑 Exiting the program.") + custom_print("❌ No G-code file found in the current directory.\n" "🛑 Exiting the program.", lvl=1) exit() elif files_list.__len__() == 1: g_code_file = files_list[0] else: - custom_print("❌ Multiple G-code files found in the current directory:") + custom_print("❌ Multiple G-code files found in the current directory:", lvl=1) for file in files_list: - custom_print(f" - {file.resolve()}") - custom_print("🛑 Exiting the program.") + custom_print(f" - {file.resolve()}", lvl=1) + custom_print("🛑 Exiting the program.", lvl=1) exit() custom_print(f"✅ Using the G-code file:\n{g_code_file.resolve()}") @@ -57,7 +58,8 @@ def _get_presets_file(presets_file: pathlib.Path | None) -> pathlib.Path: presets_file = importlib.resources.files("pyGCodeDecode").joinpath("data/default_printer_presets.yaml") elif not presets_file.is_file(): custom_print( - f"❌ The specified presets file:\n{presets_file.resolve()}\nis not valid.\n" "🛑 Exiting the program." + f"❌ The specified presets file:\n{presets_file.resolve()}\nis not valid.\n" "🛑 Exiting the program.", + lvl=1, ) exit() else: diff --git a/pyGCodeDecode/gcode_interpreter.py b/pyGCodeDecode/gcode_interpreter.py index 9554529..9cf1e93 100644 --- a/pyGCodeDecode/gcode_interpreter.py +++ b/pyGCodeDecode/gcode_interpreter.py @@ -3,7 +3,6 @@ import importlib.resources import os import pathlib -import sys import time from typing import List, Tuple, Union @@ -12,53 +11,13 @@ import yaml from matplotlib.figure import Figure -from pyGCodeDecode.helpers import custom_print +from pyGCodeDecode.helpers import ProgressBar, custom_print, set_verbosity_level from .planner_block import planner_block from .state import state from .state_generator import generate_states from .utils import segment, velocity -last_progress_update: float = 0.0 - - -def update_progress(progress: float, name: str = "Percent") -> None: - """Display or update a console progress bar. - - Args: - progress: float between 0 and 1 for percentage, < 0 represents a 'halt', > 1 represents 100% - name: (string, default = "Percent") customizable name for progress bar - """ - global last_progress_update - - barLength = 10 - status = "" - - # check whether the input is valid - if progress is int: - progress = float(progress) - if not isinstance(progress, float): - progress = 0.0 - status = "error: progress var must be float\r\n" - - # progress outside [0, 1] - if progress < 0.0: - progress = 0.0 - status = "Halt...\r\n" - if progress >= 1.0: - progress = 1.0 - status = "Done...\r\n" - - progress_percent = round(progress * 100, ndigits=1) - - # check whether the progress has changed - if last_progress_update != progress_percent or status != "": - block = int(round(barLength * progress, ndigits=0)) - text = f"\r[{'#' * block + '-' * (barLength - block)}] {progress_percent} % of {name} {status}" - sys.stdout.write(text) - sys.stdout.flush() - last_progress_update = progress_percent - def generate_planner_blocks(states: List[state], firmware=None): """Convert list of states to trajectory repr. by planner blocks. @@ -71,6 +30,7 @@ def generate_planner_blocks(states: List[state], firmware=None): block_list (list[planner_block]) list of all planner blocks to complete travel between all states """ block_list = [] + bar = ProgressBar(name="Planner Blocks") for i, this_state in enumerate(states): prev_block = block_list[-1] if len(block_list) > 0 else None # grab prev block from block_list new_block = planner_block(state=this_state, prev_block=prev_block, firmware=firmware) # generate new block @@ -78,7 +38,7 @@ def generate_planner_blocks(states: List[state], firmware=None): if new_block.prev_block is not None: new_block.prev_block.next_block = new_block # update nb list block_list.append(new_block) - update_progress((i + 1) / len(states), "Planner Block Generation") + bar.update((i + 1) / len(states)) return block_list @@ -89,7 +49,8 @@ def find_current_segment(path: List[segment], t: float, last_index: int = None, path: (list[segment]) all segments to be searched t: (float) time of search last_index: (int) last found index for optimizing search - keep_position: (bool) keeps position of last segment, use this when working with gaps of no movement between segments + keep_position: (bool) keeps position of last segment, use this when working with + gaps of no movement between segments Returns: segment: (segment) the segment which defines movement at that point in time @@ -133,10 +94,10 @@ def find_current_segment(path: List[segment], t: float, last_index: int = None, # original function untouched # some robustness checks if path[-1].t_end < t: - custom_print("No movement at this time in Path!") + custom_print("No movement at this time in Path!", lvl=1) return None, None elif last_index is None or len(path) - 1 < last_index or path[last_index].t_begin > t: - # custom_print(f"unoptimized Search, last index: {last_index}") + custom_print(f"unoptimized Search, last index: {last_index}", lvl=3) for last_index, segm in enumerate(path): if t >= segm.t_begin and t < segm.t_end: return segm, last_index @@ -171,6 +132,7 @@ def __init__( machine_name: str = None, initial_machine_setup: "setup" = None, output_unit_system: str = "SI (mm)", + verbosity_level: int = None, ): """Initialize the Simulation of a given G-code with initial machine setup or default machine. @@ -183,6 +145,7 @@ def __init__( machine name: (string, default = None) name of the default machine to use initial_machine_setup: (setup, default = None) setup instance output_unit_system: (string, default = "SI (mm)") available unit systems: SI, SI (mm) & inch + verbosity_level: (int, default = None) set verbosity level (0: no output, 1: warnings, 2: info, 3: debug) Example: ```python @@ -193,6 +156,7 @@ def __init__( self.last_index = None # used to optimize search in segment list self.filename = gcode_path self.firmware = None + set_verbosity_level(verbosity_level) # set output unit system self.available_unit_systems = {"SI": 1e-3, "SI (mm)": 1.0, "inch": 1 / 25.4} @@ -212,7 +176,9 @@ def __init__( raise ValueError("Neither a printer name nor a printer setup was specified. At least one is required!") else: custom_print( - "Only a machine name was specified but no full setup. Trying to create a setup from pyGCD's default values..." + "Only a machine name was specified but no full setup." + "Trying to create a setup from pyGCD's default values...", + lvl=1, ) default_presets_file = importlib.resources.files("pyGCodeDecode").joinpath( "data/default_printer_presets.yaml" @@ -232,7 +198,8 @@ def __init__( ) custom_print( - f"Simulating \"{self.filename}\" with {self.initial_machine_setup['printer_name']} using the {self.firmware} firmware.\n" + f"Simulating \"{self.filename}\" with {self.initial_machine_setup['printer_name']} using " + f"the {self.firmware} firmware.\n" ) self.blocklist: List[planner_block] = generate_planner_blocks(states=self.states, firmware=self.firmware) self.trajectory_self_correct() @@ -292,8 +259,9 @@ def interp_2D(x, y, cvar, spatial_resolution=1): y.append(segments[0].pos_begin.get_vec()[1]) cvar.append(segments[0].vel_begin.get_norm()) + bar = ProgressBar(name="2D Plot Lines") for i, segm in enumerate(segments): - update_progress((i + 1) / len(segments), name="2D Plot Lines") + bar.update((i + 1) / len(segments)) x.append(segm.pos_end.get_vec()[0]) y.append(segm.pos_end.get_vec()[1]) cvar.append(segm.vel_end.get_norm()) @@ -325,7 +293,7 @@ def interp_2D(x, y, cvar, spatial_resolution=1): x.append(segments[0].pos_begin.get_vec()[0]) y.append(segments[0].pos_begin.get_vec()[1]) for i, segm in enumerate(segments): - update_progress((i + 1) / len(segments), name="2D Plot Lines") + bar.update((i + 1) / len(segments)) x.append(segm.pos_end.get_vec()[0]) y.append(segm.pos_end.get_vec()[1]) fig = plt.subplot() @@ -333,7 +301,7 @@ def interp_2D(x, y, cvar, spatial_resolution=1): if show_points: for i, block in enumerate(self.blocklist): - update_progress(i / len(self.blocklist), name="2D Plot Points") + bar.update(i / len(self.blocklist)) fig.scatter( block.get_segments()[-1].pos_end.get_vec()[0], block.get_segments()[-1].pos_end.get_vec()[1], @@ -365,9 +333,11 @@ def plot_3d( Args: extrusion_only (bool, optional): Plot only parts where material is extruded. Defaults to True. - screenshot_path (pathlib.Path, optional): Path to screenshot to be saved. Prevents interactive plot. Defaults to None. + screenshot_path (pathlib.Path, optional): Path to screenshot to be saved. Prevents + interactive plot. Defaults to None. vtk_path (pathlib.Path, optional): Path to vtk to be saved. Prevents interactive plot. Defaults to None. - mesh (pv.MultiBlock, optional): A pyvista mesh from a previous run to avoid running the mesh generation again. Defaults to None. + mesh (pv.MultiBlock, optional): A pyvista mesh from a previous run to avoid running the + mesh generation again. Defaults to None. Returns: pv.MultiBlock: The mesh used in the plot so it can be used (e.g. in subsequent plots). @@ -383,9 +353,9 @@ def plot_3d( mesh = pv.MultiBlock() x, y, z, e, vel = [], [], [], [], [] - + bar = ProgressBar(name="3D Plot") for n, segm in enumerate(segments): - update_progress((n + 1) / len(segments), name="3D Plot") + bar.update((n + 1) / len(segments)) if (not extrusion_only) or (segm.is_extruding()): if len(x) == 0: @@ -447,7 +417,7 @@ def plot_3d( p.screenshot(filename=screenshot_path) custom_print(f"Screenshot saved to:\n{screenshot_path}") else: - custom_print("Screenshot can not be created without a display!") + custom_print("Screenshot can not be created without a display!", lvl=1) if not off_screen and display_available: p.show() @@ -473,7 +443,8 @@ def plot_vel( show_planner_blocks: (bool, default = True) show planner_blocks as vertical lines show_segments: (bool, default = False) show segments as vertical lines show_jv: (bool, default = False) show junction velocity as x - time_steps: (int or string, default = "constrained") number of time steps or constrain plot vertices to segment vertices + time_steps: (int or string, default = "constrained") number of time steps or constrain plot + vertices to segment vertices filepath: (Path, default = None) save fig as image if filepath is provided dpi: (int, default = 400) select dpi @@ -507,6 +478,7 @@ def plot_vel( vel = [[], [], [], []] abs = [] # initialize value arrays index_saved = 0 + bar = ProgressBar(name="Velocity Plot") for i, t in enumerate(times): segm, index_saved = find_current_segment(path=segments, t=t, last_index=index_saved, keep_position=True) @@ -518,7 +490,7 @@ def plot_vel( vel[axis_dict[ax]].append(tmp_vel[axis_dict[ax]]) abs.append(np.linalg.norm(tmp_vel[:3])) - update_progress((i + 1) / len(times), name="Velocity Plot") + bar.update((i + 1) / len(times)) fig, ax1 = plt.subplots() ax2 = ax1.twinx() @@ -569,15 +541,16 @@ def plot_vel( def trajectory_self_correct(self): """Self correct all blocks in the blocklist with self_correction() method.""" n_max = len(self.blocklist) - last_progress_update = 0 + bar = ProgressBar(name="Block Correction") for n, block in enumerate(self.blocklist): progress = round(n / n_max, ndigits=3) - if progress > last_progress_update: - update_progress((n + 1) / len(self.blocklist), name="Block Correction") - last_progress_update = progress + if progress > bar.last_progress_update: + bar.update((n + 1) / len(self.blocklist)) + bar.last_progress_update = progress block.self_correction() + bar.update(1.0) def get_values(self, t: float, output_unit_system: str = None) -> Tuple[List[float]]: """Return unit system scaled values for vel and pos. @@ -668,7 +641,8 @@ def check_initial_setup(self, initial_machine_setup): for key in req_keys: if key not in initial_machine_setup: raise ValueError( - f'Missing Key: "{key}" is not provided in Setup Dictionary, check for typos. Required keys are: {req_keys}' + f'Missing Key: "{key}" is not provided in Setup Dictionary,' + f" check for typos. Required keys are: {req_keys}" ) def print_summary(self, start_time: float): @@ -678,8 +652,10 @@ def print_summary(self, start_time: float): start_time (float): time when the simulation run was started """ custom_print( - f" >> pyGCodeDecode extracted {len(self.states)} states from {self.filename} and generated {len(self.blocklist)} planner blocks.\n" - f"Estimated time to travel all states with provided printer settings is {self.blocklist[-1].get_segments()[-1].t_end:.2f} seconds.\n" + f" >> pyGCodeDecode extracted {len(self.states)} states from {self.filename}" + f" and generated {len(self.blocklist)} planner blocks.\n" + f"Estimated time to travel all states with provided" + f" printer settings is {self.blocklist[-1].get_segments()[-1].t_end:.2f} seconds.\n" f"The Simulation took {(time.time()-start_time):.2f} s." ) @@ -687,7 +663,8 @@ def refresh(self, new_state_list: List[state] = None): """Refresh simulation. Either through new state list or by rerunning the self.states as input. Args: - new_state_list: (list[state], default = None) new list of states, if None is provided, existing states get resimulated + new_state_list: (list[state], default = None) new list of states, + if None is provided, existing states get resimulated """ if new_state_list is not None: self.states = new_state_list @@ -804,18 +781,21 @@ def __init__( presets_file: str, printer: str = None, layer_cue: str = None, - ) -> None: + verbosity_level: int = None, + ): """Create simulation setup. Args: presets_file: (string) choose setup yaml file with printer presets printer: (string) select printer from preset file layer_cue: (string) set slicer specific layer change cue from comment + verbosity_level: (int, default = None) set verbosity level (0: no output, 1: warnings, 2: info, 3: debug) """ # the input unit system is only implemented for 'set_initial_position'. # Regardless, the class has this attribute so it's more similar to the simulation class. self.available_unit_systems = {"SI": 1e3, "SI (mm)": 1.0, "inch": 25.4} self.input_unit_system = "SI (mm)" + set_verbosity_level(verbosity_level) self.initial_position = { "X": 0, @@ -862,7 +842,8 @@ def set_initial_position(self, initial_position: Union[tuple, dict], input_unit_ """Set initial Position. Args: - initial_position: (tuple or dict) set initial position as tuple of len(4) or dictionary with keys: {X, Y, Z, E}. + initial_position: (tuple or dict) set initial position as tuple of len(4) + or dictionary with keys: {X, Y, Z, E}. input_unit_system (str, optional): Wanted input unit system. Uses the one specified for the setup if None is specified. @@ -889,7 +870,9 @@ def set_initial_position(self, initial_position: Union[tuple, dict], input_unit_ raise ValueError("Set initial position through dict with keys: {X, Y, Z, E} or as tuple with length 4.") def set_property(self, property_dict: dict): - """Overwrite or add a property to the printer dictionary. Printer has to be selected through select_printer() beforehand. + """Overwrite or add a property to the printer dictionary. + + Printer has to be selected through select_printer() beforehand. Args: property_dict: (dict) set or add property to the setup diff --git a/pyGCodeDecode/helpers.py b/pyGCodeDecode/helpers.py index 595e185..6462e45 100644 --- a/pyGCodeDecode/helpers.py +++ b/pyGCodeDecode/helpers.py @@ -9,9 +9,31 @@ else: FLAG_USING_ABAQUS = False +# global verbosity level +VERBOSITY_LEVEL = 2 # default to INFO -def custom_print(*args, **kwargs) -> None: - """Sanitize outputs for ABAQUS and print them. Takes regular arguments for print.""" + +def set_verbosity_level(level: int) -> None: + """Set the global verbosity level.""" + global VERBOSITY_LEVEL + if level is not None: + VERBOSITY_LEVEL = level + + +def get_verbosity_level() -> int: + """Get the current global verbosity level.""" + return VERBOSITY_LEVEL + + +def custom_print(*args, lvl=2, **kwargs) -> None: + """Sanitize outputs for ABAQUS and print them. Takes regular arguments for print. + + Args: + *args: arguments to be printed + lvl: verbosity level of the print (1 = WARNING, 2 = INFO, 3 = DEBUG) + **kwargs: keyword arguments to be passed to print + + """ sanitized_args = [] if FLAG_USING_ABAQUS: # remove non-ascii characters like emojis as ABAQUS can't handle them @@ -20,4 +42,53 @@ def custom_print(*args, **kwargs) -> None: else: sanitized_args = args - print(*sanitized_args, **kwargs) + # print with verbosity level + if lvl <= VERBOSITY_LEVEL: + levels = {3: "[DEBUG]", 2: "[INFO]: ", 1: "[WARNING]: "} + prefix = levels.get(lvl, "") + print(prefix, *sanitized_args, **kwargs) + + +class ProgressBar: + """A simple progress bar for the console.""" + + def __init__(self, name: str = "Percent", barLength: int = 10): + """Initialize a progress bar.""" + self.name = name + self.barLength = barLength + self.last_progress_update = -1 + + def update(self, progress: float) -> None: + """Display or update a console progress bar. + + Args: + progress: float between 0 and 1 for percentage, < 0 represents a 'halt', > 1 represents 100% + """ + barLength = self.barLength + status = "" + + if VERBOSITY_LEVEL >= 2: # only print if verbosity level is high enough + # check whether the input is valid + if progress is int: + progress = float(progress) + if not isinstance(progress, float): + progress = 0.0 + status = "error: progress var must be float\r\n" + + # progress outside [0, 1] + if progress < 0.0: + progress = 0.0 + status = "- Waiting \r\n" + if progress >= 1.0: + progress = 1.0 + status = "- Done\r\n" + + progress_percent = round(progress * 100, ndigits=1) + + # check whether the progress has changed + if self.last_progress_update != progress_percent or status != "": + block = int(round(barLength * progress, ndigits=0)) + text = f"\r[{'#' * block + '-' * (barLength - block)}] {progress_percent} % of {self.name} {status}" + sys.stdout.write(text) + sys.stdout.flush() + self.last_progress_update = progress_percent diff --git a/pyGCodeDecode/planner_block.py b/pyGCodeDecode/planner_block.py index 9c43909..8e2a0bf 100644 --- a/pyGCodeDecode/planner_block.py +++ b/pyGCodeDecode/planner_block.py @@ -198,7 +198,7 @@ def singl_dwn(): ) except ValueError as ve: - custom_print(f"Segments to state: {str(self.state_B)} could not be modeled.\n {ve}") + custom_print(f"Segments to state: {str(self.state_B)} could not be modeled.\n {ve}", lvl=1) raise RuntimeError() def self_correction(self, tolerance=float("1e-12")): @@ -241,7 +241,7 @@ def self_correction(self, tolerance=float("1e-12")): for segm in self.segments: segm.self_check(p_settings=self.state_B.state_p_settings) except ValueError as ve: - custom_print(f"Segment for {self.state_B} does not adhere to machine limits: {ve}") + custom_print(f"Segment for {self.state_B} does not adhere to machine limits: {ve}", lvl=1) return flag_correct diff --git a/pyGCodeDecode/state_generator.py b/pyGCodeDecode/state_generator.py index 7acc298..caa8119 100644 --- a/pyGCodeDecode/state_generator.py +++ b/pyGCodeDecode/state_generator.py @@ -400,9 +400,9 @@ def check_for_unsupported_commands(line_dict_list: dict) -> dict: } if unsupported_commands_found != []: - custom_print(f"⚠️ Warning: {len(unsupported_command_counts.keys())} known but unsupported command(s) found:") + custom_print(f"{len(unsupported_command_counts.keys())} known but unsupported command(s) found:", lvl=1) for key, value in unsupported_command_counts.items(): - custom_print(f" - Command '{key}' found {value} time(s).") + custom_print(f" - Command '{key}' found {value} time(s).", lvl=1) else: custom_print("Great, the G-code does not contain any unsupported commands known to pyGCD 🎈.") diff --git a/pyGCodeDecode/tools.py b/pyGCodeDecode/tools.py index ac79908..759fab2 100644 --- a/pyGCodeDecode/tools.py +++ b/pyGCodeDecode/tools.py @@ -29,7 +29,9 @@ def save_layer_metrics( """ # check if a layer cue was specified if "layer_cue" not in simulation.initial_machine_setup: - custom_print("⚠️ No layer_cue was specified in the simulation setup. Therefore, layer metrics can not be saved!") + custom_print( + "⚠️ No layer_cue was specified in the simulation setup. Therefore, layer metrics can not be saved!", lvl=1 + ) return None if locale is None: From d2946e25979eb1b3f9e0f05236e43ddc5b9f9650 Mon Sep 17 00:00:00 2001 From: usmfi Date: Sun, 16 Mar 2025 17:11:38 +0100 Subject: [PATCH 03/68] fix inconsistent use of apostrophes --- pyGCodeDecode/gcode_interpreter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyGCodeDecode/gcode_interpreter.py b/pyGCodeDecode/gcode_interpreter.py index 9cf1e93..ea0d009 100644 --- a/pyGCodeDecode/gcode_interpreter.py +++ b/pyGCodeDecode/gcode_interpreter.py @@ -634,14 +634,14 @@ def check_initial_setup(self, initial_machine_setup): for key in initial_machine_setup: if key not in valid_keys: raise ValueError( - f'Invalid Key: "{key}" in Setup Dictionary, check for typos. Valid keys are: {valid_keys}' + f"Invalid Key: '{key}' in Setup Dictionary, check for typos. Valid keys are: {valid_keys}" ) # check if every required key is proivded for key in req_keys: if key not in initial_machine_setup: raise ValueError( - f'Missing Key: "{key}" is not provided in Setup Dictionary,' + f"Missing Key: '{key}' is not provided in Setup Dictionary," f" check for typos. Required keys are: {req_keys}" ) From 0a6a1446b189b07f7d0a9b2689098e98b905264c Mon Sep 17 00:00:00 2001 From: Lukas Hof Date: Mon, 17 Mar 2025 10:00:29 +0000 Subject: [PATCH 04/68] Apply 6 suggestion(s) to 4 file(s) --- pyGCodeDecode/cli.py | 2 +- pyGCodeDecode/gcode_interpreter.py | 4 ++-- pyGCodeDecode/helpers.py | 5 +++-- pyGCodeDecode/state_generator.py | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pyGCodeDecode/cli.py b/pyGCodeDecode/cli.py index 99a8e49..19182b6 100644 --- a/pyGCodeDecode/cli.py +++ b/pyGCodeDecode/cli.py @@ -34,7 +34,7 @@ def _find_gcode_file(specified_path: pathlib.Path | None) -> pathlib.Path: ) exit() else: - custom_print("⚠️ No G-code file specified. Looking for a G-code file in the current directory... 👀") + custom_print("⚠️ No G-code file specified. Looking for a G-code file in the current directory... 👀", lvl=1) files_list = list(pathlib.Path.cwd().glob("*.gcode")) if files_list.__len__() == 0: custom_print("❌ No G-code file found in the current directory.\n" "🛑 Exiting the program.", lvl=1) diff --git a/pyGCodeDecode/gcode_interpreter.py b/pyGCodeDecode/gcode_interpreter.py index ea0d009..938f2b1 100644 --- a/pyGCodeDecode/gcode_interpreter.py +++ b/pyGCodeDecode/gcode_interpreter.py @@ -132,7 +132,7 @@ def __init__( machine_name: str = None, initial_machine_setup: "setup" = None, output_unit_system: str = "SI (mm)", - verbosity_level: int = None, + verbosity_level: int | None = None, ): """Initialize the Simulation of a given G-code with initial machine setup or default machine. @@ -781,7 +781,7 @@ def __init__( presets_file: str, printer: str = None, layer_cue: str = None, - verbosity_level: int = None, + verbosity_level: int | None = None, ): """Create simulation setup. diff --git a/pyGCodeDecode/helpers.py b/pyGCodeDecode/helpers.py index 6462e45..527416f 100644 --- a/pyGCodeDecode/helpers.py +++ b/pyGCodeDecode/helpers.py @@ -13,7 +13,7 @@ VERBOSITY_LEVEL = 2 # default to INFO -def set_verbosity_level(level: int) -> None: +def set_verbosity_level(level: int | None) -> None: """Set the global verbosity level.""" global VERBOSITY_LEVEL if level is not None: @@ -26,7 +26,8 @@ def get_verbosity_level() -> int: def custom_print(*args, lvl=2, **kwargs) -> None: - """Sanitize outputs for ABAQUS and print them. Takes regular arguments for print. + """Sanitize outputs for ABAQUS and print them if the log level is high enough. + Takes all regular arguments for print. Args: *args: arguments to be printed diff --git a/pyGCodeDecode/state_generator.py b/pyGCodeDecode/state_generator.py index caa8119..3fee299 100644 --- a/pyGCodeDecode/state_generator.py +++ b/pyGCodeDecode/state_generator.py @@ -400,7 +400,7 @@ def check_for_unsupported_commands(line_dict_list: dict) -> dict: } if unsupported_commands_found != []: - custom_print(f"{len(unsupported_command_counts.keys())} known but unsupported command(s) found:", lvl=1) + custom_print(f"⚠️ {len(unsupported_command_counts.keys())} known but unsupported command(s) found:", lvl=1) for key, value in unsupported_command_counts.items(): custom_print(f" - Command '{key}' found {value} time(s).", lvl=1) else: From ed9cb76051b84e80e284963f01ae581c7123718d Mon Sep 17 00:00:00 2001 From: Lukas Hof Date: Mon, 17 Mar 2025 11:11:47 +0100 Subject: [PATCH 05/68] satisfying linter --- .pre-commit-config.yaml | 8 ++++---- pyGCodeDecode/helpers.py | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3ab8ee5..5cc8b23 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,19 +13,19 @@ repos: - --maxkb=5000 # Black code style formatter - repo: https://github.com/psf/black - rev: 24.10.0 + rev: 25.1.0 hooks: - id: black - id: black-jupyter # isort import sorting - repo: https://github.com/timothycrosley/isort - rev: "5.13.2" + rev: "6.0.1" hooks: - id: isort args: ["--profile", "black"] # Flake8 for linting - repo: https://github.com/pycqa/flake8 - rev: "7.1.1" + rev: "7.1.2" hooks: - id: flake8 additional_dependencies: [flake8-docstrings] @@ -36,6 +36,6 @@ repos: - id: nbstripout # pyupgrade - repo: https://github.com/asottile/pyupgrade - rev: v3.19.0 + rev: v3.19.1 hooks: - id: pyupgrade diff --git a/pyGCodeDecode/helpers.py b/pyGCodeDecode/helpers.py index 527416f..8133688 100644 --- a/pyGCodeDecode/helpers.py +++ b/pyGCodeDecode/helpers.py @@ -26,8 +26,7 @@ def get_verbosity_level() -> int: def custom_print(*args, lvl=2, **kwargs) -> None: - """Sanitize outputs for ABAQUS and print them if the log level is high enough. - Takes all regular arguments for print. + """Sanitize outputs for ABAQUS and print them if the log level is high enough. Takes all arguments for print. Args: *args: arguments to be printed From 364d0f887f38ba9e5ff1a32d081f580c21242ecb Mon Sep 17 00:00:00 2001 From: Lukas Hof Date: Mon, 17 Mar 2025 11:21:13 +0100 Subject: [PATCH 06/68] 3.9 compatible type hints --- pyGCodeDecode/gcode_interpreter.py | 6 +++--- pyGCodeDecode/helpers.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyGCodeDecode/gcode_interpreter.py b/pyGCodeDecode/gcode_interpreter.py index 938f2b1..ff320bd 100644 --- a/pyGCodeDecode/gcode_interpreter.py +++ b/pyGCodeDecode/gcode_interpreter.py @@ -4,7 +4,7 @@ import os import pathlib import time -from typing import List, Tuple, Union +from typing import List, Optional, Tuple, Union import numpy as np import pyvista as pv @@ -132,7 +132,7 @@ def __init__( machine_name: str = None, initial_machine_setup: "setup" = None, output_unit_system: str = "SI (mm)", - verbosity_level: int | None = None, + verbosity_level: Optional[int] = None, ): """Initialize the Simulation of a given G-code with initial machine setup or default machine. @@ -781,7 +781,7 @@ def __init__( presets_file: str, printer: str = None, layer_cue: str = None, - verbosity_level: int | None = None, + verbosity_level: Optional[int] = None, ): """Create simulation setup. diff --git a/pyGCodeDecode/helpers.py b/pyGCodeDecode/helpers.py index 8133688..6c981d7 100644 --- a/pyGCodeDecode/helpers.py +++ b/pyGCodeDecode/helpers.py @@ -1,6 +1,7 @@ """Helper functions.""" import sys +from typing import Optional # global flags # check if program is running in ABAQUS-Python @@ -13,7 +14,7 @@ VERBOSITY_LEVEL = 2 # default to INFO -def set_verbosity_level(level: int | None) -> None: +def set_verbosity_level(level: Optional[int]) -> None: """Set the global verbosity level.""" global VERBOSITY_LEVEL if level is not None: From da299e5dc18d333f9d836b1d40263501b000847b Mon Sep 17 00:00:00 2001 From: usmfi Date: Tue, 18 Mar 2025 10:42:17 +0100 Subject: [PATCH 07/68] fix comment --- pyGCodeDecode/gcode_interpreter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyGCodeDecode/gcode_interpreter.py b/pyGCodeDecode/gcode_interpreter.py index ff320bd..3fffe71 100644 --- a/pyGCodeDecode/gcode_interpreter.py +++ b/pyGCodeDecode/gcode_interpreter.py @@ -142,7 +142,7 @@ def __init__( Args: gcode_path: (Path) path to GCode - machine name: (string, default = None) name of the default machine to use + machine_name: (string, default = None) name of the default machine to use initial_machine_setup: (setup, default = None) setup instance output_unit_system: (string, default = "SI (mm)") available unit systems: SI, SI (mm) & inch verbosity_level: (int, default = None) set verbosity level (0: no output, 1: warnings, 2: info, 3: debug) From 3276cc4982327f90d71e8bcd0e81bb000452e2b6 Mon Sep 17 00:00:00 2001 From: usmfi Date: Mon, 24 Mar 2025 09:18:37 +0100 Subject: [PATCH 08/68] print prefix --- pyGCodeDecode/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyGCodeDecode/helpers.py b/pyGCodeDecode/helpers.py index 6c981d7..4d3a36d 100644 --- a/pyGCodeDecode/helpers.py +++ b/pyGCodeDecode/helpers.py @@ -45,7 +45,7 @@ def custom_print(*args, lvl=2, **kwargs) -> None: # print with verbosity level if lvl <= VERBOSITY_LEVEL: - levels = {3: "[DEBUG]", 2: "[INFO]: ", 1: "[WARNING]: "} + levels = {3: "[DEBUG]:", 2: "[INFO]:", 1: "[WARNING]:"} prefix = levels.get(lvl, "") print(prefix, *sanitized_args, **kwargs) From 4f3eb7177a2d5cd2ca781128d62c242ab5fc46f1 Mon Sep 17 00:00:00 2001 From: usmfi Date: Mon, 24 Mar 2025 10:30:10 +0100 Subject: [PATCH 09/68] cleanup and tests --- pyGCodeDecode/state_generator.py | 23 +++++++++++---------- tests/test_end_to_end.py | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/pyGCodeDecode/state_generator.py b/pyGCodeDecode/state_generator.py index 7bc60b6..97ccf48 100644 --- a/pyGCodeDecode/state_generator.py +++ b/pyGCodeDecode/state_generator.py @@ -81,6 +81,7 @@ default_virtual_machine = { "absolute_position": True, "absolute_extrusion": True, + "extrusion_volumetric": False, "units": "SI (mm)", "initial_position": None, # general properties @@ -208,7 +209,7 @@ def dict_list_traveler(line_dict_list: List[dict], initial_machine_setup: dict) """ - def apply_extrusion(line_dict: dict, virtual_machine: dict, command: str, initial_machine_setup: dict) -> dict: + def apply_extrusion(line_dict: dict, virtual_machine: dict, command: str) -> dict: import math e_value = line_dict[command]["E"] @@ -216,13 +217,13 @@ def apply_extrusion(line_dict: dict, virtual_machine: dict, command: str, initia # volumetric to length conversion # (1) V = (d/2)^2 * pi * E # (2) E = V / ((d/2)^2 * pi) - if initial_machine_setup.get("extrusion_volumetric", False): + if virtual_machine.get("extrusion_volumetric", False): # volumetric extrusion - e_value = e_value / (math.pi * (initial_machine_setup["filament_diam"] / 2) ** 2) + e_value = e_value / (math.pi * (virtual_machine["filament_diam"] / 2) ** 2) - if virtual_machine["absolute_extrusion"] is True: + if virtual_machine["absolute_extrusion"]: virtual_machine["E"] = e_value - if virtual_machine["absolute_extrusion"] is False: # redundant + else: virtual_machine["E"] = virtual_machine["E"] + e_value return virtual_machine @@ -245,17 +246,15 @@ def apply_extrusion(line_dict: dict, virtual_machine: dict, command: str, initia layer_counter = 0 # overwrite default values from initial machine setup - """TODO: depending on the setting the user should be informed that a default value is used. - I prepared a warning below. - Are all these settings necessary?""" for key in default_virtual_machine: if initial_machine_setup is not None and key in initial_machine_setup: virtual_machine[key] = initial_machine_setup[key] else: - """print( + custom_print( f"The parameter '{key}' was not specified in your machine presets. " - f"Using the the default value of '{default_virtual_machine[key]}' to continue." - )""" + f"Using the the default value of '{default_virtual_machine[key]}' to continue.", + lvl=3, + ) virtual_machine[key] = default_virtual_machine[key] # initial state creation @@ -318,7 +317,7 @@ def apply_extrusion(line_dict: dict, virtual_machine: dict, command: str, initia # look for extrusion commands and apply abs/rel if "E" in line_dict[command]: - virtual_machine = apply_extrusion(line_dict, virtual_machine, command, initial_machine_setup) + virtual_machine = apply_extrusion(line_dict, virtual_machine, command) # feed rates in unit/min to unit/sec if "F" in line_dict[command]: diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index 49a2568..5749a00 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -2,6 +2,8 @@ import pathlib +import numpy as np + def test_end_to_end_compact(): """Testing the simulation functionality with automatic setup, similarly to the brace example.""" @@ -13,6 +15,38 @@ def test_end_to_end_compact(): ) +def test_end_to_end_volumetr(): + """Testing the simulation functionality with automatic setup, using volumetric or distance based extrusion.""" + from pyGCodeDecode.gcode_interpreter import setup, simulation + + preset = setup(pathlib.Path("./tests/data/test_printer_setups.yaml"), "test") + preset.set_property({"extrusion_volumetric": False}) + + sim = simulation( + gcode_path=pathlib.Path("./tests/data/test_state_generator.gcode"), + initial_machine_setup=preset, + ) + + end_extrusion = sim.blocklist[-1].segments[-1].pos_end.e + expected_extrusion = 14.0 + assert end_extrusion == expected_extrusion, f"Expected {expected_extrusion}, but got {end_extrusion}" + + preset = setup(pathlib.Path("./tests/data/test_printer_setups.yaml"), "test") + preset.set_property({"extrusion_volumetric": True}) + + sim = simulation( + gcode_path=pathlib.Path("./tests/data/test_state_generator.gcode"), + initial_machine_setup=preset, + ) + + end_extrusion = sim.blocklist[-1].segments[-1].pos_end.e + expected_extrusion = 14.0 / ((1.75 / 2) ** 2 * np.pi) + + assert np.isclose( + end_extrusion, expected_extrusion, rtol=1e-9 + ), f"Expected {expected_extrusion}, but got {end_extrusion}" + + def test_end_to_end_extensive(): """Testing the simulation functionality as well as the various outputs, similarly to the benchy example.""" from pyGCodeDecode.abaqus_file_generator import generate_abaqus_event_series From 65e3e999104812c41a423ace1b39acf44bc4a152 Mon Sep 17 00:00:00 2001 From: Jonathan Emil Knirsch Date: Mon, 24 Mar 2025 09:33:08 +0000 Subject: [PATCH 10/68] lukas suggestions --- pyGCodeDecode/gcode_interpreter.py | 2 +- pyGCodeDecode/state_generator.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pyGCodeDecode/gcode_interpreter.py b/pyGCodeDecode/gcode_interpreter.py index ef30465..3ff8776 100644 --- a/pyGCodeDecode/gcode_interpreter.py +++ b/pyGCodeDecode/gcode_interpreter.py @@ -577,7 +577,7 @@ def get_values(self, t: float, output_unit_system: str = None) -> Tuple[List[flo return tmp_vel, tmp_pos - def get_width(self, t: float, extrusion_h: float, filament_dia: float = None) -> float: + def get_width(self, t: float, extrusion_h: float, filament_dia: Optional[float] = None) -> float: """Return the extrusion width for a certain extrusion height at time. Args: diff --git a/pyGCodeDecode/state_generator.py b/pyGCodeDecode/state_generator.py index 97ccf48..483852c 100644 --- a/pyGCodeDecode/state_generator.py +++ b/pyGCodeDecode/state_generator.py @@ -1,5 +1,6 @@ """State generator module.""" +import math import pathlib import re from typing import List, Match @@ -210,8 +211,6 @@ def dict_list_traveler(line_dict_list: List[dict], initial_machine_setup: dict) """ def apply_extrusion(line_dict: dict, virtual_machine: dict, command: str) -> dict: - import math - e_value = line_dict[command]["E"] # volumetric to length conversion From f225457b800ce78a790327fb09deddb3433ab1e9 Mon Sep 17 00:00:00 2001 From: usmfi Date: Mon, 24 Mar 2025 10:35:31 +0100 Subject: [PATCH 11/68] rename of key --- pyGCodeDecode/data/default_printer_presets.yaml | 2 +- pyGCodeDecode/gcode_interpreter.py | 2 +- pyGCodeDecode/state_generator.py | 4 ++-- tests/test_end_to_end.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyGCodeDecode/data/default_printer_presets.yaml b/pyGCodeDecode/data/default_printer_presets.yaml index fd98083..c6b33aa 100644 --- a/pyGCodeDecode/data/default_printer_presets.yaml +++ b/pyGCodeDecode/data/default_printer_presets.yaml @@ -49,7 +49,7 @@ debugging: # general properties nozzle_diam: 0.4 filament_diam: 2.85 - extrusion_volumetric: True # true: mm3 with UltiGCode, else mm length + volumetric_extrusion: True # true: mm3 with UltiGCode, else mm length # default settings p_vel: 85 p_acc: 100 diff --git a/pyGCodeDecode/gcode_interpreter.py b/pyGCodeDecode/gcode_interpreter.py index 3ff8776..98ed230 100644 --- a/pyGCodeDecode/gcode_interpreter.py +++ b/pyGCodeDecode/gcode_interpreter.py @@ -623,7 +623,7 @@ def check_initial_setup(self, initial_machine_setup): "printer_name", "firmware", ] - optional_keys = ["layer_cue", "nozzle_diam", "filament_diam", "extrusion_volumetric"] + optional_keys = ["layer_cue", "nozzle_diam", "filament_diam", "volumetric_extrusion"] valid_keys = req_keys + optional_keys diff --git a/pyGCodeDecode/state_generator.py b/pyGCodeDecode/state_generator.py index 483852c..faa706a 100644 --- a/pyGCodeDecode/state_generator.py +++ b/pyGCodeDecode/state_generator.py @@ -82,7 +82,7 @@ default_virtual_machine = { "absolute_position": True, "absolute_extrusion": True, - "extrusion_volumetric": False, + "volumetric_extrusion": False, "units": "SI (mm)", "initial_position": None, # general properties @@ -216,7 +216,7 @@ def apply_extrusion(line_dict: dict, virtual_machine: dict, command: str) -> dic # volumetric to length conversion # (1) V = (d/2)^2 * pi * E # (2) E = V / ((d/2)^2 * pi) - if virtual_machine.get("extrusion_volumetric", False): + if virtual_machine.get("volumetric_extrusion", False): # volumetric extrusion e_value = e_value / (math.pi * (virtual_machine["filament_diam"] / 2) ** 2) diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index 5749a00..abba55c 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -20,7 +20,7 @@ def test_end_to_end_volumetr(): from pyGCodeDecode.gcode_interpreter import setup, simulation preset = setup(pathlib.Path("./tests/data/test_printer_setups.yaml"), "test") - preset.set_property({"extrusion_volumetric": False}) + preset.set_property({"volumetric_extrusion": False}) sim = simulation( gcode_path=pathlib.Path("./tests/data/test_state_generator.gcode"), @@ -32,7 +32,7 @@ def test_end_to_end_volumetr(): assert end_extrusion == expected_extrusion, f"Expected {expected_extrusion}, but got {end_extrusion}" preset = setup(pathlib.Path("./tests/data/test_printer_setups.yaml"), "test") - preset.set_property({"extrusion_volumetric": True}) + preset.set_property({"volumetric_extrusion": True}) sim = simulation( gcode_path=pathlib.Path("./tests/data/test_state_generator.gcode"), From 6880db5eba11bd8c5e5dfe41c99658f034089b05 Mon Sep 17 00:00:00 2001 From: usmfi Date: Thu, 31 Jul 2025 15:46:43 +0200 Subject: [PATCH 12/68] hard push of private updates --- .gitignore | 1 + pyGCodeDecode/abaqus_file_generator.py | 7 +- .../data/default_printer_presets.yaml | 38 +- pyGCodeDecode/examples/benchy.py | 6 +- pyGCodeDecode/examples/brace.py | 3 +- pyGCodeDecode/examples/data/bee.gcode | 3 + pyGCodeDecode/examples/data/cubhelix.gcode | 3 + .../examples/data/printer_presets.yaml | 21 +- pyGCodeDecode/examples/data/triangle.gcode | 3 + pyGCodeDecode/gcode_interpreter.py | 498 ++++----------- pyGCodeDecode/junction_handling.py | 571 +++++++++++++----- pyGCodeDecode/planner_block.py | 49 +- pyGCodeDecode/plotter.py | 523 ++++++++++++++++ pyGCodeDecode/result.py | 121 ++++ pyGCodeDecode/state_generator.py | 1 + pyGCodeDecode/tools.py | 2 +- pyGCodeDecode/utils.py | 344 +++++++---- pyproject.toml | 5 +- tests/data/test_simplest.gcode | 3 + tests/self_test/self_test.gcode | 3 + tests/test_end_to_end.py | 40 +- tests/test_junction_handling.py | 223 +++++++ tests/test_planner_block.py | 2 +- tests/test_result.py | 117 ++++ tests/test_vector4d.py | 40 ++ 25 files changed, 1881 insertions(+), 746 deletions(-) create mode 100644 pyGCodeDecode/examples/data/bee.gcode create mode 100644 pyGCodeDecode/examples/data/cubhelix.gcode create mode 100644 pyGCodeDecode/examples/data/triangle.gcode create mode 100644 pyGCodeDecode/plotter.py create mode 100644 pyGCodeDecode/result.py create mode 100644 tests/data/test_simplest.gcode create mode 100644 tests/self_test/self_test.gcode create mode 100644 tests/test_junction_handling.py create mode 100644 tests/test_result.py create mode 100644 tests/test_vector4d.py diff --git a/.gitignore b/.gitignore index bac875e..22fb4a8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.png *.inp *.csv +*.prof # build artifacts __pycache__ diff --git a/pyGCodeDecode/abaqus_file_generator.py b/pyGCodeDecode/abaqus_file_generator.py index 253b41a..f41cd53 100644 --- a/pyGCodeDecode/abaqus_file_generator.py +++ b/pyGCodeDecode/abaqus_file_generator.py @@ -25,6 +25,7 @@ def generate_abaqus_event_series( filepath: str = "pyGcodeDecode_abaqus_events.inp", tolerance: float = 1e-12, output_unit_system: str = None, + return_tuple: bool = False, ) -> tuple: """Generate abaqus event series. @@ -34,9 +35,10 @@ def generate_abaqus_event_series( tolerance (float, default = 1e-12): tolerance to determine whether extrusion is happening output_unit_system (str, optional): Unit system for the output. The one from the simulation is used, in None is specified. + return_tuple (bool, default = False): return the event series as tuple. Returns: - tuple: the event series as a tuple for use in ABAQUS-Python + (optional) tuple: the event series as a tuple for use in ABAQUS-Python """ unpacked = gi.unpack_blocklist(simulation.blocklist) pos = [unpacked[0].pos_begin.get_vec(withExtrusion=True)] @@ -71,4 +73,5 @@ def generate_abaqus_event_series( custom_print(f"ABAQUS event series written to: \n{outfile.name}") - return tuple(event_series_list) + if return_tuple: + return tuple(event_series_list) diff --git a/pyGCodeDecode/data/default_printer_presets.yaml b/pyGCodeDecode/data/default_printer_presets.yaml index c6b33aa..8b9933b 100644 --- a/pyGCodeDecode/data/default_printer_presets.yaml +++ b/pyGCodeDecode/data/default_printer_presets.yaml @@ -1,49 +1,49 @@ # -*- coding: utf-8 -*- - -anisoprint_a4: +prusa_mini: # general properties nozzle_diam: 0.4 filament_diam: 1.75 # default settings p_vel: 35 - p_acc: 1000 - jerk: 10 + p_acc: 1250 + jerk: 8 # axis max speeds vX: 180 vY: 180 - vZ: 30 - vE: 33 - firmware: MKA + vZ: 12 + vE: 80 + firmware: prusa -prusa_mini: +anisoprint_a4: # general properties nozzle_diam: 0.4 filament_diam: 1.75 # default settings p_vel: 35 - p_acc: 1250 - jerk: 8 + p_acc: 1000 + jerk: 10 # axis max speeds vX: 180 vY: 180 - vZ: 12 - vE: 80 - firmware: marlin_jerk + vZ: 30 + vE: 33 + firmware: mka -prusa_mini_klipper: +ultimaker_2plus: # general properties nozzle_diam: 0.4 - filament_diam: 1.75 + filament_diam: 2.85 + volumetric_extrusion: True # true: mm3 with UltiGCode, else mm length # default settings - p_vel: 35 - p_acc: 2500 + p_vel: 85 + p_acc: 100 jerk: 8 # axis max speeds vX: 180 vY: 180 vZ: 12 vE: 80 - firmware: klipper + firmware: ultimaker debugging: # general properties @@ -59,4 +59,4 @@ debugging: vY: 180 vZ: 12 vE: 80 - firmware: + firmware: marlin # junction_deviation diff --git a/pyGCodeDecode/examples/benchy.py b/pyGCodeDecode/examples/benchy.py index 1d5310d..ca3484b 100644 --- a/pyGCodeDecode/examples/benchy.py +++ b/pyGCodeDecode/examples/benchy.py @@ -5,6 +5,7 @@ from pyGCodeDecode.abaqus_file_generator import generate_abaqus_event_series from pyGCodeDecode.gcode_interpreter import setup, simulation +from pyGCodeDecode.plotter import plot_3d from pyGCodeDecode.tools import save_layer_metrics @@ -67,7 +68,8 @@ def benchy_example(): ) # create a 3D-plot and save a VTK as well as a screenshot - benchy_mesh = benchy_simulation.plot_3d( + benchy_mesh = plot_3d( + benchy_simulation, extrusion_only=True, screenshot_path=output_dir / "benchy.png", vtk_path=output_dir / "benchy.vtk", @@ -75,7 +77,7 @@ def benchy_example(): # create an interactive 3D-plot # the mesh from the previous run can be used to avoid generating a mesh again - benchy_simulation.plot_3d(mesh=benchy_mesh) + plot_3d(benchy_simulation, mesh=benchy_mesh) if __name__ == "__main__": diff --git a/pyGCodeDecode/examples/brace.py b/pyGCodeDecode/examples/brace.py index 989a7b8..3c40f18 100644 --- a/pyGCodeDecode/examples/brace.py +++ b/pyGCodeDecode/examples/brace.py @@ -3,6 +3,7 @@ import importlib.resources from pyGCodeDecode.gcode_interpreter import simulation +from pyGCodeDecode.plotter import plot_3d def brace_example(): @@ -19,7 +20,7 @@ def brace_example(): brace_simulation = simulation(gcode_path=gcode_path, machine_name="anisoprint_a4") # create a 3D-plot - brace_simulation.plot_3d(extrusion_only=True) + plot_3d(brace_simulation, extrusion_only=True) if __name__ == "__main__": diff --git a/pyGCodeDecode/examples/data/bee.gcode b/pyGCodeDecode/examples/data/bee.gcode new file mode 100644 index 0000000..03b9d77 --- /dev/null +++ b/pyGCodeDecode/examples/data/bee.gcode @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd71d107cc7d4a12902437ca79a956c1bbce86f3e23fda4ae8916ee63e3f7afb +size 10187 diff --git a/pyGCodeDecode/examples/data/cubhelix.gcode b/pyGCodeDecode/examples/data/cubhelix.gcode new file mode 100644 index 0000000..f451c07 --- /dev/null +++ b/pyGCodeDecode/examples/data/cubhelix.gcode @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64057d07f29f1f28f23a995e29a3c9428586c81752bc7646eccb5c770077d1bc +size 140525 diff --git a/pyGCodeDecode/examples/data/printer_presets.yaml b/pyGCodeDecode/examples/data/printer_presets.yaml index f2e4e68..359a1f7 100644 --- a/pyGCodeDecode/examples/data/printer_presets.yaml +++ b/pyGCodeDecode/examples/data/printer_presets.yaml @@ -13,7 +13,7 @@ anisoprint_a4: vY: 180 vZ: 30 vE: 33 - firmware: MKA + firmware: mka prusa_mini: # general properties @@ -28,22 +28,7 @@ prusa_mini: vY: 180 vZ: 12 vE: 80 - firmware: marlin_jerk - -prusa_mini_klipper: - # general properties - nozzle_diam: 0.4 - filament_diam: 1.75 - # default settings - p_vel: 35 - p_acc: 2500 - jerk: 8 - # axis max speeds - vX: 180 - vY: 180 - vZ: 12 - vE: 80 - firmware: klipper + firmware: prusa debugging: # general properties @@ -58,4 +43,4 @@ debugging: vY: 180 vZ: 12 vE: 80 - firmware: + firmware: ultimaker diff --git a/pyGCodeDecode/examples/data/triangle.gcode b/pyGCodeDecode/examples/data/triangle.gcode new file mode 100644 index 0000000..b4c1876 --- /dev/null +++ b/pyGCodeDecode/examples/data/triangle.gcode @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:599bde84f966821396c3a2eaf8867c2bdf2923c34abbb8416b69abf7c06da0c9 +size 1483 diff --git a/pyGCodeDecode/gcode_interpreter.py b/pyGCodeDecode/gcode_interpreter.py index 98ed230..abd8d87 100644 --- a/pyGCodeDecode/gcode_interpreter.py +++ b/pyGCodeDecode/gcode_interpreter.py @@ -1,19 +1,17 @@ """GCode Interpreter Module.""" import importlib.resources -import os import pathlib import time from typing import List, Optional, Tuple, Union import numpy as np -import pyvista as pv import yaml -from matplotlib.figure import Figure from pyGCodeDecode.helpers import ProgressBar, custom_print, set_verbosity_level from .planner_block import planner_block +from .result import get_all_result_calculators from .state import state from .state_generator import generate_states from .utils import segment, velocity @@ -31,9 +29,21 @@ def generate_planner_blocks(states: List[state], firmware=None): """ block_list = [] bar = ProgressBar(name="Planner Blocks") + + colordict = {"infill": "blue", "perimeter": "green"} + last_type = None + for i, this_state in enumerate(states): prev_block = block_list[-1] if len(block_list) > 0 else None # grab prev block from block_list new_block = planner_block(state=this_state, prev_block=prev_block, firmware=firmware) # generate new block + + if this_state.comment is not None: + for key in colordict.keys(): + if key in this_state.comment.lower(): + last_type = key + + new_block.e_type = last_type + if len(new_block.get_segments()) > 0: if new_block.prev_block is not None: new_block.prev_block.next_block = new_block # update nb list @@ -176,7 +186,7 @@ def __init__( raise ValueError("Neither a printer name nor a printer setup was specified. At least one is required!") else: custom_print( - "Only a machine name was specified but no full setup." + "Only a machine name was specified but no full setup. " "Trying to create a setup from pyGCD's default values...", lvl=1, ) @@ -189,354 +199,31 @@ def __init__( ) # SET INITIAL SETTINGS - self.initial_machine_setup = initial_machine_setup.get_dict() - self.check_initial_setup(initial_machine_setup=self.initial_machine_setup) # TODO: move this to setup class - self.firmware = self.initial_machine_setup["firmware"] + self.initial_machine_setup_dict = initial_machine_setup.check_initial_setup() + self.firmware = self.initial_machine_setup_dict["firmware"] self.states: List[state] = generate_states( - filepath=gcode_path, initial_machine_setup=self.initial_machine_setup + filepath=gcode_path, initial_machine_setup=self.initial_machine_setup_dict ) custom_print( - f"Simulating \"{self.filename}\" with {self.initial_machine_setup['printer_name']} using " + f"Simulating \"{self.filename}\" with {self.initial_machine_setup_dict['printer_name']} using " f"the {self.firmware} firmware.\n" ) self.blocklist: List[planner_block] = generate_planner_blocks(states=self.states, firmware=self.firmware) self.trajectory_self_correct() - self.print_summary(start_time=simulation_start_time) - - def plot_2d_position( - self, - filepath: pathlib.Path = pathlib.Path("trajectory_2D.png"), - colvar="Velocity", - show_points=False, - colvar_spatial_resolution=1, - dpi=400, - scaled=True, - show=False, - ): - """Plot 2D position (XY plane) with matplotlib (unmaintained).""" - import matplotlib.pyplot as plt - from matplotlib import cm - from matplotlib.collections import LineCollection - - colvar_label = { - "Velocity": "Velocity in mm/s", - "Acceleration": "Acceleration in mm/s^2", - } - - def interp_2D(x, y, cvar, spatial_resolution=1): - segm_length = np.linalg.norm([np.ediff1d(x), np.ediff1d(y)], axis=0) - segm_cvar_delt = np.greater(np.abs(np.ediff1d(cvar)), 0) - segm_interpol = np.r_[ - 0, - np.where(segm_cvar_delt, np.ceil(segm_length / spatial_resolution) + 1, 1), - ] # get nmbr of segments for required resolution, dont interpolate if there is no change - points = np.array([x, y, cvar]).T - points = np.c_[points, segm_interpol] - - # generate intermediate points with set resolution - old_point = None - interpolated = np.zeros((1, 3)) - for point in points: - if old_point is not None: - steps = np.linspace(0, 1, int(point[3]), endpoint=True) - x_i = np.interp(steps, [0, 1], [old_point[0], point[0]]) - y_i = np.interp(steps, [0, 1], [old_point[1], point[1]]) - colvar_i = np.interp(steps, [0, 1], [old_point[2], point[2]]) - interpolated = np.r_[interpolated, np.array([x_i, y_i, colvar_i]).T] - old_point = point - interpolated = np.delete(interpolated, 0, 0) - - return interpolated - - segments = unpack_blocklist(blocklist=self.blocklist) - if colvar == "Velocity": - # get all planned trajectory vertices + color variable - x, y, cvar = [], [], [] - x.append(segments[0].pos_begin.get_vec()[0]) - y.append(segments[0].pos_begin.get_vec()[1]) - cvar.append(segments[0].vel_begin.get_norm()) - - bar = ProgressBar(name="2D Plot Lines") - for i, segm in enumerate(segments): - bar.update((i + 1) / len(segments)) - x.append(segm.pos_end.get_vec()[0]) - y.append(segm.pos_end.get_vec()[1]) - cvar.append(segm.vel_end.get_norm()) - - # interpolate values for smooth coloring - interpolated = interp_2D(x, y, cvar, spatial_resolution=colvar_spatial_resolution) - - x = interpolated[:, 0] - y = interpolated[:, 1] - cvar = interpolated[:, 2] # maybe change interpolation to return tuple? - - # generate point pairs for line collection - point_pairs = [] - for i in np.arange(len(x) - 1): - point_pairs.append([(x[i], y[i]), (x[i + 1], y[i + 1])]) - - # generate collection from pairs - collection = LineCollection(point_pairs) - collection.set_array(cvar) - collection.set_cmap(cm.jet) - - fig = plt.figure() - ax1 = fig.add_subplot(1, 1, 1) - ax1.add_collection(collection) - ax1.autoscale() - plt.colorbar(collection, label=colvar_label[colvar], shrink=0.6, location="right") - else: - x, y = [], [] - x.append(segments[0].pos_begin.get_vec()[0]) - y.append(segments[0].pos_begin.get_vec()[1]) - for i, segm in enumerate(segments): - bar.update((i + 1) / len(segments)) - x.append(segm.pos_end.get_vec()[0]) - y.append(segm.pos_end.get_vec()[1]) - fig = plt.subplot() - fig.plot(x, y, color="black") - - if show_points: - for i, block in enumerate(self.blocklist): - bar.update(i / len(self.blocklist)) - fig.scatter( - block.get_segments()[-1].pos_end.get_vec()[0], - block.get_segments()[-1].pos_end.get_vec()[1], - color="blue", - marker="x", - ) - - plt.xlabel("x position") - plt.ylabel("y position") - plt.title("2D Position") - if scaled: - plt.axis("scaled") - if filepath is not False: - plt.savefig(filepath, dpi=dpi) - custom_print(f"2D Plot saved:\n👉 {filepath}") - if show: - plt.show() - return fig - plt.close() - - def plot_3d( - self, - extrusion_only: bool = True, - screenshot_path: pathlib.Path = None, - vtk_path: pathlib.Path = None, - mesh: pv.MultiBlock = None, - ) -> pv.MultiBlock: - """3D Plot with PyVista. - - Args: - extrusion_only (bool, optional): Plot only parts where material is extruded. Defaults to True. - screenshot_path (pathlib.Path, optional): Path to screenshot to be saved. Prevents - interactive plot. Defaults to None. - vtk_path (pathlib.Path, optional): Path to vtk to be saved. Prevents interactive plot. Defaults to None. - mesh (pv.MultiBlock, optional): A pyvista mesh from a previous run to avoid running the - mesh generation again. Defaults to None. - - Returns: - pv.MultiBlock: The mesh used in the plot so it can be used (e.g. in subsequent plots). - """ - # https://docs.pyvista.org/version/stable/api/core/_autosummary/pyvista.polydatafilters.extrude - # https://docs.pyvista.org/version/stable/examples/01-filter/extrude-rotate - - # get all data for plots - segments = unpack_blocklist(blocklist=self.blocklist) - - # mesh generation is skipped, if a mesh is given already - if mesh is None: - mesh = pv.MultiBlock() - - x, y, z, e, vel = [], [], [], [], [] - bar = ProgressBar(name="3D Plot") - for n, segm in enumerate(segments): - bar.update((n + 1) / len(segments)) - - if (not extrusion_only) or (segm.is_extruding()): - if len(x) == 0: - # append segm begin values to plotting array for first segm - pos_begin_vec = segm.pos_begin.get_vec(withExtrusion=True) - x.append(pos_begin_vec[0]) - y.append(pos_begin_vec[1]) - z.append(pos_begin_vec[2]) - e.append(pos_begin_vec[3]) - vel.append(segm.vel_begin.get_norm()) - - # append segm end values to plotting array - pos_end_vec = segm.pos_end.get_vec(withExtrusion=True) - - x.append(pos_end_vec[0]) - y.append(pos_end_vec[1]) - z.append(pos_end_vec[2]) - e.append(pos_end_vec[3]) - vel.append(segm.vel_end.get_norm()) - - # plot if following segment is not extruding or if it's the last segment - if (extrusion_only and (len(x) > 0 and not segm.is_extruding())) or ( - len(x) > 0 and n == len(segments) - 1 - ): - points_3d = np.column_stack((x, y, z)) - line = pv.lines_from_points(points_3d) - line["velocity"] = vel - tube = line.tube(radius=0.2, n_sides=6) - mesh.append(tube) - x, y, z, e, vel = [], [], [], [], [] # clear plotting array - - mesh = mesh.combine() - - # check wether a display is available or Windows is used - if os.name == "nt" or "DISPLAY" in os.environ: - display_available = True - else: - display_available = False - - # saving a screenshot and an interactive plot aren't possible at the same tim - if screenshot_path is None: - off_screen = False - else: - off_screen = True - - p = pv.Plotter(off_screen=off_screen) - p.add_mesh( - mesh, - scalars="velocity", - smooth_shading=True, - scalar_bar_args={"title": "travel velocity in mm/s"}, - ) - - if vtk_path is not None: - mesh.save(filename=vtk_path) - custom_print(f"VTK saved to:\n{vtk_path}") - if screenshot_path is not None: - if display_available: - p.screenshot(filename=screenshot_path) - custom_print(f"Screenshot saved to:\n{screenshot_path}") - else: - custom_print("Screenshot can not be created without a display!", lvl=1) + # calculate results + self.results = {} + self.calc_results() + self.calculate_averages() - if not off_screen and display_available: - p.show() - - return mesh - - def plot_vel( - self, - axis: Tuple[str] = ("x", "y", "z", "e"), - show: bool = True, - show_planner_blocks: bool = True, - show_segments: bool = False, - show_jv: bool = False, - time_steps: Union[int, str] = "constrained", - filepath: pathlib.Path = None, - dpi: int = 400, - ) -> Figure: - """Plot axis velocity with matplotlib. - - Args: - axis: (tuple(string), default = ("x", "y", "z", "e")) select plot axis - show: (bool, default = True) show plot and return plot figure - show_planner_blocks: (bool, default = True) show planner_blocks as vertical lines - show_segments: (bool, default = False) show segments as vertical lines - show_jv: (bool, default = False) show junction velocity as x - time_steps: (int or string, default = "constrained") number of time steps or constrain plot - vertices to segment vertices - filepath: (Path, default = None) save fig as image if filepath is provided - dpi: (int, default = 400) select dpi - - Returns: - (optionally) - fig: (figure) - """ - import matplotlib.pyplot as plt - - axis_dict = {"x": 0, "y": 1, "z": 2, "e": 3} - - segments = unpack_blocklist(blocklist=self.blocklist) # unpack - - # time steps - if type(time_steps) is int: # evenly distributed time steps - times = np.linspace( - 0, - self.blocklist[-1].get_segments()[-1].t_end, - time_steps, - endpoint=False, - ) - elif time_steps == "constrained": # use segment time points as plot constrains - times = [0] - for segm in segments: - times.append(segm.t_end) - else: - raise ValueError("Invalid value for 'time_steps', either use Integer or 'constrained' as argument.") - - # gathering values - pos = [[], [], [], []] - vel = [[], [], [], []] - abs = [] # initialize value arrays - index_saved = 0 - bar = ProgressBar(name="Velocity Plot") - - for i, t in enumerate(times): - segm, index_saved = find_current_segment(path=segments, t=t, last_index=index_saved, keep_position=True) - - tmp_vel = segm.get_velocity(t=t).get_vec(withExtrusion=True) - tmp_pos = segm.get_position(t=t).get_vec(withExtrusion=True) - for ax in axis: - pos[axis_dict[ax]].append(tmp_pos[axis_dict[ax]]) - vel[axis_dict[ax]].append(tmp_vel[axis_dict[ax]]) - - abs.append(np.linalg.norm(tmp_vel[:3])) - bar.update((i + 1) / len(times)) - - fig, ax1 = plt.subplots() - ax2 = ax1.twinx() - - # plot JD-Limits - for block in self.blocklist: - # planner blocks vertical line plot - if show_planner_blocks: - ax1.axvline(x=block.get_segments()[-1].t_end, color="black", lw=0.5) - - # segments vertical line plot - if show_segments: - for segm in block.get_segments(): - ax1.axvline(x=segm.t_end, color="green", lw=0.25) - - if show_jv: - # absolute JD Marker - absJD = np.linalg.norm([block.JD[0], block.JD[1], block.JD[2]]) - ax1.scatter(x=block.get_segments()[-1].t_end, y=absJD, color="red", marker="x") - for ax in axis: - ax1.scatter( - x=block.get_segments()[-1].t_end, - y=block.JD[axis_dict[ax]], - marker="x", - color="black", - lw=0.5, - ) + self.print_summary(start_time=simulation_start_time) - # plot all axis in velocity and position - for ax in axis: - ax1.plot(times, vel[axis_dict[ax]], label=ax) # velocity - ax2.plot(times, pos[axis_dict[ax]], linestyle="--") # position w/ extrusion - # if not ax == "e": ax2.plot(times,pos[axis_dict[ax]],linestyle="--") #position ignoring extrusion - ax1.plot(times, abs, color="black", label="abs") # absolute velocity - - ax1.set_xlabel("time in s") - ax1.set_ylabel("velocity in mm/s") - ax2.set_ylabel("position in mm") - ax1.legend(loc="lower left") - plt.title("Velocity and Position over Time") - if filepath is not None: - plt.savefig(filepath, dpi=dpi) - if show: - plt.show() - plt.close() - return fig + def __getattr__(self, name): + """Get result by name.""" + if name in self.results: + return self.results[name] def trajectory_self_correct(self): """Self correct all blocks in the blocklist with self_correction() method.""" @@ -552,6 +239,51 @@ def trajectory_self_correct(self): block.self_correction() bar.update(1.0) + def calc_results(self): + """Calculate the results.""" + calculators = get_all_result_calculators() + + for pb in self.blocklist: + pb.calc_results(*calculators) + + def calculate_averages(self): + """Calculate averages for averageable results.""" + + def spatial_average(calculator): + total_dist = 0 + glob_result = 0 + for segm in unpack_blocklist(self.blocklist): + len = segm.get_segm_len() + segm_result = segm.get_result(calculator.name + "_savg") + if segm.is_extruding(): + total_dist += len + glob_result += segm_result * len + if total_dist > 0: + return glob_result / total_dist + + def time_average(calculator): + total_time = 0 + glob_result = 0 + for segm in unpack_blocklist(self.blocklist): + duration = segm.get_segm_duration() + segm_result = segm.get_result(calculator.name + "_tavg") + if segm.is_extruding(): + total_time += duration + glob_result += segm_result * duration + if total_time > 0: + return glob_result / total_time + + calculators = get_all_result_calculators() + for calculator in calculators: + if hasattr(calculator, "avgs") and isinstance(calculator.avgs, (list, tuple)): + for avg in calculator.avgs: + if avg == "_savg": + self.results[calculator.name + "_savg"] = spatial_average(calculator) + elif avg == "_tavg": + self.results[calculator.name + "_tavg"] = time_average(calculator) + else: + raise ValueError(f"Unknown average type: {avg} for {calculator.name}") + def get_values(self, t: float, output_unit_system: str = None) -> Tuple[List[float]]: """Return unit system scaled values for vel and pos. @@ -588,7 +320,7 @@ def get_width(self, t: float, extrusion_h: float, filament_dia: Optional[float] Returns: float: width """ - filament_dia = self.initial_machine_setup["filament_diam"] if filament_dia is None else filament_dia + filament_dia = self.initial_machine_setup_dict["filament_diam"] if filament_dia is None else filament_dia curr_val = self.get_values(t=t) @@ -602,46 +334,6 @@ def get_width(self, t: float, extrusion_h: float, filament_dia: Optional[float] return width - def check_initial_setup(self, initial_machine_setup): - """Check the printer Dict for typos or missing parameters and raise errors if invalid. - - Args: - initial_machine_setup: (dict) initial machine setup dictionary - """ - req_keys = [ - "p_vel", - "p_acc", - "jerk", - "vX", - "vY", - "vZ", - "vE", - "X", - "Y", - "Z", - "E", - "printer_name", - "firmware", - ] - optional_keys = ["layer_cue", "nozzle_diam", "filament_diam", "volumetric_extrusion"] - - valid_keys = req_keys + optional_keys - - # check if all provided keys are valid - for key in initial_machine_setup: - if key not in valid_keys: - raise ValueError( - f"Invalid Key: '{key}' in Setup Dictionary, check for typos. Valid keys are: {valid_keys}" - ) - - # check if every required key is proivded - for key in req_keys: - if key not in initial_machine_setup: - raise ValueError( - f"Missing Key: '{key}' is not provided in Setup Dictionary," - f" check for typos. Required keys are: {req_keys}" - ) - def print_summary(self, start_time: float): """Print simulation summary to console. @@ -667,7 +359,7 @@ def refresh(self, new_state_list: List[state] = None): self.states = new_state_list self.blocklist: List[planner_block] = generate_planner_blocks( - states=self.states, firmware=self.initial_machine_setup["firmware"] + states=self.states, firmware=self.initial_machine_setup_dict["firmware"] ) self.trajectory_self_correct() @@ -824,6 +516,48 @@ def load_setup(self, filepath): setup_dict = yaml.load(file, Loader=Loader) return setup_dict + def check_initial_setup(self): + """Check the printer Dict for typos or missing parameters and raise errors if invalid. + + Args: + initial_machine_setup: (dict) initial machine setup dictionary + """ + req_keys = [ + "p_vel", + "p_acc", + "jerk", + "vX", + "vY", + "vZ", + "vE", + "X", + "Y", + "Z", + "E", + "printer_name", + "firmware", + ] + optional_keys = ["layer_cue", "nozzle_diam", "filament_diam", "volumetric_extrusion"] + + valid_keys = req_keys + optional_keys + initial_machine_setup = self.get_dict() + + # check if all provided keys are valid + for key in initial_machine_setup: + if key not in valid_keys: + raise ValueError( + f"Invalid Key: '{key}' in Setup Dictionary, check for typos. Valid keys are: {valid_keys}" + ) + + # check if every required key is proivded + for key in req_keys: + if key not in initial_machine_setup: + raise ValueError( + f"Missing Key: '{key}' is not provided in Setup Dictionary," + f" check for typos. Required keys are: {req_keys}" + ) + return initial_machine_setup + def select_printer(self, printer_name): """Select printer by name. diff --git a/pyGCodeDecode/junction_handling.py b/pyGCodeDecode/junction_handling.py index ddc7371..a89cc39 100644 --- a/pyGCodeDecode/junction_handling.py +++ b/pyGCodeDecode/junction_handling.py @@ -1,9 +1,12 @@ """Junction handling module.""" -import math +import inspect +import sys import numpy as np +from pyGCodeDecode.helpers import custom_print + from .state import state from .utils import velocity @@ -11,6 +14,18 @@ class junction_handling: """Junction handling super class.""" + def __init__(self, state_A: state, state_B: state): + """Initialize the junction handling. + + Args: + state_A: (state) start state + state_B: (state) end state + """ + self.state_A = state_A + self.state_B = state_B + self.target_vel = self.connect_state(state_A=state_A, state_B=state_B) + self.vel_next = self.calc_vel_next() + def connect_state(self, state_A: state, state_B: state): """ Connect two states and generates the velocity for the move from state_A to state_B. @@ -57,18 +72,6 @@ def get_target_vel(self): """Return target velocity.""" return self.target_vel - def __init__(self, state_A: state, state_B: state): - """Initialize the junction handling. - - Args: - state_A: (state) start state - state_B: (state) end state - """ - self.state_A = state_A - self.state_B = state_B - self.target_vel = self.connect_state(state_A=state_A, state_B=state_B) - self.vel_next = self.calc_vel_next() - def get_junction_vel(self): """Return default junction velocity of zero. @@ -78,75 +81,112 @@ def get_junction_vel(self): return 0 -class junction_handling_marlin_jd(junction_handling): - """Marlin specific junction handling with Junction Deviation.""" +class prusa(junction_handling): + """Prusa specific classic jerk junction handling (validated on Prusa Mini). - def calc_JD(self, vel_0: velocity, vel_1: velocity, p_settings: state.p_settings): + **Reference** + [Prusa Firmware Buddy GitHub](https://github.com/prusa3d/Prusa-Firmware-Buddy/blob/818d812f954802903ea0ff39bf44376fb0b35dd2/lib/Marlin/Marlin/src/module/planner.cpp#L1911) # noqa: E501 + + + **Code reference:** + [Prusa-Firmware-Buddy/lib/Marlin/Marlin/src/module/planner.cpp](https://github.com/prusa3d/Prusa-Firmware-Buddy/blob/818d812f954802903ea0ff39bf44376fb0b35dd2/lib/Marlin/Marlin/src/module/planner.cpp#L1951) + ... + // Factor to multiply the previous / current nominal velocities to get componentwise limited velocities. + float v_factor = 1; + limited = 0; + + // The junction velocity will be shared between successive segments. Limit the junction velocity to their minimum. + // Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting. + vmax_junction = _MIN(block->nominal_speed, previous_nominal_speed); + + // Now limit the jerk in all axes. + const float smaller_speed_factor = vmax_junction / previous_nominal_speed; + #if HAS_LINEAR_E_JERK + LOOP_XYZ(axis) + #else + LOOP_XYZE(axis) + #endif + { + // Limit an axis. We have to differentiate: coasting, reversal of an axis, full stop. + float v_exit = previous_speed[axis] * smaller_speed_factor, + v_entry = current_speed[axis]; + if (limited) { + v_exit *= v_factor; + v_entry *= v_factor; + } + + // Calculate jerk depending on whether the axis is coasting in the same direction or reversing. + const float jerk = (v_exit > v_entry) + ? // coasting axis reversal + ( (v_entry > 0 || v_exit < 0) ? (v_exit - v_entry) : _MAX(v_exit, -v_entry) ) + : // v_exit <= v_entry coasting axis reversal + ( (v_entry < 0 || v_exit > 0) ? (v_entry - v_exit) : _MAX(-v_exit, v_entry) ); + + if (jerk > settings.max_jerk[axis]) { + v_factor *= settings.max_jerk[axis] / jerk; + ++limited; + } + } + if (limited) vmax_junction *= v_factor; + // Now the transition velocity is known, which maximizes the shared exit / entry velocity while + // respecting the jerk factors, it may be possible, that applying separate safe exit / entry velocities will achieve faster prints. + const float vmax_junction_threshold = vmax_junction * 0.99f; + if (previous_safe_speed > vmax_junction_threshold && safe_speed > vmax_junction_threshold) + vmax_junction = safe_speed; + } + ... + """ + + def __init__(self, state_A: state, state_B: state): + """Marlin classic jerk specific junction velocity calculation. + + Args: + state_A: (state) start state + state_B: (state) end state """ - Calculate junction deviation velocity from 2 velocitys. + super().__init__(state_A, state_B) - **Reference:** + self.calc_j_vel() - [https://onehossshay.wordpress.com/2011/09/24/improving_grbl_cornering_algorithm/](https://onehossshay.wordpress.com/2011/09/24/improving_grbl_cornering_algorithm/) - [http://blog.kyneticcnc.com/2018/10/computing-junction-deviation-for-marlin.html](http://blog.kyneticcnc.com/2018/10/computing-junction-deviation-for-marlin.html) + def calc_j_vel(self): + """Calculate the junction velocity.""" + vel_0 = self.target_vel + vel_1 = self.vel_next + self.jerk = self.state_B.state_p_settings.jerk + v_max_junction = min(vel_0.get_norm(), vel_1.get_norm()) + smaller_speed_factor = v_max_junction / vel_0.get_norm() if v_max_junction > 0 else 0 - Args: - vel_0: (velocity) entry - vel_1: (velocity) exit - p_settings: (state.p_settings) print settings + v_factor = 1.0 + limited = False - Returns: - velocity: (float) velocity abs value - """ - # Junction deviation settings - JD_acc = p_settings.p_acc - if p_settings.jerk == 0: - return 0 - JD_delta = 0.4 * p_settings.jerk * p_settings.jerk / JD_acc # [2] - JD_minAngle = 18 - JD_maxAngle = 180 - 18 - vel_0_vec = vel_0.get_vec() - vel_1_vec = vel_1.get_vec() - if vel_0.get_norm() == 0 or vel_1.get_norm() == 0: - return 0 - # calculate junction angle - JD_cos_theta = np.dot(-np.asarray(vel_0_vec), np.asarray(vel_1_vec)) / ( - np.linalg.norm(vel_0_vec) * np.linalg.norm(vel_1_vec) - ) # cos of theta, theta: small angle between velocity vectors - if JD_cos_theta < 1: # catch numerical errors where cos theta is slightly larger than one - JD_sin_theta_half = np.sqrt((1 - JD_cos_theta) / 2) - else: - JD_sin_theta_half = 0 - if JD_sin_theta_half < np.sin(JD_maxAngle * np.pi / (2 * 180)): # smaller than max angle - if JD_sin_theta_half > np.sin( - JD_minAngle * np.pi / (2 * 180) - ): # and larger than min angle --> apply Junction Deviation Calculation - # calculate scalar junction velocity - JD_Radius = JD_delta * JD_sin_theta_half / (1 - JD_sin_theta_half) - JD_velocity_scalar = np.sqrt(JD_acc * JD_Radius) + for axis in range(4): # Include extrusion axis + v_exit = vel_0.get_vec(withExtrusion=True)[axis] * smaller_speed_factor + v_entry = vel_1.get_vec(withExtrusion=True)[axis] - # return JD_velocity_scalar if JD_velocity_scalar < vel_0.get_norm() else vel_0.get_norm() - return JD_velocity_scalar if JD_velocity_scalar < p_settings.speed else p_settings.speed + if limited: + v_exit *= v_factor + v_entry *= v_factor + + # Calculate jerk depending on whether the axis is coasting in the same direction or reversing + if v_exit > v_entry: + # coasting: (v_entry > 0 or v_exit < 0), axis reversal: else + jerk = (v_exit - v_entry) if (v_entry > 0 or v_exit < 0) else max(v_exit, -v_entry) else: - return 0 # angle smaller than min angle, stop completely - else: - return p_settings.speed # angle larger than max angle, full speed pass + # coasting: (v_entry < 0 or v_exit > 0), axis reversal: else + jerk = (v_entry - v_exit) if (v_entry < 0 or v_exit > 0) else max(-v_exit, v_entry) - def __init__(self, state_A: state, state_B: state): - """Marlin specific junction velocity calculation with Junction Deviation. + if jerk > self.jerk: + v_factor *= self.jerk / jerk + limited = True - Args: - state_A: (state) start state - state_B: (state) end state - """ - super().__init__(state_A, state_B) - self.junction_vel = self.calc_JD( - vel_0=self.target_vel, vel_1=self.vel_next, p_settings=self.state_B.state_p_settings - ) + if limited: + v_max_junction *= v_factor + + self.junction_vel = v_max_junction def get_junction_vel(self): - """Return junction velocity. + """Return the calculated junction velocity. Returns: junction_vel: (float) junction velocity @@ -154,13 +194,26 @@ def get_junction_vel(self): return self.junction_vel -class junction_handling_marlin_jerk(junction_handling): +class marlin(junction_handling): """Marlin classic jerk specific junction handling. **Reference** [https://github.com/MarlinFirmware/Marlin/pull/8887](https://github.com/MarlinFirmware/Marlin/pull/8887) [https://github.com/MarlinFirmware/Marlin/pull/8888](https://github.com/MarlinFirmware/Marlin/pull/8888) [https://github.com/MarlinFirmware/Marlin/issues/367#issuecomment-12505768](https://github.com/MarlinFirmware/Marlin/issues/367#issuecomment-12505768) + + + **Code reference:** + [Marlin/src/module/planner.cpp](https://github.com/MarlinFirmware/Marlin/blob/8ec9c379405bb9962aff170d305ddd0725bd64e2/Marlin/src/module/planner.cpp#L2762) + ... + float v_factor = 1.0f; + LOOP_LOGICAL_AXES(i) { + // Jerk is the per-axis velocity difference. + const float jerk = ABS(speed_diff[i]), maxj = max_j[i]; + if (jerk * v_factor > maxj) v_factor = maxj / jerk; + } + vmax_junction_sqr = sq(vmax_junction * v_factor); + ... """ def __init__(self, state_A: state, state_B: state): @@ -178,14 +231,18 @@ def calc_j_vel(self): """Calculate the junction velocity.""" vel_0 = self.target_vel vel_1 = self.vel_next - self.jerk = self.state_B.state_p_settings.jerk * 2 + self.jerk = self.state_B.state_p_settings.jerk vel_diff = vel_0 - vel_1 - jerk_move = vel_diff.get_norm() - scale = jerk_move / self.jerk if self.jerk > 0 else 0 - if scale >= 1: - self.junction_vel = (vel_0 / scale).get_norm() + scale = 1.0 + for axx in range(4): + ax_jerk = abs(vel_diff.get_vec(withExtrusion=True)[axx]) + if ax_jerk * scale > self.jerk: + scale = self.jerk / ax_jerk + + if scale < 1: + self.junction_vel = (vel_0 * scale).get_norm() else: self.junction_vel = vel_0.get_norm() @@ -198,72 +255,96 @@ def get_junction_vel(self): return self.junction_vel -class junction_handling_klipper(junction_handling): - """Klipper specific junction handling. - - - similar junction deviation calc - - corner vel set by: square_corner_velocity - end_velocity^2 = start_velocity^2 + 2*accel*move_distance - for 90deg turn - - todo: smoothed look ahead - - **Reference:** - [https://www.klipper3d.org/Kinematics.html](https://www.klipper3d.org/Kinematics.html) - [https://github.com/Klipper3d/klipper/blob/ea2f6bc0f544132738c7f052ffcc586fa884a19a/klippy/toolhead.py](https://github.com/Klipper3d/klipper/blob/ea2f6bc0f544132738c7f052ffcc586fa884a19a/klippy/toolhead.py) +class ultimaker(junction_handling): + """Ultimaker specific junction handling. + + **Code reference:** + [UM2.1-Firmware/Marlin/planner.cpp](https://github.com/Ultimaker/UM2.1-Firmware/blob/f6e69344c00d7f300dace730990652ba614a2105/Marlin/planner.cpp#L840) + ... + float vmax_junction = max_xy_jerk/2; + float vmax_junction_factor = 1.0; + if(fabs(current_speed[Z_AXIS]) > max_z_jerk/2) + vmax_junction = min(vmax_junction, max_z_jerk/2); + if(fabs(current_speed[E_AXIS]) > max_e_jerk/2) + vmax_junction = min(vmax_junction, max_e_jerk/2); + vmax_junction = min(vmax_junction, block->nominal_speed); + float safe_speed = vmax_junction; + + if ((moves_queued > 1) && (previous_nominal_speed > 0.0001)) { + float xy_jerk = sqrt(square(current_speed[X_AXIS]-previous_speed[X_AXIS])+square(current_speed[Y_AXIS]-previous_speed[Y_AXIS])); + // if((fabs(previous_speed[X_AXIS]) > 0.0001) || (fabs(previous_speed[Y_AXIS]) > 0.0001)) { + vmax_junction = block->nominal_speed; + // } + if (xy_jerk > max_xy_jerk) { + vmax_junction_factor = (max_xy_jerk / xy_jerk); + } + if(fabs(current_speed[Z_AXIS] - previous_speed[Z_AXIS]) > max_z_jerk) { + vmax_junction_factor= min(vmax_junction_factor, (max_z_jerk/fabs(current_speed[Z_AXIS] - previous_speed[Z_AXIS]))); + } + if(fabs(current_speed[E_AXIS] - previous_speed[E_AXIS]) > max_e_jerk) { + vmax_junction_factor = min(vmax_junction_factor, (max_e_jerk/fabs(current_speed[E_AXIS] - previous_speed[E_AXIS]))); + } + vmax_junction = min(previous_nominal_speed, vmax_junction * vmax_junction_factor); // Limit speed to max previous speed + } + // Max entry speed of this block equals the max exit speed of the previous block. + block->max_entry_speed = vmax_junction; + ... """ def __init__(self, state_A: state, state_B: state): - """Klipper specific junction velocity calculation. + """Ultimaker specific junction velocity calculation. Args: state_A: (state) start state state_B: (state) end state """ super().__init__(state_A, state_B) - - self.calc_j_delta() self.calc_j_vel() - def calc_j_delta(self): - """Calculate the junction deviation with klipper specific values. - - The jerk value represents the square_corner_velocity! - """ - sc_vel = (self.state_B.state_p_settings.jerk) ** 2 - self.j_delta = sc_vel * (math.sqrt(2.0) - 1.0) / self.state_B.state_p_settings.p_acc - def calc_j_vel(self): """Calculate the junction velocity.""" vel_0 = self.target_vel vel_1 = self.vel_next - - if vel_0.get_norm() == 0 or vel_1.get_norm() == 0: - self.junction_vel = 0 - return - - # calculate junction angle - dir0 = vel_0.get_norm_dir() - dir1 = vel_1.get_norm_dir() - j_cos_theta = -( - dir0[0] * dir1[0] + dir0[1] * dir1[1] + dir0[2] * dir1[2] - ) # cos of theta, theta: small angle between velocity vectors - - j_cos_theta = max(j_cos_theta, -0.999999) # limit - if j_cos_theta > 0.999999: - self.junction_vel = 0 # self.target_vel.get_norm() # if self.target_vel.get_norm() is not None else 0 - return - j_sin_theta_d2 = math.sqrt(0.5 * (1.0 - j_cos_theta)) - - j_R = self.j_delta * j_sin_theta_d2 / (1.0 - j_sin_theta_d2) - - # [from klipper]: Approximated circle must contact moves no further away than mid-move - j_tan_theta_d2 = j_sin_theta_d2 / math.sqrt(0.5 * (1.0 + j_cos_theta)) - - move_centripetal_v2 = 0.5 * self.t_distance * j_tan_theta_d2 * self.state_B.state_p_settings.p_acc - - self.junction_vel = math.sqrt( - min(self.state_B.state_p_settings.p_acc * j_R, move_centripetal_v2, self.state_B.state_p_settings.speed**2) - ) + p_settings = self.state_B.state_p_settings + + # max jerk values + max_xy_jerk = p_settings.jerk + max_z_jerk = getattr(p_settings, "jerk_z", p_settings.jerk) + max_e_jerk = getattr(p_settings, "jerk_e", p_settings.jerk) + + # current and previous speeds + curr_speed = vel_1.get_vec(withExtrusion=True) + prev_speed = vel_0.get_vec(withExtrusion=True) + + # XY jerk + xy_jerk = np.sqrt((curr_speed[0] - prev_speed[0]) ** 2 + (curr_speed[1] - prev_speed[1]) ** 2) + z_jerk = abs(curr_speed[2] - prev_speed[2]) + e_jerk = abs(curr_speed[3] - prev_speed[3]) + + # Initial vmax_junction + vmax_junction = max_xy_jerk / 2.0 + vmax_junction_factor = 1.0 + + if abs(curr_speed[2]) > max_z_jerk / 2.0: + vmax_junction = min(vmax_junction, max_z_jerk / 2.0) + if abs(curr_speed[3]) > max_e_jerk / 2.0: + vmax_junction = min(vmax_junction, max_e_jerk / 2.0) + + vmax_junction = min(vmax_junction, vel_1.get_norm()) + # safe_speed = vmax_junction + + # If there is a previous move (simulate moves_queued > 1) + if vel_0.get_norm() > 0.0001: + vmax_junction = vel_1.get_norm() + if xy_jerk > max_xy_jerk: + vmax_junction_factor = max_xy_jerk / xy_jerk + if z_jerk > max_z_jerk: + vmax_junction_factor = min(vmax_junction_factor, max_z_jerk / z_jerk) + if e_jerk > max_e_jerk: + vmax_junction_factor = min(vmax_junction_factor, max_e_jerk / e_jerk) + vmax_junction = min(vel_0.get_norm(), vmax_junction * vmax_junction_factor) + + self.junction_vel = vmax_junction def get_junction_vel(self): """Return the calculated junction velocity. @@ -274,62 +355,230 @@ def get_junction_vel(self): return self.junction_vel -class junction_handling_MKA(junction_handling): +class mka(prusa): """Anisoprint A4 like junction handling. + **Code reference:** + [anisoprint/MKA-firmware/src/core/planner/planner.cpp#L1830](https://github.com/anisoprint/MKA-firmware/blob/6e02973b1b8f325040cc3dbf66ac545ffc5c06b3/src/core/planner/planner.cpp#L1830) + ... + float v_exit = previous_speed[axis] * smaller_speed_factor, + v_entry = current_speed[axis]; + if (limited) { + v_exit *= v_factor; + v_entry *= v_factor; + } + + // Calculate jerk depending on whether the axis is coasting in the same direction or reversing. + const float jerk = (v_exit > v_entry) + ? // coasting axis reversal + ( (v_entry > 0 || v_exit < 0) ? (v_exit - v_entry) : max(v_exit, -v_entry) ) + : // v_exit <= v_entry coasting axis reversal + ( (v_entry < 0 || v_exit > 0) ? (v_entry - v_exit) : max(-v_exit, v_entry) ); + + const float maxj = mechanics.max_jerk[axis]; + if (jerk > maxj) { + v_factor *= maxj / jerk; + ++limited; + } + } + if (limited) vmax_junction *= v_factor; + ... + + """ + + # MKA is similar to Prusa jerk handling + + +class junction_deviation(junction_handling): + """Marlin specific junction handling with Junction Deviation. + **Reference:** - [https://github.com/anisoprint/MKA-firmware/blob/6e02973b1b8f325040cc3dbf66ac545ffc5c06b3/src/core/planner/planner.cpp#L1830](https://github.com/anisoprint/MKA-firmware/blob/6e02973b1b8f325040cc3dbf66ac545ffc5c06b3/src/core/planner/planner.cpp#L1830) + 1: [Developer Blog](https://onehossshay.wordpress.com/2011/09/24/improving_grbl_cornering_algorithm/) + 2: [Kynetic CNC Blog](http://blog.kyneticcnc.com/2018/10/computing-junction-deviation-for-marlin.html) """ + def calc_JD(self, vel_0: velocity, vel_1: velocity, p_settings: state.p_settings): + """Calculate junction deviation velocity from 2 velocitys. + + Args: + vel_0: (velocity) entry + vel_1: (velocity) exit + p_settings: (state.p_settings) print settings + + Returns: + velocity: (float) velocity abs value + """ + # Junction deviation settings + JD_acc = p_settings.p_acc + if p_settings.jerk == 0: + return 0 + JD_delta = 0.414 * p_settings.jerk * p_settings.jerk / JD_acc # [2] + JD_minAngle = 18 + JD_maxAngle = 180 - 18 + vel_0_vec = vel_0.get_vec() + vel_1_vec = vel_1.get_vec() + if vel_0.get_norm() == 0 or vel_1.get_norm() == 0: + return 0 + # calculate junction angle + JD_cos_theta = np.dot(-np.asarray(vel_0_vec), np.asarray(vel_1_vec)) / ( + np.linalg.norm(vel_0_vec) * np.linalg.norm(vel_1_vec) + ) # cos of theta, theta: small angle between velocity vectors + if JD_cos_theta < 1: # catch numerical errors where cos theta is slightly larger than one + JD_sin_theta_half = np.sqrt((1 - JD_cos_theta) / 2) + else: + JD_sin_theta_half = 0 + if JD_sin_theta_half < np.sin(JD_maxAngle * np.pi / (2 * 180)): # smaller than max angle + if JD_sin_theta_half > np.sin( + JD_minAngle * np.pi / (2 * 180) + ): # and larger than min angle --> apply Junction Deviation Calculation + # calculate scalar junction velocity + JD_Radius = JD_delta * JD_sin_theta_half / (1 - JD_sin_theta_half) + JD_velocity_scalar = np.sqrt(JD_acc * JD_Radius) + + # return JD_velocity_scalar if JD_velocity_scalar < vel_0.get_norm() else vel_0.get_norm() + return JD_velocity_scalar if JD_velocity_scalar < p_settings.speed else p_settings.speed + else: + return 0 # angle smaller than min angle, stop completely + else: + return p_settings.speed # angle larger than max angle, full speed pass + def __init__(self, state_A: state, state_B: state): - """Marlin classic jerk specific junction velocity calculation. + """Marlin specific junction velocity calculation with Junction Deviation. Args: state_A: (state) start state state_B: (state) end state """ super().__init__(state_A, state_B) + self.junction_vel = self.calc_JD( + vel_0=self.target_vel, vel_1=self.vel_next, p_settings=self.state_B.state_p_settings + ) - self.calc_j_vel() + def get_junction_vel(self): + """Return junction velocity. - def calc_j_vel(self): - """Calculate the junction velocity.""" - vel_0 = self.target_vel - vel_1 = self.vel_next - self.jerk = self.state_B.state_p_settings.jerk + Returns: + junction_vel: (float) junction velocity + """ + return self.junction_vel - v_max_junc = min(vel_0.get_norm(), vel_1.get_norm()) - small_speed_fac = v_max_junc / vel_0.get_norm() if v_max_junc > 0 else 0 - v_factor = 1 - lim_flag = False +# class junction_handling_klipper(junction_handling): - for v_entry, v_exit in zip(vel_0.get_vec(withExtrusion=True), vel_1.get_vec(withExtrusion=True)): - v_exit *= small_speed_fac +# """Klipper specific junction handling. - if lim_flag: - v_entry *= v_factor - v_exit *= v_factor +# - similar junction deviation calc +# - corner vel set by: square_corner_velocity +# end_velocity^2 = start_velocity^2 + 2*accel*move_distance +# for 90deg turn +# - todo: smoothed look ahead - jerk = ( - ((v_exit - v_entry) if (v_entry > 0 or v_exit < 0) else max(v_exit, -v_entry)) - if (v_exit > v_entry) - else (v_entry - v_exit if (v_entry < 0 or v_exit > 0) else max(-v_exit, v_entry)) - ) # calc logic taken from MKA firmware +# **Reference:** +# [https://www.klipper3d.org/Kinematics.html](https://www.klipper3d.org/Kinematics.html) +# [https://github.com/Klipper3d/klipper/blob/ea2f6bc0f544132738c7f052ffcc586fa884a19a/klippy/toolhead.py](https://github.com/Klipper3d/klipper/blob/ea2f6bc0f544132738c7f052ffcc586fa884a19a/klippy/toolhead.py) +# """ +# import math - if jerk > self.jerk: - v_factor *= self.jerk / jerk - lim_flag = True +# def __init__(self, state_A: state, state_B: state): +# """Klipper specific junction velocity calculation. - if lim_flag: - v_max_junc *= v_factor +# Args: +# state_A: (state) start state +# state_B: (state) end state +# """ +# super().__init__(state_A, state_B) - self.junction_vel = v_max_junc +# self.calc_j_delta() +# self.calc_j_vel() - def get_junction_vel(self): - """Return the calculated junction velocity. +# def calc_j_delta(self): +# """Calculate the junction deviation with klipper specific values. - Returns: - junction_vel: (float) junction velocity - """ - return self.junction_vel +# The jerk value represents the square_corner_velocity! +# """ +# sc_vel = (self.state_B.state_p_settings.jerk) ** 2 +# self.j_delta = sc_vel * (math.sqrt(2.0) - 1.0) / self.state_B.state_p_settings.p_acc + +# def calc_j_vel(self): +# """Calculate the junction velocity.""" +# vel_0 = self.target_vel +# vel_1 = self.vel_next + +# if vel_0.get_norm() == 0 or vel_1.get_norm() == 0: +# self.junction_vel = 0 +# return + +# # calculate junction angle +# dir0 = vel_0.get_norm_dir() +# dir1 = vel_1.get_norm_dir() +# j_cos_theta = -( +# dir0[0] * dir1[0] + dir0[1] * dir1[1] + dir0[2] * dir1[2] +# ) # cos of theta, theta: small angle between velocity vectors + +# j_cos_theta = max(j_cos_theta, -0.999999) # limit +# if j_cos_theta > 0.999999: +# self.junction_vel = 0 # self.target_vel.get_norm() # if self.target_vel.get_norm() is not None else 0 +# return +# j_sin_theta_d2 = math.sqrt(0.5 * (1.0 - j_cos_theta)) + +# j_R = self.j_delta * j_sin_theta_d2 / (1.0 - j_sin_theta_d2) + +# # [from klipper]: Approximated circle must contact moves no further away than mid-move +# j_tan_theta_d2 = j_sin_theta_d2 / math.sqrt(0.5 * (1.0 + j_cos_theta)) + +# move_centripetal_v2 = 0.5 * self.t_distance * j_tan_theta_d2 * self.state_B.state_p_settings.p_acc + +# self.junction_vel = math.sqrt( +# min(self.state_B.state_p_settings.p_acc * j_R, move_centripetal_v2, self.state_B.state_p_settings.speed**2) +# ) + +# def get_junction_vel(self): +# """Return the calculated junction velocity. + +# Returns: +# junction_vel: (float) junction velocity +# """ +# return self.junction_vel + + +def get_handler(firmware_name: str) -> type[junction_handling]: + """Get the junction handling class for the given firmware name. + + Args: + firmware_name: (str) name of the firmware + + Returns: + junction_handling: (type[junction_handling]) junction handling class + """ + if firmware_name == "prusa": + return prusa + elif firmware_name == "junction_deviation": + return junction_deviation + elif firmware_name == "marlin": + return marlin + elif firmware_name == "ultimaker": + return ultimaker + elif firmware_name == "mka": + return mka + else: + custom_print( + f"Using NO (zero interfacing velocity) junction handling handling for provided '{firmware_name}' firmware name.", + f"Use one of the following: {', '.join(_get_handler_names())} for proper junction handling.", + lvl=1, + ) + return junction_handling + + +def _get_handler_names() -> list[str]: + """Get the names of all available junction handling classes. + + Returns: + list[str]: List of junction handling class names. + """ + # Get all classes defined in this module that are subclasses of junction_handling (excluding the base itself) + current_module = sys.modules[__name__] + return [ + name + for name, obj in inspect.getmembers(current_module, inspect.isclass) + if issubclass(obj, junction_handling) and obj is not junction_handling + ] diff --git a/pyGCodeDecode/planner_block.py b/pyGCodeDecode/planner_block.py index 8e2a0bf..6446856 100644 --- a/pyGCodeDecode/planner_block.py +++ b/pyGCodeDecode/planner_block.py @@ -5,14 +5,9 @@ import numpy as np from pyGCodeDecode.helpers import custom_print +from pyGCodeDecode.result import abstract_result, acceleration_result, velocity_result -from .junction_handling import ( - junction_handling, - junction_handling_klipper, - junction_handling_marlin_jd, - junction_handling_marlin_jerk, - junction_handling_MKA, -) +from .junction_handling import get_handler from .state import state from .utils import segment, velocity @@ -20,7 +15,12 @@ class planner_block: """Planner Block Class.""" - def move_maker2(self, v_end): + result_calculators: List[abstract_result] = [ + acceleration_result(), + velocity_result(), + ] + + def move_maker(self, v_end): """ Calculate the correct move type (trapezoidal,triangular or singular) and generate the corresponding segments. @@ -218,7 +218,7 @@ def self_correction(self, tolerance=float("1e-12")): # Correct error by recalculating velocitys with new vel_end if self.next_block is not None and flag_correct: vel_end = self.next_block.get_segments()[0].vel_begin.get_norm() - self.move_maker2(v_end=vel_end) + self.move_maker(v_end=vel_end) if self.blocktype == "single": self.prev_block.self_correction() # forward correction? @@ -278,6 +278,16 @@ def extrusion_block_max_vel(self) -> Union[np.ndarray, None]: else: return None + def calc_results(self, *additional_calculators: abstract_result): + """Calculate the result of the planner block.""" + for calculator in self.result_calculators: + calculator.calc_pblock(self) + + if additional_calculators: + for calculator in additional_calculators: + if calculator not in self.result_calculators: + calculator.calc_pblock(self) + def __init__(self, state: state, prev_block: "planner_block", firmware=None): """Calculate and store planner block consisting of one or multiple segments. @@ -295,31 +305,24 @@ def __init__(self, state: state, prev_block: "planner_block", firmware=None): self.segments: List[segment] = [] # store segments here self.blocktype = None + self.e_type = None # use for extrusion type e.g. perimeter, infill ... - if firmware == "marlin_jd": - junction = junction_handling_marlin_jd(state_A=self.state_A, state_B=self.state_B) - elif firmware == "klipper": - junction = junction_handling_klipper(state_A=self.state_A, state_B=self.state_B) - elif firmware == "marlin_jerk": - junction = junction_handling_marlin_jerk(state_A=self.state_A, state_B=self.state_B) - elif firmware == "MKA": - junction = junction_handling_MKA(state_A=self.state_A, state_B=self.state_B) - else: - junction = junction_handling(state_A=self.state_A, state_B=self.state_B) + handler = get_handler(firmware_name=firmware) # get junction handler + junction = handler(state_A=self.state_A, state_B=self.state_B) # planner block calculation - target_vel = junction.get_target_vel() # target velocity for this planner block + self.target_vel = junction.get_target_vel() # target velocity for this planner block v_JD = junction.get_junction_vel() - self.direction = target_vel.get_norm_dir(withExtrusion=True) # direction vector of pb + self.direction = self.target_vel.get_norm_dir(withExtrusion=True) # direction vector of pb - self.valid = target_vel.not_zero() # valid planner block + self.valid = self.target_vel.not_zero() # valid planner block # standard move maker if self.valid: self.JD = v_JD * self.direction # jd writeout for debugging plot - self.move_maker2(v_end=v_JD) + self.move_maker(v_end=v_JD) self.is_extruding = self.state_A.state_position.is_extruding( self.state_B.state_position ) # store extrusion flag diff --git a/pyGCodeDecode/plotter.py b/pyGCodeDecode/plotter.py new file mode 100644 index 0000000..b96900b --- /dev/null +++ b/pyGCodeDecode/plotter.py @@ -0,0 +1,523 @@ +"""This module provides functionality for 3D plotting of G-code simulation data using PyVista. + +Functions: + plot_3d: Generates a 3D plot of the simulation data, with options for customization such as + extrusion-only plotting, scalar value selection, layer selection, and saving the plot + as a screenshot or VTK file. + plot_2d: Generates a 2D plot of the simulation data, showing the position of the extruder head + over time. + +Dependencies: + - pyGCodeDecode.gcode_interpreter.simulation + - pyGCodeDecode.gcode_interpreter.unpack_blocklist + - pyGCodeDecode.utils + - numpy + - pyvista + - pathlib +""" + +import os +import pathlib +from typing import Tuple, Union + +import numpy as np +import pyvista as pv +from matplotlib.figure import Figure + +from pyGCodeDecode.gcode_interpreter import ( + find_current_segment, + simulation, + unpack_blocklist, +) +from pyGCodeDecode.helpers import ProgressBar, custom_print + + +def plot_3d( + sim: simulation, + extrusion_only: bool = True, + scalar_value: str = "velocity", + screenshot_path: pathlib.Path = None, + camera_settings: dict = None, + vtk_path: pathlib.Path = None, + mesh: pv.MultiBlock = None, + layer_select: int = None, + window_size: tuple = (2048, 1536), + mpl_subplot: bool = False, + mpl_rcParams: Union[dict, None] = None, + solid_color: str = "black", + transparent_background: bool = True, + parallel_projection: bool = False, + lighting: bool = True, + block_colorbar: bool = False, + extra_plotting: callable = None, # function to add plotting, args: plotter, mesh + overwrite_labels: Union[dict, None] = None, + scalar_value_bounds: Union[Tuple[float, float], None] = None, +) -> pv.MultiBlock: + """3D Plot with PyVista. + + Args: + extrusion_only (bool, optional): Plot only parts where material is extruded. + Defaults to True. + scalar_value (str, optional): scalar value to plot. Defaults to Velocity (vel). + Options: vel, rel_vel_err, or None. + screenshot_path (pathlib.Path, optional): Path to screenshot to be saved. + Prevents interactive plot. Defaults to None. + vtk_path (pathlib.Path, optional): Path to vtk to be saved. + Prevents interactive plot. Defaults to None. + mesh (pv.MultiBlock, optional): A pyvista mesh from a previous run to + avoid running the mesh generation again. Defaults to None. + layer_select (int, optional): Select the layer to be plotted. + Defaults to None, which plots all layers. + window_size (tuple, optional): Size of the plot window. + Defaults to (2048, 1536). + mpl_subplot (bool, optional): Use matplotlib subplot for the screenshot. + Defaults to False. + solid_color (str, optional): Background color of the plot. Defaults to "black". + transparent_background (bool, optional): Use a transparent background for + the screenshot. Defaults to True. + Returns: + pv.MultiBlock: The mesh used in the plot so it can be used (e.g. in subsequent plots). + """ + + def safe_screenshot(plotter: pv.Plotter, screenshot_path=None): + if display_available: + img = plotter.screenshot( + transparent_background=transparent_background, + filename=screenshot_path, + ) + if screenshot_path is not None: + custom_print(f"PyVista Screenshot saved to:\n{screenshot_path}") + else: + img = None + custom_print("PyVista Screenshot can not be created without a display!", lvl=1) + return img + + colorbar_label = { + "velocity": [r"$v$ in $\frac{mm}{s}$", "v in mm/s", "viridis"], + "rel_vel_err": [r"$\epsilon_{\mathrm{loc}}$", "vel. Error", "Reds"], + "acceleration": [r"$a$ in $\frac{mm}{s^2}$", "a in mm/s^2", "viridis"], + } + if overwrite_labels: + colorbar_label.update(overwrite_labels) + + if layer_select is not None: + layer_blcklst = [block for block in sim.blocklist if block.state_B.layer == layer_select] + segments = unpack_blocklist(blocklist=layer_blcklst) + custom_print("Number of Segments in this Layer: ", len(segments), lvl=3) + else: + segments = unpack_blocklist(blocklist=sim.blocklist) + + if mesh is None: + mesh = pv.MultiBlock() + x, y, z, e, scalar = [], [], [], [], [] + + e_width = 0.45 + l_height = 0.2 + z_scaler = e_width / l_height + bar = ProgressBar(name="3D Plot") + custom_print("result: ", lvl=3) + for n, segm in enumerate(segments): + bar.update((n + 1) / len(segments)) + + if (not extrusion_only) or (segm.is_extruding()): + if len(x) == 0: + pos_begin_vec = segm.pos_begin.get_vec(withExtrusion=True) + x.append(pos_begin_vec[0]) + y.append(pos_begin_vec[1]) + z.append(pos_begin_vec[2] * z_scaler) + e.append(pos_begin_vec[3]) + + if scalar_value is not None: + sc = segm.get_result(scalar_value) + if hasattr(sc, "__len__"): + scalar.append(sc[0]) + else: + scalar.append(sc) + + pos_end_vec = segm.pos_end.get_vec(withExtrusion=True) + x.append(pos_end_vec[0]) + y.append(pos_end_vec[1]) + z.append(pos_end_vec[2] * z_scaler) + e.append(pos_end_vec[3]) + + if scalar_value is not None: + sc = segm.get_result(scalar_value) + if hasattr(sc, "__len__"): + scalar.append(sc[1]) + else: + scalar.append(sc) + + if (extrusion_only and (len(x) > 0 and not segm.is_extruding())) or ( + len(x) > 0 and n == (len(segments) - 1) + ): + points_3d = np.column_stack((x, y, z)) + line = pv.lines_from_points(points_3d) + if scalar_value is not None: + line[scalar_value] = scalar + tube = line.tube(radius=e_width / 2, n_sides=6) + mesh.append(tube) + x, y, z, e, scalar = [], [], [], [], [] + + mesh = mesh.combine() + + # check wether a display is available or Windows is used + if os.name == "nt" or "DISPLAY" in os.environ: + display_available = True + else: + display_available = False + + # saving a screenshot and an interactive plot aren't possible at the same tim + if screenshot_path is None: + off_screen = False + else: + off_screen = True + + if off_screen: + p = pv.Plotter(off_screen=off_screen, window_size=window_size) + else: + p = pv.Plotter(off_screen=off_screen) + + if parallel_projection: + p.enable_parallel_projection() + + p.set_scale(zscale=1 / z_scaler) + + if extra_plotting: + try: + custom_print("Running extra plotting function", lvl=3) + extra_plotting(p, mesh) + except Exception as e: + custom_print(f"Error in extra plotting function: {e}", lvl=1) + + if scalar_value is not None: + actor = p.add_mesh( + mesh, + scalars=scalar_value, + smooth_shading=True, + scalar_bar_args={ + "title": colorbar_label[scalar_value][1], + "title_font_size": 40, + "label_font_size": 25, + "width": 0.05, + "vertical": True, + "font_family": "arial", + }, + cmap=colorbar_label[scalar_value][-1], + lighting=lighting, + ) + if scalar_value == "rel_vel_err": + p.update_scalar_bar_range([0, 1]) + + if scalar_value_bounds is not None: + p.update_scalar_bar_range(scalar_value_bounds) + else: + p.set_background(solid_color) + p.add_mesh(mesh, color=solid_color, smooth_shading=True, lighting=lighting) + + if layer_select is not None: + p.view_xy() + + if camera_settings is not None: + p.camera_position = camera_settings["camera_position"] + if camera_settings.get("elevation", False): + p.camera.elevation = camera_settings["elevation"] + if camera_settings.get("azimuth", False): + p.camera.azimuth = camera_settings["azimuth"] + if camera_settings.get("roll", False): + p.camera.roll = camera_settings["roll"] + + if vtk_path is not None: + mesh.save(filename=vtk_path) + custom_print(f"VTK saved to:\n{vtk_path}", lvl=2) + + if screenshot_path is not None: + custom_print(f"Offscreen plotting, with resolution {p.window_size}", lvl=3) + + if mpl_subplot: + import matplotlib.pyplot as plt + + if isinstance(mpl_rcParams, dict): + custom_print( + f"Using custom matplotlib rcParams in plot_3d.\n{mpl_rcParams}", + lvl=3, + ) + # print("rcParams:", mpl_rcParams) + plt.rcParams.update(mpl_rcParams) + # print("Updated rcParams:", plt.rcParams) + + fig, ax = plt.subplots() + + if scalar_value is not None: + lut = actor.mapper.lookup_table + if scalar_value_bounds is None: + min_val = lut.GetRange()[0] + max_val = lut.GetRange()[1] + else: + min_val, max_val = scalar_value_bounds + + dummy_img = ax.imshow( + np.array([[min_val, max_val]]), cmap=colorbar_label[scalar_value][-1], vmin=min_val, vmax=max_val + ) + + p.remove_scalar_bar() + + # image = p.screenshot(transparent_background=True, window_size=window_size) + image = safe_screenshot(p) + # ax.axis("off") + if not block_colorbar: + cbar = fig.colorbar(dummy_img, ax=ax, shrink=0.6) + cbar.set_label(colorbar_label[scalar_value][0], fontsize=20) + + else: + # image = p.screenshot(transparent_background=True) + image = safe_screenshot(p) + ax.axis("off") + if image is not None: + ax.imshow(image) + fig.tight_layout() + dpi = window_size[1] / fig.get_size_inches()[1] + fig.savefig(screenshot_path, dpi=dpi, transparent=transparent_background) # bbox_inches="tight", + + custom_print(f"MPL Screenshot saved to:\n{screenshot_path}") + else: + if block_colorbar: + p.remove_scalar_bar() + image = safe_screenshot(p, screenshot_path) + return image # TODO this not good + + if not off_screen and display_available: + p.show() + + return mesh + + +def plot_2d( + sim: simulation, + filepath: pathlib.Path = pathlib.Path("trajectory_2D.png"), + colvar="Velocity", + show_points=False, + colvar_spatial_resolution=1, + dpi=400, + scaled=True, + show=False, +): + """Plot 2D position (XY plane) with matplotlib (unmaintained).""" + import matplotlib.pyplot as plt + from matplotlib import cm + from matplotlib.collections import LineCollection + + colvar_label = { + "Velocity": "Velocity in mm/s", + "Acceleration": "Acceleration in mm/s^2", + } + + def interp_2D(x, y, cvar, spatial_resolution=1): + segm_length = np.linalg.norm([np.ediff1d(x), np.ediff1d(y)], axis=0) + segm_cvar_delt = np.greater(np.abs(np.ediff1d(cvar)), 0) + segm_interpol = np.r_[ + 0, + np.where(segm_cvar_delt, np.ceil(segm_length / spatial_resolution) + 1, 1), + ] # get nmbr of segments for required resolution, dont interpolate if there is no change + points = np.array([x, y, cvar]).T + points = np.c_[points, segm_interpol] + + # generate intermediate points with set resolution + old_point = None + interpolated = np.zeros((1, 3)) + for point in points: + if old_point is not None: + steps = np.linspace(0, 1, int(point[3]), endpoint=True) + x_i = np.interp(steps, [0, 1], [old_point[0], point[0]]) + y_i = np.interp(steps, [0, 1], [old_point[1], point[1]]) + colvar_i = np.interp(steps, [0, 1], [old_point[2], point[2]]) + interpolated = np.r_[interpolated, np.array([x_i, y_i, colvar_i]).T] + old_point = point + interpolated = np.delete(interpolated, 0, 0) + + return interpolated + + segments = unpack_blocklist(blocklist=sim.blocklist) + if colvar == "Velocity": + # get all planned trajectory vertices + color variable + x, y, cvar = [], [], [] + x.append(segments[0].pos_begin.get_vec()[0]) + y.append(segments[0].pos_begin.get_vec()[1]) + cvar.append(segments[0].vel_begin.get_norm()) + + bar = ProgressBar(name="2D Plot Lines") + for i, segm in enumerate(segments): + bar.update((i + 1) / len(segments)) + x.append(segm.pos_end.get_vec()[0]) + y.append(segm.pos_end.get_vec()[1]) + cvar.append(segm.vel_end.get_norm()) + + # interpolate values for smooth coloring + interpolated = interp_2D(x, y, cvar, spatial_resolution=colvar_spatial_resolution) + + x = interpolated[:, 0] + y = interpolated[:, 1] + cvar = interpolated[:, 2] # maybe change interpolation to return tuple? + + # generate point pairs for line collection + point_pairs = [] + for i in np.arange(len(x) - 1): + point_pairs.append([(x[i], y[i]), (x[i + 1], y[i + 1])]) + + # generate collection from pairs + collection = LineCollection(point_pairs) + collection.set_array(cvar) + collection.set_cmap(cm.jet) + + fig = plt.figure() + ax1 = fig.add_subplot(1, 1, 1) + ax1.add_collection(collection) + ax1.autoscale() + plt.colorbar(collection, label=colvar_label[colvar], shrink=0.6, location="right") + else: + x, y = [], [] + x.append(segments[0].pos_begin.get_vec()[0]) + y.append(segments[0].pos_begin.get_vec()[1]) + for i, segm in enumerate(segments): + bar.update((i + 1) / len(segments)) + x.append(segm.pos_end.get_vec()[0]) + y.append(segm.pos_end.get_vec()[1]) + fig = plt.subplot() + fig.plot(x, y, color="black") + + if show_points: + for i, block in enumerate(sim.blocklist): + bar.update(i / len(sim.blocklist)) + fig.scatter( + block.get_segments()[-1].pos_end.get_vec()[0], + block.get_segments()[-1].pos_end.get_vec()[1], + color="blue", + marker="x", + ) + + plt.xlabel("x position") + plt.ylabel("y position") + plt.title("2D Position") + if scaled: + plt.axis("scaled") + if filepath is not False: + plt.savefig(filepath, dpi=dpi) + custom_print(f"2D Plot saved:\n👉 {filepath}") + if show: + plt.show() + return fig + plt.close() + + +def plot_vel( + sim: simulation, + axis: Tuple[str] = ("x", "y", "z", "e"), + show: bool = True, + show_planner_blocks: bool = True, + show_segments: bool = False, + show_jv: bool = False, + time_steps: Union[int, str] = "constrained", + filepath: pathlib.Path = None, + dpi: int = 400, +) -> Figure: + """Plot axis velocity with matplotlib. + + Args: + axis: (tuple(string), default = ("x", "y", "z", "e")) select plot axis + show: (bool, default = True) show plot and return plot figure + show_planner_blocks: (bool, default = True) show planner_blocks as vertical lines + show_segments: (bool, default = False) show segments as vertical lines + show_jv: (bool, default = False) show junction velocity as x + time_steps: (int or string, default = "constrained") number of time steps or constrain plot + vertices to segment vertices + filepath: (Path, default = None) save fig as image if filepath is provided + dpi: (int, default = 400) select dpi + + Returns: + (optionally) + fig: (figure) + """ + import matplotlib.pyplot as plt + + axis_dict = {"x": 0, "y": 1, "z": 2, "e": 3} + + segments = unpack_blocklist(blocklist=sim.blocklist) # unpack + + # time steps + if type(time_steps) is int: # evenly distributed time steps + times = np.linspace( + 0, + sim.blocklist[-1].get_segments()[-1].t_end, + time_steps, + endpoint=False, + ) + elif time_steps == "constrained": # use segment time points as plot constrains + times = [0] + for segm in segments: + times.append(segm.t_end) + else: + raise ValueError("Invalid value for 'time_steps', either use Integer or 'constrained' as argument.") + + # gathering values + pos = [[], [], [], []] + vel = [[], [], [], []] + abs = [] # initialize value arrays + index_saved = 0 + bar = ProgressBar(name="Velocity Plot") + + for i, t in enumerate(times): + segm, index_saved = find_current_segment(path=segments, t=t, last_index=index_saved, keep_position=True) + + tmp_vel = segm.get_velocity(t=t).get_vec(withExtrusion=True) + tmp_pos = segm.get_position(t=t).get_vec(withExtrusion=True) + for ax in axis: + pos[axis_dict[ax]].append(tmp_pos[axis_dict[ax]]) + vel[axis_dict[ax]].append(tmp_vel[axis_dict[ax]]) + + abs.append(np.linalg.norm(tmp_vel[:3])) + bar.update((i + 1) / len(times)) + + fig, ax1 = plt.subplots() + ax2 = ax1.twinx() + + # plot JD-Limits + for block in sim.blocklist: + # planner blocks vertical line plot + if show_planner_blocks: + ax1.axvline(x=block.get_segments()[-1].t_end, color="black", lw=0.5) + + # segments vertical line plot + if show_segments: + for segm in block.get_segments(): + ax1.axvline(x=segm.t_end, color="green", lw=0.25) + + if show_jv: + # absolute JD Marker + absJD = np.linalg.norm([block.JD[0], block.JD[1], block.JD[2]]) + ax1.scatter(x=block.get_segments()[-1].t_end, y=absJD, color="red", marker="x") + for ax in axis: + ax1.scatter( + x=block.get_segments()[-1].t_end, + y=block.JD[axis_dict[ax]], + marker="x", + color="black", + lw=0.5, + ) + + # plot all axis in velocity and position + for ax in axis: + ax1.plot(times, vel[axis_dict[ax]], label=ax) # velocity + ax2.plot(times, pos[axis_dict[ax]], linestyle="--") # position w/ extrusion + # if not ax == "e": ax2.plot(times,pos[axis_dict[ax]],linestyle="--") #position ignoring extrusion + ax1.plot(times, abs, color="black", label="abs") # absolute velocity + + ax1.set_xlabel("time in s") + ax1.set_ylabel("velocity in mm/s") + ax2.set_ylabel("position in mm") + ax1.legend(loc="lower left") + plt.title("Velocity and Position over Time") + if filepath is not None: + plt.savefig(filepath, dpi=dpi) + if show: + plt.show() + plt.close() + return fig diff --git a/pyGCodeDecode/result.py b/pyGCodeDecode/result.py new file mode 100644 index 0000000..ec0e5cd --- /dev/null +++ b/pyGCodeDecode/result.py @@ -0,0 +1,121 @@ +"""Result calculation for segments and planner blocks.""" + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pyGCodeDecode.planner_block import planner_block + +from pyGCodeDecode.utils import segment + + +# new segment class spanned by pos, rest is "result" +class abstract_result(ABC): + """Abstract class for result calculation.""" + + @property + @abstractmethod + def name(self): + """Name of the result. Has to be set in the derived class.""" + pass + + @abstractmethod + def calc_pblock(self, pblock: "planner_block", **kwargs): + """Calculate the result for a planner block.""" + pass + + @abstractmethod + def calc_segm(self, segm: "segment", **kwargs): + """Calculate the result for a segment.""" + pass + + +class acceleration_result(abstract_result): + """The acceleration.""" + + name = "acceleration" + + def calc_segm(self, segm: "segment", **kwargs): + """Calculate the acceleration for a segment.""" + delta_v = segm.vel_end.get_norm() - segm.vel_begin.get_norm() + delta_t = segm.get_segm_duration() + + if delta_t > 0: + acc = delta_v / delta_t + else: + acc = 0 + segm.result[self.name] = acc + + def calc_pblock(self, pblock, **kwargs): + """Calculate the acceleration for a planner block.""" + for segm in pblock.segments: + self.calc_segm(segm, **kwargs) + + +class velocity_result(abstract_result): + """The velocity.""" + + name = "velocity" + + def calc_segm(self, segm: "segment", **kwargs): + """Calculate the velocity for a segment.""" + segm.result[self.name] = [ + segm.vel_begin.get_norm(), + segm.vel_end.get_norm(), + ] + + def calc_pblock(self, pblock: "planner_block", **kwargs): + """Calculate the velocity for a planner block.""" + for segm in pblock.segments: + self.calc_segm(segm, **kwargs) + + +def get_all_result_calculators(): + """Get all results.""" + public_results = [ + acceleration_result(), + velocity_result(), + ] + + private_results = [] + # Try to import private results if the module exists + try: + from pyGCodeDecode.private_result import get_private_result_calculators + + private_results = get_private_result_calculators() + except ImportError: + # Private results module not available, continue with only public results + pass + + return public_results + private_results + + +def has_private_results(): + """Check if private results are available.""" + try: + from pyGCodeDecode.private_result import ( # noqa: F401 + get_private_result_calculators, + ) + + return True + except ImportError: + return False + + +def get_result_info(): + """Get information about available result calculators.""" + all_calcs = get_all_result_calculators() + public_count = 2 # acceleration_result and velocity_result + private_count = len(all_calcs) - public_count + + return { + "total_count": len(all_calcs), + "public_count": public_count, + "private_count": private_count, + "has_private": has_private_results(), + "result_names": [calc.name for calc in all_calcs], + } + + +if __name__ == "__main__": + pass diff --git a/pyGCodeDecode/state_generator.py b/pyGCodeDecode/state_generator.py index faa706a..1d5132d 100644 --- a/pyGCodeDecode/state_generator.py +++ b/pyGCodeDecode/state_generator.py @@ -120,6 +120,7 @@ def arg_extract(string: str, key_dict: dict) -> dict: """ arg_dict = dict() # dict to store found arguments for each key matches: List[Match] = list() # list to store matching keywords + string = str(string) # typecasting to prevent TypeError for key in key_dict.keys(): # look for each key in the dictionary match = re.search(key, string) # regex search for key in string diff --git a/pyGCodeDecode/tools.py b/pyGCodeDecode/tools.py index 759fab2..6db75fe 100644 --- a/pyGCodeDecode/tools.py +++ b/pyGCodeDecode/tools.py @@ -28,7 +28,7 @@ def save_layer_metrics( Layers are detected using the given layer cue. """ # check if a layer cue was specified - if "layer_cue" not in simulation.initial_machine_setup: + if "layer_cue" not in simulation.initial_machine_setup_dict: custom_print( "⚠️ No layer_cue was specified in the simulation setup. Therefore, layer metrics can not be saved!", lvl=1 ) diff --git a/pyGCodeDecode/utils.py b/pyGCodeDecode/utils.py index 5405adb..e844624 100644 --- a/pyGCodeDecode/utils.py +++ b/pyGCodeDecode/utils.py @@ -7,10 +7,46 @@ - position """ -from typing import List +from typing import TYPE_CHECKING, List, Union import numpy as np +if TYPE_CHECKING: + from pyGCodeDecode.state import state + + +class seconds(float): + """A float subclass representing a time duration in seconds. + + Args: + value (float or int): The time duration in seconds. + Examples: + >>> t = seconds(5) + >>> str(t) + '5.0 s' + >>> t.seconds + 5.0 + """ + + """Time class for storing time, behaves like a float with additional methods.""" + + def __new__(cls, value): + """Create a new instance of seconds.""" + return float.__new__(cls, value) + + def __str__(self) -> str: + """Return string representation of the time in seconds.""" + return f"{float(self)} s" + + def __repr__(self): + """Return a string representation of the seconds object.""" + return self.__str__() + + @property + def seconds(self): + """Return the float value of the seconds instance.""" + return float(self) + class vector_4D: """The vector_4D class stores 4D vector in x,y,z,e. @@ -139,7 +175,7 @@ def __truediv__(self, other): return self.__class__(x, y, z, e) def __eq__(self, other): - """Check for equality and return True if equal. + """Check for equality and return True if equal (with tolerance). Args: other: (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') @@ -148,22 +184,30 @@ def __eq__(self, other): eq: (bool) true if equal (with tolerance) """ if isinstance(other, type(self)): - if ( - np.isclose(self.x, other.x) - and np.isclose(self.y, other.y) - and np.isclose(self.z, other.z) - and np.isclose(self.e, other.e) - ): - return True + other_vec = [other.x, other.y, other.z, other.e] + elif isinstance(other, (np.ndarray, list, tuple)) and len(other) == 4: + other_vec = list(other) + else: + return False - elif (isinstance(other, np.ndarray) or isinstance(other, list) or isinstance(other, tuple)) and len(other) == 4: - if ( - np.isclose(self.x, other[0]) - and np.isclose(self.y, other[1]) - and np.isclose(self.z, other[2]) - and np.isclose(self.e, other[3]) - ): - return True + self_vec = [self.x, self.y, self.z, self.e] + return np.allclose(self_vec, other_vec) + + def __gt__(self, other): + """Check for greater than and return True if greater. + + Args: + other: (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') + + Returns: + gt: (bool) true if greater + """ + if isinstance(other, type(self)): + return self.get_norm() > other.get_norm() + elif (isinstance(other, np.ndarray)) or (isinstance(other, (list, tuple)) and len(other) == 4): + return self.get_norm() > np.linalg.norm(other) + elif isinstance(other, (float, int)): + return self.get_norm() > other def get_vec(self, withExtrusion=False) -> List[float]: """Return the 4D vector, optionally with extrusion. @@ -191,6 +235,73 @@ def get_norm(self, withExtrusion=False) -> float: return np.linalg.norm(self.get_vec(withExtrusion=withExtrusion)) +class position(vector_4D): + """4D - Position object for (Cartesian) 3D printer.""" + + def __str__(self) -> str: + """Print out position.""" + return "position: " + super().__str__() + + def is_travel(self, other) -> bool: + """Return True if there is travel between self and other position. + + Args: + other: (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') + + Returns: + is_travel: (bool) true if between self and other is distance + """ + if abs(other.x - self.x) + abs(other.y - self.y) + abs(other.z - self.z) > 0: + return True + else: + return False + + def is_extruding(self, other: "position", ignore_retract: bool = True) -> bool: + """Return True if there is extrusion between self and other position. + + Args: + other: (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') + ignore_retract: (bool, default = True) if true ignore retract movements else retract is also extrusion + + Returns: + is_extruding: (bool) true if between self and other is extrusion + """ + extrusion = other.e - self.e if ignore_retract else abs(other.e - self.e) + + if extrusion > 0: + return True + else: + return False + + def get_t_distance(self, other=None, withExtrusion=False) -> float: + """Calculate the travel distance between self and other position. If none is provided, zero will be used. + + Args: + other: (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray', default = None) + withExtrusion: (bool, default = False) use or ignore extrusion + + Returns: + travel: (float) travel or extrusion and travel distance + """ + if other is None: + other = position(0, 0, 0, 0) + return np.linalg.norm( + np.subtract(self.get_vec(withExtrusion=withExtrusion), other.get_vec(withExtrusion=withExtrusion)) + ) + + def __truediv__(self, other): + """Divide position by seconds to get velocity.""" + if isinstance(other, seconds): + return velocity( + self.x / other.seconds, + self.y / other.seconds, + self.z / other.seconds, + self.e / other.seconds, + ) + else: + return super().__truediv__(other) + + class velocity(vector_4D): """4D - Velocity object for (Cartesian) 3D printer.""" @@ -248,60 +359,70 @@ def is_extruding(self): """ return True if self.e > 0 else False + def __mul__(self, other): + """Multiply velocity by a time to get position, or by scalar.""" + if isinstance(other, seconds): + # velocity * seconds = position + return position( + self.x * other.seconds, + self.y * other.seconds, + self.z * other.seconds, + self.e * other.seconds, + ) + elif isinstance(other, (float, int, np.float64)): + return self.__class__( + self.x * other, + self.y * other, + self.z * other, + self.e * other, + ) + else: + raise TypeError("Multiplication only supports seconds, float, or int.") -class position(vector_4D): - """4D - Position object for (Cartesian) 3D printer.""" - - def __str__(self) -> str: - """Print out position.""" - return "position: " + super().__str__() - - def is_travel(self, other) -> bool: - """Return True if there is travel between self and other position. - - Args: - other: (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') - - Returns: - is_travel: (bool) true if between self and other is distance - """ - if abs(other.x - self.x) + abs(other.y - self.y) + abs(other.z - self.z) > 0: - return True + def __truediv__(self, other): + """Divide velocity by scalar.""" + if isinstance(other, seconds): + # velocity / seconds = acceleration + return acceleration( + self.x / other.seconds, + self.y / other.seconds, + self.z / other.seconds, + self.e / other.seconds, + ) else: - return False + return super().__truediv__(other) - def is_extruding(self, other: "position", ignore_retract: bool = True) -> bool: - """Return True if there is extrusion between self and other position. - Args: - other: (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') - ignore_retract: (bool, default = True) if true ignore retract movements else retract is also extrusion +class acceleration(vector_4D): + """4D - Acceleration object for (Cartesian) 3D printer.""" - Returns: - is_extruding: (bool) true if between self and other is extrusion - """ - extrusion = other.e - self.e if ignore_retract else abs(other.e - self.e) + def __str__(self) -> str: + """Print out acceleration.""" + return "acceleration: " + super().__str__() - if extrusion > 0: - return True + def __mul__(self, other): + """Multiply acceleration by a time to get velocity, or by scalar.""" + if isinstance(other, seconds): + # acceleration * time = velocity + return velocity( + self.x * other.seconds, + self.y * other.seconds, + self.z * other.seconds, + self.e * other.seconds, + ) + elif isinstance(other, (float, int, np.float64)): + return self.__class__( + self.x * other, + self.y * other, + self.z * other, + self.e * other, + ) else: - return False + raise TypeError("Multiplication only supports seconds, float, or int.") - def get_t_distance(self, other=None, withExtrusion=False) -> float: - """Calculate the travel distance between self and other position. If none is provided, zero will be used. - - Args: - other: (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray', default = None) - withExtrusion: (bool, default = False) use or ignore extrusion - - Returns: - travel: (float) travel or extrusion and travel distance - """ - if other is None: - other = position(0, 0, 0, 0) - return np.linalg.norm( - np.subtract(self.get_vec(withExtrusion=withExtrusion), other.get_vec(withExtrusion=withExtrusion)) - ) + def __truediv__(self, other): + """Divide acceleration by scalar.""" + return super().__truediv__(other) class segment: @@ -315,6 +436,7 @@ class segment: - move_segment_time: moves Segment in time by a specified interval - get_velocity: returns the calculated Velocity for all axis at a given point in time - get_position: returns the calculated Position for all axis at a given point in time + - get_segm_len: returns the length of the segment. **Class method** - create_initial: returns the artificial initial segment where everything is at standstill, intervall length = 0 @@ -323,8 +445,8 @@ class segment: def __init__( self, - t_begin: float, - t_end: float, + t_begin: Union[float, seconds], + t_end: Union[float, seconds], pos_begin: position, vel_begin: velocity, pos_end: position = None, @@ -341,13 +463,15 @@ def __init__( vel_end: (velocity, default = None) ending velocity of segment """ - self.t_begin = t_begin - self.t_end = t_end - self.pos_begin = pos_begin - self.pos_end = pos_end - self.vel_begin = vel_begin - self.vel_end = vel_end - self.self_check() + self.t_begin: seconds = seconds(t_begin) + self.t_end: seconds = seconds(t_end) + self.pos_begin: position = pos_begin + self.pos_end: position = pos_end + self.vel_begin: velocity = vel_begin + self.vel_end: velocity = vel_end + # self.self_check() + + self.result = {} def __str__(self) -> str: """Create string from segment.""" @@ -385,6 +509,16 @@ def get_velocity(self, t: float) -> velocity: current_vel = self.vel_begin + slope * (t - self.t_begin) return current_vel + def get_velocity_by_dist(self, dist): + """Return the velocity at a certain local segment distance.""" + # t_begin, t_end, vel_begin, vel_end + a = (self.vel_end.get_norm() - self.vel_begin.get_norm()) / (self.t_end - self.t_begin) + + v_sq = 2 * a * dist + self.vel_begin.get_norm() ** 2 + v = np.sqrt(v_sq) if v_sq > 0 else 0 + + return v + def get_position(self, t: float) -> position: """Get current position of segment at a certain time. @@ -411,12 +545,11 @@ def get_segm_duration(self): """Return the duration of the segment.""" return self.t_end - self.t_begin - def self_check(self, p_settings=None): # ,, state:state=None): + def self_check(self, p_settings: "state.p_settings" = None): """Check the segment for self consistency. - todo: - - max acceleration - + Raises: + ValueError: if self check fails Args: p_settings: (p_setting, default = None) printing settings to verify """ @@ -443,6 +576,13 @@ def self_check(self, p_settings=None): # ,, state:state=None): if self.vel_end.get_norm() > p_settings.speed and not np.isclose(self.vel_end.get_norm(), p_settings.speed): raise ValueError(f"Target Velocity of {p_settings.speed} exceeded with {self.vel_end.get_norm()}.") + # max acceleration + if p_settings is not None: + if self.t_end - self.t_begin > 0: + acc = (self.vel_end - self.vel_begin) / (self.t_end - self.t_begin) + if acc.get_norm() > p_settings.p_acc and not np.isclose(acc.get_norm(), p_settings.p_acc): + raise ValueError(f"Maximum acceleration of {p_settings.p_acc} exceeded with {acc.get_norm()}.") + def is_extruding(self) -> bool: """Return true if the segment is pos. extruding. @@ -481,53 +621,6 @@ def get_time(x): return scalar - def calc_results(self, v_target): - """Calculate and store the segment results. - - Args: - v_target: (float) target velocity - """ - # GLOBAL ERROR METRIC - - def error_integral(a, v_begin, v_target, x_end): - sqr = 2 * a * x_end + v_begin**2 if not np.isclose(self.vel_end.get_norm(), 0.0) else 0 - - integral = ((v_begin**3) + 3 * a * v_target * x_end - (2 * a * x_end + v_begin**2) * np.sqrt(sqr)) / ( - 3 * a * v_target - ) - return integral - - v_begin = self.vel_begin.get_norm() - x_end = self.get_segm_len() # segment length - - delta_t = self.get_segm_duration() - delta_v = self.vel_end.get_norm() - self.vel_begin.get_norm() - - if not np.isclose(v_target, 0.0) and not np.isclose(delta_t, 0.0) and not np.isclose(delta_v, 0.0): - - a = delta_v / delta_t - - # print("vbegin: ", v_begin, "vend", self.vel_end.get_norm(), "vtarget: ", v_target, "xend", x_end ) - avg_error = error_integral(a=a, v_begin=v_begin, v_target=v_target, x_end=x_end) - else: - avg_error = 0 - - self.result.update({"segm_error": [avg_error]}) - - # LOCAL VELOCITY ERROR - - if v_target > 0: - vel_rel_err_begin = (v_target - v_begin) / v_target - vel_rel_err_end = (v_target - self.vel_end.get_norm()) / v_target - else: # if no target vel, error is zero. - vel_rel_err_begin = 0 - vel_rel_err_end = 0 - self.result.update({"rel_vel_err": [vel_rel_err_begin, vel_rel_err_end]}) - - # LOCAL VELOCITY - - self.result.update({"vel": [self.vel_begin.get_norm(), self.vel_end.get_norm()]}) - def get_result(self, key): """Return the requested result. @@ -538,10 +631,7 @@ def get_result(self, key): result: (list) """ if key in self.result: - if len(self.result[key]) == 2: # linear - return self.result[key] - elif len(self.result[key]) == 1: # constant - return [self.result[key][0], self.result[key][0]] + return self.result[key] else: raise ValueError(f"Key: {key} not found.") diff --git a/pyproject.toml b/pyproject.toml index cc6ca65..3a00b3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ requires-python = ">=3.9" dependencies = ["numpy", "matplotlib", "PyYAML", "pyvista"] [project.optional-dependencies] -TEST = ["pytest-cov"] +TEST = ["pytest-cov", "pandas"] DEVELOPER = [ "flake8", "isort", @@ -56,6 +56,9 @@ pygcd = "pyGCodeDecode.cli:_main" [tool.black] line-length = 120 +extend-exclude = ''' +^.*\.ipynb$ +''' [tool.isort] profile = "black" diff --git a/tests/data/test_simplest.gcode b/tests/data/test_simplest.gcode new file mode 100644 index 0000000..5d98171 --- /dev/null +++ b/tests/data/test_simplest.gcode @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e50ac8a0ed03bef69b4ec17e49ee941ae6a03c54ede9293228264d3dc008c8a +size 59 diff --git a/tests/self_test/self_test.gcode b/tests/self_test/self_test.gcode new file mode 100644 index 0000000..0a1c601 --- /dev/null +++ b/tests/self_test/self_test.gcode @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d9ccd739beb37cfc3c10a17db1420c3a2b9bbc15bf55de0f83e2aacd821c68a +size 91 diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index abba55c..4bee3b6 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -4,6 +4,8 @@ import numpy as np +from pyGCodeDecode.plotter import plot_vel + def test_end_to_end_compact(): """Testing the simulation functionality with automatic setup, similarly to the brace example.""" @@ -29,8 +31,9 @@ def test_end_to_end_volumetr(): end_extrusion = sim.blocklist[-1].segments[-1].pos_end.e expected_extrusion = 14.0 - assert end_extrusion == expected_extrusion, f"Expected {expected_extrusion}, but got {end_extrusion}" - + assert np.isclose( + end_extrusion, expected_extrusion, rtol=1e-4 + ), f"Expected {expected_extrusion}, but got {end_extrusion}" preset = setup(pathlib.Path("./tests/data/test_printer_setups.yaml"), "test") preset.set_property({"volumetric_extrusion": True}) @@ -43,7 +46,7 @@ def test_end_to_end_volumetr(): expected_extrusion = 14.0 / ((1.75 / 2) ** 2 * np.pi) assert np.isclose( - end_extrusion, expected_extrusion, rtol=1e-9 + end_extrusion, expected_extrusion, rtol=1e-4 ), f"Expected {expected_extrusion}, but got {end_extrusion}" @@ -51,6 +54,7 @@ def test_end_to_end_extensive(): """Testing the simulation functionality as well as the various outputs, similarly to the benchy example.""" from pyGCodeDecode.abaqus_file_generator import generate_abaqus_event_series from pyGCodeDecode.gcode_interpreter import setup, simulation + from pyGCodeDecode.plotter import plot_2d, plot_3d from pyGCodeDecode.tools import save_layer_metrics data_dir = pathlib.Path("./tests/data/") @@ -74,6 +78,7 @@ def test_end_to_end_extensive(): gcode_path=data_dir / "test_state_generator.gcode", initial_machine_setup=printer_setup, output_unit_system="SI (mm)", + verbosity_level=3, ) # save a short summary of the simulation @@ -94,18 +99,37 @@ def test_end_to_end_extensive(): output_unit_system="SI (mm)", ) - # create a 3D-plot and save a VTK as well as a screenshot - end_to_end_simulation.plot_3d( - extrusion_only=True, + # create a 3D-plot + plot_3d( + sim=end_to_end_simulation, screenshot_path=output_dir / "test_end_to_end_3d.png", vtk_path=output_dir / "test_end_to_end.vtk", + camera_settings={"camera_position": "xy"}, + ) + + # 3D-plot with matplotlib + plot_3d( + sim=end_to_end_simulation, + screenshot_path=output_dir / "test_end_to_end_3d_mpl.png", + vtk_path=output_dir / "test_end_to_end.vtk", + camera_settings={"camera_position": "xy"}, + mpl_subplot=True, ) # create a 2D-plot with matplotlib - end_to_end_simulation.plot_2d_position(filepath=output_dir / "test_end_to_end_2d.png", show=False) + plot_2d( + sim=end_to_end_simulation, + show=False, + filepath=output_dir / "test_end_to_end_2d.png", + ) + # end_to_end_simulation.plot_2d_position(filepath=output_dir / "test_end_to_end_2d.png", show=False) # plotting velocities with matplotlib - end_to_end_simulation.plot_vel(show=False, filepath=output_dir / "velocities.png") + plot_vel( + sim=end_to_end_simulation, + show=False, + filepath=output_dir / "velocities.png", + ) # assert that the output files exists (pyvista screenshot cannot be created without a display) output_files = [ diff --git a/tests/test_junction_handling.py b/tests/test_junction_handling.py new file mode 100644 index 0000000..9380c42 --- /dev/null +++ b/tests/test_junction_handling.py @@ -0,0 +1,223 @@ +"""Test for the junction handling.""" + +import copy +import math +import os +import sys + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd + +from pyGCodeDecode.junction_handling import ( + _get_handler_names, + get_handler, + junction_handling, +) +from pyGCodeDecode.state import state +from pyGCodeDecode.utils import position + + +def _rotate_pos(pos: position, alpha): # 2D Rotation + alpha_r = math.radians(alpha) + pos_v = np.array(pos.get_vec()[:2]) + rot_M = np.array([[math.cos(alpha_r), -math.sin(alpha_r)], [math.sin(alpha_r), math.cos(alpha_r)]]) + new_pos = np.matmul(rot_M, pos_v) + return new_pos + + +def _rotate_state(state, alpha=45): + pos = state.state_position + new_pos = _rotate_pos(pos, alpha) + state.state_position = position(new_pos[0], new_pos[1], pos.z, pos.e) + + return state + + +def _initialize_dummy_states(p_acc, jerk, speed): + settings = state.p_settings(p_acc=p_acc, jerk=jerk, vX=100, vY=100, vZ=100, vE=100, speed=speed) + stateA = state(state_p_settings=settings) + stateB = state(state_p_settings=settings) + stateC = state(state_p_settings=settings) + + stateA.next_state = stateB + stateB.prev_state = stateA + stateB.next_state = stateC + stateC.prev_state = stateB + return stateA, stateB, stateC + + +def test_junction_handlings(): + """Test for junction handling.""" + # Test cases for junction handling + test_firmwares = _get_handler_names() + test_firmwares.append("unknown") # Add "unknown" for default handler + print(f"Testing the following firmwares: {test_firmwares}", file=sys.stderr) + + angles = np.arange(0, 181, 1) + results = {"turning angle": angles} + for firmware in test_firmwares: + handler = get_handler(firmware) + assert handler is not None + assert handler is not junction_handling if firmware != "unknown" else handler is junction_handling + assert callable(handler) + + stateA, stateB, stateC = _initialize_dummy_states(p_acc=1000, jerk=10, speed=50) + stateA.state_position = position(0, 0, 0, 0) + stateB.state_position = position(50, 0, 0, 0) + + ang = 0 + stateA = _rotate_state(stateA, ang) + stateB = _rotate_state(stateB, ang) + + firmware_results = [] + for angle in angles: + coordXY = [math.cos(math.radians(angle)) * 50 + 50, math.sin(math.radians(angle)) * 50] + stateC.state_position = position(*coordXY, 0, 0) + stateC = _rotate_state(stateC, ang) + + result = handler(state_A=stateA, state_B=stateB).get_junction_vel() + firmware_results.append(result) + results[firmware] = firmware_results + + # Convert to DataFrame + df = pd.DataFrame(results) + + fig, ax = plt.subplots(figsize=(6, 4)) + # Plotting with Pandas built-in functions + df.plot( + ax=ax, + x="turning angle", + y=test_firmwares, + ) + + ax.set_ylabel(r"cornering velocity in $\frac{\mathrm{mm}}{\mathrm{s}}$") + ax.set_xlabel(r"turning angle in $\deg$") + ax.set_xticks(range(0, 181, 30)) + ax.set_yticks(range(0, 51, 5)) + ax.grid(visible=True, which="major", axis="both", alpha=0.8, linestyle="dotted") + + fig.savefig("tests/output/junction_handlings.png", dpi=300, bbox_inches="tight") + # plt.show() + + +def test_junction_handlings_rotating_COS(): + """Test for junction handling with coordinate system rotation.""" + test_firmwares = _get_handler_names() + test_firmwares.append("unknown") # Add "unknown" for default handler + print(f"Testing the following firmwares: {test_firmwares}", file=sys.stderr) + + angles = np.arange(0, 181, 1) + results = {"turning angle": angles} + + cos_rot_angles = np.arange(0, 360, 5) + rotated_results = {"turning angle": angles} + + for firmware in test_firmwares: + handler = get_handler(firmware) + assert handler is not None + assert handler is not junction_handling if firmware != "unknown" else handler is junction_handling + assert callable(handler) + + stateA, stateB, stateC = _initialize_dummy_states(p_acc=1000, jerk=10, speed=50) + stateA.state_position = position(0, 0, 0, 0) + stateB.state_position = position(50, 0, 0, 0) + + ang = 0 + stateA = _rotate_state(stateA, ang) + stateB = _rotate_state(stateB, ang) + + firmware_results = [] + rotated_firmware_results = [] + + for angle in angles: + coordXY = [math.cos(math.radians(angle)) * 50 + 50, math.sin(math.radians(angle)) * 50] + stateC.state_position = position(*coordXY, 0, 0) + stateC = _rotate_state(stateC, ang) + + # Unrotated + result = handler(state_A=stateA, state_B=stateB).get_junction_vel() + firmware_results.append(result) + + # Rotated: rotate all positions by rot_angle + rotations = [] + for rot_angle in cos_rot_angles: + # Deepcopy states to avoid mutation + stateA_r = copy.deepcopy(stateA) + stateB_r = copy.deepcopy(stateB) + stateC_r = copy.deepcopy(stateC) + stateA_r.next_state = stateB_r + stateB_r.prev_state = stateA_r + stateB_r.next_state = stateC_r + stateC_r.prev_state = stateB_r + stateA_r = _rotate_state(stateA_r, rot_angle) + stateB_r = _rotate_state(stateB_r, rot_angle) + stateC_r = _rotate_state(stateC_r, rot_angle) + + # Sanity check for angle between vectors + vec1 = np.array(stateB_r.state_position.get_vec()[:2]) - np.array(stateA_r.state_position.get_vec()[:2]) + vec2 = np.array(stateC_r.state_position.get_vec()[:2]) - np.array(stateB_r.state_position.get_vec()[:2]) + dot_product = np.dot(vec1, vec2) + norm1 = np.linalg.norm(vec1) + norm2 = np.linalg.norm(vec2) + cos_theta = dot_product / (norm1 * norm2) + angle_between = np.degrees(np.arccos(np.clip(cos_theta, -1.0, 1.0))) + if not np.isclose(angle_between, angle, atol=1e-2): + print(f"Angle between vectors: {angle_between:.2f} degrees, should be {angle:.2f} degrees") + # Sanity check end + + result_rot = handler(state_A=stateA_r, state_B=stateB_r).get_junction_vel() + rotations.append(result_rot) + rotated_firmware_results.append((angle, rotations)) + + results[firmware] = firmware_results + rotated_results[firmware] = rotated_firmware_results + + # Convert to DataFrame + df = pd.DataFrame(results) + + fig, ax = plt.subplots(figsize=(6, 4)) + color_map = {} + # Plot solid lines for unrotated + for idx, fw in enumerate(test_firmwares): + line = ax.plot( + angles, + df[fw], + label=fw, + linewidth=2, + )[0] + color_map[fw] = line.get_color() + + # For each firmware, plot all rotated results as curves (one per rotation angle) + for fw in test_firmwares: + # rotated_results[fw] is a list of (angle, [results for each rotation]) + # transpose the data: for each rotation, collect the results for all angles + all_rotations = list(zip(*[rot[1] for rot in rotated_results[fw]])) # shape: (num_rotations, num_angles) + for rot_curve in all_rotations: + ax.plot( + angles, + rot_curve, + color=color_map[fw], + linewidth=1, + alpha=0.075, + ) + + ax.set_ylabel(r"cornering velocity in $\frac{\mathrm{mm}}{\mathrm{s}}$") + ax.set_xlabel(r"turning angle in $\deg$") + ax.set_xticks(range(0, 181, 30)) + ax.set_yticks(range(0, 51, 5)) + ax.grid(visible=True, which="major", axis="both", alpha=0.8, linestyle="dotted") + ax.legend() + + # Ensure output directory exists + output_dir = "tests/output" + os.makedirs(output_dir, exist_ok=True) + fig.savefig(os.path.join(output_dir, "junction_handlings_rotating_COS.png"), dpi=300, bbox_inches="tight") + # plt.show() + + +if __name__ == "__main__": + test_junction_handlings() + plt.show() + test_junction_handlings_rotating_COS() + plt.show() diff --git a/tests/test_planner_block.py b/tests/test_planner_block.py index d7d98ee..12460e5 100644 --- a/tests/test_planner_block.py +++ b/tests/test_planner_block.py @@ -73,7 +73,7 @@ def test_planner_block(): state_2 = state(state_position=pos_2, state_p_settings=settings) state_1.prev_state = state_0 state_1.next_state = state_2 # needed next state to create singular PB with non zero ending vel - block_3 = planner_block(state=state_1, prev_block=None, firmware="marlin_jd") + block_3 = planner_block(state=state_1, prev_block=None, firmware="junction_deviation") # single block test assert block_3.blocktype == "single" diff --git a/tests/test_result.py b/tests/test_result.py new file mode 100644 index 0000000..71bf3da --- /dev/null +++ b/tests/test_result.py @@ -0,0 +1,117 @@ +"""Test the result calculation.""" + +import pathlib + +import numpy as np + +from pyGCodeDecode.planner_block import planner_block +from pyGCodeDecode.result import get_all_result_calculators, has_private_results +from pyGCodeDecode.state import state +from pyGCodeDecode.utils import position + + +def test_result_calc_within_pb(): + """Test the result calculation within a planner block.""" + statA = state( + state_position=position(0, 0, 0, 0), + state_p_settings=state.p_settings( + p_acc=1.0, + jerk=1.0, + vX=10.0, + vY=10.0, + vZ=10.0, + vE=10.0, + speed=10.0, + ), + ) + statB = state( + state_position=position(150, 0, 0, 0), + state_p_settings=state.p_settings( + p_acc=1.0, + jerk=1.0, + vX=10.0, + vY=10.0, + vZ=10.0, + vE=10.0, + speed=10.0, + ), + ) + statB.prev_state = statA + pb = planner_block(statB, prev_block=None) + + if has_private_results(): + # Add private results to the planner block + all_calcs = get_all_result_calculators() + + err = all_calcs + print("errs: ", err) + pb.calc_results(*err) + + errs = [calcs.name for calcs in pb.result_calculators] + errs.extend(calculator.name for calculator in err if calculator.name not in errs) + for calculator in err: + if hasattr(calculator, "avgs"): + for avg in calculator.avgs: + errs.append(calculator.name + avg) + + # Check if all results are calculated + for segm in pb.segments: + segm.self_check() + for name in errs: + print(f"Result {name}: {segm.result[name]}") + assert isinstance(segm.result[name], (int, float, list)), f"Error {name} is not a number" + + # Check if the results are calculated correctly + assert len(pb.segments) == 3, "There should be 3 segments in the planner block" + + segm = pb.segments[0] + assert np.isclose(segm.result["acceleration"], 1.0) + assert all(np.isclose(segm.result["velocity"], [0, 10.0])) + assert np.isclose(segm.result["rel_vel_err_tavg"], 0.5) + assert np.isclose(segm.result["rel_vel_err_savg"], 0.333333) + + segm = pb.segments[1] + assert np.isclose(segm.result["rel_vel_err_tavg"], 0.0) + assert np.isclose(segm.result["rel_vel_err_savg"], 0.0) + + segm = pb.segments[2] + assert np.isclose(segm.result["acceleration"], -1.0) + assert all(np.isclose(segm.result["velocity"], [10, 0])) + assert np.isclose(segm.result["rel_vel_err_tavg"], 0.5) + assert np.isclose(segm.result["rel_vel_err_savg"], 0.333333) + + +def test_result_calc_simulation(): + """Testing the result calculation in a simulation.""" + from pyGCodeDecode.gcode_interpreter import setup, simulation + from pyGCodeDecode.result import get_all_result_calculators + + setup_test = setup( + presets_file=pathlib.Path("./tests/data/test_printer_setups.yaml"), + printer="prusa_mini", + ) + + sim = simulation( + gcode_path=pathlib.Path("./tests/data/test_simplest.gcode"), + # machine_name="prusa_mini", + initial_machine_setup=setup_test, + ) + + result_calculators = get_all_result_calculators() + + for calculator in result_calculators: + if hasattr(calculator, "avgs") and isinstance(calculator.avgs, (list, tuple)): + for avg in calculator.avgs: + print(f"Testing existence of average {avg} for {calculator.name}") + assert ( + calculator.name + avg in sim.results + ), f"Result {calculator.name + avg} not found in simulation results" + + if has_private_results(): + print("Private results are available.") + print("Whole simulation results:") + for key, value in sim.results.items(): + print(f" {key}: {value}") + + assert np.isclose(getattr(sim, "rel_vel_err_savg", None), 0.33333, atol=1e-5) + assert np.isclose(getattr(sim, "rel_vel_err_tavg", None), 0.5, atol=1e-5) diff --git a/tests/test_vector4d.py b/tests/test_vector4d.py new file mode 100644 index 0000000..ac2814a --- /dev/null +++ b/tests/test_vector4d.py @@ -0,0 +1,40 @@ +"""Test the result calculation.""" + +import numpy as np + +from pyGCodeDecode.utils import acceleration, position, seconds, velocity + + +def test_4d_vectors(): + """Test the 4d vector functions.""" + pos1 = position(1, 2, 3, 4) + pos2 = position(4, 5, 6, 7) + t = seconds(6) + + dist = pos2 - pos1 + # Check the distance vector + assert np.isclose(dist.x, 3) + assert np.isclose(dist.y, 3) + assert np.isclose(dist.z, 3) + assert np.isclose(dist.e, 3) + + vel = dist / t + # Check the velocity vector + assert isinstance(vel, velocity) + assert np.isclose(vel.x, 0.5) + assert np.isclose(vel.y, 0.5) + assert np.isclose(vel.z, 0.5) + assert np.isclose(vel.e, 0.5) + + # Check the norm of the velocity vector + assert np.isclose(vel.get_norm(), np.sqrt(0.5**2 + 0.5**2 + 0.5**2)) + assert np.isclose(vel.get_norm(withExtrusion=True), np.sqrt(0.5**2 + 0.5**2 + 0.5**2 + 0.5**2)) + + # Check the acceleration vector + acc = vel / seconds(0.5) + + assert isinstance(acc, acceleration) + assert np.isclose(acc.x, 1.0) + assert np.isclose(acc.y, 1.0) + assert np.isclose(acc.z, 1.0) + assert np.isclose(acc.e, 1.0) From cefa904f5b24aa7012a251ef15965f075b029c29 Mon Sep 17 00:00:00 2001 From: usmfi Date: Tue, 5 Aug 2025 14:53:19 +0200 Subject: [PATCH 13/68] ignore doc generation --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 22fb4a8..56235a9 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,7 @@ dist/ # output folders from examples and tests /output_benchy_example/* /tests/output/* - +/docs/site # coverage reports .coverage From 3ecef3bdd1f651e18c766467f9cdf143b8efe6d9 Mon Sep 17 00:00:00 2001 From: usmfi Date: Tue, 5 Aug 2025 15:02:02 +0200 Subject: [PATCH 14/68] updated Doc.md & more consistent docstrings --- doc.md | 1375 +++++++++++++----------- pyGCodeDecode/abaqus_file_generator.py | 8 +- pyGCodeDecode/gcode_interpreter.py | 6 +- pyGCodeDecode/junction_handling.py | 27 +- pyGCodeDecode/planner_block.py | 2 +- pyGCodeDecode/state.py | 2 +- pyGCodeDecode/state_generator.py | 4 +- pyGCodeDecode/utils.py | 2 +- pydoc-markdown.yml | 60 ++ 9 files changed, 828 insertions(+), 658 deletions(-) create mode 100644 pydoc-markdown.yml diff --git a/doc.md b/doc.md index 01f335f..2d0acd8 100644 --- a/doc.md +++ b/doc.md @@ -1,6 +1,8 @@ +# pyGCodeDecode Reference + -# pyGCodeDecode.abaqus\_file\_generator +## pyGCodeDecode.abaqus\_file\_generator Module for generating Abaqus .inp files for AMSIM. @@ -10,86 +12,41 @@ Module for generating Abaqus .inp files for AMSIM. ```python def generate_abaqus_event_series( - simulation: gi.simulation, + simulation: gcode_interpreter.simulation, filepath: str = "pyGcodeDecode_abaqus_events.inp", tolerance: float = 1e-12, - output_unit_system: str = None) -> tuple + output_unit_system: str = None, + return_tuple: bool = False) -> tuple ``` Generate abaqus event series. **Arguments**: -- `simulation` _gi.simulation_ - simulation instance +- `simulation` _gcode_interpreter.simulation_ - simulation instance - `filepath` _string, default = "pyGcodeDecode_abaqus_events.inp"_ - output file path - `tolerance` _float, default = 1e-12_ - tolerance to determine whether extrusion is happening - `output_unit_system` _str, optional_ - Unit system for the output. The one from the simulation is used, in None is specified. +- `return_tuple` _bool, default = False_ - return the event series as tuple. **Returns**: -- `tuple` - the event series as a tuple for use in ABAQUS-Python + (optional) tuple: the event series as a tuple for use in ABAQUS-Python -# pyGCodeDecode.cli +## pyGCodeDecode.cli The CLI for the pyGCodeDecode package. - - -# pyGCodeDecode.examples.benchy - -Simulating the G-code of a 3DBenchy from PrusaSlicer on a Prusa MINI. - - - -#### benchy\_example - -```python -def benchy_example() -``` - -Extensive example for the usage of pyGCodeDecode simulating G-code for the famous 3DBenchy. - - - -# pyGCodeDecode.examples.brace - -Minimal example simulating the G-code of a brace from Aura-slicer on an Anisoprint Composer A4. - - - -#### brace\_example - -```python -def brace_example() -``` - -Minimal example for the usage of pyGCodeDecode simulating the G-code of a brace. - -# pyGCodeDecode.gcode\_interpreter +## pyGCodeDecode.gcode\_interpreter GCode Interpreter Module. - - -#### update\_progress - -```python -def update_progress(progress: float, name: str = "Percent") -> None -``` - -Display or update a console progress bar. - -**Arguments**: - -- `progress` - float between 0 and 1 for percentage, < 0 represents a 'halt', > 1 represents 100% -- `name` - (string, default = "Percent") customizable name for progress bar - #### generate\_planner\_blocks @@ -128,7 +85,8 @@ Find the current segment. - `path` - (list[segment]) all segments to be searched - `t` - (float) time of search - `last_index` - (int) last found index for optimizing search -- `keep_position` - (bool) keeps position of last segment, use this when working with gaps of no movement between segments +- `keep_position` - (bool) keeps position of last segment, use this when working with + gaps of no movement between segments **Returns**: @@ -157,7 +115,7 @@ Return list of segments by unpacking list of planner blocks. -## simulation Objects +### simulation Objects ```python class simulation() @@ -165,122 +123,35 @@ class simulation() Simulation of .gcode with given machine parameters. - - -#### \_\_init\_\_ - -```python -def __init__(gcode_path: pathlib.Path, - machine_name: str = None, - initial_machine_setup: "setup" = None, - output_unit_system: str = "SImm") -``` - -Initialize the Simulation of a given G-code with initial machine setup or default machine. - -- Generate all states from GCode. -- Connect states with planner blocks, consisting of segments -- Self correct inconsistencies. - -**Arguments**: - -- `gcode_path` - (Path) path to GCode - machine name: (string, default = None) name of the default machine to use -- `initial_machine_setup` - (setup, default = None) setup instance -- `output_unit_system` - (string, default = "SImm") available unit systems: SI, SImm & inch - - -**Example**: - -```python -gcode_interpreter.simulation(gcode_path=r"path/to/part.gcode", initial_machine_setup=printer_setup) -``` - - - -#### plot\_2d\_position - -```python -def plot_2d_position( - filepath: pathlib.Path = pathlib.Path("trajectory_2D.png"), - colvar="Velocity", - show_points=False, - colvar_spatial_resolution=1, - dpi=400, - scaled=True, - show=False) -``` - -Plot 2D position (XY plane) with matplotlib (unmaintained). - - + -#### plot\_3d +#### trajectory\_self\_correct ```python -def plot_3d(extrusion_only: bool = True, - screenshot_path: pathlib.Path = None, - vtk_path: pathlib.Path = None, - mesh: pv.MultiBlock = None) -> pv.MultiBlock +def trajectory_self_correct() ``` -3D Plot with PyVista. - -**Arguments**: - -- `extrusion_only` _bool, optional_ - Plot only parts where material is extruded. Defaults to True. -- `screenshot_path` _pathlib.Path, optional_ - Path to screenshot to be saved. Prevents interactive plot. Defaults to None. -- `vtk_path` _pathlib.Path, optional_ - Path to vtk to be saved. Prevents interactive plot. Defaults to None. -- `mesh` _pv.MultiBlock, optional_ - A pyvista mesh from a previous run to avoid running the mesh generation again. Defaults to None. - - -**Returns**: - -- `pv.MultiBlock` - The mesh used in the plot so it can be used (e.g. in subsequent plots). +Self correct all blocks in the blocklist with self_correction() method. - + -#### plot\_vel +#### calc\_results ```python -def plot_vel(axis: Tuple[str] = ("x", "y", "z", "e"), - show: bool = True, - show_planner_blocks: bool = True, - show_segments: bool = False, - show_jv: bool = False, - time_steps: Union[int, str] = "constrained", - filepath: pathlib.Path = None, - dpi: int = 400) -> Figure +def calc_results() ``` -Plot axis velocity with matplotlib. +Calculate the results. -**Arguments**: - -- `axis` - (tuple(string), default = ("x", "y", "z", "e")) select plot axis -- `show` - (bool, default = True) show plot and return plot figure -- `show_planner_blocks` - (bool, default = True) show planner_blocks as vertical lines -- `show_segments` - (bool, default = False) show segments as vertical lines -- `show_jv` - (bool, default = False) show junction velocity as x -- `time_steps` - (int or string, default = "constrained") number of time steps or constrain plot vertices to segment vertices -- `filepath` - (Path, default = None) save fig as image if filepath is provided -- `dpi` - (int, default = 400) select dpi + - -**Returns**: - - (optionally) -- `fig` - (figure) - - - -#### trajectory\_self\_correct +#### calculate\_averages ```python -def trajectory_self_correct() +def calculate_averages() ``` -Self correct all blocks in the blocklist with self_correction() method. +Calculate averages for averageable results. @@ -304,19 +175,28 @@ Return unit system scaled values for vel and pos. - `list` - [vel_x, vel_y, vel_z, vel_e] velocity - `list` - [pos_x, pos_y, pos_z, pos_e] position - + -#### check\_initial\_setup +#### get\_width ```python -def check_initial_setup(initial_machine_setup) +def get_width(t: float, + extrusion_h: float, + filament_dia: Optional[float] = None) -> float ``` -Check the printer Dict for typos or missing parameters and raise errors if invalid. +Return the extrusion width for a certain extrusion height at time. **Arguments**: -- `initial_machine_setup` - (dict) initial machine setup dictionary +- `t` _float_ - time +- `extrusion_h` _float_ - extrusion height / layer height +- `filament_dia` _float_ - filament_diameter + + +**Returns**: + +- `float` - width @@ -344,7 +224,8 @@ Refresh simulation. Either through new state list or by rerunning the self.state **Arguments**: -- `new_state_list` - (list[state], default = None) new list of states, if None is provided, existing states get resimulated +- `new_state_list` - (list[state], default = None) new list of states, + if None is provided, existing states get resimulated @@ -433,7 +314,7 @@ Get a scaling factor to convert lengths from mm to another supported unit system -## setup Objects +### setup Objects ```python class setup() @@ -441,37 +322,29 @@ class setup() Setup for printing simulation. - + -#### \_\_init\_\_ +#### load\_setup ```python -def __init__(presets_file: str, - printer: str = None, - layer_cue: str = None) -> None +def load_setup(filepath) ``` -Create simulation setup. +Load setup from file. **Arguments**: -- `presets_file` - (string) choose setup yaml file with printer presets -- `printer` - (string) select printer from preset file -- `layer_cue` - (string) set slicer specific layer change cue from comment +- `filepath` - (string) specify path to setup file - + -#### load\_setup +#### check\_initial\_setup ```python -def load_setup(filepath) +def check_initial_setup() ``` -Load setup from file. - -**Arguments**: - -- `filepath` - (string) specify path to setup file +Check the printer Dict for typos or missing parameters and raise errors if invalid. @@ -500,7 +373,8 @@ Set initial Position. **Arguments**: -- `initial_position` - (tuple or dict) set initial position as tuple of len(4) or dictionary with keys: {X, Y, Z, E}. +- `initial_position` - (tuple or dict) set initial position as tuple of len(4) + or dictionary with keys: {X, Y, Z, E}. - `input_unit_system` _str, optional_ - Wanted input unit system. Uses the one specified for the setup if None is specified. @@ -520,7 +394,9 @@ setup.set_initial_position({"X": 1, "Y": 2, "Z": 3, "E": 4}) def set_property(property_dict: dict) ``` -Overwrite or add a property to the printer dictionary. Printer has to be selected through select_printer() beforehand. +Overwrite or add a property to the printer dictionary. + +Printer has to be selected through select_printer() beforehand. **Arguments**: @@ -567,15 +443,87 @@ Get a scaling factor to convert lengths from mm to another supported unit system - `float` - scaling factor + + +## pyGCodeDecode.helpers + +Helper functions. + + + +#### VERBOSITY\_LEVEL + +default to INFO + + + +#### set\_verbosity\_level + +```python +def set_verbosity_level(level: Optional[int]) -> None +``` + +Set the global verbosity level. + + + +#### get\_verbosity\_level + +```python +def get_verbosity_level() -> int +``` + +Get the current global verbosity level. + + + +#### custom\_print + +```python +def custom_print(*args, lvl=2, **kwargs) -> None +``` + +Sanitize outputs for ABAQUS and print them if the log level is high enough. Takes all arguments for print. + +**Arguments**: + +- `*args` - arguments to be printed +- `lvl` - verbosity level of the print (1 = WARNING, 2 = INFO, 3 = DEBUG) +- `**kwargs` - keyword arguments to be passed to print + + + +### ProgressBar Objects + +```python +class ProgressBar() +``` + +A simple progress bar for the console. + + + +#### update + +```python +def update(progress: float) -> None +``` + +Display or update a console progress bar. + +**Arguments**: + +- `progress` - float between 0 and 1 for percentage, < 0 represents a 'halt', > 1 represents 100% + -# pyGCodeDecode.junction\_handling +## pyGCodeDecode.junction\_handling Junction handling module. -## junction\_handling Objects +### junction\_handling Objects ```python class junction_handling() @@ -623,21 +571,6 @@ def get_target_vel() Return target velocity. - - -#### \_\_init\_\_ - -```python -def __init__(state_A: state, state_B: state) -``` - -Initialize the junction handling. - -**Arguments**: - -- `state_A` - (state) start state -- `state_B` - (state) end state - #### get\_junction\_vel @@ -652,59 +585,82 @@ Return default junction velocity of zero. - `0` - zero for default full stop junction handling - + -## junction\_handling\_marlin\_jd Objects +### prusa Objects ```python -class junction_handling_marlin_jd(junction_handling) +class prusa(junction_handling) ``` -Marlin specific junction handling with Junction Deviation. - - +Prusa specific classic jerk junction handling (validated on Prusa Mini). -#### calc\_JD - -```python -def calc_JD(vel_0: velocity, vel_1: velocity, p_settings: state.p_settings) -``` - -Calculate junction deviation velocity from 2 velocitys. - -**Reference:** - -[https://onehossshay.wordpress.com/2011/09/24/improving_grbl_cornering_algorithm/](https://onehossshay.wordpress.com/2011/09/24/improving_grbl_cornering_algorithm/) -[http://blog.kyneticcnc.com/2018/10/computing-junction-deviation-for-marlin.html](http://blog.kyneticcnc.com/2018/10/computing-junction-deviation-for-marlin.html) - - -**Arguments**: - -- `vel_0` - (velocity) entry -- `vel_1` - (velocity) exit -- `p_settings` - (state.p_settings) print settings - - -**Returns**: - -- `velocity` - (float) velocity abs value - - +**Reference** +[Prusa Firmware Buddy GitHub](https://github.com/prusa3d/Prusa-Firmware-Buddy/blob/818d812f954802903ea0ff39bf44376fb0b35dd2/lib/Marlin/Marlin/src/module/planner.cpp#L1911) # noqa: E501 + + +**Code reference:** +[Prusa-Firmware-Buddy/lib/Marlin/Marlin/src/module/planner.cpp](https://github.com/prusa3d/Prusa-Firmware-Buddy/blob/818d812f954802903ea0ff39bf44376fb0b35dd2/lib/Marlin/Marlin/src/module/planner.cpp#L1951) + +```cpp +// ... +// Factor to multiply the previous / current nominal velocities to get componentwise limited velocities. + float v_factor = 1; + limited = 0; + + // The junction velocity will be shared between successive segments. Limit the junction velocity to their minimum. + // Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting. + vmax_junction = _MIN(block->nominal_speed, previous_nominal_speed); + + // Now limit the jerk in all axes. + const float smaller_speed_factor = vmax_junction / previous_nominal_speed; + `if` HAS_LINEAR_E_JERK + LOOP_XYZ(axis) + `else` + LOOP_XYZE(axis) + `endif` + { + // Limit an axis. We have to differentiate: coasting, reversal of an axis, full stop. + float v_exit = previous_speed[axis] * smaller_speed_factor, + v_entry = current_speed[axis]; + if (limited) { + v_exit *= v_factor; + v_entry *= v_factor; + } + + // Calculate jerk depending on whether the axis is coasting in the same direction or reversing. + const float jerk = (v_exit > v_entry) + ? // coasting axis reversal + ( (v_entry > 0 || v_exit < 0) ? (v_exit - v_entry) : _MAX(v_exit, -v_entry) ) + : // v_exit <= v_entry coasting axis reversal + ( (v_entry < 0 || v_exit > 0) ? (v_entry - v_exit) : _MAX(-v_exit, v_entry) ); + + if (jerk > settings.max_jerk[axis]) { + v_factor *= settings.max_jerk[axis] / jerk; + ++limited; + } + } + if (limited) vmax_junction *= v_factor; + // Now the transition velocity is known, which maximizes the shared exit / entry velocity while + // respecting the jerk factors, it may be possible, that applying separate safe exit / entry velocities will achieve faster prints. + const float vmax_junction_threshold = vmax_junction * 0.99f; + if (previous_safe_speed > vmax_junction_threshold && safe_speed > vmax_junction_threshold) + vmax_junction = safe_speed; +} +// ... +``` + + -#### \_\_init\_\_ +#### calc\_j\_vel ```python -def __init__(state_A: state, state_B: state) +def calc_j_vel() ``` -Marlin specific junction velocity calculation with Junction Deviation. - -**Arguments**: - -- `state_A` - (state) start state -- `state_B` - (state) end state +Calculate the junction velocity. - + #### get\_junction\_vel @@ -712,18 +668,18 @@ Marlin specific junction velocity calculation with Junction Deviation. def get_junction_vel() ``` -Return junction velocity. +Return the calculated junction velocity. **Returns**: - `junction_vel` - (float) junction velocity - + -## junction\_handling\_marlin\_jerk Objects +### marlin Objects ```python -class junction_handling_marlin_jerk(junction_handling) +class marlin(junction_handling) ``` Marlin classic jerk specific junction handling. @@ -733,22 +689,22 @@ Marlin classic jerk specific junction handling. [https://github.com/MarlinFirmware/Marlin/pull/8888](https://github.com/MarlinFirmware/Marlin/pull/8888) [https://github.com/MarlinFirmware/Marlin/issues/367#issuecomment-12505768](https://github.com/MarlinFirmware/Marlin/issues/367#issuecomment-12505768) - - -#### \_\_init\_\_ -```python -def __init__(state_A: state, state_B: state) +**Code reference:** +[Marlin/src/module/planner.cpp](https://github.com/MarlinFirmware/Marlin/blob/8ec9c379405bb9962aff170d305ddd0725bd64e2/Marlin/src/module/planner.cpp#L2762) +```cpp +// ... +float v_factor = 1.0f; +LOOP_LOGICAL_AXES(i) { + // Jerk is the per-axis velocity difference. + const float jerk = ABS(speed_diff[i]), maxj = max_j[i]; + if (jerk * v_factor > maxj) v_factor = maxj / jerk; +} +vmax_junction_sqr = sq(vmax_junction * v_factor); +// ... ``` -Marlin classic jerk specific junction velocity calculation. - -**Arguments**: - -- `state_A` - (state) start state -- `state_B` - (state) end state - - + #### calc\_j\_vel @@ -758,7 +714,7 @@ def calc_j_vel() Calculate the junction velocity. - + #### get\_junction\_vel @@ -772,54 +728,51 @@ Return the calculated junction velocity. - `junction_vel` - (float) junction velocity - - -## junction\_handling\_klipper Objects - -```python -class junction_handling_klipper(junction_handling) -``` - -Klipper specific junction handling. - -- similar junction deviation calc -- corner vel set by: square_corner_velocity - end_velocity^2 = start_velocity^2 + 2*accel*move_distance - for 90deg turn -- todo: smoothed look ahead - -**Reference:** -[https://www.klipper3d.org/Kinematics.html](https://www.klipper3d.org/Kinematics.html) -[https://github.com/Klipper3d/klipper/blob/ea2f6bc0f544132738c7f052ffcc586fa884a19a/klippy/toolhead.py](https://github.com/Klipper3d/klipper/blob/ea2f6bc0f544132738c7f052ffcc586fa884a19a/klippy/toolhead.py) - - + -#### \_\_init\_\_ +### ultimaker Objects ```python -def __init__(state_A: state, state_B: state) +class ultimaker(junction_handling) ``` -Klipper specific junction velocity calculation. - -**Arguments**: - -- `state_A` - (state) start state -- `state_B` - (state) end state - - +Ultimaker specific junction handling. -#### calc\_j\_delta +**Code reference:** +[UM2.1-Firmware/Marlin/planner.cpp](https://github.com/Ultimaker/UM2.1-Firmware/blob/f6e69344c00d7f300dace730990652ba614a2105/Marlin/planner.cpp#L840) +```cpp +// ... +float vmax_junction = max_xy_jerk/2; +float vmax_junction_factor = 1.0; +if(fabs(current_speed[Z_AXIS]) > max_z_jerk/2) + vmax_junction = min(vmax_junction, max_z_jerk/2); +if(fabs(current_speed[E_AXIS]) > max_e_jerk/2) + vmax_junction = min(vmax_junction, max_e_jerk/2); +vmax_junction = min(vmax_junction, block->nominal_speed); +float safe_speed = vmax_junction; -```python -def calc_j_delta() +if ((moves_queued > 1) && (previous_nominal_speed > 0.0001)) { + float xy_jerk = sqrt(square(current_speed[X_AXIS]-previous_speed[X_AXIS])+square(current_speed[Y_AXIS]-previous_speed[Y_AXIS])); + // if((fabs(previous_speed[X_AXIS]) > 0.0001) || (fabs(previous_speed[Y_AXIS]) > 0.0001)) { + vmax_junction = block->nominal_speed; + // } + if (xy_jerk > max_xy_jerk) { + vmax_junction_factor = (max_xy_jerk / xy_jerk); + } + if(fabs(current_speed[Z_AXIS] - previous_speed[Z_AXIS]) > max_z_jerk) { + vmax_junction_factor= min(vmax_junction_factor, (max_z_jerk/fabs(current_speed[Z_AXIS] - previous_speed[Z_AXIS]))); + } + if(fabs(current_speed[E_AXIS] - previous_speed[E_AXIS]) > max_e_jerk) { + vmax_junction_factor = min(vmax_junction_factor, (max_e_jerk/fabs(current_speed[E_AXIS] - previous_speed[E_AXIS]))); + } + vmax_junction = min(previous_nominal_speed, vmax_junction * vmax_junction_factor); // Limit speed to max previous speed +} +// Max entry speed of this block equals the max exit speed of the previous block. +block->max_entry_speed = vmax_junction; +// ... ``` -Calculate the junction deviation with klipper specific values. - -The jerk value represents the square_corner_velocity! - - + #### calc\_j\_vel @@ -829,7 +782,7 @@ def calc_j_vel() Calculate the junction velocity. - + #### get\_junction\_vel @@ -843,45 +796,80 @@ Return the calculated junction velocity. - `junction_vel` - (float) junction velocity - + -## junction\_handling\_MKA Objects +### mka Objects ```python -class junction_handling_MKA(junction_handling) +class mka(prusa) ``` Anisoprint A4 like junction handling. -**Reference:** -[https://github.com/anisoprint/MKA-firmware/blob/6e02973b1b8f325040cc3dbf66ac545ffc5c06b3/src/core/planner/planner.cpp#L1830](https://github.com/anisoprint/MKA-firmware/blob/6e02973b1b8f325040cc3dbf66ac545ffc5c06b3/src/core/planner/planner.cpp#L1830) +**Code reference:** +[anisoprint/MKA-firmware/src/core/planner/planner.cpp#L1830](https://github.com/anisoprint/MKA-firmware/blob/6e02973b1b8f325040cc3dbf66ac545ffc5c06b3/src/core/planner/planner.cpp#L1830) +```cpp +// ... +float v_exit = previous_speed[axis] * smaller_speed_factor, + v_entry = current_speed[axis]; + if (limited) { + v_exit *= v_factor; + v_entry *= v_factor; + } + + // Calculate jerk depending on whether the axis is coasting in the same direction or reversing. + const float jerk = (v_exit > v_entry) + ? // coasting axis reversal + ( (v_entry > 0 || v_exit < 0) ? (v_exit - v_entry) : max(v_exit, -v_entry) ) + : // v_exit <= v_entry coasting axis reversal + ( (v_entry < 0 || v_exit > 0) ? (v_entry - v_exit) : max(-v_exit, v_entry) ); - + const float maxj = mechanics.max_jerk[axis]; + if (jerk > maxj) { + v_factor *= maxj / jerk; + ++limited; + } +} +if (limited) vmax_junction *= v_factor; +// ... +``` -#### \_\_init\_\_ + + +### junction\_deviation Objects ```python -def __init__(state_A: state, state_B: state) +class junction_deviation(junction_handling) ``` -Marlin classic jerk specific junction velocity calculation. - -**Arguments**: +Marlin specific junction handling with Junction Deviation. -- `state_A` - (state) start state -- `state_B` - (state) end state +**Reference:** +1: [Developer Blog](https://onehossshay.wordpress.com/2011/09/24/improving_grbl_cornering_algorithm/) +2: [Kynetic CNC Blog](http://blog.kyneticcnc.com/2018/10/computing-junction-deviation-for-marlin.html) - + -#### calc\_j\_vel +#### calc\_JD ```python -def calc_j_vel() +def calc_JD(vel_0: velocity, vel_1: velocity, p_settings: state.p_settings) ``` -Calculate the junction velocity. +Calculate junction deviation velocity from 2 velocities. - +**Arguments**: + +- `vel_0` - (velocity) entry +- `vel_1` - (velocity) exit +- `p_settings` - (state.p_settings) print settings + + +**Returns**: + +- `velocity` - (float) velocity abs value + + #### get\_junction\_vel @@ -889,21 +877,40 @@ Calculate the junction velocity. def get_junction_vel() ``` -Return the calculated junction velocity. +Return junction velocity. **Returns**: - `junction_vel` - (float) junction velocity + + +#### get\_handler + +```python +def get_handler(firmware_name: str) -> type[junction_handling] +``` + +Get the junction handling class for the given firmware name. + +**Arguments**: + +- `firmware_name` - (str) name of the firmware + + +**Returns**: + +- `junction_handling` - (type[junction_handling]) junction handling class + -# pyGCodeDecode.planner\_block +## pyGCodeDecode.planner\_block Planner block Module. -## planner\_block Objects +### planner\_block Objects ```python class planner_block() @@ -911,19 +918,19 @@ class planner_block() Planner Block Class. - + -#### move\_maker2 +#### move\_maker ```python -def move_maker2(v_end) +def move_maker(v_end) ``` Calculate the correct move type (trapezoidal,triangular or singular) and generate the corresponding segments. **Arguments**: -- `vel_end` - (velocity) target velocity for end of move +- `v_end` - (velocity) target velocity for end of move @@ -964,21 +971,15 @@ Return max vel from planner block while extruding. - `block_max_vel` - (np.ndarray 1x4) maximum axis velocity while extruding in block or None if no extrusion is happening - + -#### \_\_init\_\_ +#### calc\_results ```python -def __init__(state: state, prev_block: "planner_block", firmware=None) +def calc_results(*additional_calculators: abstract_result) ``` -Calculate and store planner block consisting of one or multiple segments. - -**Arguments**: - -- `state` - (state) the current state -- `prev_block` - (planner_block) previous planner block -- `firmware` - (string, default = None) firmware selection for junction +Calculate the result of the planner block. @@ -1002,128 +1003,340 @@ def next_block() Define next_block as property. - + -#### \_\_str\_\_ +#### get\_segments ```python -def __str__() -> str +def get_segments() ``` -Create string from planner block. +Return segments, contained by the planner block. - + -#### \_\_repr\_\_ +#### get\_block\_travel ```python -def __repr__() -> str +def get_block_travel() ``` -Represent planner block. +Return the travel length of the planner block. - + -#### get\_segments +#### inverse\_time\_at\_pos ```python -def get_segments() +def inverse_time_at_pos(dist_local) ``` -Return segments, contained by the planner block. +Get the global time, at which the local length is reached. - +**Arguments**: -#### get\_block\_travel +- `dist_local` - (float) local (relative to planner block start) distance + + +**Returns**: + +- `time_global` - (float) global time when the point will be reached. + + + +## pyGCodeDecode.plotter + +This module provides functionality for 3D plotting of G-code simulation data using PyVista. + +Functions: + plot_3d: Generates a 3D plot of the simulation data, with options for customization such as + extrusion-only plotting, scalar value selection, layer selection, and saving the plot + as a screenshot or VTK file. + plot_2d: Generates a 2D plot of the simulation data, showing the position of the extruder head + over time. + +Dependencies: + - pyGCodeDecode.gcode_interpreter.simulation + - pyGCodeDecode.gcode_interpreter.unpack_blocklist + - pyGCodeDecode.utils + - numpy + - pyvista + - pathlib + + + +#### plot\_3d ```python -def get_block_travel() +def plot_3d( + sim: simulation, + extrusion_only: bool = True, + scalar_value: str = "velocity", + screenshot_path: pathlib.Path = None, + camera_settings: dict = None, + vtk_path: pathlib.Path = None, + mesh: pv.MultiBlock = None, + layer_select: int = None, + window_size: tuple = (2048, 1536), + mpl_subplot: bool = False, + mpl_rcParams: Union[dict, None] = None, + solid_color: str = "black", + transparent_background: bool = True, + parallel_projection: bool = False, + lighting: bool = True, + block_colorbar: bool = False, + extra_plotting: callable = None, + overwrite_labels: Union[dict, None] = None, + scalar_value_bounds: Union[Tuple[float, float], + None] = None) -> pv.MultiBlock ``` -Return the travel length of the planner block. +3D Plot with PyVista. - +**Arguments**: -# pyGCodeDecode.state +- `extrusion_only` _bool, optional_ - Plot only parts where material is extruded. + Defaults to True. +- `scalar_value` _str, optional_ - scalar value to plot. Defaults to Velocity (vel). +- `Options` - vel, rel_vel_err, or None. +- `screenshot_path` _pathlib.Path, optional_ - Path to screenshot to be saved. + Prevents interactive plot. Defaults to None. +- `vtk_path` _pathlib.Path, optional_ - Path to vtk to be saved. + Prevents interactive plot. Defaults to None. +- `mesh` _pv.MultiBlock, optional_ - A pyvista mesh from a previous run to + avoid running the mesh generation again. Defaults to None. +- `layer_select` _int, optional_ - Select the layer to be plotted. + Defaults to None, which plots all layers. +- `window_size` _tuple, optional_ - Size of the plot window. + Defaults to (2048, 1536). +- `mpl_subplot` _bool, optional_ - Use matplotlib subplot for the screenshot. + Defaults to False. +- `solid_color` _str, optional_ - Background color of the plot. Defaults to "black". +- `transparent_background` _bool, optional_ - Use a transparent background for + the screenshot. Defaults to True. -State module with state. +**Returns**: + +- `pv.MultiBlock` - The mesh used in the plot so it can be used (e.g. in subsequent plots). + + + +#### plot\_2d + +```python +def plot_2d(sim: simulation, + filepath: pathlib.Path = pathlib.Path("trajectory_2D.png"), + colvar="Velocity", + show_points=False, + colvar_spatial_resolution=1, + dpi=400, + scaled=True, + show=False) +``` + +Plot 2D position (XY plane) with matplotlib (unmaintained). + + + +#### plot\_vel + +```python +def plot_vel(sim: simulation, + axis: Tuple[str] = ("x", "y", "z", "e"), + show: bool = True, + show_planner_blocks: bool = True, + show_segments: bool = False, + show_jv: bool = False, + time_steps: Union[int, str] = "constrained", + filepath: pathlib.Path = None, + dpi: int = 400) -> Figure +``` + +Plot axis velocity with matplotlib. + +**Arguments**: + +- `axis` - (tuple(string), default = ("x", "y", "z", "e")) select plot axis +- `show` - (bool, default = True) show plot and return plot figure +- `show_planner_blocks` - (bool, default = True) show planner_blocks as vertical lines +- `show_segments` - (bool, default = False) show segments as vertical lines +- `show_jv` - (bool, default = False) show junction velocity as x +- `time_steps` - (int or string, default = "constrained") number of time steps or constrain plot + vertices to segment vertices +- `filepath` - (Path, default = None) save fig as image if filepath is provided +- `dpi` - (int, default = 400) select dpi + + +**Returns**: + + (optionally) +- `fig` - (figure) + + + +## pyGCodeDecode.result + +Result calculation for segments and planner blocks. + + + +### abstract\_result Objects + +```python +class abstract_result(ABC) +``` + +Abstract class for result calculation. + + + +#### name + +```python +@property +@abstractmethod +def name() +``` + +Name of the result. Has to be set in the derived class. + + + +#### calc\_pblock + +```python +@abstractmethod +def calc_pblock(pblock: "planner_block", **kwargs) +``` + +Calculate the result for a planner block. + + + +#### calc\_segm + +```python +@abstractmethod +def calc_segm(segm: "segment", **kwargs) +``` + +Calculate the result for a segment. + + + +### acceleration\_result Objects + +```python +class acceleration_result(abstract_result) +``` + +The acceleration. + + + +#### calc\_segm + +```python +def calc_segm(segm: "segment", **kwargs) +``` + +Calculate the acceleration for a segment. + + + +#### calc\_pblock + +```python +def calc_pblock(pblock, **kwargs) +``` + +Calculate the acceleration for a planner block. - + -## state Objects +### velocity\_result Objects ```python -class state() +class velocity_result(abstract_result) ``` -State contains a Position and Printing Settings (p_settings) to apply for the corresponding move to this State. +The velocity. - + -## p\_settings Objects +#### calc\_segm ```python -class p_settings() +def calc_segm(segm: "segment", **kwargs) ``` -Store Printing Settings. +Calculate the velocity for a segment. - + -#### \_\_init\_\_ +#### calc\_pblock ```python -def __init__(p_acc, jerk, vX, vY, vZ, vE, speed, units="SImm") +def calc_pblock(pblock: "planner_block", **kwargs) ``` -Initialize printing settings. +Calculate the velocity for a planner block. -**Arguments**: + + +#### get\_all\_result\_calculators -- `p_acc` - (float) printing acceleration -- `jerk` - (float) jerk or similar -- `vX` - (float) max x velocity -- `vY` - (float) max y velocity -- `vZ` - (float) max z velocity -- `vE` - (float) max e velocity -- `speed` - (float) default target velocity -- `units` - (string, default = "SImm") unit settings +```python +def get_all_result_calculators() +``` - +Get all results. -#### \_\_str\_\_ + + +#### has\_private\_results ```python -def __str__() -> str +def has_private_results() ``` -Create summary string for p_settings. +Check if private results are available. - + -#### \_\_repr\_\_ +#### get\_result\_info ```python -def __repr__() -> str +def get_result_info() ``` -Define representation. +Get information about available result calculators. + + + +## pyGCodeDecode.state + +State module with state. - + -#### \_\_init\_\_ +### state Objects ```python -def __init__(state_position: position = None, - state_p_settings: p_settings = None) +class state() ``` -Initialize a state. +State contains a Position and Printing Settings (p_settings) to apply for the corresponding move to this State. -**Arguments**: + + +### p\_settings Objects + +```python +class p_settings() +``` -- `state_position` - (position) state position -- `state_p_settings` - (p_settings) state printing settings +Store Printing Settings. @@ -1156,7 +1369,7 @@ Define property state_p_settings. def line_number() ``` -Define property line_nmbr. +Define property line_number. @@ -1225,29 +1438,9 @@ Set previous state. - `state` - (state) previous state - - -#### \_\_str\_\_ - -```python -def __str__() -> str -``` - -Generate string for representation. - - - -#### \_\_repr\_\_ - -```python -def __repr__() -> str -``` - -Call __str__() for representation. - -# pyGCodeDecode.state\_generator +## pyGCodeDecode.state\_generator State generator module. @@ -1283,7 +1476,7 @@ Read gcode from .gcode file. **Arguments**: -- `filename` - (Path) filepath of the .gcode file +- `filepath` - (Path) filepath of the .gcode file **Returns**: @@ -1353,7 +1546,7 @@ Generate state list from GCode file. -# pyGCodeDecode.tools +## pyGCodeDecode.tools Tools for pyGCD. @@ -1362,11 +1555,11 @@ Tools for pyGCD. #### save\_layer\_metrics ```python -def save_layer_metrics(simulation: simulation, - filepath: Union[pathlib.Path, - str] = "./layer_metrics.csv", - locale: str = None, - delimiter: str = ";") +def save_layer_metrics( + simulation: simulation, + filepath: Optional[pathlib.Path] = pathlib.Path("./layer_metrics.csv"), + locale: str = None, + delimiter: str = ";") -> Optional[tuple[list, list, list, list]] ``` Print out print times, distance traveled and the average travel speed to a csv-file. @@ -1374,8 +1567,8 @@ Print out print times, distance traveled and the average travel speed to a csv-f **Arguments**: - `simulation` - (simulation) simulation instance -- `filepath` - (Path | string, default = "./layer_metrics.csv") file name -- `locale` - (string, default = None) select locale settings, e.g. "en_US.utf8" "de_DE.utf8", None = use system locale +- `filepath` - (Path , default = "./layer_metrics.csv") file name +- `locale` - (string, default = None) select locale settings, e.g. "en_US.utf8", None = use system locale - `delimiter` - (string, default = ";") select delimiter Layers are detected using the given layer cue. @@ -1390,8 +1583,9 @@ def write_submodel_times(simulation: simulation, sub_side_x_len: float, sub_side_y_len: float, sub_side_z_len: float, - filename="submodel_times.yaml", - **kwargs) + filename: Optional[pathlib.Path] = pathlib.Path( + "submodel_times.yaml"), + **kwargs) -> dict ``` Write the submodel entry and exit times to a yaml file. @@ -1406,7 +1600,7 @@ Write the submodel entry and exit times to a yaml file. -# pyGCodeDecode.utils +## pyGCodeDecode.utils Utilitys. @@ -1415,133 +1609,114 @@ Utils for the GCode Reader contains: - velocity - position - + -## vector\_4D Objects +### seconds Objects ```python -class vector_4D() +class seconds(float) ``` -The vector_4D class stores 4D vector in x,y,z,e. - -**Supports:** -- str -- add -- sub -- mul (scalar) -- truediv (scalar) -- eq - - - -#### \_\_init\_\_ +A float subclass representing a time duration in seconds. -```python -def __init__(*args) -``` +**Arguments**: -Store 3D position + extrusion axis. +- `value` _float or int_ - The time duration in seconds. -**Arguments**: +**Examples**: -- `args` - coordinates as arguments x,y,z,e or (tuple or list) [x,y,z,e] + >>> t = seconds(5) + >>> str(t) + '5.0 s' + >>> t.seconds + 5.0 - + -#### \_\_str\_\_ +#### seconds ```python -def __str__() -> str +@property +def seconds() ``` -Return string representation. +Return the float value of the seconds instance. - + -#### \_\_add\_\_ +### vector\_4D Objects ```python -def __add__(other) +class vector_4D() ``` -Add functionality for 4D vectors. - -**Arguments**: - -- `other` - (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') - - -**Returns**: +The vector_4D class stores 4D vector in x,y,z,e. -- `add` - (self) component wise addition +**Supports:** +- str +- add +- sub +- mul (scalar) +- truediv (scalar) +- eq - + -#### \_\_sub\_\_ +#### get\_vec ```python -def __sub__(other) +def get_vec(withExtrusion=False) -> List[float] ``` -Sub functionality for 4D vectors. +Return the 4D vector, optionally with extrusion. **Arguments**: -- `other` - (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') +- `withExtrusion` - (bool, default = False) choose if vec repr contains extrusion **Returns**: -- `sub` - (self) component wise subtraction +- `vec` - (list[3 or 4]) with (x,y,z,(optionally e)) - + -#### \_\_mul\_\_ +#### get\_norm ```python -def __mul__(other) +def get_norm(withExtrusion=False) -> float ``` -Scalar multiplication functionality for 4D vectors. +Return the 4D vector norm. Optional with extrusion. **Arguments**: -- `other` - (float or int) +- `withExtrusion` - (bool, default = False) choose if norm contains extrusion **Returns**: -- `mul` - (self) scalar multiplication, scaling +- `norm` - (float) length/norm of 3D or 4D vector - + -#### \_\_truediv\_\_ +### position Objects ```python -def __truediv__(other) +class position(vector_4D) ``` -Scalar division functionality for 4D Vectors. - -**Arguments**: - -- `other` - (float or int) - - -**Returns**: - -- `div` - (self) scalar division, scaling +4D - Position object for (Cartesian) 3D printer. - + -#### \_\_eq\_\_ +#### is\_travel ```python -def __eq__(other) +def is_travel(other) -> bool ``` -Check for equality and return True if equal. +Return True if there is travel between self and other position. **Arguments**: @@ -1550,49 +1725,51 @@ Check for equality and return True if equal. **Returns**: -- `eq` - (bool) true if equal (with tolerance) +- `is_travel` - (bool) true if between self and other is distance - + -#### get\_vec +#### is\_extruding ```python -def get_vec(withExtrusion=False) -> List[float] +def is_extruding(other: "position", ignore_retract: bool = True) -> bool ``` -Return the 4D vector, optionally with extrusion. +Return True if there is extrusion between self and other position. **Arguments**: -- `withExtrusion` - (bool, default = False) choose if vec repr contains extrusion +- `other` - (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') +- `ignore_retract` - (bool, default = True) if true ignore retract movements else retract is also extrusion **Returns**: -- `vec` - (list[3 or 4]) with (x,y,z,(optionally e)) +- `is_extruding` - (bool) true if between self and other is extrusion - + -#### get\_norm +#### get\_t\_distance ```python -def get_norm(withExtrusion=False) -> float +def get_t_distance(other=None, withExtrusion=False) -> float ``` -Return the 4D vector norm. Optional with extrusion. +Calculate the travel distance between self and other position. If none is provided, zero will be used. **Arguments**: -- `withExtrusion` - (bool, default = False) choose if norm contains extrusion +- `other` - (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray', default = None) +- `withExtrusion` - (bool, default = False) use or ignore extrusion **Returns**: -- `norm` - (float) length/norm of 3D or 4D vector +- `travel` - (float) travel or extrusion and travel distance -## velocity Objects +### velocity Objects ```python class velocity(vector_4D) @@ -1600,16 +1777,6 @@ class velocity(vector_4D) 4D - Velocity object for (Cartesian) 3D printer. - - -#### \_\_str\_\_ - -```python -def __str__() -> str -``` - -Print out velocity. - #### get\_norm\_dir @@ -1676,88 +1843,19 @@ Return True if extrusion velocity is not zero. - `is_extruding` - (bool) true if positive extrusion velocity - - -## position Objects - -```python -class position(vector_4D) -``` - -4D - Position object for (Cartesian) 3D printer. - - - -#### \_\_str\_\_ - -```python -def __str__() -> str -``` - -Print out position. - - - -#### is\_travel - -```python -def is_travel(other) -> bool -``` - -Return True if there is travel between self and other position. - -**Arguments**: - -- `other` - (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') - - -**Returns**: - -- `is_travel` - (bool) true if between self and other is distance - - - -#### is\_extruding - -```python -def is_extruding(other: "position", ignore_retract: bool = True) -> bool -``` - -Return True if there is extrusion between self and other position. - -**Arguments**: - -- `other` - (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') -- `ignore_retract` - (bool, default = True) if true ignore retract movements else retract is also extrusion - - -**Returns**: - -- `is_extruding` - (bool) true if between self and other is extrusion - - + -#### get\_t\_distance +### acceleration Objects ```python -def get_t_distance(other=None, withExtrusion=False) -> float +class acceleration(vector_4D) ``` -Calculate the travel distance between self and other position. If none is provided, zero will be used. - -**Arguments**: - -- `other` - (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray', default = None) -- `withExtrusion` - (bool, default = False) use or ignore extrusion - - -**Returns**: - -- `travel` - (float) travel or extrusion and travel distance +4D - Acceleration object for (Cartesian) 3D printer. -## segment Objects +### segment Objects ```python class segment() @@ -1773,55 +1871,12 @@ contains: time, position, velocity - move_segment_time: moves Segment in time by a specified interval - get_velocity: returns the calculated Velocity for all axis at a given point in time - get_position: returns the calculated Position for all axis at a given point in time +- get_segm_len: returns the length of the segment. **Class method** - create_initial: returns the artificial initial segment where everything is at standstill, intervall length = 0 - self_check: returns True if all self checks have been successfull - - -#### \_\_init\_\_ - -```python -def __init__(t_begin: float, - t_end: float, - pos_begin: position, - vel_begin: velocity, - pos_end: position = None, - vel_end: velocity = None) -``` - -Initialize a segment. - -**Arguments**: - -- `t_begin` - (float) begin of segment -- `t_end` - (float) end of segment -- `pos_begin` - (position) beginning position of segment -- `vel_begin` - (velocity) beginning velocity of segment -- `pos_end` - (position, default = None) ending position of segment -- `vel_end` - (velocity, default = None) ending velocity of segment - - - -#### \_\_str\_\_ - -```python -def __str__() -> str -``` - -Create string from segment. - - - -#### \_\_repr\_\_ - -```python -def __repr__() -``` - -Segment representation. - #### move\_segment\_time @@ -1855,6 +1910,16 @@ Get current velocity of segment at a certain time. - `current_vel` - (velocity) velocity at time t + + +#### get\_velocity\_by\_dist + +```python +def get_velocity_by_dist(dist) +``` + +Return the velocity at a certain local segment distance. + #### get\_position @@ -1874,22 +1939,43 @@ Get current position of segment at a certain time. - `pos` - (position) position at time t + + +#### get\_segm\_len + +```python +def get_segm_len() +``` + +Return the length of the segment. + + + +#### get\_segm\_duration + +```python +def get_segm_duration() +``` + +Return the duration of the segment. + #### self\_check ```python -def self_check(p_settings=None) +def self_check(p_settings: "state.p_settings" = None) ``` Check the segment for self consistency. -todo: -- max acceleration +**Raises**: + +- `ValueError` - if self check fails **Arguments**: -- `p_settings` - (p_setting, default = None) printing settings to verify +- `p_settings` - (p_settings, default = None) printing settings to verify @@ -1905,6 +1991,25 @@ Return true if the segment is pos. extruding. - `is_extruding` - (bool) true if positive extrusion + + +#### get\_result + +```python +def get_result(key) +``` + +Return the requested result. + +**Arguments**: + +- `key` - (str) choose result + + +**Returns**: + +- `result` - (list) + #### create\_initial diff --git a/pyGCodeDecode/abaqus_file_generator.py b/pyGCodeDecode/abaqus_file_generator.py index f41cd53..65c2702 100644 --- a/pyGCodeDecode/abaqus_file_generator.py +++ b/pyGCodeDecode/abaqus_file_generator.py @@ -4,7 +4,7 @@ from pyGCodeDecode.helpers import custom_print -from . import gcode_interpreter as gi +from . import gcode_interpreter """ This script is to convert gcode into an event series as abaqus input @@ -21,7 +21,7 @@ def generate_abaqus_event_series( - simulation: gi.simulation, + simulation: gcode_interpreter.simulation, filepath: str = "pyGcodeDecode_abaqus_events.inp", tolerance: float = 1e-12, output_unit_system: str = None, @@ -30,7 +30,7 @@ def generate_abaqus_event_series( """Generate abaqus event series. Args: - simulation (gi.simulation): simulation instance + simulation (gcode_interpreter.simulation): simulation instance filepath (string, default = "pyGcodeDecode_abaqus_events.inp"): output file path tolerance (float, default = 1e-12): tolerance to determine whether extrusion is happening output_unit_system (str, optional): Unit system for the output. @@ -40,7 +40,7 @@ def generate_abaqus_event_series( Returns: (optional) tuple: the event series as a tuple for use in ABAQUS-Python """ - unpacked = gi.unpack_blocklist(simulation.blocklist) + unpacked = gcode_interpreter.unpack_blocklist(simulation.blocklist) pos = [unpacked[0].pos_begin.get_vec(withExtrusion=True)] time = [0] for segment in unpacked: diff --git a/pyGCodeDecode/gcode_interpreter.py b/pyGCodeDecode/gcode_interpreter.py index abd8d87..95003a2 100644 --- a/pyGCodeDecode/gcode_interpreter.py +++ b/pyGCodeDecode/gcode_interpreter.py @@ -517,11 +517,7 @@ def load_setup(self, filepath): return setup_dict def check_initial_setup(self): - """Check the printer Dict for typos or missing parameters and raise errors if invalid. - - Args: - initial_machine_setup: (dict) initial machine setup dictionary - """ + """Check the printer Dict for typos or missing parameters and raise errors if invalid.""" req_keys = [ "p_vel", "p_acc", diff --git a/pyGCodeDecode/junction_handling.py b/pyGCodeDecode/junction_handling.py index a89cc39..a7ced3f 100644 --- a/pyGCodeDecode/junction_handling.py +++ b/pyGCodeDecode/junction_handling.py @@ -90,7 +90,9 @@ class prusa(junction_handling): **Code reference:** [Prusa-Firmware-Buddy/lib/Marlin/Marlin/src/module/planner.cpp](https://github.com/prusa3d/Prusa-Firmware-Buddy/blob/818d812f954802903ea0ff39bf44376fb0b35dd2/lib/Marlin/Marlin/src/module/planner.cpp#L1951) - ... + + ```cpp + // ... // Factor to multiply the previous / current nominal velocities to get componentwise limited velocities. float v_factor = 1; limited = 0; @@ -134,7 +136,8 @@ class prusa(junction_handling): if (previous_safe_speed > vmax_junction_threshold && safe_speed > vmax_junction_threshold) vmax_junction = safe_speed; } - ... + // ... + ``` """ def __init__(self, state_A: state, state_B: state): @@ -205,7 +208,8 @@ class marlin(junction_handling): **Code reference:** [Marlin/src/module/planner.cpp](https://github.com/MarlinFirmware/Marlin/blob/8ec9c379405bb9962aff170d305ddd0725bd64e2/Marlin/src/module/planner.cpp#L2762) - ... + ```cpp + // ... float v_factor = 1.0f; LOOP_LOGICAL_AXES(i) { // Jerk is the per-axis velocity difference. @@ -213,7 +217,8 @@ class marlin(junction_handling): if (jerk * v_factor > maxj) v_factor = maxj / jerk; } vmax_junction_sqr = sq(vmax_junction * v_factor); - ... + // ... + ``` """ def __init__(self, state_A: state, state_B: state): @@ -260,7 +265,8 @@ class ultimaker(junction_handling): **Code reference:** [UM2.1-Firmware/Marlin/planner.cpp](https://github.com/Ultimaker/UM2.1-Firmware/blob/f6e69344c00d7f300dace730990652ba614a2105/Marlin/planner.cpp#L840) - ... + ```cpp + // ... float vmax_junction = max_xy_jerk/2; float vmax_junction_factor = 1.0; if(fabs(current_speed[Z_AXIS]) > max_z_jerk/2) @@ -288,7 +294,8 @@ class ultimaker(junction_handling): } // Max entry speed of this block equals the max exit speed of the previous block. block->max_entry_speed = vmax_junction; - ... + // ... + ``` """ def __init__(self, state_A: state, state_B: state): @@ -360,7 +367,8 @@ class mka(prusa): **Code reference:** [anisoprint/MKA-firmware/src/core/planner/planner.cpp#L1830](https://github.com/anisoprint/MKA-firmware/blob/6e02973b1b8f325040cc3dbf66ac545ffc5c06b3/src/core/planner/planner.cpp#L1830) - ... + ```cpp + // ... float v_exit = previous_speed[axis] * smaller_speed_factor, v_entry = current_speed[axis]; if (limited) { @@ -382,7 +390,8 @@ class mka(prusa): } } if (limited) vmax_junction *= v_factor; - ... + // ... + ``` """ @@ -398,7 +407,7 @@ class junction_deviation(junction_handling): """ def calc_JD(self, vel_0: velocity, vel_1: velocity, p_settings: state.p_settings): - """Calculate junction deviation velocity from 2 velocitys. + """Calculate junction deviation velocity from 2 velocities. Args: vel_0: (velocity) entry diff --git a/pyGCodeDecode/planner_block.py b/pyGCodeDecode/planner_block.py index 6446856..1ca72c2 100644 --- a/pyGCodeDecode/planner_block.py +++ b/pyGCodeDecode/planner_block.py @@ -25,7 +25,7 @@ def move_maker(self, v_end): Calculate the correct move type (trapezoidal,triangular or singular) and generate the corresponding segments. Args: - vel_end: (velocity) target velocity for end of move + v_end: (velocity) target velocity for end of move """ def trapezoid(extrusion_only=False): diff --git a/pyGCodeDecode/state.py b/pyGCodeDecode/state.py index a181e0a..9a76873 100644 --- a/pyGCodeDecode/state.py +++ b/pyGCodeDecode/state.py @@ -93,7 +93,7 @@ def state_p_settings(self, set_p_settings: p_settings): @property def line_number(self): - """Define property line_nmbr.""" + """Define property line_number.""" return self._line_nmbr @line_number.setter diff --git a/pyGCodeDecode/state_generator.py b/pyGCodeDecode/state_generator.py index 1d5132d..aa39cc2 100644 --- a/pyGCodeDecode/state_generator.py +++ b/pyGCodeDecode/state_generator.py @@ -179,7 +179,7 @@ def read_gcode_to_dict_list(filepath: pathlib.Path) -> List[dict]: Read gcode from .gcode file. Args: - filename: (Path) filepath of the .gcode file + filepath: (Path) filepath of the .gcode file Returns: dict_list: (list[dict]) list with every line as dict @@ -202,7 +202,7 @@ def dict_list_traveler(line_dict_list: List[dict], initial_machine_setup: dict) """ Convert the line dictionary to a state. - Parameters: + Args: line_dict_list: (dict) dict list with commands initial_machine_setup: (dict) dict with initial machine setup [absolute_position, absolute_extrusion, units, initial_position...] diff --git a/pyGCodeDecode/utils.py b/pyGCodeDecode/utils.py index e844624..c938e24 100644 --- a/pyGCodeDecode/utils.py +++ b/pyGCodeDecode/utils.py @@ -551,7 +551,7 @@ def self_check(self, p_settings: "state.p_settings" = None): Raises: ValueError: if self check fails Args: - p_settings: (p_setting, default = None) printing settings to verify + p_settings: (p_settings, default = None) printing settings to verify """ # position self check: tolerance = float("1e-6") diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml new file mode 100644 index 0000000..ca85fd5 --- /dev/null +++ b/pydoc-markdown.yml @@ -0,0 +1,60 @@ +loaders: + - type: python + search_path: [.] + packages: ["pyGCodeDecode"] + +processors: + - type: filter + expression: not name.startswith('_') # Skip private members + - type: filter + expression: not '.examples.' in name # Skip example modules + - type: filter + documented_only: true # Skip empty modules + - type: smart + - type: crossref + +renderer: + # type: markdown + type: mkdocs + output_directory: docs + markdown: + insert_header_anchors: true + render_page_title: true + toc_maxdepth: 4 + render_toc: false + use_fixed_header_levels: true + add_module_prefix: true + header_level_by_type: + Module: 2 + Class: 3 + Function: 4 + + pages: + - title: Home + name: index + source: README.md + - title: pyGCodeDecode Reference + name: api + contents: + - pyGCodeDecode.* + mkdocs_config: + site_name: pyGCodeDecode Documentation + site_description: Generate time dependent boundary conditions from a .gcode file + site_author: FAST-LB at KIT + repo_url: https://github.com/FAST-LB/pyGCodeDecode + repo_name: FAST-LB/pyGCodeDecode + edit_uri: edit/main/docs/ + theme: + name: readthedocs + highlightjs: true + nav: + - Home: index.md + - API Reference: api.md + markdown_extensions: + - toc: + permalink: true + - codehilite + - admonition + - tables + plugins: + - search From b98d52960a05e19b5c4fe652e954550ba022e51f Mon Sep 17 00:00:00 2001 From: usmfi Date: Tue, 5 Aug 2025 15:06:29 +0200 Subject: [PATCH 15/68] attempt at auto_docs --- .github/workflows/docs.yml | 43 +++++++++++++++ .gitignore | 2 +- docs_manager.py | 106 +++++++++++++++++++++++++++++++++++++ pyproject.toml | 16 ++---- 4 files changed, 154 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs_manager.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..ef77b89 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,43 @@ +name: Build and Deploy Documentation + +on: + push: + branches: [ main, public_pre_main ] + pull_request: + branches: [ main, public_pre_main ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Upgrade pip + run: pip install --upgrade pip + - name: Install dependencies + run: pip install -e .[DOCS] + - name: Build docs with pydoc-markdown + run: pydoc-markdown + - name: Build site with mkdocs + run: mkdocs build + - name: Upload site artifact + uses: actions/upload-artifact@v4 + with: + name: site + path: docs/site + + deploy: + needs: build + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/public_pre_main' + steps: + - uses: actions/checkout@v4 + - name: Download site artifact + uses: actions/download-artifact@v4 + with: + name: site + path: public + # Add your deployment steps here (e.g., GitHub Pages, FTP, etc.) diff --git a/.gitignore b/.gitignore index 56235a9..436bf2e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,7 @@ dist/ # output folders from examples and tests /output_benchy_example/* /tests/output/* -/docs/site +/docs/ # coverage reports .coverage diff --git a/docs_manager.py b/docs_manager.py new file mode 100644 index 0000000..709b8b3 --- /dev/null +++ b/docs_manager.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +""" +Documentation generation script for pyGCodeDecode. + +This script automates the process of generating documentation using +pydoc-markdown and serving it with mkdocs. +""" + +import os +import subprocess +import sys +from pathlib import Path + + +def run_command(cmd, description): + """Run a shell command and handle errors.""" + print(f"🔄 {description}...") + try: + result = subprocess.run(cmd, shell=True, check=True, capture_output=True, text=True) + print(f"✅ {description} completed successfully") + if result.stdout.strip(): + print(f" Output: {result.stdout.strip()}") + return True + except subprocess.CalledProcessError as e: + print(f"❌ {description} failed:") + print(f" Error: {e.stderr.strip()}") + return False + + +def generate_docs(): + """Generate API documentation using pydoc-markdown.""" + print("📚 Generating API documentation...") + + # Change to project root directory + project_root = Path(__file__).parent + os.chdir(project_root) + + # Generate documentation + if not run_command("pydoc-markdown", "Generating API documentation with pydoc-markdown"): + return False + + print("✅ Documentation generation completed!") + return True + + +def serve_docs(): + """Serve documentation using mkdocs.""" + print("🌐 Starting documentation server...") + + docs_dir = Path(__file__).parent / "docs" + os.chdir(docs_dir) + + print("Starting mkdocs server at http://127.0.0.1:8000") + print("Press Ctrl+C to stop the server") + + try: + subprocess.run("mkdocs serve", shell=True, check=True) + except KeyboardInterrupt: + print("\n👋 Documentation server stopped") + except subprocess.CalledProcessError as e: + print(f"❌ Failed to start mkdocs server: {e}") + return False + + return True + + +def build_docs(): + """Build static documentation files.""" + print("🏗️ Building static documentation...") + + docs_dir = Path(__file__).parent / "docs" + os.chdir(docs_dir) + + return run_command("mkdocs build", "Building static documentation") + + +def main(): + """Handle command line arguments and execute requested actions.""" + if len(sys.argv) < 2: + print("📖 pyGCodeDecode Documentation Manager") + print("\nUsage:") + print(" python docs_manager.py generate - Generate API documentation") + print(" python docs_manager.py serve - Serve documentation locally") + print(" python docs_manager.py build - Build static documentation") + print(" python docs_manager.py all - Generate and serve documentation") + return + + command = sys.argv[1].lower() + + if command == "generate": + generate_docs() + elif command == "serve": + serve_docs() + elif command == "build": + if generate_docs(): + build_docs() + elif command == "all": + if generate_docs(): + serve_docs() + else: + print(f"❌ Unknown command: {command}") + print("Available commands: generate, serve, build, all") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 3a00b3b..31e6a8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,10 @@ DEVELOPER = [ "pre-commit", "pytest-cov", ] +DOCS = [ + "mkdocs", + "pydoc-markdown", +] [project.urls] Code = "https://github.com/FAST-LB/pyGCodeDecode" @@ -72,15 +76,3 @@ omit = ["./tests/*", "./example/*"] [tool.coverage.report] # Regexes for lines to exclude from consideration exclude_also = ["if __name__ == .__main__.:", "except", "import"] - -[[tool.pydoc-markdown.loaders]] -type = "python" -search_path = ["."] - -[tool.pydoc-markdown.renderer] -type = "mkdocs" - -[[tool.pydoc-markdown.renderer.pages]] -title = "API Documentation" -name = "index" -contents = ["pyGCodeDecode.*"] From fc0c4696e7ea9d51dae89c456dcb1a79b0eaf46f Mon Sep 17 00:00:00 2001 From: usmfi Date: Tue, 5 Aug 2025 15:14:32 +0200 Subject: [PATCH 16/68] fix readme link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 561de2a..b252e30 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ pygcd plot --gcode ## Creating a script using pyGCD -Example simulations are provided in [./examples/](https://github.com/FAST-LB/pyGCodeDecode/blob/main/examples/) and can be modified to suit your needs. If you want to start from scratch, the following instructions will help you setup and run a simulation. +Example simulations are provided in [./examples/](https://github.com/FAST-LB/pyGCodeDecode/blob/main/pyGCodeDecode/examples/) and can be modified to suit your needs. If you want to start from scratch, the following instructions will help you setup and run a simulation. ### Define your printer defaults in a `.yaml` file From c8c11be38435a06a9f4e3904ea388f9274aa062d Mon Sep 17 00:00:00 2001 From: usmfi Date: Tue, 5 Aug 2025 15:20:53 +0200 Subject: [PATCH 17/68] more readme updates --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b252e30..1b479aa 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ prusa_mini: vY: 180 vZ: 12 vE: 80 - firmware: marlin_jerk + firmware: prusa ``` The default settings usually are machine specific and often can be read from the printer using a serial connection by sending a GCode command. You can use `M503` for Marlin, Prusa and some other firmwares. @@ -104,7 +104,7 @@ from pyGCodeDecode import gcode_interpreter 1. Load your setup `.yaml` file through: ```python -setup = gcode_interpreter.setup(filename=r"e./pygcodedecode/data/default_printer_presets.yaml") +setup = gcode_interpreter.setup(filename=r"./pyGCodeDecode/data/default_printer_presets.yaml") ``` 1. Select your printer from the setup by name: @@ -138,15 +138,18 @@ simulation.get_values(t=2.6) You can visualize the GCode by plotting it in 3D: ```python -simulation.plot_3d() +from pyGCodeDecode.plotter import plot_3d +plot_3d(simulation) ``` pyGCD can also be used to create files defining an event series for ABAQUS simulations. ```python +from pyGCodeDecode.abaqus_file_generator import generate_abaqus_event_series + generate_abaqus_event_series( simulation=simulation, - filpath="path/to/event_series.csv" + filepath="path/to/event_series.csv" ) ``` From c939ae5406bd9e99d4e87c7226d298952c7a3f49 Mon Sep 17 00:00:00 2001 From: usmfi Date: Tue, 5 Aug 2025 15:25:40 +0200 Subject: [PATCH 18/68] doc builder? --- .gitlab-ci.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 282bb5c..842d7f6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -36,6 +36,23 @@ test-package: path: ./tests/coverage.xml when: always +doc-build: + stage: doc + image: "python:3.11" + needs: [] + before_script: + - pip install --upgrade pip + script: + - pip install -e .[DOCS] + - pydoc-markdown + - mkdocs build + artifacts: + untracked: false + paths: + - ./docs/site/ + when: always + expire_in: "30 days" + doc-compile_paper: stage: doc needs: [] From 7c069b439eeb4a0c0cb09efdff57797ec256f4e8 Mon Sep 17 00:00:00 2001 From: usmfi Date: Tue, 5 Aug 2025 15:39:10 +0200 Subject: [PATCH 19/68] fix attempt --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 842d7f6..2fc0c7d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -45,7 +45,7 @@ doc-build: script: - pip install -e .[DOCS] - pydoc-markdown - - mkdocs build + - cd docs && mkdocs build artifacts: untracked: false paths: From 300ad47d0d7b599c17279dc2b2b8f73c865e5217 Mon Sep 17 00:00:00 2001 From: usmfi Date: Tue, 5 Aug 2025 15:45:32 +0200 Subject: [PATCH 20/68] incl mds --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2fc0c7d..de9af59 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -50,6 +50,7 @@ doc-build: untracked: false paths: - ./docs/site/ + - ./docs/content/*.md when: always expire_in: "30 days" From e3d8f93d30cab3436ae584ffacaf1a0ca396760e Mon Sep 17 00:00:00 2001 From: usmfi Date: Tue, 5 Aug 2025 16:01:00 +0200 Subject: [PATCH 21/68] manual ordering? --- pyGCodeDecode/plotter.py | 18 +----------------- pydoc-markdown.yml | 12 ++++++++++++ 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/pyGCodeDecode/plotter.py b/pyGCodeDecode/plotter.py index b96900b..c356d75 100644 --- a/pyGCodeDecode/plotter.py +++ b/pyGCodeDecode/plotter.py @@ -1,20 +1,4 @@ -"""This module provides functionality for 3D plotting of G-code simulation data using PyVista. - -Functions: - plot_3d: Generates a 3D plot of the simulation data, with options for customization such as - extrusion-only plotting, scalar value selection, layer selection, and saving the plot - as a screenshot or VTK file. - plot_2d: Generates a 2D plot of the simulation data, showing the position of the extruder head - over time. - -Dependencies: - - pyGCodeDecode.gcode_interpreter.simulation - - pyGCodeDecode.gcode_interpreter.unpack_blocklist - - pyGCodeDecode.utils - - numpy - - pyvista - - pathlib -""" +"""This module provides functionality for 3D plotting of G-code simulation data using PyVista.""" import os import pathlib diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml index ca85fd5..5cce07e 100644 --- a/pydoc-markdown.yml +++ b/pydoc-markdown.yml @@ -37,6 +37,18 @@ renderer: name: api contents: - pyGCodeDecode.* + # - pyGCodeDecode.cli + # - pyGCodeDecode.gcode_interpreter + # - pyGCodeDecode.abaqus_file_generator + # - pyGCodeDecode.plotter + # - pyGCodeDecode.tools + # - pyGCodeDecode.result + # - pyGCodeDecode.helpers + # - pyGCodeDecode.junction_handling + # - pyGCodeDecode.planner_block + # - pyGCodeDecode.state + # - pyGCodeDecode.state_generator + # - pyGCodeDecode.utils mkdocs_config: site_name: pyGCodeDecode Documentation site_description: Generate time dependent boundary conditions from a .gcode file From b31e994a9728ee26297e72a4a31c6acae25c395f Mon Sep 17 00:00:00 2001 From: usmfi Date: Tue, 5 Aug 2025 21:07:12 +0200 Subject: [PATCH 22/68] use my version of pydoc for sorted modules --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 31e6a8b..4e4132a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ DEVELOPER = [ ] DOCS = [ "mkdocs", - "pydoc-markdown", + "pydoc-markdown @ git+https://github.com/jeknirsch/pydoc-markdown.git", ] [project.urls] From 9ed21a0fbc71debdbb737020324eab9c1ae51aca Mon Sep 17 00:00:00 2001 From: usmfi Date: Tue, 5 Aug 2025 21:12:40 +0200 Subject: [PATCH 23/68] He who loves his repo, pushes --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4e4132a..19ef65e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ DEVELOPER = [ ] DOCS = [ "mkdocs", - "pydoc-markdown @ git+https://github.com/jeknirsch/pydoc-markdown.git", + "pydoc-markdown @ git+https://github.com/jeknirsch/pydoc-markdown.git@sort_modules", ] [project.urls] From 92761215442869e85d9d4ef59ff61671baa1f8ec Mon Sep 17 00:00:00 2001 From: usmfi Date: Wed, 6 Aug 2025 12:58:37 +0200 Subject: [PATCH 24/68] fix 3d plotter for benchy example --- pyGCodeDecode/plotter.py | 63 ++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/pyGCodeDecode/plotter.py b/pyGCodeDecode/plotter.py index c356d75..4266606 100644 --- a/pyGCodeDecode/plotter.py +++ b/pyGCodeDecode/plotter.py @@ -25,6 +25,7 @@ def plot_3d( vtk_path: pathlib.Path = None, mesh: pv.MultiBlock = None, layer_select: int = None, + z_scaler: float = None, window_size: tuple = (2048, 1536), mpl_subplot: bool = False, mpl_rcParams: Union[dict, None] = None, @@ -36,31 +37,38 @@ def plot_3d( extra_plotting: callable = None, # function to add plotting, args: plotter, mesh overwrite_labels: Union[dict, None] = None, scalar_value_bounds: Union[Tuple[float, float], None] = None, + return_type: str = "mesh", # "mesh" or "image", only available with screenshot_path ) -> pv.MultiBlock: - """3D Plot with PyVista. + """Plot a 3D visualization of G-code simulation data using PyVista. Args: - extrusion_only (bool, optional): Plot only parts where material is extruded. - Defaults to True. - scalar_value (str, optional): scalar value to plot. Defaults to Velocity (vel). - Options: vel, rel_vel_err, or None. - screenshot_path (pathlib.Path, optional): Path to screenshot to be saved. - Prevents interactive plot. Defaults to None. - vtk_path (pathlib.Path, optional): Path to vtk to be saved. - Prevents interactive plot. Defaults to None. - mesh (pv.MultiBlock, optional): A pyvista mesh from a previous run to - avoid running the mesh generation again. Defaults to None. - layer_select (int, optional): Select the layer to be plotted. - Defaults to None, which plots all layers. - window_size (tuple, optional): Size of the plot window. - Defaults to (2048, 1536). - mpl_subplot (bool, optional): Use matplotlib subplot for the screenshot. - Defaults to False. - solid_color (str, optional): Background color of the plot. Defaults to "black". - transparent_background (bool, optional): Use a transparent background for - the screenshot. Defaults to True. + sim (simulation): The simulation object containing blocklist and segment data. + extrusion_only (bool, optional): If True, plot only segments where extrusion occurs. Defaults to True. + scalar_value (str, optional): Scalar value to color the plot. Options: "velocity", "rel_vel_err", "acceleration", or None. Defaults to "velocity". + screenshot_path (pathlib.Path, optional): If provided, saves a screenshot to this path and disables interactive plotting. Defaults to None. + camera_settings (dict, optional): Camera settings for the plotter. Keys: "camera_position", "elevation", "azimuth", "roll". Defaults to None. + vtk_path (pathlib.Path, optional): If provided, saves the mesh as a VTK file to this path. Defaults to None. + mesh (pv.MultiBlock, optional): Precomputed PyVista mesh to use instead of generating a new one. Defaults to None. + layer_select (int, optional): If provided, only plot the specified layer. Defaults to None (all layers). + z_scaler (float, optional): Scaling factor for the z-axis layer squishing (z_scaler = width/height of extrusion). Defaults to None (automatic scaling). + window_size (tuple, optional): Size of the plot window in pixels. Defaults to (2048, 1536). + mpl_subplot (bool, optional): If True, use matplotlib for screenshot and colorbar. Defaults to False. + mpl_rcParams (dict or None, optional): Custom matplotlib rcParams for styling. Defaults to None. + solid_color (str, optional): Background color for the plot. Defaults to "black". + transparent_background (bool, optional): If True, screenshot background is transparent. Defaults to True. + parallel_projection (bool, optional): If True, enables parallel projection in PyVista. Defaults to False. + lighting (bool, optional): If True, enables lighting in the plot. Defaults to True. + block_colorbar (bool, optional): If True, removes the scalar colorbar from the plot. Defaults to False. + extra_plotting (callable, optional): Function to add extra plotting to the PyVista plotter. Signature: (plotter, mesh). Defaults to None. + overwrite_labels (dict or None, optional): Dictionary to overwrite colorbar labels. Defaults to None. + scalar_value_bounds (tuple or None, optional): Tuple (min, max) to set scalar colorbar range. Defaults to None. + return_type (str, optional): Return type, "mesh" or "image". Defaults to "mesh". + Returns: - pv.MultiBlock: The mesh used in the plot so it can be used (e.g. in subsequent plots). + pv.MultiBlock: The PyVista mesh used for plotting. + or + np.ndarray: The screenshot image if `screenshot_path` is provided and `return_type` is "image". + """ def safe_screenshot(plotter: pv.Plotter, screenshot_path=None): @@ -91,13 +99,15 @@ def safe_screenshot(plotter: pv.Plotter, screenshot_path=None): else: segments = unpack_blocklist(blocklist=sim.blocklist) + if z_scaler is None: + e_width = 0.45 + l_height = 0.2 + z_scaler = e_width / l_height + if mesh is None: mesh = pv.MultiBlock() x, y, z, e, scalar = [], [], [], [], [] - e_width = 0.45 - l_height = 0.2 - z_scaler = e_width / l_height bar = ProgressBar(name="3D Plot") custom_print("result: ", lvl=3) for n, segm in enumerate(segments): @@ -267,7 +277,10 @@ def safe_screenshot(plotter: pv.Plotter, screenshot_path=None): if block_colorbar: p.remove_scalar_bar() image = safe_screenshot(p, screenshot_path) - return image # TODO this not good + + if return_type == "image": + return image + return mesh if not off_screen and display_available: p.show() From 77fe8b7735a9a3112ed8d85bfca2445d924227b8 Mon Sep 17 00:00:00 2001 From: usmfi Date: Wed, 6 Aug 2025 13:00:40 +0200 Subject: [PATCH 25/68] cli docu and typos --- pyGCodeDecode/cli.py | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/pyGCodeDecode/cli.py b/pyGCodeDecode/cli.py index 19182b6..df5fcd1 100644 --- a/pyGCodeDecode/cli.py +++ b/pyGCodeDecode/cli.py @@ -1,4 +1,26 @@ -"""The CLI for the pyGCodeDecode package.""" +"""The pyGCodeDecode CLI Module. + +Interact with pyGCodeDecode via the command line to run examples and plot GCode files. + +Features: +- Run built-in examples: `brace`, `benchy` +- Plot GCode files with printer presets and output options +- Save simulation summaries, metrics, screenshots, and VTK files + +Usage Examples: +- `pygcd --help` +- `pygcd run_example brace` +- `pygcd plot -g myfile.gcode` +- `pygcd plot -g myfile.gcode -p presets.yaml -pn my_printer` +- `pygcd plot -g myfile.gcode -o ./outputs -lc ";LAYER"` + +Plot Options: +- `-g, --gcode ` Path to GCode file (searches CWD if not specified) +- `-p, --presets ` Printer presets YAML file +- `-pn, --printer_name ` Printer name from presets +- `-o, --out_dir ` Output directory +- `-lc, --layer_cue ` Layer switch cue in GCode +""" import argparse import importlib.resources @@ -135,7 +157,7 @@ def _get_out_dir(out_dir: pathlib.Path | None, g_code_file: pathlib.Path) -> pat sim.plot_3d(mesh=mesh) -def _main(*args): +def _main(args=None): """Entry point function for the command-line interface (CLI).""" global_parser = argparse.ArgumentParser( prog="pygcd", @@ -148,7 +170,7 @@ def _main(*args): version=__version__, ) - # subparsers vor various functions + # subparsers for various functions subparsers = global_parser.add_subparsers( title="subcommands", description="Functions accessible via this CLI.", @@ -215,10 +237,10 @@ def _main(*args): ) # parse the arguments - args = global_parser.parse_args() + parsed_args = global_parser.parse_args(args) # call the respective function specified by the subparser - if hasattr(args, "func"): - args.func(args) + if hasattr(parsed_args, "func"): + parsed_args.func(parsed_args) else: global_parser.print_help() From 591b7b91db4738722109802d2af96735c51fb3d7 Mon Sep 17 00:00:00 2001 From: usmfi Date: Wed, 6 Aug 2025 13:12:41 +0200 Subject: [PATCH 26/68] formatting --- pyGCodeDecode/cli.py | 9 ++------- pyGCodeDecode/plotter.py | 1 - 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/pyGCodeDecode/cli.py b/pyGCodeDecode/cli.py index df5fcd1..cdd7ed2 100644 --- a/pyGCodeDecode/cli.py +++ b/pyGCodeDecode/cli.py @@ -3,23 +3,18 @@ Interact with pyGCodeDecode via the command line to run examples and plot GCode files. Features: + - Run built-in examples: `brace`, `benchy` - Plot GCode files with printer presets and output options - Save simulation summaries, metrics, screenshots, and VTK files Usage Examples: + - `pygcd --help` - `pygcd run_example brace` - `pygcd plot -g myfile.gcode` - `pygcd plot -g myfile.gcode -p presets.yaml -pn my_printer` - `pygcd plot -g myfile.gcode -o ./outputs -lc ";LAYER"` - -Plot Options: -- `-g, --gcode ` Path to GCode file (searches CWD if not specified) -- `-p, --presets ` Printer presets YAML file -- `-pn, --printer_name ` Printer name from presets -- `-o, --out_dir ` Output directory -- `-lc, --layer_cue ` Layer switch cue in GCode """ import argparse diff --git a/pyGCodeDecode/plotter.py b/pyGCodeDecode/plotter.py index 4266606..818d647 100644 --- a/pyGCodeDecode/plotter.py +++ b/pyGCodeDecode/plotter.py @@ -68,7 +68,6 @@ def plot_3d( pv.MultiBlock: The PyVista mesh used for plotting. or np.ndarray: The screenshot image if `screenshot_path` is provided and `return_type` is "image". - """ def safe_screenshot(plotter: pv.Plotter, screenshot_path=None): From 05ab3043549f53d8ee950c640f0233f02f503b36 Mon Sep 17 00:00:00 2001 From: usmfi Date: Wed, 6 Aug 2025 15:27:17 +0200 Subject: [PATCH 27/68] more descriptive method in doc --- pydoc-markdown.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml index 5cce07e..76c0f00 100644 --- a/pydoc-markdown.yml +++ b/pydoc-markdown.yml @@ -24,6 +24,8 @@ renderer: render_toc: false use_fixed_header_levels: true add_module_prefix: true + add_method_class_prefix: true + add_member_class_prefix: true header_level_by_type: Module: 2 Class: 3 @@ -33,7 +35,7 @@ renderer: - title: Home name: index source: README.md - - title: pyGCodeDecode Reference + - title: pyGCodeDecode API Reference name: api contents: - pyGCodeDecode.* From 884a398817bab315779916602658074ae8e4a782 Mon Sep 17 00:00:00 2001 From: usmfi Date: Wed, 6 Aug 2025 15:35:38 +0200 Subject: [PATCH 28/68] privatizing funcs and docstring cleanup --- pyGCodeDecode/gcode_interpreter.py | 2 +- pyGCodeDecode/junction_handling.py | 28 +++++++++++++--------------- pyGCodeDecode/plotter.py | 12 ++++++------ pyGCodeDecode/state_generator.py | 18 +++++++++--------- pyGCodeDecode/tools.py | 18 +++++++++--------- pyGCodeDecode/utils.py | 13 ++++++++----- 6 files changed, 46 insertions(+), 45 deletions(-) diff --git a/pyGCodeDecode/gcode_interpreter.py b/pyGCodeDecode/gcode_interpreter.py index 95003a2..6362da0 100644 --- a/pyGCodeDecode/gcode_interpreter.py +++ b/pyGCodeDecode/gcode_interpreter.py @@ -208,7 +208,7 @@ def __init__( custom_print( f"Simulating \"{self.filename}\" with {self.initial_machine_setup_dict['printer_name']} using " - f"the {self.firmware} firmware.\n" + f"the {self.firmware} firmware." ) self.blocklist: List[planner_block] = generate_planner_blocks(states=self.states, firmware=self.firmware) self.trajectory_self_correct() diff --git a/pyGCodeDecode/junction_handling.py b/pyGCodeDecode/junction_handling.py index a7ced3f..68bb493 100644 --- a/pyGCodeDecode/junction_handling.py +++ b/pyGCodeDecode/junction_handling.py @@ -1,4 +1,4 @@ -"""Junction handling module.""" +"""Junction handling module for calculating the velocity at junctions.""" import inspect import sys @@ -24,7 +24,7 @@ def __init__(self, state_A: state, state_B: state): self.state_A = state_A self.state_B = state_B self.target_vel = self.connect_state(state_A=state_A, state_B=state_B) - self.vel_next = self.calc_vel_next() + self.vel_next = self._calc_vel_next() def connect_state(self, state_A: state, state_B: state): """ @@ -52,7 +52,7 @@ def connect_state(self, state_A: state, state_B: state): target_vel = velocity(state_B.state_p_settings.speed * travel_direction) return target_vel - def calc_vel_next(self): + def _calc_vel_next(self): """Return the target velocity for the following move.""" next_next_state = self.state_B.next_state if self.state_B.next_state is not None else self.state_B while True: @@ -84,10 +84,6 @@ def get_junction_vel(self): class prusa(junction_handling): """Prusa specific classic jerk junction handling (validated on Prusa Mini). - **Reference** - [Prusa Firmware Buddy GitHub](https://github.com/prusa3d/Prusa-Firmware-Buddy/blob/818d812f954802903ea0ff39bf44376fb0b35dd2/lib/Marlin/Marlin/src/module/planner.cpp#L1911) # noqa: E501 - - **Code reference:** [Prusa-Firmware-Buddy/lib/Marlin/Marlin/src/module/planner.cpp](https://github.com/prusa3d/Prusa-Firmware-Buddy/blob/818d812f954802903ea0ff39bf44376fb0b35dd2/lib/Marlin/Marlin/src/module/planner.cpp#L1951) @@ -200,12 +196,6 @@ def get_junction_vel(self): class marlin(junction_handling): """Marlin classic jerk specific junction handling. - **Reference** - [https://github.com/MarlinFirmware/Marlin/pull/8887](https://github.com/MarlinFirmware/Marlin/pull/8887) - [https://github.com/MarlinFirmware/Marlin/pull/8888](https://github.com/MarlinFirmware/Marlin/pull/8888) - [https://github.com/MarlinFirmware/Marlin/issues/367#issuecomment-12505768](https://github.com/MarlinFirmware/Marlin/issues/367#issuecomment-12505768) - - **Code reference:** [Marlin/src/module/planner.cpp](https://github.com/MarlinFirmware/Marlin/blob/8ec9c379405bb9962aff170d305ddd0725bd64e2/Marlin/src/module/planner.cpp#L2762) ```cpp @@ -221,6 +211,12 @@ class marlin(junction_handling): ``` """ + """" **Reference** + [https://github.com/MarlinFirmware/Marlin/pull/8887](https://github.com/MarlinFirmware/Marlin/pull/8887) + [https://github.com/MarlinFirmware/Marlin/pull/8888](https://github.com/MarlinFirmware/Marlin/pull/8888) + [https://github.com/MarlinFirmware/Marlin/issues/367#issuecomment-12505768](https://github.com/MarlinFirmware/Marlin/issues/367#issuecomment-12505768) + """ + def __init__(self, state_A: state, state_B: state): """Marlin classic jerk specific junction velocity calculation. @@ -363,10 +359,12 @@ def get_junction_vel(self): class mka(prusa): - """Anisoprint A4 like junction handling. + """Anisoprint Composer models using MKA Firmware junction handling. + + The MKA firmware uses a similar approach to Prusa's classic jerk handling. **Code reference:** - [anisoprint/MKA-firmware/src/core/planner/planner.cpp#L1830](https://github.com/anisoprint/MKA-firmware/blob/6e02973b1b8f325040cc3dbf66ac545ffc5c06b3/src/core/planner/planner.cpp#L1830) + [anisoprint/MKA-firmware/src/core/planner/planner.cpp](https://github.com/anisoprint/MKA-firmware/blob/6e02973b1b8f325040cc3dbf66ac545ffc5c06b3/src/core/planner/planner.cpp#L1830) ```cpp // ... float v_exit = previous_speed[axis] * smaller_speed_factor, diff --git a/pyGCodeDecode/plotter.py b/pyGCodeDecode/plotter.py index 818d647..5baa1e5 100644 --- a/pyGCodeDecode/plotter.py +++ b/pyGCodeDecode/plotter.py @@ -70,7 +70,7 @@ def plot_3d( np.ndarray: The screenshot image if `screenshot_path` is provided and `return_type` is "image". """ - def safe_screenshot(plotter: pv.Plotter, screenshot_path=None): + def _safe_screenshot(plotter: pv.Plotter, screenshot_path=None): if display_available: img = plotter.screenshot( transparent_background=transparent_background, @@ -255,7 +255,7 @@ def safe_screenshot(plotter: pv.Plotter, screenshot_path=None): p.remove_scalar_bar() # image = p.screenshot(transparent_background=True, window_size=window_size) - image = safe_screenshot(p) + image = _safe_screenshot(p) # ax.axis("off") if not block_colorbar: cbar = fig.colorbar(dummy_img, ax=ax, shrink=0.6) @@ -263,7 +263,7 @@ def safe_screenshot(plotter: pv.Plotter, screenshot_path=None): else: # image = p.screenshot(transparent_background=True) - image = safe_screenshot(p) + image = _safe_screenshot(p) ax.axis("off") if image is not None: ax.imshow(image) @@ -275,7 +275,7 @@ def safe_screenshot(plotter: pv.Plotter, screenshot_path=None): else: if block_colorbar: p.remove_scalar_bar() - image = safe_screenshot(p, screenshot_path) + image = _safe_screenshot(p, screenshot_path) if return_type == "image": return image @@ -307,7 +307,7 @@ def plot_2d( "Acceleration": "Acceleration in mm/s^2", } - def interp_2D(x, y, cvar, spatial_resolution=1): + def _interp_2D(x, y, cvar, spatial_resolution=1): segm_length = np.linalg.norm([np.ediff1d(x), np.ediff1d(y)], axis=0) segm_cvar_delt = np.greater(np.abs(np.ediff1d(cvar)), 0) segm_interpol = np.r_[ @@ -348,7 +348,7 @@ def interp_2D(x, y, cvar, spatial_resolution=1): cvar.append(segm.vel_end.get_norm()) # interpolate values for smooth coloring - interpolated = interp_2D(x, y, cvar, spatial_resolution=colvar_spatial_resolution) + interpolated = _interp_2D(x, y, cvar, spatial_resolution=colvar_spatial_resolution) x = interpolated[:, 0] y = interpolated[:, 1] diff --git a/pyGCodeDecode/state_generator.py b/pyGCodeDecode/state_generator.py index aa39cc2..385abc2 100644 --- a/pyGCodeDecode/state_generator.py +++ b/pyGCodeDecode/state_generator.py @@ -106,7 +106,7 @@ } -def arg_extract(string: str, key_dict: dict) -> dict: +def _arg_extract(string: str, key_dict: dict) -> dict: """ Extract arguments from known command dictionaries. @@ -165,7 +165,7 @@ def arg_extract(string: str, key_dict: dict) -> dict: arg = string[match_end:] # special case for comments where everything coming after match is arg if key_dict[key] is not None: # check for nested commands - arg = arg_extract(arg, key_dict[key]) # call arg_extract through recursion + arg = _arg_extract(arg, key_dict[key]) # call _arg_extract through recursion # save matches found outside of comments, not applying for comments if match.end() <= comment_begin or key == ";": @@ -174,7 +174,7 @@ def arg_extract(string: str, key_dict: dict) -> dict: return arg_dict -def read_gcode_to_dict_list(filepath: pathlib.Path) -> List[dict]: +def _read_gcode_to_dict_list(filepath: pathlib.Path) -> List[dict]: """ Read gcode from .gcode file. @@ -189,7 +189,7 @@ def read_gcode_to_dict_list(filepath: pathlib.Path) -> List[dict]: with open(file=filepath) as file_gcode: for i, line in enumerate(file_gcode): - line_dict = arg_extract(string=line, key_dict=known_commands) + line_dict = _arg_extract(string=line, key_dict=known_commands) line_dict["line_number"] = i + 1 dict_list.append(line_dict) @@ -198,7 +198,7 @@ def read_gcode_to_dict_list(filepath: pathlib.Path) -> List[dict]: return dict_list -def dict_list_traveler(line_dict_list: List[dict], initial_machine_setup: dict) -> List[state]: +def _dict_list_traveler(line_dict_list: List[dict], initial_machine_setup: dict) -> List[state]: """ Convert the line dictionary to a state. @@ -396,7 +396,7 @@ def apply_extrusion(line_dict: dict, virtual_machine: dict, command: str) -> dic return state_list -def check_for_unsupported_commands(line_dict_list: dict) -> dict: +def _check_for_unsupported_commands(line_dict_list: dict) -> dict: """Search for unsupported commands used in the G-code, warn the user and return the occurrences. Args: @@ -437,8 +437,8 @@ def generate_states(filepath: pathlib.Path, initial_machine_setup: dict) -> List Returns: states: (list[states]) all states in a list """ - line_dict_list = read_gcode_to_dict_list(filepath=filepath) - check_for_unsupported_commands(line_dict_list=line_dict_list) - states = dict_list_traveler(line_dict_list=line_dict_list, initial_machine_setup=initial_machine_setup) + line_dict_list = _read_gcode_to_dict_list(filepath=filepath) + _check_for_unsupported_commands(line_dict_list=line_dict_list) + states = _dict_list_traveler(line_dict_list=line_dict_list, initial_machine_setup=initial_machine_setup) return states diff --git a/pyGCodeDecode/tools.py b/pyGCodeDecode/tools.py index 6db75fe..5eb1051 100644 --- a/pyGCodeDecode/tools.py +++ b/pyGCodeDecode/tools.py @@ -168,7 +168,7 @@ def get_plane_orig(self): return [X_pos, X_neg, Y_pos, Y_neg, Z_pos, Z_neg] - def point_eval(point, pl_lim): + def _point_eval(point, pl_lim): p_eval = [] for lim_n, p_n in zip(pl_lim, point): inters_pl = [ @@ -178,10 +178,10 @@ def point_eval(point, pl_lim): p_eval.append(inters_pl) return p_eval - def point_inside(p_eval): + def _point_inside(p_eval): return all([all(p_ev_ax) for p_ev_ax in p_eval]) - def intersect_possible(p_eval0, p_eval1): + def _intersect_possible(p_eval0, p_eval1): possible = False for ax_eval0, ax_eval1 in zip(p_eval0, p_eval1): if ax_eval0 != ax_eval1: @@ -196,7 +196,7 @@ def intersect_possible(p_eval0, p_eval1): return possible - def isect_line_plane(p0, p1, p_co, p_no, epsilon=1e-6): + def _isect_line_plane(p0, p1, p_co, p_no, epsilon=1e-6): """Return a Vector or None (when the intersection can't be found). p0, p1: Define the line. @@ -239,19 +239,19 @@ def isect_line_plane(p0, p1, p_co, p_no, epsilon=1e-6): timetable = [] for block in simulation.blocklist: - p_eval_A = point_eval(block.state_A.state_position.get_vec(), control_volume.get_plane_lim()) - p_eval_B = point_eval(block.state_B.state_position.get_vec(), control_volume.get_plane_lim()) + p_eval_A = _point_eval(block.state_A.state_position.get_vec(), control_volume.get_plane_lim()) + p_eval_B = _point_eval(block.state_B.state_position.get_vec(), control_volume.get_plane_lim()) - if intersect_possible(p_eval0=p_eval_A, p_eval1=p_eval_B): + if _intersect_possible(p_eval0=p_eval_A, p_eval1=p_eval_B): for plane_orig, plane_normal in zip(control_volume.get_plane_orig(), control_volume.get_plane_normals()): - isec, s_len, sgn = isect_line_plane( + isec, s_len, sgn = _isect_line_plane( p0=block.state_A.state_position.get_vec(), p1=block.state_B.state_position.get_vec(), p_co=plane_orig, p_no=plane_normal, ) - if isec is not None and point_inside(p_eval=point_eval(isec, control_volume.get_plane_lim())): + if isec is not None and _point_inside(p_eval=_point_eval(isec, control_volume.get_plane_lim())): timetable.append([float(block.inverse_time_at_pos(s_len)), sgn]) timetable = np.asarray(timetable) # convert list to array for sorting diff --git a/pyGCodeDecode/utils.py b/pyGCodeDecode/utils.py index c938e24..c746700 100644 --- a/pyGCodeDecode/utils.py +++ b/pyGCodeDecode/utils.py @@ -21,11 +21,14 @@ class seconds(float): Args: value (float or int): The time duration in seconds. Examples: - >>> t = seconds(5) - >>> str(t) - '5.0 s' - >>> t.seconds - 5.0 + ```python + >>> from pyGCodeDecode.utils import seconds + >>> t = seconds(5) + >>> str(t) + '5.0 s' + >>> t.seconds + 5.0 + ``` """ """Time class for storing time, behaves like a float with additional methods.""" From 63477242f90b523947ff390b072f79619666c78d Mon Sep 17 00:00:00 2001 From: usmfi Date: Wed, 6 Aug 2025 15:36:05 +0200 Subject: [PATCH 29/68] prettier progressbar and prints without overwriting the line plus test --- pyGCodeDecode/helpers.py | 52 ++++++++++++++++++++-- tests/test_progress_fix.py | 88 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 tests/test_progress_fix.py diff --git a/pyGCodeDecode/helpers.py b/pyGCodeDecode/helpers.py index 4d3a36d..c6c4f06 100644 --- a/pyGCodeDecode/helpers.py +++ b/pyGCodeDecode/helpers.py @@ -13,6 +13,9 @@ # global verbosity level VERBOSITY_LEVEL = 2 # default to INFO +# global progress bar state +_active_progress_bar = None + def set_verbosity_level(level: Optional[int]) -> None: """Set the global verbosity level.""" @@ -35,6 +38,8 @@ def custom_print(*args, lvl=2, **kwargs) -> None: **kwargs: keyword arguments to be passed to print """ + global _active_progress_bar + sanitized_args = [] if FLAG_USING_ABAQUS: # remove non-ascii characters like emojis as ABAQUS can't handle them @@ -45,9 +50,28 @@ def custom_print(*args, lvl=2, **kwargs) -> None: # print with verbosity level if lvl <= VERBOSITY_LEVEL: - levels = {3: "[DEBUG]:", 2: "[INFO]:", 1: "[WARNING]:"} + # If there's an active progress bar, clear the line first + if _active_progress_bar is not None: + # Clear the current line + sys.stdout.write("\r" + " " * 80 + "\r") + sys.stdout.flush() + + levels = { + 3: "[ DEBUG ]:", + 2: "[ INFO ]:", + 1: "[WARNING]:", + } prefix = levels.get(lvl, "") - print(prefix, *sanitized_args, **kwargs) + + # Print the message + if _active_progress_bar is not None: + # When progress bar is active, print without extra newlines + print(prefix, *sanitized_args, **kwargs) + # Redraw the progress bar immediately + _active_progress_bar._redraw_current_state() + else: + # Normal print when no progress bar is active + print(prefix, *sanitized_args, **kwargs) class ProgressBar: @@ -58,6 +82,15 @@ def __init__(self, name: str = "Percent", barLength: int = 10): self.name = name self.barLength = barLength self.last_progress_update = -1 + self.last_text = "" # Store the last progress bar text + + def _redraw_current_state(self) -> None: + """Redraw the current progress bar state.""" + if self.last_text and VERBOSITY_LEVEL >= 2: + # Remove any existing \r from the stored text and add it fresh + clean_text = self.last_text.lstrip("\r") + sys.stdout.write("\r" + clean_text) + sys.stdout.flush() def update(self, progress: float) -> None: """Display or update a console progress bar. @@ -65,10 +98,15 @@ def update(self, progress: float) -> None: Args: progress: float between 0 and 1 for percentage, < 0 represents a 'halt', > 1 represents 100% """ + global _active_progress_bar + barLength = self.barLength status = "" if VERBOSITY_LEVEL >= 2: # only print if verbosity level is high enough + # Register this progress bar as active + _active_progress_bar = self + # check whether the input is valid if progress is int: progress = float(progress) @@ -82,7 +120,7 @@ def update(self, progress: float) -> None: status = "- Waiting \r\n" if progress >= 1.0: progress = 1.0 - status = "- Done\r\n" + status = "- Done ✅" progress_percent = round(progress * 100, ndigits=1) @@ -90,6 +128,14 @@ def update(self, progress: float) -> None: if self.last_progress_update != progress_percent or status != "": block = int(round(barLength * progress, ndigits=0)) text = f"\r[{'#' * block + '-' * (barLength - block)}] {progress_percent} % of {self.name} {status}" + self.last_text = text sys.stdout.write(text) sys.stdout.flush() self.last_progress_update = progress_percent + + # If we're done, add a newline and unregister + if progress >= 1.0: + sys.stdout.write("\n") + sys.stdout.flush() + self.last_text = "" + _active_progress_bar = None diff --git a/tests/test_progress_fix.py b/tests/test_progress_fix.py new file mode 100644 index 0000000..370d40d --- /dev/null +++ b/tests/test_progress_fix.py @@ -0,0 +1,88 @@ +"""Test for progress bar and custom_print interaction.""" + +import time + +from pyGCodeDecode.helpers import ProgressBar, custom_print, set_verbosity_level + + +def test_progress_bar_with_interruptions(capsys): + """Run the test and capture output.""" + set_verbosity_level(3) + pb1 = ProgressBar("Test Process 1", 20) + for i in range(101): + pb1.update(i / 100.0) + if i == 30: + custom_print("This is a warning message during progress", lvl=1) + elif i == 60: + custom_print("This is an info message during progress", lvl=2) + time.sleep(0.001) + assert pb1.last_progress_update == 100 + + pb2 = ProgressBar("Test Process 2", 15) + for i in range(101): + pb2.update(i / 100.0) + if i == 50: + custom_print("Another message in the middle", lvl=2) + time.sleep(0.001) + assert pb2.last_progress_update == 100 + + custom_print("All tests completed!", lvl=2) + + out, _ = capsys.readouterr() + expected = ( + "[WARNING]: This is a warning message during progress\n" + "[ INFO ]: This is an info message during progress\n" + "[####################] 100.0 % of Test Process 1 - Done ✅\n" + "[ INFO ]: Another message in the middle\n" + "[###############] 100.0 % of Test Process 2 - Done ✅\n" + "[ INFO ]: All tests completed!\n" + ) + + # Normalize whitespace for progress bar lines + def normalize(line): + return line.rstrip() + + out_lines = [normalize(lin) for lin in out.splitlines()] + exp_lines = [normalize(lin) for lin in expected.splitlines()] + # Only keep lines that are expected (messages and final progress bars) + filtered_out_lines = [ + lin + for lin in out_lines + if lin.startswith("[WARNING]:") or lin.startswith("[ INFO ]:") or lin.endswith("- Done ✅") + ] + assert exp_lines == filtered_out_lines + + +def test_progress_bar_no_interruptions(capsys): + """Test progress bar without interruptions.""" + set_verbosity_level(3) + pb = ProgressBar("No Interruptions", 10) + for i in range(101): + pb.update(i / 100.0) + time.sleep(0.001) + assert pb.last_progress_update == 100 + custom_print("Done without interruptions", lvl=2) + out, _ = capsys.readouterr() + assert "[##########] 100.0 % of No Interruptions - Done ✅" in out + assert "[ INFO ]: Done without interruptions" in out + + +def test_progress_bar_multiple_messages(capsys): + """Test progress bar with multiple messages at different points.""" + set_verbosity_level(3) + pb = ProgressBar("Multiple Messages", 5) + for i in range(101): + pb.update(i / 100.0) + if i == 20: + custom_print("First info", lvl=2) + if i == 40: + custom_print("Second warning", lvl=1) + if i == 80: + custom_print("Third info", lvl=2) + time.sleep(0.001) + assert pb.last_progress_update == 100 + out, _ = capsys.readouterr() + assert "[ INFO ]: First info" in out + assert "[WARNING]: Second warning" in out + assert "[ INFO ]: Third info" in out + assert "[#####] 100.0 % of Multiple Messages - Done ✅" in out From c087f4b88d95896c041a4171ef3881cf7fdca78a Mon Sep 17 00:00:00 2001 From: usmfi Date: Wed, 6 Aug 2025 15:37:42 +0200 Subject: [PATCH 30/68] new doc push --- doc.md | 400 ++++++++++++++++++++++----------------------------------- 1 file changed, 156 insertions(+), 244 deletions(-) diff --git a/doc.md b/doc.md index 2d0acd8..c8a8134 100644 --- a/doc.md +++ b/doc.md @@ -1,4 +1,4 @@ -# pyGCodeDecode Reference +# pyGCodeDecode API Reference @@ -39,7 +39,23 @@ Generate abaqus event series. ## pyGCodeDecode.cli -The CLI for the pyGCodeDecode package. +The pyGCodeDecode CLI Module. + +Interact with pyGCodeDecode via the command line to run examples and plot GCode files. + +Features: + +- Run built-in examples: `brace`, `benchy` +- Plot GCode files with printer presets and output options +- Save simulation summaries, metrics, screenshots, and VTK files + +Usage Examples: + +- `pygcd --help` +- `pygcd run_example brace` +- `pygcd plot -g myfile.gcode` +- `pygcd plot -g myfile.gcode -p presets.yaml -pn my_printer` +- `pygcd plot -g myfile.gcode -o ./outputs -lc ";LAYER"` @@ -125,7 +141,7 @@ Simulation of .gcode with given machine parameters. -#### trajectory\_self\_correct +#### simulation.trajectory\_self\_correct ```python def trajectory_self_correct() @@ -135,7 +151,7 @@ Self correct all blocks in the blocklist with self_correction() method. -#### calc\_results +#### simulation.calc\_results ```python def calc_results() @@ -145,7 +161,7 @@ Calculate the results. -#### calculate\_averages +#### simulation.calculate\_averages ```python def calculate_averages() @@ -155,7 +171,7 @@ Calculate averages for averageable results. -#### get\_values +#### simulation.get\_values ```python def get_values(t: float, output_unit_system: str = None) -> Tuple[List[float]] @@ -177,7 +193,7 @@ Return unit system scaled values for vel and pos. -#### get\_width +#### simulation.get\_width ```python def get_width(t: float, @@ -200,7 +216,7 @@ Return the extrusion width for a certain extrusion height at time. -#### print\_summary +#### simulation.print\_summary ```python def print_summary(start_time: float) @@ -214,7 +230,7 @@ Print simulation summary to console. -#### refresh +#### simulation.refresh ```python def refresh(new_state_list: List[state] = None) @@ -229,7 +245,7 @@ Refresh simulation. Either through new state list or by rerunning the self.state -#### extrusion\_extent +#### simulation.extrusion\_extent ```python def extrusion_extent(output_unit_system: str = None) -> np.ndarray @@ -254,7 +270,7 @@ Return scaled xyz min & max while extruding. -#### extrusion\_max\_vel +#### simulation.extrusion\_max\_vel ```python def extrusion_max_vel(output_unit_system: str = None) -> np.float64 @@ -274,7 +290,7 @@ Return scaled maximum velocity while extruding. -#### save\_summary +#### simulation.save\_summary ```python def save_summary(filepath: Union[pathlib.Path, str]) @@ -294,7 +310,7 @@ Save summary to .yaml file. -#### get\_scaling\_factor +#### simulation.get\_scaling\_factor ```python def get_scaling_factor(output_unit_system: str = None) -> float @@ -324,7 +340,7 @@ Setup for printing simulation. -#### load\_setup +#### setup.load\_setup ```python def load_setup(filepath) @@ -338,7 +354,7 @@ Load setup from file. -#### check\_initial\_setup +#### setup.check\_initial\_setup ```python def check_initial_setup() @@ -348,7 +364,7 @@ Check the printer Dict for typos or missing parameters and raise errors if inval -#### select\_printer +#### setup.select\_printer ```python def select_printer(printer_name) @@ -362,7 +378,7 @@ Select printer by name. -#### set\_initial\_position +#### setup.set\_initial\_position ```python def set_initial_position(initial_position: Union[tuple, dict], @@ -388,7 +404,7 @@ setup.set_initial_position({"X": 1, "Y": 2, "Z": 3, "E": 4}) -#### set\_property +#### setup.set\_property ```python def set_property(property_dict: dict) @@ -411,7 +427,7 @@ setup.set_property({"layer_cue": "LAYER_CHANGE"}) -#### get\_dict +#### setup.get\_dict ```python def get_dict() -> dict @@ -425,7 +441,7 @@ Return the setup for the selected printer. -#### get\_scaling\_factor +#### setup.get\_scaling\_factor ```python def get_scaling_factor(input_unit_system: str = None) -> float @@ -451,7 +467,7 @@ Helper functions. -#### VERBOSITY\_LEVEL +#### pyGCodeDecode.helpers.VERBOSITY\_LEVEL default to INFO @@ -503,7 +519,7 @@ A simple progress bar for the console. -#### update +#### ProgressBar.update ```python def update(progress: float) -> None @@ -519,7 +535,7 @@ Display or update a console progress bar. ## pyGCodeDecode.junction\_handling -Junction handling module. +Junction handling module for calculating the velocity at junctions. @@ -533,7 +549,7 @@ Junction handling super class. -#### connect\_state +#### junction\_handling.connect\_state ```python def connect_state(state_A: state, state_B: state) @@ -551,19 +567,9 @@ Connect two states and generates the velocity for the move from state_A to state - `velocity` - (float) the target velocity for that travel move - - -#### calc\_vel\_next - -```python -def calc_vel_next() -``` - -Return the target velocity for the following move. - -#### get\_target\_vel +#### junction\_handling.get\_target\_vel ```python def get_target_vel() @@ -573,7 +579,7 @@ Return target velocity. -#### get\_junction\_vel +#### junction\_handling.get\_junction\_vel ```python def get_junction_vel() @@ -595,10 +601,6 @@ class prusa(junction_handling) Prusa specific classic jerk junction handling (validated on Prusa Mini). -**Reference** -[Prusa Firmware Buddy GitHub](https://github.com/prusa3d/Prusa-Firmware-Buddy/blob/818d812f954802903ea0ff39bf44376fb0b35dd2/lib/Marlin/Marlin/src/module/planner.cpp#L1911) # noqa: E501 - - **Code reference:** [Prusa-Firmware-Buddy/lib/Marlin/Marlin/src/module/planner.cpp](https://github.com/prusa3d/Prusa-Firmware-Buddy/blob/818d812f954802903ea0ff39bf44376fb0b35dd2/lib/Marlin/Marlin/src/module/planner.cpp#L1951) @@ -652,7 +654,7 @@ Prusa specific classic jerk junction handling (validated on Prusa Mini). -#### calc\_j\_vel +#### prusa.calc\_j\_vel ```python def calc_j_vel() @@ -662,7 +664,7 @@ Calculate the junction velocity. -#### get\_junction\_vel +#### prusa.get\_junction\_vel ```python def get_junction_vel() @@ -684,12 +686,6 @@ class marlin(junction_handling) Marlin classic jerk specific junction handling. -**Reference** -[https://github.com/MarlinFirmware/Marlin/pull/8887](https://github.com/MarlinFirmware/Marlin/pull/8887) -[https://github.com/MarlinFirmware/Marlin/pull/8888](https://github.com/MarlinFirmware/Marlin/pull/8888) -[https://github.com/MarlinFirmware/Marlin/issues/367#issuecomment-12505768](https://github.com/MarlinFirmware/Marlin/issues/367#issuecomment-12505768) - - **Code reference:** [Marlin/src/module/planner.cpp](https://github.com/MarlinFirmware/Marlin/blob/8ec9c379405bb9962aff170d305ddd0725bd64e2/Marlin/src/module/planner.cpp#L2762) ```cpp @@ -706,7 +702,7 @@ vmax_junction_sqr = sq(vmax_junction * v_factor); -#### calc\_j\_vel +#### marlin.calc\_j\_vel ```python def calc_j_vel() @@ -716,7 +712,7 @@ Calculate the junction velocity. -#### get\_junction\_vel +#### marlin.get\_junction\_vel ```python def get_junction_vel() @@ -774,7 +770,7 @@ block->max_entry_speed = vmax_junction; -#### calc\_j\_vel +#### ultimaker.calc\_j\_vel ```python def calc_j_vel() @@ -784,7 +780,7 @@ Calculate the junction velocity. -#### get\_junction\_vel +#### ultimaker.get\_junction\_vel ```python def get_junction_vel() @@ -804,10 +800,12 @@ Return the calculated junction velocity. class mka(prusa) ``` -Anisoprint A4 like junction handling. +Anisoprint Composer models using MKA Firmware junction handling. + +The MKA firmware uses a similar approach to Prusa's classic jerk handling. **Code reference:** -[anisoprint/MKA-firmware/src/core/planner/planner.cpp#L1830](https://github.com/anisoprint/MKA-firmware/blob/6e02973b1b8f325040cc3dbf66ac545ffc5c06b3/src/core/planner/planner.cpp#L1830) +[anisoprint/MKA-firmware/src/core/planner/planner.cpp](https://github.com/anisoprint/MKA-firmware/blob/6e02973b1b8f325040cc3dbf66ac545ffc5c06b3/src/core/planner/planner.cpp#L1830) ```cpp // ... float v_exit = previous_speed[axis] * smaller_speed_factor, @@ -850,7 +848,7 @@ Marlin specific junction handling with Junction Deviation. -#### calc\_JD +#### junction\_deviation.calc\_JD ```python def calc_JD(vel_0: velocity, vel_1: velocity, p_settings: state.p_settings) @@ -871,7 +869,7 @@ Calculate junction deviation velocity from 2 velocities. -#### get\_junction\_vel +#### junction\_deviation.get\_junction\_vel ```python def get_junction_vel() @@ -920,7 +918,7 @@ Planner Block Class. -#### move\_maker +#### planner\_block.move\_maker ```python def move_maker(v_end) @@ -934,7 +932,7 @@ Calculate the correct move type (trapezoidal,triangular or singular) and generat -#### self\_correction +#### planner\_block.self\_correction ```python def self_correction(tolerance=float("1e-12")) @@ -944,7 +942,7 @@ Check for interfacing vel and self correct. -#### timeshift +#### planner\_block.timeshift ```python def timeshift(delta_t: float) @@ -958,7 +956,7 @@ Shift planner block in time. -#### extrusion\_block\_max\_vel +#### planner\_block.extrusion\_block\_max\_vel ```python def extrusion_block_max_vel() -> Union[np.ndarray, None] @@ -973,7 +971,7 @@ Return max vel from planner block while extruding. -#### calc\_results +#### planner\_block.calc\_results ```python def calc_results(*additional_calculators: abstract_result) @@ -983,7 +981,7 @@ Calculate the result of the planner block. -#### prev\_block +#### planner\_block.prev\_block ```python @property @@ -994,7 +992,7 @@ Define prev_block as property. -#### next\_block +#### planner\_block.next\_block ```python @property @@ -1005,7 +1003,7 @@ Define next_block as property. -#### get\_segments +#### planner\_block.get\_segments ```python def get_segments() @@ -1015,7 +1013,7 @@ Return segments, contained by the planner block. -#### get\_block\_travel +#### planner\_block.get\_block\_travel ```python def get_block_travel() @@ -1025,7 +1023,7 @@ Return the travel length of the planner block. -#### inverse\_time\_at\_pos +#### planner\_block.inverse\_time\_at\_pos ```python def inverse_time_at_pos(dist_local) @@ -1048,76 +1046,66 @@ Get the global time, at which the local length is reached. This module provides functionality for 3D plotting of G-code simulation data using PyVista. -Functions: - plot_3d: Generates a 3D plot of the simulation data, with options for customization such as - extrusion-only plotting, scalar value selection, layer selection, and saving the plot - as a screenshot or VTK file. - plot_2d: Generates a 2D plot of the simulation data, showing the position of the extruder head - over time. - -Dependencies: - - pyGCodeDecode.gcode_interpreter.simulation - - pyGCodeDecode.gcode_interpreter.unpack_blocklist - - pyGCodeDecode.utils - - numpy - - pyvista - - pathlib - #### plot\_3d ```python -def plot_3d( - sim: simulation, - extrusion_only: bool = True, - scalar_value: str = "velocity", - screenshot_path: pathlib.Path = None, - camera_settings: dict = None, - vtk_path: pathlib.Path = None, - mesh: pv.MultiBlock = None, - layer_select: int = None, - window_size: tuple = (2048, 1536), - mpl_subplot: bool = False, - mpl_rcParams: Union[dict, None] = None, - solid_color: str = "black", - transparent_background: bool = True, - parallel_projection: bool = False, - lighting: bool = True, - block_colorbar: bool = False, - extra_plotting: callable = None, - overwrite_labels: Union[dict, None] = None, - scalar_value_bounds: Union[Tuple[float, float], - None] = None) -> pv.MultiBlock -``` - -3D Plot with PyVista. +def plot_3d(sim: simulation, + extrusion_only: bool = True, + scalar_value: str = "velocity", + screenshot_path: pathlib.Path = None, + camera_settings: dict = None, + vtk_path: pathlib.Path = None, + mesh: pv.MultiBlock = None, + layer_select: int = None, + z_scaler: float = None, + window_size: tuple = (2048, 1536), + mpl_subplot: bool = False, + mpl_rcParams: Union[dict, None] = None, + solid_color: str = "black", + transparent_background: bool = True, + parallel_projection: bool = False, + lighting: bool = True, + block_colorbar: bool = False, + extra_plotting: callable = None, + overwrite_labels: Union[dict, None] = None, + scalar_value_bounds: Union[Tuple[float, float], None] = None, + return_type: str = "mesh") -> pv.MultiBlock +``` + +Plot a 3D visualization of G-code simulation data using PyVista. **Arguments**: -- `extrusion_only` _bool, optional_ - Plot only parts where material is extruded. - Defaults to True. -- `scalar_value` _str, optional_ - scalar value to plot. Defaults to Velocity (vel). -- `Options` - vel, rel_vel_err, or None. -- `screenshot_path` _pathlib.Path, optional_ - Path to screenshot to be saved. - Prevents interactive plot. Defaults to None. -- `vtk_path` _pathlib.Path, optional_ - Path to vtk to be saved. - Prevents interactive plot. Defaults to None. -- `mesh` _pv.MultiBlock, optional_ - A pyvista mesh from a previous run to - avoid running the mesh generation again. Defaults to None. -- `layer_select` _int, optional_ - Select the layer to be plotted. - Defaults to None, which plots all layers. -- `window_size` _tuple, optional_ - Size of the plot window. - Defaults to (2048, 1536). -- `mpl_subplot` _bool, optional_ - Use matplotlib subplot for the screenshot. - Defaults to False. -- `solid_color` _str, optional_ - Background color of the plot. Defaults to "black". -- `transparent_background` _bool, optional_ - Use a transparent background for - the screenshot. Defaults to True. +- `sim` _simulation_ - The simulation object containing blocklist and segment data. +- `extrusion_only` _bool, optional_ - If True, plot only segments where extrusion occurs. Defaults to True. +- `scalar_value` _str, optional_ - Scalar value to color the plot. Options: "velocity", "rel_vel_err", "acceleration", or None. Defaults to "velocity". +- `screenshot_path` _pathlib.Path, optional_ - If provided, saves a screenshot to this path and disables interactive plotting. Defaults to None. +- `camera_settings` _dict, optional_ - Camera settings for the plotter. Keys: "camera_position", "elevation", "azimuth", "roll". Defaults to None. +- `vtk_path` _pathlib.Path, optional_ - If provided, saves the mesh as a VTK file to this path. Defaults to None. +- `mesh` _pv.MultiBlock, optional_ - Precomputed PyVista mesh to use instead of generating a new one. Defaults to None. +- `layer_select` _int, optional_ - If provided, only plot the specified layer. Defaults to None (all layers). +- `z_scaler` _float, optional_ - Scaling factor for the z-axis layer squishing (z_scaler = width/height of extrusion). Defaults to None (automatic scaling). +- `window_size` _tuple, optional_ - Size of the plot window in pixels. Defaults to (2048, 1536). +- `mpl_subplot` _bool, optional_ - If True, use matplotlib for screenshot and colorbar. Defaults to False. +- `mpl_rcParams` _dict or None, optional_ - Custom matplotlib rcParams for styling. Defaults to None. +- `solid_color` _str, optional_ - Background color for the plot. Defaults to "black". +- `transparent_background` _bool, optional_ - If True, screenshot background is transparent. Defaults to True. +- `parallel_projection` _bool, optional_ - If True, enables parallel projection in PyVista. Defaults to False. +- `lighting` _bool, optional_ - If True, enables lighting in the plot. Defaults to True. +- `block_colorbar` _bool, optional_ - If True, removes the scalar colorbar from the plot. Defaults to False. +- `extra_plotting` _callable, optional_ - Function to add extra plotting to the PyVista plotter. Signature: (plotter, mesh). Defaults to None. +- `overwrite_labels` _dict or None, optional_ - Dictionary to overwrite colorbar labels. Defaults to None. +- `scalar_value_bounds` _tuple or None, optional_ - Tuple (min, max) to set scalar colorbar range. Defaults to None. +- `return_type` _str, optional_ - Return type, "mesh" or "image". Defaults to "mesh". + **Returns**: -- `pv.MultiBlock` - The mesh used in the plot so it can be used (e.g. in subsequent plots). +- `pv.MultiBlock` - The PyVista mesh used for plotting. + or +- `np.ndarray` - The screenshot image if `screenshot_path` is provided and `return_type` is "image". @@ -1190,7 +1178,7 @@ Abstract class for result calculation. -#### name +#### abstract\_result.name ```python @property @@ -1202,7 +1190,7 @@ Name of the result. Has to be set in the derived class. -#### calc\_pblock +#### abstract\_result.calc\_pblock ```python @abstractmethod @@ -1213,7 +1201,7 @@ Calculate the result for a planner block. -#### calc\_segm +#### abstract\_result.calc\_segm ```python @abstractmethod @@ -1234,7 +1222,7 @@ The acceleration. -#### calc\_segm +#### acceleration\_result.calc\_segm ```python def calc_segm(segm: "segment", **kwargs) @@ -1244,7 +1232,7 @@ Calculate the acceleration for a segment. -#### calc\_pblock +#### acceleration\_result.calc\_pblock ```python def calc_pblock(pblock, **kwargs) @@ -1264,7 +1252,7 @@ The velocity. -#### calc\_segm +#### velocity\_result.calc\_segm ```python def calc_segm(segm: "segment", **kwargs) @@ -1274,7 +1262,7 @@ Calculate the velocity for a segment. -#### calc\_pblock +#### velocity\_result.calc\_pblock ```python def calc_pblock(pblock: "planner_block", **kwargs) @@ -1340,7 +1328,7 @@ Store Printing Settings. -#### state\_position +#### state.state\_position ```python @property @@ -1351,7 +1339,7 @@ Define property state_position. -#### state\_p\_settings +#### state.state\_p\_settings ```python @property @@ -1362,7 +1350,7 @@ Define property state_p_settings. -#### line\_number +#### state.line\_number ```python @property @@ -1373,7 +1361,7 @@ Define property line_number. -#### line\_number +#### state.line\_number ```python @line_number.setter @@ -1388,7 +1376,7 @@ Set line number. -#### next\_state +#### state.next\_state ```python @property @@ -1399,7 +1387,7 @@ Define property next_state. -#### next\_state +#### state.next\_state ```python @next_state.setter @@ -1414,7 +1402,7 @@ Set next state. -#### prev\_state +#### state.prev\_state ```python @property @@ -1425,7 +1413,7 @@ Define property prev_state. -#### prev\_state +#### state.prev\_state ```python @prev_state.setter @@ -1444,85 +1432,6 @@ Set previous state. State generator module. - - -#### arg\_extract - -```python -def arg_extract(string: str, key_dict: dict) -> dict -``` - -Extract arguments from known command dictionaries. - -**Arguments**: - -- `string` - (str) string of Commands -- `key_dict` - (dict) dictionary with known commands and subcommands - - -**Returns**: - -- `dict` - (dict) dictionary with all found keys and their arguments - - - -#### read\_gcode\_to\_dict\_list - -```python -def read_gcode_to_dict_list(filepath: pathlib.Path) -> List[dict] -``` - -Read gcode from .gcode file. - -**Arguments**: - -- `filepath` - (Path) filepath of the .gcode file - - -**Returns**: - -- `dict_list` - (list[dict]) list with every line as dict - - - -#### dict\_list\_traveler - -```python -def dict_list_traveler(line_dict_list: List[dict], - initial_machine_setup: dict) -> List[state] -``` - -Convert the line dictionary to a state. - -**Arguments**: - -- `line_dict_list` - (dict) dict list with commands -- `initial_machine_setup` - (dict) dict with initial machine setup [absolute_position, absolute_extrusion, units, initial_position...] - - -**Returns**: - -- `state_list` - (list[state]) all states in a list - - - -#### check\_for\_unsupported\_commands - -```python -def check_for_unsupported_commands(line_dict_list: dict) -> dict -``` - -Search for unsupported commands used in the G-code, warn the user and return the occurrences. - -**Arguments**: - -- `line_dict_list` _dict_ - list of dicts containing all commands appearing - - -**Returns**: - -- `dict` - a dict containing the appearing unsupported commands and how often they appear. - #### generate\_states @@ -1625,15 +1534,18 @@ A float subclass representing a time duration in seconds. **Examples**: - >>> t = seconds(5) - >>> str(t) - '5.0 s' - >>> t.seconds - 5.0 +```python +>>> from pyGCodeDecode.utils import seconds +>>> t = seconds(5) +>>> str(t) +'5.0 s' +>>> t.seconds +5.0 +``` -#### seconds +#### seconds.seconds ```python @property @@ -1662,7 +1574,7 @@ The vector_4D class stores 4D vector in x,y,z,e. -#### get\_vec +#### vector\_4D.get\_vec ```python def get_vec(withExtrusion=False) -> List[float] @@ -1681,7 +1593,7 @@ Return the 4D vector, optionally with extrusion. -#### get\_norm +#### vector\_4D.get\_norm ```python def get_norm(withExtrusion=False) -> float @@ -1710,7 +1622,7 @@ class position(vector_4D) -#### is\_travel +#### position.is\_travel ```python def is_travel(other) -> bool @@ -1729,7 +1641,7 @@ Return True if there is travel between self and other position. -#### is\_extruding +#### position.is\_extruding ```python def is_extruding(other: "position", ignore_retract: bool = True) -> bool @@ -1749,7 +1661,7 @@ Return True if there is extrusion between self and other position. -#### get\_t\_distance +#### position.get\_t\_distance ```python def get_t_distance(other=None, withExtrusion=False) -> float @@ -1779,7 +1691,7 @@ class velocity(vector_4D) -#### get\_norm\_dir +#### velocity.get\_norm\_dir ```python def get_norm_dir(withExtrusion=False) @@ -1798,7 +1710,7 @@ Get normalized vector (regarding travel distance), if only extrusion occurs, nor -#### avoid\_overspeed +#### velocity.avoid\_overspeed ```python def avoid_overspeed(p_settings) @@ -1817,7 +1729,7 @@ Return velocity without any axis overspeed. -#### not\_zero +#### velocity.not\_zero ```python def not_zero() @@ -1831,7 +1743,7 @@ Return True if velocity is not zero. -#### is\_extruding +#### velocity.is\_extruding ```python def is_extruding() @@ -1879,7 +1791,7 @@ contains: time, position, velocity -#### move\_segment\_time +#### segment.move\_segment\_time ```python def move_segment_time(delta_t: float) @@ -1893,7 +1805,7 @@ Move segment in time. -#### get\_velocity +#### segment.get\_velocity ```python def get_velocity(t: float) -> velocity @@ -1912,7 +1824,7 @@ Get current velocity of segment at a certain time. -#### get\_velocity\_by\_dist +#### segment.get\_velocity\_by\_dist ```python def get_velocity_by_dist(dist) @@ -1922,7 +1834,7 @@ Return the velocity at a certain local segment distance. -#### get\_position +#### segment.get\_position ```python def get_position(t: float) -> position @@ -1941,7 +1853,7 @@ Get current position of segment at a certain time. -#### get\_segm\_len +#### segment.get\_segm\_len ```python def get_segm_len() @@ -1951,7 +1863,7 @@ Return the length of the segment. -#### get\_segm\_duration +#### segment.get\_segm\_duration ```python def get_segm_duration() @@ -1961,7 +1873,7 @@ Return the duration of the segment. -#### self\_check +#### segment.self\_check ```python def self_check(p_settings: "state.p_settings" = None) @@ -1979,7 +1891,7 @@ Check the segment for self consistency. -#### is\_extruding +#### segment.is\_extruding ```python def is_extruding() -> bool @@ -1993,7 +1905,7 @@ Return true if the segment is pos. extruding. -#### get\_result +#### segment.get\_result ```python def get_result(key) @@ -2012,7 +1924,7 @@ Return the requested result. -#### create\_initial +#### segment.create\_initial ```python @classmethod From 1d7d8ab904c13d8dc1cc257ac99747ddcc878dc1 Mon Sep 17 00:00:00 2001 From: usmfi Date: Wed, 6 Aug 2025 19:24:10 +0200 Subject: [PATCH 31/68] flake8 is confused or me is confused --- pyGCodeDecode/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyGCodeDecode/helpers.py b/pyGCodeDecode/helpers.py index c6c4f06..998f522 100644 --- a/pyGCodeDecode/helpers.py +++ b/pyGCodeDecode/helpers.py @@ -38,7 +38,7 @@ def custom_print(*args, lvl=2, **kwargs) -> None: **kwargs: keyword arguments to be passed to print """ - global _active_progress_bar + # global _active_progress_bar sanitized_args = [] if FLAG_USING_ABAQUS: From 53a23f4b5e077b9225825fd39d8a2ee8dee0b9e5 Mon Sep 17 00:00:00 2001 From: usmfi Date: Thu, 7 Aug 2025 12:42:16 +0200 Subject: [PATCH 32/68] updated plot in CLI to work with new plotter --- pyGCodeDecode/cli.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyGCodeDecode/cli.py b/pyGCodeDecode/cli.py index cdd7ed2..be8d514 100644 --- a/pyGCodeDecode/cli.py +++ b/pyGCodeDecode/cli.py @@ -26,6 +26,7 @@ from pyGCodeDecode.examples.brace import brace_example from pyGCodeDecode.gcode_interpreter import setup, simulation from pyGCodeDecode.helpers import custom_print +from pyGCodeDecode.plotter import plot_3d from pyGCodeDecode.tools import save_layer_metrics @@ -140,7 +141,8 @@ def _get_out_dir(out_dir: pathlib.Path | None, g_code_file: pathlib.Path) -> pat delimiter=",", ) # create a 3D-plot and save a VTK as well as a screenshot - mesh = sim.plot_3d( + mesh = plot_3d( + sim, extrusion_only=True, screenshot_path=out_dir / f"{g_code_file.stem}.png", vtk_path=out_dir / f"{g_code_file.stem}.vtk", @@ -149,7 +151,7 @@ def _get_out_dir(out_dir: pathlib.Path | None, g_code_file: pathlib.Path) -> pat mesh = None # create an interactive 3D-plot - sim.plot_3d(mesh=mesh) + plot_3d(sim, mesh=mesh) def _main(args=None): From 39d04a5da1240942374ae99de23f1da5b816462c Mon Sep 17 00:00:00 2001 From: usmfi Date: Thu, 7 Aug 2025 14:17:38 +0200 Subject: [PATCH 33/68] einheitliche prints everywhere --- pyGCodeDecode/abaqus_file_generator.py | 2 +- pyGCodeDecode/cli.py | 12 +++--- pyGCodeDecode/examples/benchy.py | 9 ++-- pyGCodeDecode/examples/brace.py | 5 ++- pyGCodeDecode/gcode_interpreter.py | 12 +++--- pyGCodeDecode/helpers.py | 49 +++++++++++++++++----- pyGCodeDecode/planner_block.py | 4 +- pyGCodeDecode/plotter.py | 8 ++-- pyGCodeDecode/state.py | 58 +++++++++++++++++--------- pyGCodeDecode/state_generator.py | 24 ++++++++--- pyGCodeDecode/tools.py | 4 +- pyGCodeDecode/utils.py | 2 +- tests/test_progress_fix.py | 43 ++++++++++++------- 13 files changed, 153 insertions(+), 79 deletions(-) diff --git a/pyGCodeDecode/abaqus_file_generator.py b/pyGCodeDecode/abaqus_file_generator.py index 65c2702..a759a3b 100644 --- a/pyGCodeDecode/abaqus_file_generator.py +++ b/pyGCodeDecode/abaqus_file_generator.py @@ -71,7 +71,7 @@ def generate_abaqus_event_series( ) event_series_list.append((time, scaling * pos[0], scaling * pos[1], scaling * pos[2], pos[3])) - custom_print(f"ABAQUS event series written to: \n{outfile.name}") + custom_print(f"💾 ABAQUS event series written to 👉 {outfile.name}") if return_tuple: return tuple(event_series_list) diff --git a/pyGCodeDecode/cli.py b/pyGCodeDecode/cli.py index be8d514..8da0b86 100644 --- a/pyGCodeDecode/cli.py +++ b/pyGCodeDecode/cli.py @@ -52,7 +52,7 @@ def _find_gcode_file(specified_path: pathlib.Path | None) -> pathlib.Path: ) exit() else: - custom_print("⚠️ No G-code file specified. Looking for a G-code file in the current directory... 👀", lvl=1) + custom_print("⚠️ No G-code file specified. Looking for a G-code file in the current directory... 👀", lvl=1) files_list = list(pathlib.Path.cwd().glob("*.gcode")) if files_list.__len__() == 0: custom_print("❌ No G-code file found in the current directory.\n" "🛑 Exiting the program.", lvl=1) @@ -72,7 +72,7 @@ def _find_gcode_file(specified_path: pathlib.Path | None) -> pathlib.Path: def _get_presets_file(presets_file: pathlib.Path | None) -> pathlib.Path: """Get the machine setup from the presets file.""" if presets_file is None: - custom_print("⚠️ No presets file specified. Using the default presets shipped with pyGCD.") + custom_print("⚠️ No presets file specified. Using the default presets shipped with pyGCD.") presets_file = importlib.resources.files("pyGCodeDecode").joinpath("data/default_printer_presets.yaml") elif not presets_file.is_file(): custom_print( @@ -91,18 +91,18 @@ def _get_out_dir(out_dir: pathlib.Path | None, g_code_file: pathlib.Path) -> pat answer = "" while answer.lower() not in ("y", "yes", "n", "no"): answer = input( - "⚠️ No output directory specified! Do you want to create one in the current working directory?" + "⚠️ No output directory specified! Do you want to create one in the current working directory?" "\nOtherwise no outputs will be saved!" "\nYou must answer with yes (y) or no (n)!\n" ) if answer.lower() in ["n", "no"]: - custom_print("⚠️ Not creating any output files.") + custom_print("⚠️ Not creating any output files.") return None elif answer.lower() in ["y", "yes"]: out_dir = pathlib.Path.cwd() / f"output_{g_code_file.stem}" - custom_print(f"✅ Using the output directory:\n{out_dir.resolve()}") + custom_print(f"✅ Using the output directory: {out_dir.resolve()}") return out_dir g_code_file = _find_gcode_file(args.gcode) @@ -113,7 +113,7 @@ def _get_out_dir(out_dir: pathlib.Path | None, g_code_file: pathlib.Path) -> pat printer_name = args.printer_name custom_print(f"✅ Using the printer: {printer_name}") else: - custom_print("⚠️ No printer specified. Using the default printer Anisoprint A4.") + custom_print("⚠️ No printer specified. Using the default printer Anisoprint A4.") printer_name = "anisoprint_a4" # setting up the printer diff --git a/pyGCodeDecode/examples/benchy.py b/pyGCodeDecode/examples/benchy.py index ca3484b..6d0f59e 100644 --- a/pyGCodeDecode/examples/benchy.py +++ b/pyGCodeDecode/examples/benchy.py @@ -5,6 +5,7 @@ from pyGCodeDecode.abaqus_file_generator import generate_abaqus_event_series from pyGCodeDecode.gcode_interpreter import setup, simulation +from pyGCodeDecode.helpers import custom_print from pyGCodeDecode.plotter import plot_3d from pyGCodeDecode.tools import save_layer_metrics @@ -15,12 +16,12 @@ def benchy_example(): data_dir = importlib.resources.files("pyGCodeDecode").joinpath("examples/data/") output_dir = pathlib.Path.cwd() / "output_benchy_example" - print( + custom_print( "Running pyGCD's benchy example! 🛥️" - "\nThis example illustrates an extensive use of the package: A gcode is simulated with default presets from a " + "\nThis example illustrates an extensive use of the package: \nA gcode is simulated with default presets from a " "file provided alongside this example. After the simulation, an interactive 3D-plot is shown." - "\nThe following files are saved to a new folder in your current directory: 💾\n", - output_dir.__str__(), + "\nThe following files are saved to a new folder in your current directory: ", + output_dir.__str__() + " 💾", "\n - a screenshot of the 3D-plot 📸" "\n - a .vtk of the mesh 🕸️" "\n - a summary of the simulation 📝" diff --git a/pyGCodeDecode/examples/brace.py b/pyGCodeDecode/examples/brace.py index 3c40f18..3da5cb3 100644 --- a/pyGCodeDecode/examples/brace.py +++ b/pyGCodeDecode/examples/brace.py @@ -3,15 +3,16 @@ import importlib.resources from pyGCodeDecode.gcode_interpreter import simulation +from pyGCodeDecode.helpers import custom_print from pyGCodeDecode.plotter import plot_3d def brace_example(): """Minimal example for the usage of pyGCodeDecode simulating the G-code of a brace.""" - print( + custom_print( "Running pyGCD's brace example! 📎" "\nThis example illustrates the simplest use of the package: A gcode is simulated with default presets " - "provided by the package. After the simulation, an interactive 3D-plot is shown. No output is saved." + "\nprovided by the package. After the simulation, an interactive 3D-plot is shown. No output is saved." ) gcode_path = importlib.resources.files("pyGCodeDecode").joinpath("examples/data/brace.gcode") diff --git a/pyGCodeDecode/gcode_interpreter.py b/pyGCodeDecode/gcode_interpreter.py index 6362da0..8436e9f 100644 --- a/pyGCodeDecode/gcode_interpreter.py +++ b/pyGCodeDecode/gcode_interpreter.py @@ -207,7 +207,7 @@ def __init__( ) custom_print( - f"Simulating \"{self.filename}\" with {self.initial_machine_setup_dict['printer_name']} using " + f"Simulating {self.filename} with {self.initial_machine_setup_dict['printer_name']} using " f"the {self.firmware} firmware." ) self.blocklist: List[planner_block] = generate_planner_blocks(states=self.states, firmware=self.firmware) @@ -341,11 +341,11 @@ def print_summary(self, start_time: float): start_time (float): time when the simulation run was started """ custom_print( - f" >> pyGCodeDecode extracted {len(self.states)} states from {self.filename}" + f"✅ Simulation finished: pyGCodeDecode extracted {len(self.states)} states from {self.filename}" f" and generated {len(self.blocklist)} planner blocks.\n" - f"Estimated time to travel all states with provided" - f" printer settings is {self.blocklist[-1].get_segments()[-1].t_end:.2f} seconds.\n" - f"The Simulation took {(time.time()-start_time):.2f} s." + f"Estimated time to travel all states with provided " + f"printer settings is {self.blocklist[-1].get_segments()[-1].t_end:.2f} seconds.\n" + f"The Simulation took {(time.time()-start_time):.2f} s of computation time." ) def refresh(self, new_state_list: List[state] = None): @@ -443,7 +443,7 @@ def save_summary(self, filepath: Union[pathlib.Path, str]): with open(file=filepath, mode="w") as file: yaml.dump(data=summary, stream=file) - custom_print(f"💾 Summary written to:\n👉 {str(filepath)}") + custom_print(f"💾 Summary written to 👉 {str(filepath)}") def get_scaling_factor(self, output_unit_system: str = None) -> float: """Get a scaling factor to convert lengths from mm to another supported unit system. diff --git a/pyGCodeDecode/helpers.py b/pyGCodeDecode/helpers.py index 998f522..3932f3f 100644 --- a/pyGCodeDecode/helpers.py +++ b/pyGCodeDecode/helpers.py @@ -17,6 +17,20 @@ _active_progress_bar = None +# Define verbosity levels for custom_print +# _levels = { +# 3: "[ DEBUG ]", +# 2: "[ INFO ]", +# 1: "[WARNING]", +# } + +_levels = { + 3: "[DEBU]", + 2: "[INFO]", + 1: "[WARN]", +} + + def set_verbosity_level(level: Optional[int]) -> None: """Set the global verbosity level.""" global VERBOSITY_LEVEL @@ -56,33 +70,42 @@ def custom_print(*args, lvl=2, **kwargs) -> None: sys.stdout.write("\r" + " " * 80 + "\r") sys.stdout.flush() - levels = { - 3: "[ DEBUG ]:", - 2: "[ INFO ]:", - 1: "[WARNING]:", - } - prefix = levels.get(lvl, "") + prefix = _levels.get(lvl, "") + + # Process arguments to handle newline alignment + processed_args = [] + for arg in sanitized_args: + if isinstance(arg, str) and "\n" in arg: + # Split by newlines and add appropriate spacing after each newline + lines = arg.split("\n") + # Join with newline followed by spacing to align with prefix + spacing = " " * len(prefix + " ") # +1 for the space after prefix + processed_arg = ("\n" + spacing).join(lines) + processed_args.append(processed_arg) + else: + processed_args.append(arg) # Print the message if _active_progress_bar is not None: # When progress bar is active, print without extra newlines - print(prefix, *sanitized_args, **kwargs) + print(prefix, *processed_args, **kwargs) # Redraw the progress bar immediately _active_progress_bar._redraw_current_state() else: # Normal print when no progress bar is active - print(prefix, *sanitized_args, **kwargs) + print(prefix, *processed_args, **kwargs) class ProgressBar: """A simple progress bar for the console.""" - def __init__(self, name: str = "Percent", barLength: int = 10): + def __init__(self, name: str = "Percent", barLength: int = 4, verbosity_level: int = 2): """Initialize a progress bar.""" self.name = name self.barLength = barLength self.last_progress_update = -1 self.last_text = "" # Store the last progress bar text + self.verbosity_level = verbosity_level def _redraw_current_state(self) -> None: """Redraw the current progress bar state.""" @@ -103,7 +126,7 @@ def update(self, progress: float) -> None: barLength = self.barLength status = "" - if VERBOSITY_LEVEL >= 2: # only print if verbosity level is high enough + if VERBOSITY_LEVEL >= self.verbosity_level: # only print if verbosity level is high enough # Register this progress bar as active _active_progress_bar = self @@ -127,7 +150,11 @@ def update(self, progress: float) -> None: # check whether the progress has changed if self.last_progress_update != progress_percent or status != "": block = int(round(barLength * progress, ndigits=0)) - text = f"\r[{'#' * block + '-' * (barLength - block)}] {progress_percent} % of {self.name} {status}" + if progress < 1.0: + text = f"\r[{'#' * block + '-' * (barLength - block)}] {progress_percent} % of {self.name} {status}" + else: + text = f"\r{_levels.get(self.verbosity_level, '')} ✅ Done with {self.name}" + # text = f"\r[{'#' * block + '-' * (barLength - block)}] ✅ Done with {self.name}" self.last_text = text sys.stdout.write(text) sys.stdout.flush() diff --git a/pyGCodeDecode/planner_block.py b/pyGCodeDecode/planner_block.py index 1ca72c2..82d9552 100644 --- a/pyGCodeDecode/planner_block.py +++ b/pyGCodeDecode/planner_block.py @@ -241,7 +241,9 @@ def self_correction(self, tolerance=float("1e-12")): for segm in self.segments: segm.self_check(p_settings=self.state_B.state_p_settings) except ValueError as ve: - custom_print(f"Segment for {self.state_B} does not adhere to machine limits: {ve}", lvl=1) + custom_print( + f"⚠️ Segment modeling travel to \n\t{self.state_B}\ndoes not adhere to machine limits: {ve}", lvl=1 + ) return flag_correct diff --git a/pyGCodeDecode/plotter.py b/pyGCodeDecode/plotter.py index 5baa1e5..85055c2 100644 --- a/pyGCodeDecode/plotter.py +++ b/pyGCodeDecode/plotter.py @@ -77,7 +77,7 @@ def _safe_screenshot(plotter: pv.Plotter, screenshot_path=None): filename=screenshot_path, ) if screenshot_path is not None: - custom_print(f"PyVista Screenshot saved to:\n{screenshot_path}") + custom_print(f"💾 PyVista Screenshot saved to 👉 {screenshot_path}") else: img = None custom_print("PyVista Screenshot can not be created without a display!", lvl=1) @@ -221,7 +221,7 @@ def _safe_screenshot(plotter: pv.Plotter, screenshot_path=None): if vtk_path is not None: mesh.save(filename=vtk_path) - custom_print(f"VTK saved to:\n{vtk_path}", lvl=2) + custom_print(f"💾 VTK saved to 👉 {vtk_path}", lvl=2) if screenshot_path is not None: custom_print(f"Offscreen plotting, with resolution {p.window_size}", lvl=3) @@ -271,7 +271,7 @@ def _safe_screenshot(plotter: pv.Plotter, screenshot_path=None): dpi = window_size[1] / fig.get_size_inches()[1] fig.savefig(screenshot_path, dpi=dpi, transparent=transparent_background) # bbox_inches="tight", - custom_print(f"MPL Screenshot saved to:\n{screenshot_path}") + custom_print(f"💾 MPL Screenshot saved to 👉{screenshot_path}") else: if block_colorbar: p.remove_scalar_bar() @@ -397,7 +397,7 @@ def _interp_2D(x, y, cvar, spatial_resolution=1): plt.axis("scaled") if filepath is not False: plt.savefig(filepath, dpi=dpi) - custom_print(f"2D Plot saved:\n👉 {filepath}") + custom_print(f"💾 2D Plot saved 👉 {filepath}") if show: plt.show() return fig diff --git a/pyGCodeDecode/state.py b/pyGCodeDecode/state.py index 9a76873..1991a17 100644 --- a/pyGCodeDecode/state.py +++ b/pyGCodeDecode/state.py @@ -34,23 +34,13 @@ def __init__(self, p_acc, jerk, vX, vY, vZ, vE, speed, units="SI (mm)"): def __str__(self) -> str: """Create summary string for p_settings.""" return ( - "jerk: " - + str(self.jerk) - + ", p_acc: " - + str(self.p_acc) - + ", max_ax_vel: [" - + str(self.vX) - + ", " - + str(self.vY) - + ", " - + str(self.vZ) - + ", " - + str(self.vE) - + "]" - + ", p_vel: " - + str(self.speed) - + ", units: " - + str(self.units) + f"Settings:\n" + f" jerk: {self.jerk}\n" + f" acceleration: {self.p_acc}\n" + f" max_velocities: [X: {self.vX}, Y: {self.vY}, Z: {self.vZ}, E: {self.vE}]\n" + f" speed: {self.speed}\n" + f" units: {self.units}" + f"" ) def __repr__(self) -> str: @@ -136,10 +126,40 @@ def prev_state(self, state: "state"): def __str__(self) -> str: """Generate string for representation.""" + # Format the basic state info + state_info = f"State(line: {self.line_number or 'N/A'})" + + # Add layer if available if self.layer is not None: - return f"\n" # noqa E501 + state_info += f" [Layer: {self.layer}]" + + # Add pause status if set + if self.pause: + state_info += " [PAUSED]" + + # Build the complete representation + result = f"{state_info}\n" + + # Add position information + if self.state_position: + result += f"\t{self.state_position}\n" else: - return f"\n" # noqa E501 + result += "Position: Not set\n" + + # Add settings information with indentation + if self.state_p_settings: + settings_str = str(self.state_p_settings) + # Indent each line of settings + indented_settings = "\n".join(f"\t{line}" for line in settings_str.split("\n")) + result += f" {indented_settings}" + else: + result += "Settings: Not set" + + # Add comment if available + if self.comment: + result += f"Comment: {self.comment}" + + return result def __repr__(self) -> str: """Call __str__() for representation.""" diff --git a/pyGCodeDecode/state_generator.py b/pyGCodeDecode/state_generator.py index 385abc2..7d4e5a8 100644 --- a/pyGCodeDecode/state_generator.py +++ b/pyGCodeDecode/state_generator.py @@ -5,7 +5,7 @@ import re from typing import List, Match -from pyGCodeDecode.helpers import custom_print +from pyGCodeDecode.helpers import ProgressBar, custom_print from .state import state from .utils import position @@ -184,16 +184,27 @@ def _read_gcode_to_dict_list(filepath: pathlib.Path) -> List[dict]: Returns: dict_list: (list[dict]) list with every line as dict """ - custom_print("Parsing the gcode...") dict_list = [] + # First pass to count total lines + with open(file=filepath) as file_gcode: + total_lines = sum(1 for _ in file_gcode) + + # Initialize progress bar with the total number of lines + progress_bar = ProgressBar(name=f"Parsing {total_lines} lines of {filepath.name}") + + # Second pass to process the lines with open(file=filepath) as file_gcode: for i, line in enumerate(file_gcode): line_dict = _arg_extract(string=line, key_dict=known_commands) line_dict["line_number"] = i + 1 dict_list.append(line_dict) - custom_print(f"Parsing done. {len(dict_list)} lines parsed.") + # Update progress bar + progress = (i + 1) / total_lines + progress_bar.update(progress) + + # custom_print(f"Parsing done. {len(dict_list)} lines parsed.") return dict_list @@ -418,9 +429,10 @@ def _check_for_unsupported_commands(line_dict_list: dict) -> dict: } if unsupported_commands_found != []: - custom_print(f"⚠️ {len(unsupported_command_counts.keys())} known but unsupported command(s) found:", lvl=1) - for key, value in unsupported_command_counts.items(): - custom_print(f" - Command '{key}' found {value} time(s).", lvl=1) + commands_str = ", ".join([f"'{key}' ({value} time(s))" for key, value in unsupported_command_counts.items()]) + custom_print( + f"⚠️ {len(unsupported_command_counts.keys())} known but unsupported command(s) found: {commands_str}", lvl=1 + ) else: custom_print("Great, the G-code does not contain any unsupported commands known to pyGCD 🎈.") diff --git a/pyGCodeDecode/tools.py b/pyGCodeDecode/tools.py index 5eb1051..d1c4370 100644 --- a/pyGCodeDecode/tools.py +++ b/pyGCodeDecode/tools.py @@ -30,7 +30,7 @@ def save_layer_metrics( # check if a layer cue was specified if "layer_cue" not in simulation.initial_machine_setup_dict: custom_print( - "⚠️ No layer_cue was specified in the simulation setup. Therefore, layer metrics can not be saved!", lvl=1 + "⚠️ No layer_cue was specified in the simulation setup. Therefore, layer metrics can not be saved!", lvl=1 ) return None @@ -94,7 +94,7 @@ def save_layer_metrics( header=header, comments="", ) - custom_print(f"💾 Layer metrics written to:\n👉 {filepath.__str__()}") + custom_print(f"💾 Layer metrics written to 👉 {filepath.__str__()}") return layers, durations, travel_distances, avg_speeds diff --git a/pyGCodeDecode/utils.py b/pyGCodeDecode/utils.py index c746700..7cf54a2 100644 --- a/pyGCodeDecode/utils.py +++ b/pyGCodeDecode/utils.py @@ -243,7 +243,7 @@ class position(vector_4D): def __str__(self) -> str: """Print out position.""" - return "position: " + super().__str__() + return "Position: " + super().__str__() def is_travel(self, other) -> bool: """Return True if there is travel between self and other position. diff --git a/tests/test_progress_fix.py b/tests/test_progress_fix.py index 370d40d..4d4b24e 100644 --- a/tests/test_progress_fix.py +++ b/tests/test_progress_fix.py @@ -18,7 +18,7 @@ def test_progress_bar_with_interruptions(capsys): time.sleep(0.001) assert pb1.last_progress_update == 100 - pb2 = ProgressBar("Test Process 2", 15) + pb2 = ProgressBar("Test Process 2", 15, verbosity_level=1) for i in range(101): pb2.update(i / 100.0) if i == 50: @@ -30,13 +30,21 @@ def test_progress_bar_with_interruptions(capsys): out, _ = capsys.readouterr() expected = ( - "[WARNING]: This is a warning message during progress\n" - "[ INFO ]: This is an info message during progress\n" - "[####################] 100.0 % of Test Process 1 - Done ✅\n" - "[ INFO ]: Another message in the middle\n" - "[###############] 100.0 % of Test Process 2 - Done ✅\n" - "[ INFO ]: All tests completed!\n" + "[WARN] This is a warning message during progress\n" + "[INFO] This is an info message during progress\n" + "[INFO] ✅ Done with Test Process 1\n" + "[INFO] Another message in the middle\n" + "[WARN] ✅ Done with Test Process 2\n" + "[INFO] All tests completed!\n" ) + # expected = ( + # "[WARNING] This is a warning message during progress\n" + # "[ INFO ] This is an info message during progress\n" + # "[ INFO ] ✅ Done with Test Process 1\n" + # "[ INFO ] Another message in the middle\n" + # "[WARNING] ✅ Done with Test Process 2\n" + # "[ INFO ] All tests completed!\n" + # ) # Normalize whitespace for progress bar lines def normalize(line): @@ -46,10 +54,13 @@ def normalize(line): exp_lines = [normalize(lin) for lin in expected.splitlines()] # Only keep lines that are expected (messages and final progress bars) filtered_out_lines = [ - lin - for lin in out_lines - if lin.startswith("[WARNING]:") or lin.startswith("[ INFO ]:") or lin.endswith("- Done ✅") + lin for lin in out_lines if lin.startswith("[WARN]") or lin.startswith("[INFO]") or "✅ Done" in lin ] + # filtered_out_lines = [ + # lin + # for lin in out_lines + # if lin.startswith("[WARNING]") or lin.startswith("[ INFO ]") or "✅ Done" in lin + # ] assert exp_lines == filtered_out_lines @@ -63,8 +74,8 @@ def test_progress_bar_no_interruptions(capsys): assert pb.last_progress_update == 100 custom_print("Done without interruptions", lvl=2) out, _ = capsys.readouterr() - assert "[##########] 100.0 % of No Interruptions - Done ✅" in out - assert "[ INFO ]: Done without interruptions" in out + assert "[INFO] ✅ Done with No Interruptions" in out + assert "[INFO] Done without interruptions" in out def test_progress_bar_multiple_messages(capsys): @@ -82,7 +93,7 @@ def test_progress_bar_multiple_messages(capsys): time.sleep(0.001) assert pb.last_progress_update == 100 out, _ = capsys.readouterr() - assert "[ INFO ]: First info" in out - assert "[WARNING]: Second warning" in out - assert "[ INFO ]: Third info" in out - assert "[#####] 100.0 % of Multiple Messages - Done ✅" in out + assert "[INFO] First info" in out + assert "[WARN] Second warning" in out + assert "[INFO] Third info" in out + assert "[INFO] ✅ Done with Multiple Messages" in out From eb5e52b1c60be61972fac984c511ecf8fa899936 Mon Sep 17 00:00:00 2001 From: usmfi Date: Thu, 7 Aug 2025 14:20:23 +0200 Subject: [PATCH 34/68] rmvd intro line in benchy example --- pyGCodeDecode/examples/data/benchy.gcode | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyGCodeDecode/examples/data/benchy.gcode b/pyGCodeDecode/examples/data/benchy.gcode index d9999da..a214d87 100644 --- a/pyGCodeDecode/examples/data/benchy.gcode +++ b/pyGCodeDecode/examples/data/benchy.gcode @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f7799ab4b39630aa1b5c869672399c7cb5c5b2a158cd4a9204334495b64514d -size 5175490 +oid sha256:eb3e198460566f0317eaf032a5394100b8fe465bb85b9f836581c3ce11f01d2a +size 5175410 From 11f326f3ff2975f463bb01eaa4e3a61a25763ca5 Mon Sep 17 00:00:00 2001 From: usmfi Date: Tue, 12 Aug 2025 12:05:15 +0200 Subject: [PATCH 35/68] cleaned up utils --- pyGCodeDecode/utils.py | 203 ++++++++++++++++++++++++----------------- 1 file changed, 120 insertions(+), 83 deletions(-) diff --git a/pyGCodeDecode/utils.py b/pyGCodeDecode/utils.py index 7cf54a2..5289753 100644 --- a/pyGCodeDecode/utils.py +++ b/pyGCodeDecode/utils.py @@ -1,5 +1,5 @@ """ -Utilitys. +Utilities. Utils for the GCode Reader contains: - vector 4D @@ -7,7 +7,7 @@ - position """ -from typing import TYPE_CHECKING, List, Union +from typing import TYPE_CHECKING, List, Optional, Union import numpy as np @@ -41,12 +41,20 @@ def __str__(self) -> str: """Return string representation of the time in seconds.""" return f"{float(self)} s" - def __repr__(self): + def __sub__(self, other) -> "seconds": + """Subtract seconds or float and return a new seconds instance.""" + return seconds(float(self) - float(other)) + + def __add__(self, other) -> "seconds": + """Add seconds or float and return a new seconds instance.""" + return seconds(float(self) + float(other)) + + def __repr__(self) -> str: """Return a string representation of the seconds object.""" return self.__str__() @property - def seconds(self): + def seconds(self) -> float: """Return the float value of the seconds instance.""" return float(self) @@ -75,9 +83,9 @@ def __init__(self, *args): self.z = None self.e = None - if type(args) is tuple and len(args) == 1: + if isinstance(args[0], (tuple, list, np.ndarray)) and len(args) == 1 and len(args[0]) == 4: args = tuple(args[0]) - if type(args) is tuple and len(args) == 4: + if len(args) == 4: self.x = args[0] self.y = args[1] self.z = args[2] @@ -113,6 +121,7 @@ def __add__(self, other): else: raise ValueError( "Addition with __add__ is only possible with other 4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray'" + f" got {type(other)} instead." ) def __sub__(self, other): @@ -150,7 +159,7 @@ def __mul__(self, other): Returns: mul: (self) scalar multiplication, scaling """ - if type(other) is float or type(other) is np.float64 or type(other) is int: + if isinstance(other, (float, int, np.floating, np.integer)): x = self.x * other y = self.y * other z = self.z * other @@ -168,7 +177,7 @@ def __truediv__(self, other): Returns: div: (self) scalar division, scaling """ - if type(other) is float or type(other) is np.float64: + if isinstance(other, (float, int, np.floating, np.integer)): x = self.x / other y = self.y / other z = self.z / other @@ -177,7 +186,7 @@ def __truediv__(self, other): raise TypeError("Division of 4D Vectors only supports float and int.") return self.__class__(x, y, z, e) - def __eq__(self, other): + def __eq__(self, other) -> bool: """Check for equality and return True if equal (with tolerance). Args: @@ -196,7 +205,7 @@ def __eq__(self, other): self_vec = [self.x, self.y, self.z, self.e] return np.allclose(self_vec, other_vec) - def __gt__(self, other): + def __gt__(self, other) -> bool: """Check for greater than and return True if greater. Args: @@ -211,8 +220,10 @@ def __gt__(self, other): return self.get_norm() > np.linalg.norm(other) elif isinstance(other, (float, int)): return self.get_norm() > other + else: + return False - def get_vec(self, withExtrusion=False) -> List[float]: + def get_vec(self, withExtrusion: bool = False) -> List[float]: """Return the 4D vector, optionally with extrusion. Args: @@ -226,7 +237,7 @@ def get_vec(self, withExtrusion=False) -> List[float]: else: return [self.x, self.y, self.z] - def get_norm(self, withExtrusion=False) -> float: + def get_norm(self, withExtrusion: bool = False) -> float: """Return the 4D vector norm. Optional with extrusion. Args: @@ -276,7 +287,7 @@ def is_extruding(self, other: "position", ignore_retract: bool = True) -> bool: else: return False - def get_t_distance(self, other=None, withExtrusion=False) -> float: + def get_t_distance(self, other=None, withExtrusion: bool = False) -> float: """Calculate the travel distance between self and other position. If none is provided, zero will be used. Args: @@ -312,41 +323,40 @@ def __str__(self) -> str: """Print out velocity.""" return "velocity: " + super().__str__() - def get_norm_dir(self, withExtrusion=False): - """Get normalized vector (regarding travel distance), if only extrusion occurs, normalize to extrusion length. + def get_norm_dir(self, withExtrusion: bool = False) -> Optional[np.ndarray]: + """Get normalized direction vector as numpy array. - Args: - withExtrusion: (bool, default = False) choose if norm dir contains extrusion + If only extrusion occurs and withExtrusion=True, normalize to the extrusion length. - Returns: - dir: (list[3 or 4]) normalized direction vector as list + Returns None if both travel and extrusion are zero. """ - abs_val = self.get_norm() - if abs_val > 0: - return self.get_vec(withExtrusion=withExtrusion) / abs_val - elif withExtrusion and self.get_norm(withExtrusion=withExtrusion) > 0: - return self.get_vec(withExtrusion=withExtrusion) / self.get_norm(withExtrusion=withExtrusion) - else: - return None - - def avoid_overspeed(self, p_settings): - """Return velocity without any axis overspeed. - - Args: - p_settings: (p_settings) printing settings - - Returns: - vel: (velocity) constrained by max velocity - """ - scale = 1.0 - scale = p_settings.Vx / self.Vx if self.Vx > 0 and p_settings.Vx / self.Vx < scale else scale - scale = p_settings.Vy / self.Vy if self.Vy > 0 and p_settings.Vy / self.Vy < scale else scale - scale = p_settings.Vz / self.Vz if self.Vz > 0 and p_settings.Vz / self.Vz < scale else scale - scale = p_settings.Ve / self.Ve if self.Vz > 0 and p_settings.Ve / self.Ve < scale else scale - - return self * scale - - def not_zero(self): + # travel_vec = np.asarray(self.get_vec(withExtrusion=False), dtype=float) + # travel_norm = np.linalg.norm(travel_vec) + travel_norm = self.get_norm() + if travel_norm > 0: + vec = np.asarray(self.get_vec(withExtrusion=withExtrusion), dtype=float) + return vec / travel_norm + elif withExtrusion: + vec_e = np.asarray(self.get_vec(withExtrusion=True), dtype=float) + full_norm = np.linalg.norm(vec_e) + if full_norm > 0: + return vec_e / full_norm + return None + + # def avoid_overspeed(self, p_settings: "state.p_settings") -> "velocity": + # """Return velocity scaled to avoid any axis overspeed. + + # Scales the velocity uniformly so that no axis exceeds its configured maximum. + # """ + # scale = 1.0 + # scale = p_settings.Vx / self.Vx if self.Vx > 0 and (p_settings.Vx / self.Vx) < scale else scale + # scale = p_settings.Vy / self.Vy if self.Vy > 0 and (p_settings.Vy / self.Vy) < scale else scale + # scale = p_settings.Vz / self.Vz if self.Vz > 0 and (p_settings.Vz / self.Vz) < scale else scale + # scale = p_settings.Ve / self.Ve if self.Ve > 0 and (p_settings.Ve / self.Ve) < scale else scale + + # return self * scale + + def not_zero(self) -> bool: """Return True if velocity is not zero. Returns: @@ -354,8 +364,8 @@ def not_zero(self): """ return True if np.linalg.norm(self.get_vec(withExtrusion=True)) > 0 else False - def is_extruding(self): - """Return True if extrusion velocity is not zero. + def is_extruding(self) -> bool: + """Return True if extrusion velocity is greater than zero. Returns: is_extruding: (bool) true if positive extrusion velocity @@ -372,7 +382,7 @@ def __mul__(self, other): self.z * other.seconds, self.e * other.seconds, ) - elif isinstance(other, (float, int, np.float64)): + elif isinstance(other, (float, int, np.floating, np.integer)): return self.__class__( self.x * other, self.y * other, @@ -413,7 +423,7 @@ def __mul__(self, other): self.z * other.seconds, self.e * other.seconds, ) - elif isinstance(other, (float, int, np.float64)): + elif isinstance(other, (float, int, np.floating, np.integer)): return self.__class__( self.x * other, self.y * other, @@ -484,7 +494,7 @@ def __repr__(self): """Segment representation.""" return self.__str__() - def move_segment_time(self, delta_t: float): + def move_segment_time(self, delta_t: Union[float, seconds]) -> None: """Move segment in time. Args: @@ -493,7 +503,7 @@ def move_segment_time(self, delta_t: float): self.t_begin = self.t_begin + delta_t self.t_end = self.t_end + delta_t - def get_velocity(self, t: float) -> velocity: + def get_velocity(self, t: Union[float, seconds]) -> velocity: """Get current velocity of segment at a certain time. Args: @@ -502,27 +512,36 @@ def get_velocity(self, t: float) -> velocity: Returns: current_vel: (velocity) velocity at time t """ + if not isinstance(t, seconds): + t = seconds(t) + if t < self.t_begin or t > self.t_end: raise ValueError("Segment not defined for this point in time.") else: + delt_t = self.t_end - self.t_begin + if delt_t == 0: + return self.vel_begin # linear interpolation of velocity in Segment delt_vel = self.vel_end - self.vel_begin - delt_t = self.t_end - self.t_begin - slope = delt_vel / delt_t if delt_t > 0 else velocity(0, 0, 0, 0) - current_vel = self.vel_begin + slope * (t - self.t_begin) + slope = delt_vel / delt_t + current_vel = self.vel_begin + (slope * (t - self.t_begin)) return current_vel - def get_velocity_by_dist(self, dist): - """Return the velocity at a certain local segment distance.""" - # t_begin, t_end, vel_begin, vel_end - a = (self.vel_end.get_norm() - self.vel_begin.get_norm()) / (self.t_end - self.t_begin) + def get_velocity_by_dist(self, dist: float) -> float: + """Return the velocity magnitude at a certain local segment distance. + + Args: + dist: (float) distance from segment start + """ + dt = self.t_end - self.t_begin + a = 0.0 if dt == 0 else (self.vel_end.get_norm() - self.vel_begin.get_norm()) / dt v_sq = 2 * a * dist + self.vel_begin.get_norm() ** 2 v = np.sqrt(v_sq) if v_sq > 0 else 0 - return v + return float(v) - def get_position(self, t: float) -> position: + def get_position(self, t: Union[float, seconds]) -> position: """Get current position of segment at a certain time. Args: @@ -531,47 +550,48 @@ def get_position(self, t: float) -> position: Returns: pos: (position) position at time t """ + if not isinstance(t, seconds): + t = seconds(t) if t < self.t_begin or t > self.t_end: raise ValueError(f"Segment not defined for this point in time. {t} -->({self.t_begin}, {self.t_end})") else: current_vel = self.get_velocity(t=t) - position = self.pos_begin + ((self.vel_begin + current_vel) * (t - self.t_begin) / 2.0).get_vec( - withExtrusion=True - ) - return position + # displacement = average velocity * dt + displacement_vec = ((self.vel_begin + current_vel) * (t - self.t_begin) / 2.0).get_vec(withExtrusion=True) + position_val = self.pos_begin + displacement_vec + return position_val - def get_segm_len(self): + def get_segm_len(self) -> float: """Return the length of the segment.""" return (self.pos_end - self.pos_begin).get_norm() - def get_segm_duration(self): + def get_segm_duration(self) -> seconds: """Return the duration of the segment.""" return self.t_end - self.t_begin - def self_check(self, p_settings: "state.p_settings" = None): + def self_check(self, p_settings: "state.p_settings" = None) -> bool: """Check the segment for self consistency. Raises: ValueError: if self check fails Args: p_settings: (p_settings, default = None) printing settings to verify + Returns: + True if all checks pass """ # position self check: - tolerance = float("1e-6") - position = self.pos_begin + ((self.vel_begin + self.vel_end) * (self.t_end - self.t_begin) / 2.0).get_vec( - withExtrusion=True - ) - error_distance = np.linalg.norm(np.asarray(self.pos_end.get_vec()) - np.asarray(position.get_vec())) - - if error_distance > tolerance: + tolerance = 1e-6 + position_calc = self.pos_begin + ((self.vel_begin + self.vel_end) * (self.t_end - self.t_begin) / 2.0) + error_distance = self.pos_end - position_calc + if error_distance.get_norm(withExtrusion=True) > tolerance: raise ValueError("Error distance: " + str(error_distance)) # time consistency if self.t_begin > self.t_end: raise ValueError(f"Inconsistent segment time (t_begin/t_end): ({self.t_begin}/{self.t_end}) \n ") - # max velocity if p_settings is not None: + # max velocity if self.vel_begin.get_norm() > p_settings.speed and not np.isclose( self.vel_begin.get_norm(), p_settings.speed ): @@ -579,12 +599,29 @@ def self_check(self, p_settings: "state.p_settings" = None): if self.vel_end.get_norm() > p_settings.speed and not np.isclose(self.vel_end.get_norm(), p_settings.speed): raise ValueError(f"Target Velocity of {p_settings.speed} exceeded with {self.vel_end.get_norm()}.") - # max acceleration - if p_settings is not None: + # max acceleration if self.t_end - self.t_begin > 0: acc = (self.vel_end - self.vel_begin) / (self.t_end - self.t_begin) - if acc.get_norm() > p_settings.p_acc and not np.isclose(acc.get_norm(), p_settings.p_acc): - raise ValueError(f"Maximum acceleration of {p_settings.p_acc} exceeded with {acc.get_norm()}.") + + # Scale tolerance based on time delta to handle numerical precision issues + dt = self.t_end - self.t_begin + base_rtol = 1e-5 # Standard relative tolerance + base_atol = 0.1 # Absolute tolerance in mm/s² + + # Scale tolerance inversely with time delta (smaller dt = larger tolerance) + dt_scale = min(1e-6 / max(dt, 1e-12), 1000.0) + scaled_rtol = base_rtol * dt_scale + scaled_atol = base_atol * dt_scale + acc_norm = acc.get_norm() + + if acc_norm > p_settings.p_acc and not np.isclose( + acc_norm, p_settings.p_acc, rtol=scaled_rtol, atol=scaled_atol + ): + raise ValueError( + f"Maximum acceleration of {p_settings.p_acc} exceeded with {acc_norm}. " + f"Delta t: {dt:.2e}, tolerance used: rtol={scaled_rtol:.2e}, atol={scaled_atol:.2e}" + ) + return True def is_extruding(self) -> bool: """Return true if the segment is pos. extruding. @@ -594,7 +631,7 @@ def is_extruding(self) -> bool: """ return self.pos_begin.e < self.pos_end.e - def _interpolate_time_to_space(self, scalar_begin, scalar_end, x): + def _interpolate_time_to_space(self, scalar_begin, scalar_end, x) -> float: """ Interpolate from linear time dependant to nonlinear space dependant. @@ -624,7 +661,7 @@ def get_time(x): return scalar - def get_result(self, key): + def get_result(self, key: str): """Return the requested result. Args: @@ -639,7 +676,7 @@ def get_result(self, key): raise ValueError(f"Key: {key} not found.") @classmethod - def create_initial(cls, initial_position: position = None): + def create_initial(cls, initial_position: Optional[position] = None) -> "segment": """Create initial static segment with (optionally) initial position else start from Zero. Args: @@ -649,5 +686,5 @@ def create_initial(cls, initial_position: position = None): segment: (segment) initial beginning segment """ velocity_0 = velocity(0, 0, 0, 0) - pos_0 = position(x=0, y=0, z=0, e=0) if initial_position is None else initial_position + pos_0 = position(0, 0, 0, 0) if initial_position is None else initial_position return cls(t_begin=0, t_end=0, pos_begin=pos_0, vel_begin=velocity_0, pos_end=pos_0, vel_end=velocity_0) From ab19c6b4bc4ea41448ec0722a73b1c1bb7d3c1f9 Mon Sep 17 00:00:00 2001 From: usmfi Date: Tue, 12 Aug 2025 12:06:12 +0200 Subject: [PATCH 36/68] debugger entry for benchy --- .vscode/launch.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c74b058 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,10 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + {"name":"Python Debugger: Current File","type":"debugpy","request":"launch","program":"${file}","console":"integratedTerminal"}, + {"name":"Python Debugger: Benchy","type":"debugpy","request":"launch","program":"${workspaceFolder}/pyGCodeDecode/examples/benchy.py","console":"integratedTerminal"} + ] +} From 20d060902862396b970e200104e3014a2c398f35 Mon Sep 17 00:00:00 2001 From: usmfi Date: Tue, 12 Aug 2025 16:25:38 +0200 Subject: [PATCH 37/68] added info for custom fork --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 19ef65e..ecb003c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ DEVELOPER = [ ] DOCS = [ "mkdocs", - "pydoc-markdown @ git+https://github.com/jeknirsch/pydoc-markdown.git@sort_modules", + "pydoc-markdown @ git+https://github.com/jeknirsch/pydoc-markdown.git@sort_modules", # using custom fork to sort modules ] [project.urls] From 83f68f7effdfc6ab76eeae055275237d3f8e7eeb Mon Sep 17 00:00:00 2001 From: usmfi Date: Thu, 14 Aug 2025 11:44:51 +0200 Subject: [PATCH 38/68] releasenotes added as md --- whats_new.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 whats_new.md diff --git a/whats_new.md b/whats_new.md new file mode 100644 index 0000000..01dd5fb --- /dev/null +++ b/whats_new.md @@ -0,0 +1,22 @@ +## Latest Release Notes +pyGCodeDecode has gotten several updates which are now published with Version XX.XX. +These include QOL improvements and error fixes. + +### Result Calculation +A separate result calculation module is added, which is executed after simulation. User defined results may be calculated using the resulting trajectory and can be mapped to the segments. See current implementations for details. + +### Plotting updates +Plotting methods are moved to be separated from the simulation objects. They now allow for several arguments to be passed to the `pyvista` visualization toolkit. The scalar value plotted may be selected via keys, allowing different results to be displayed. Further individual layer plotting is supported, if a layer cue is provided. Additional arguments enable advanced settings such as transparent background, lighting and rendering options. This includes camera position and orientation. +Further, the plotting methods allow a callable to be passed; the user may modify the `pyvista`-scene through these and add geometry to the plot. +Screenshots are also improved visually by wrapping the pyvista screenshot into a `matplotlib` axis, which allows for nicer colorbars and vector graphics rendering of text. +Individual extrusions now are represented through a squiched cylinder instead of a circular one. This squiching can be set by the user and results from the layer width to height ratio. + +### Junction Handling Bug fixes +Several bug fixes are implemented for junction handling, improving the overall robustness of the simulation and ensuring accurate results. The firmware identifiers have been changed to be more consistent. + +### Prints +Print statements have been improved for better clarity and information. A verbosity control is added to declutter the output, especially when running a large number of simulations. They now include more context about the simulation state and results, making it easier to understand the output. Consistent formatting and different verbosity levels are supported. + +### Testing and Type Hints +More tests have been added to ensure a reliable simulation and framework. Especially the junction handling module is tested more thoroughly. +More type hints have been added throughout the codebase to improve readability and facilitate easier debugging and development. Physical quantities are morphed by mathematical operations to yield the correct units, allowing for more intuitive code and reducing the likelihood of errors. From a2143ffb164c107bd7464ca599ad4fb9301ce411 Mon Sep 17 00:00:00 2001 From: usmfi Date: Thu, 14 Aug 2025 12:36:55 +0200 Subject: [PATCH 39/68] version update --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ecb003c..391bb5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ where = ["."] [project] name = "pyGCodeDecode" -version = "1.3.1" +version = "1.4.0" authors = [ { name = "FAST-LB at KIT", email = "lt-github@fast.kit.edu" }, { name = "Jonathan Knirsch", email = "jonathan.knirsch@student.kit.edu" }, From 883c402ff0fe783ebf8ff8db027b50429a71acf5 Mon Sep 17 00:00:00 2001 From: usmfi Date: Thu, 14 Aug 2025 12:47:21 +0200 Subject: [PATCH 40/68] more robust path handling --- pyGCodeDecode/gcode_interpreter.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyGCodeDecode/gcode_interpreter.py b/pyGCodeDecode/gcode_interpreter.py index 8436e9f..36c7612 100644 --- a/pyGCodeDecode/gcode_interpreter.py +++ b/pyGCodeDecode/gcode_interpreter.py @@ -1,8 +1,8 @@ """GCode Interpreter Module.""" import importlib.resources -import pathlib import time +from pathlib import Path from typing import List, Optional, Tuple, Union import numpy as np @@ -138,7 +138,7 @@ class simulation: def __init__( self, - gcode_path: pathlib.Path, + gcode_path: Path, machine_name: str = None, initial_machine_setup: "setup" = None, output_unit_system: str = "SI (mm)", @@ -164,7 +164,7 @@ def __init__( """ simulation_start_time = time.time() self.last_index = None # used to optimize search in segment list - self.filename = gcode_path + self.filename = Path(gcode_path) self.firmware = None set_verbosity_level(verbosity_level) @@ -203,7 +203,7 @@ def __init__( self.firmware = self.initial_machine_setup_dict["firmware"] self.states: List[state] = generate_states( - filepath=gcode_path, initial_machine_setup=self.initial_machine_setup_dict + filepath=self.filename, initial_machine_setup=self.initial_machine_setup_dict ) custom_print( @@ -410,11 +410,11 @@ def extrusion_max_vel(self, output_unit_system: str = None) -> np.float64: return scaling * max_vel - def save_summary(self, filepath: Union[pathlib.Path, str]): + def save_summary(self, filepath: Union[Path, str]): """Save summary to .yaml file. Args: - filepath (pathlib.Path | str): path to summary file + filepath (Path | str): path to summary file Saved data keys: - filename (string, filename) @@ -438,7 +438,7 @@ def save_summary(self, filepath: Union[pathlib.Path, str]): } # create directory if necessary - pathlib.Path(filepath).parent.mkdir(parents=True, exist_ok=True) + Path(filepath).parent.mkdir(parents=True, exist_ok=True) with open(file=filepath, mode="w") as file: yaml.dump(data=summary, stream=file) From 0df01799637753d24c235b618ea6db5dcfc8a63d Mon Sep 17 00:00:00 2001 From: usmfi Date: Thu, 14 Aug 2025 13:57:21 +0200 Subject: [PATCH 41/68] fix abq --- pyGCodeDecode/abaqus_file_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyGCodeDecode/abaqus_file_generator.py b/pyGCodeDecode/abaqus_file_generator.py index a759a3b..c390569 100644 --- a/pyGCodeDecode/abaqus_file_generator.py +++ b/pyGCodeDecode/abaqus_file_generator.py @@ -67,9 +67,9 @@ def generate_abaqus_event_series( with open(filepath, "w") as outfile: for time, pos in zip(time, pos): outfile.write( - f"{time},{round(scaling*pos[0], round_to)},{round(scaling*pos[1], round_to)},{round(scaling*pos[2], round_to)},{pos[3]}\n" + f"{float(time)},{round(scaling*pos[0], round_to)},{round(scaling*pos[1], round_to)},{round(scaling*pos[2], round_to)},{pos[3]}\n" ) - event_series_list.append((time, scaling * pos[0], scaling * pos[1], scaling * pos[2], pos[3])) + event_series_list.append((float(time), scaling * pos[0], scaling * pos[1], scaling * pos[2], pos[3])) custom_print(f"💾 ABAQUS event series written to 👉 {outfile.name}") From d289cf1bc9124825f49be1a4a78df59704fcd480 Mon Sep 17 00:00:00 2001 From: usmfi Date: Mon, 18 Aug 2025 12:07:01 +0200 Subject: [PATCH 42/68] added "first" arg as initial position --- pyGCodeDecode/gcode_interpreter.py | 6 +- pyGCodeDecode/state_generator.py | 102 +++++++++++++++++++--------- pyGCodeDecode/utils.py | 4 ++ tests/data/test_printer_setups.yaml | 4 +- tests/test_state_generator.py | 38 +++++++++++ 5 files changed, 118 insertions(+), 36 deletions(-) diff --git a/pyGCodeDecode/gcode_interpreter.py b/pyGCodeDecode/gcode_interpreter.py index 36c7612..2d36994 100644 --- a/pyGCodeDecode/gcode_interpreter.py +++ b/pyGCodeDecode/gcode_interpreter.py @@ -570,7 +570,7 @@ def set_initial_position(self, initial_position: Union[tuple, dict], input_unit_ Args: initial_position: (tuple or dict) set initial position as tuple of len(4) - or dictionary with keys: {X, Y, Z, E}. + or dictionary with keys: {X, Y, Z, E} or "first" to use first occuring absolute position in GCode. input_unit_system (str, optional): Wanted input unit system. Uses the one specified for the setup if None is specified. @@ -578,6 +578,7 @@ def set_initial_position(self, initial_position: Union[tuple, dict], input_unit_ ```python setup.set_initial_position((1, 2, 3, 4)) setup.set_initial_position({"X": 1, "Y": 2, "Z": 3, "E": 4}) + setup.set_initial_position("first") # use first GCode position ``` """ @@ -593,6 +594,9 @@ def set_initial_position(self, initial_position: Union[tuple, dict], input_unit_ "Z": scaling * initial_position[2], "E": scaling * initial_position[3], } + elif initial_position == "first": # use first GCode position + self.initial_position = {"X": None, "Y": None, "Z": None, "E": None} + custom_print("Initial position set to first GCode position.", lvl=3) else: raise ValueError("Set initial position through dict with keys: {X, Y, Z, E} or as tuple with length 4.") diff --git a/pyGCodeDecode/state_generator.py b/pyGCodeDecode/state_generator.py index 7d4e5a8..98d79f2 100644 --- a/pyGCodeDecode/state_generator.py +++ b/pyGCodeDecode/state_generator.py @@ -221,6 +221,20 @@ def _dict_list_traveler(line_dict_list: List[dict], initial_machine_setup: dict) state_list: (list[state]) all states in a list """ + position_fully_defined = False + + def is_position_fully_defined(virtual_machine: dict) -> bool: + nonlocal position_fully_defined + + if position_fully_defined or all(virtual_machine[key] is not None for key in ax_keys): + position_fully_defined = True + return position_fully_defined + + def apply_pos_offset(virtual_machine: dict) -> list[float]: + pos = [] + for key in ax_keys: + pos.append(virtual_machine[key] + virtual_machine[f"_{key}"] if virtual_machine[key] is not None else None) + return pos def apply_extrusion(line_dict: dict, virtual_machine: dict, command: str) -> dict: e_value = line_dict[command]["E"] @@ -241,6 +255,9 @@ def apply_extrusion(line_dict: dict, virtual_machine: dict, command: str) -> dic state_list: List[state] = list() + pos_keys = ["X", "Y", "Z"] + ax_keys = pos_keys + ["E"] # add E for extrusion + virtual_machine = { "X": 0, # machine coordinates "Y": 0, @@ -268,30 +285,35 @@ def apply_extrusion(line_dict: dict, virtual_machine: dict, command: str) -> dic ) virtual_machine[key] = default_virtual_machine[key] - # initial state creation - state_position = position( - virtual_machine["X"] + virtual_machine["_X"], - virtual_machine["Y"] + virtual_machine["_Y"], - virtual_machine["Z"] + virtual_machine["_Z"], - virtual_machine["E"] + virtual_machine["_E"], - ) - - p_settings = state.p_settings( - speed=virtual_machine["p_vel"], - p_acc=virtual_machine["p_acc"], - jerk=virtual_machine["jerk"], - vX=virtual_machine["vX"], - vY=virtual_machine["vY"], - vZ=virtual_machine["vZ"], - vE=virtual_machine["vE"], - ) - new_state = state(state_position=state_position, state_p_settings=p_settings) # create new state - - # add initial state comment - new_state.comment = "Initial state created by pyGCD." - new_state.line_number = None - - state_list.append(new_state) + # create initial state only with initial position + if not any([virtual_machine[poskey] is None for poskey in pos_keys]): + # initial state creation + state_position = position(apply_pos_offset(virtual_machine)) + # position( + # virtual_machine["X"] + virtual_machine["_X"], + # virtual_machine["Y"] + virtual_machine["_Y"], + # virtual_machine["Z"] + virtual_machine["_Z"], + # virtual_machine["E"] + virtual_machine["_E"], + # ) + + # state_pos2 = position(apply_pos_offset(virtual_machine)) + + p_settings = state.p_settings( + speed=virtual_machine["p_vel"], + p_acc=virtual_machine["p_acc"], + jerk=virtual_machine["jerk"], + vX=virtual_machine["vX"], + vY=virtual_machine["vY"], + vZ=virtual_machine["vZ"], + vE=virtual_machine["vE"], + ) + new_state = state(state_position=state_position, state_p_settings=p_settings) # create new state + + # add initial state comment + new_state.comment = "Initial state created by pyGCD." + new_state.line_number = None + + state_list.append(new_state) # GCode functionality: for line_dict in line_dict_list: @@ -314,18 +336,22 @@ def apply_extrusion(line_dict: dict, virtual_machine: dict, command: str) -> dic virtual_machine["units"] = "SI (mm)" # position & velocity - pos_keys = ["X", "Y", "Z"] movement_commands = ["G0", "G1"] for command in movement_commands: # treat G0 and G1 the same if command in line_dict: # look for xyz movement commands and apply abs/rel for key in pos_keys: if key in line_dict[command]: + # absolute movement if virtual_machine["absolute_position"] is True: virtual_machine[key] = line_dict[command][key] - elif virtual_machine["absolute_position"] is False: # redundant - virtual_machine[key] = virtual_machine[key] + line_dict[command][key] + # relative movement + else: + if is_position_fully_defined(virtual_machine): + virtual_machine[key] = virtual_machine[key] + line_dict[command][key] + else: + raise ValueError("Position is not fully defined, cannot apply relative movement.") # look for extrusion commands and apply abs/rel if "E" in line_dict[command]: virtual_machine = apply_extrusion(line_dict, virtual_machine, command) @@ -365,12 +391,22 @@ def apply_extrusion(line_dict: dict, virtual_machine: dict, command: str) -> dic if "S" in line_dict["G4"]: pause_duration = line_dict["G4"]["S"] - state_position = position( - virtual_machine["X"] + virtual_machine["_X"], - virtual_machine["Y"] + virtual_machine["_Y"], - virtual_machine["Z"] + virtual_machine["_Z"], - virtual_machine["E"] + virtual_machine["_E"], - ) + # Ensure all axes are defined if any position is set + if any(virtual_machine[key] is not None for key in ax_keys) and not is_position_fully_defined(virtual_machine): + virtual_machine.update({key: virtual_machine.get(key, 0) or 0 for key in ax_keys}) + custom_print( + "Implicit zero position assumed for axes: '" + + ", ".join(key for key in ax_keys if virtual_machine[key] == 0) + + "' to fully define position." + ) + + state_position = position(apply_pos_offset(virtual_machine)) + # position( + # virtual_machine["X"] + virtual_machine["_X"], + # virtual_machine["Y"] + virtual_machine["_Y"], + # virtual_machine["Z"] + virtual_machine["_Z"], + # virtual_machine["E"] + virtual_machine["_E"], + # ) p_settings = state.p_settings( speed=virtual_machine["p_vel"], diff --git a/pyGCodeDecode/utils.py b/pyGCodeDecode/utils.py index 5289753..e77b3d2 100644 --- a/pyGCodeDecode/utils.py +++ b/pyGCodeDecode/utils.py @@ -97,6 +97,10 @@ def __str__(self) -> str: """Return string representation.""" return f"[{self.x}, {self.y}, {self.z}, {self.e}]" + def __repr__(self): + """Return a string representation of the 4D vector.""" + return self.__str__() + def __add__(self, other): """Add functionality for 4D vectors. diff --git a/tests/data/test_printer_setups.yaml b/tests/data/test_printer_setups.yaml index 613d728..7f5f135 100644 --- a/tests/data/test_printer_setups.yaml +++ b/tests/data/test_printer_setups.yaml @@ -13,7 +13,7 @@ prusa_mini: vY: 180 vZ: 12 vE: 80 - firmware: marlin_jd + firmware: prusa test: # general properties @@ -28,7 +28,7 @@ test: vY: 180 vZ: 30 vE: 33 - firmware: marlin_jd + firmware: junction_deviation debugging: # general properties diff --git a/tests/test_state_generator.py b/tests/test_state_generator.py index 8463be5..d887541 100644 --- a/tests/test_state_generator.py +++ b/tests/test_state_generator.py @@ -71,3 +71,41 @@ def test_state_generator(): assert states[20].state_p_settings.jerk == 5 # jerk settings (M205 X*) assert states[21].pause == 0.5 # dwell (G4) assert states[22].pause == 5 + + +def test_set_initial_position(): + """Test for initial position settings.""" + from pyGCodeDecode.gcode_interpreter import setup + + # setting coords + test_setup = setup( + presets_file=pathlib.Path("./tests/data/test_printer_setups.yaml"), + printer="test", + layer_cue="LAYER cue", + ) + initial_pos = (5, 4, 3, 2) # initial position definition + test_setup.set_initial_position(initial_pos) + + states = generate_states( + filepath=pathlib.Path("./tests/data/test_state_generator.gcode"), + initial_machine_setup=test_setup.get_dict(), + ) + + assert states[0].state_position.get_vec(withExtrusion=True) == list(initial_pos) # test for inital position + + # using first GCode pos + test_setup = setup( + presets_file=pathlib.Path("./tests/data/test_printer_setups.yaml"), + printer="test", + layer_cue="LAYER cue", + ) + initial_pos = (10, 20, 30, 0) # initial position as specified in "./data/test_state_generator.gcode" line 3 + test_setup.set_initial_position("first") + + states = generate_states( + filepath=pathlib.Path("./tests/data/test_state_generator.gcode"), + initial_machine_setup=test_setup.get_dict(), + ) + + # test for inital position at state 2 (line 3) + assert states[2].state_position.get_vec(withExtrusion=True) == list(initial_pos) From 759060550f9ee1d869f6be51c0cd31dbc584a50b Mon Sep 17 00:00:00 2001 From: usmfi Date: Wed, 20 Aug 2025 14:21:54 +0200 Subject: [PATCH 43/68] cleanup --- pyGCodeDecode/plotter.py | 1 - pyGCodeDecode/state_generator.py | 9 --------- 2 files changed, 10 deletions(-) diff --git a/pyGCodeDecode/plotter.py b/pyGCodeDecode/plotter.py index 85055c2..d1829ee 100644 --- a/pyGCodeDecode/plotter.py +++ b/pyGCodeDecode/plotter.py @@ -108,7 +108,6 @@ def _safe_screenshot(plotter: pv.Plotter, screenshot_path=None): x, y, z, e, scalar = [], [], [], [], [] bar = ProgressBar(name="3D Plot") - custom_print("result: ", lvl=3) for n, segm in enumerate(segments): bar.update((n + 1) / len(segments)) diff --git a/pyGCodeDecode/state_generator.py b/pyGCodeDecode/state_generator.py index 98d79f2..b8e7da1 100644 --- a/pyGCodeDecode/state_generator.py +++ b/pyGCodeDecode/state_generator.py @@ -90,7 +90,6 @@ "filament_diam": 1.75, # default settings "p_vel": 35, - "t_vel": 35, "p_acc": 200, "jerk": 10, # axis max speeds @@ -289,14 +288,6 @@ def apply_extrusion(line_dict: dict, virtual_machine: dict, command: str) -> dic if not any([virtual_machine[poskey] is None for poskey in pos_keys]): # initial state creation state_position = position(apply_pos_offset(virtual_machine)) - # position( - # virtual_machine["X"] + virtual_machine["_X"], - # virtual_machine["Y"] + virtual_machine["_Y"], - # virtual_machine["Z"] + virtual_machine["_Z"], - # virtual_machine["E"] + virtual_machine["_E"], - # ) - - # state_pos2 = position(apply_pos_offset(virtual_machine)) p_settings = state.p_settings( speed=virtual_machine["p_vel"], From d2ee8213ff1d4623423995d2e0a4fb2b94063376 Mon Sep 17 00:00:00 2001 From: usmfi Date: Wed, 20 Aug 2025 14:28:01 +0200 Subject: [PATCH 44/68] reworked setup --- .vscode/settings.json | 3 +- .../examples/data/printer_presets.yaml | 5 + pyGCodeDecode/gcode_interpreter.py | 137 ++++++++++-------- tests/data/test_printer_setup_single.yaml | 16 ++ tests/test_gcode_interpreter.py | 78 +++++++++- 5 files changed, 177 insertions(+), 62 deletions(-) create mode 100644 tests/data/test_printer_setup_single.yaml diff --git a/.vscode/settings.json b/.vscode/settings.json index 02940a7..d8fb24f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,7 +7,8 @@ } ], "python.testing.pytestArgs": [ - "tests" + "tests", + "-s" ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, diff --git a/pyGCodeDecode/examples/data/printer_presets.yaml b/pyGCodeDecode/examples/data/printer_presets.yaml index 359a1f7..450425f 100644 --- a/pyGCodeDecode/examples/data/printer_presets.yaml +++ b/pyGCodeDecode/examples/data/printer_presets.yaml @@ -28,6 +28,11 @@ prusa_mini: vY: 180 vZ: 12 vE: 80 + absolute_position: True + absolute_extrusion: True + volumetric_extrusion: False + initial_position: "first" + units: "SI (mm)" firmware: prusa debugging: diff --git a/pyGCodeDecode/gcode_interpreter.py b/pyGCodeDecode/gcode_interpreter.py index 2d36994..be26bb1 100644 --- a/pyGCodeDecode/gcode_interpreter.py +++ b/pyGCodeDecode/gcode_interpreter.py @@ -177,7 +177,7 @@ def __init__( # create a printer setup with default values if none was specified if initial_machine_setup is not None: - if machine_name is not None and initial_machine_setup.get_dict()["printer_name"] != machine_name: + if machine_name is not None and initial_machine_setup.get_dict()["printer"] != machine_name: raise ValueError("Both a printer name and a printer setup were specified, but they do not match!") else: pass @@ -207,7 +207,7 @@ def __init__( ) custom_print( - f"Simulating {self.filename} with {self.initial_machine_setup_dict['printer_name']} using " + f"Simulating {self.filename} with {self.initial_machine_setup_dict['printer']} using " f"the {self.firmware} firmware." ) self.blocklist: List[planner_block] = generate_planner_blocks(states=self.states, firmware=self.firmware) @@ -469,40 +469,44 @@ def __init__( self, presets_file: str, printer: str = None, - layer_cue: str = None, verbosity_level: Optional[int] = None, + **kwargs, ): - """Create simulation setup. + """Initialize the setup for the printing simulation. Args: - presets_file: (string) choose setup yaml file with printer presets - printer: (string) select printer from preset file - layer_cue: (string) set slicer specific layer change cue from comment - verbosity_level: (int, default = None) set verbosity level (0: no output, 1: warnings, 2: info, 3: debug) + presets_file (str): Path to the YAML file containing printer presets. + printer (str, optional): Name of the printer to select from the preset file. Defaults to None. + verbosity_level (int, optional): Verbosity level for logging (0: no output, 1: warnings, 2: info, 3: debug). Defaults to None. + **kwargs: Additional properties to set or override in the setup. + + Raises: + ValueError: If multiple printers are found in the preset file but none is selected. """ - # the input unit system is only implemented for 'set_initial_position'. - # Regardless, the class has this attribute so it's more similar to the simulation class. + set_verbosity_level(verbosity_level) self.available_unit_systems = {"SI": 1e3, "SI (mm)": 1.0, "inch": 25.4} self.input_unit_system = "SI (mm)" - set_verbosity_level(verbosity_level) - self.initial_position = { - "X": 0, - "Y": 0, - "Z": 0, - "E": 0, - } # default initial pos is zero - self.setup_dict = self.load_setup(presets_file) + # load the setup + self.load_setup(presets_file, printer=printer) - self.filename = presets_file - self.printer_select = printer - self.layer_cue = layer_cue + # set additional properties provided as keyword arguments + self.set_property(kwargs) - if self.printer_select is not None: - self.select_printer(printer_name=self.printer_select) - self.firmware = self.get_dict()["firmware"] + def __getattr__(self, name): + """Access to setup_dict content.""" + if name in self.setup_dict: + return self.setup_dict[name] + raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") + + def __setattr__(self, name, value): + """Set setup_dict keys.""" + if name in ["setup_dict", "filename", "available_unit_systems", "input_unit_system"]: + super().__setattr__(name, value) + else: + self.setup_dict[name] = value - def load_setup(self, filepath): + def load_setup(self, filepath, printer=None): """Load setup from file. Args: @@ -514,7 +518,32 @@ def load_setup(self, filepath): file = open(file=filepath) setup_dict = yaml.load(file, Loader=Loader) - return setup_dict + if printer: + self.setup_dict = setup_dict[printer] + self.printer = printer + else: + printers_available = [printer for printer in setup_dict] + + if len(printers_available) == 1: + printer = printers_available[0] + self.setup_dict = setup_dict[printer] + self.printer = printer + custom_print(f"Automatically selected the '{printer}' printer in the setup file {filepath}.", lvl=2) + else: + raise ValueError("Multiple printers found but none has been selected.") + + # parse initial position if set via config + if "initial_position" in self.setup_dict: + self.set_initial_position(self.setup_dict["initial_position"]) + else: + self.set_initial_position( + { + "X": 0, + "Y": 0, + "Z": 0, + "E": 0, + } + ) # default initial pos is zero def check_initial_setup(self): """Check the printer Dict for typos or missing parameters and raise errors if invalid.""" @@ -530,13 +559,22 @@ def check_initial_setup(self): "Y", "Z", "E", - "printer_name", + "printer", "firmware", ] - optional_keys = ["layer_cue", "nozzle_diam", "filament_diam", "volumetric_extrusion"] + optional_keys = [ + "layer_cue", + "nozzle_diam", + "filament_diam", + "volumetric_extrusion", + "absolute_position", + "absolute_extrusion", + "initial_position", + "units", + ] valid_keys = req_keys + optional_keys - initial_machine_setup = self.get_dict() + initial_machine_setup = self.setup_dict # check if all provided keys are valid for key in initial_machine_setup: @@ -554,17 +592,6 @@ def check_initial_setup(self): ) return initial_machine_setup - def select_printer(self, printer_name): - """Select printer by name. - - Args: - printer_name: (string) select printer by name - """ - if printer_name not in self.setup_dict: - raise ValueError(f"Selected Printer {self.printer_select} not found in setup file: {self.filename}.") - else: - self.printer_select = printer_name - def set_initial_position(self, initial_position: Union[tuple, dict], input_unit_system: str = None): """Set initial Position. @@ -586,16 +613,18 @@ def set_initial_position(self, initial_position: Union[tuple, dict], input_unit_ if isinstance(initial_position, dict) and all(key in initial_position for key in ["X", "Y", "Z", "E"]): for key in initial_position: - self.initial_position[key] = scaling * initial_position[key] + self.setup_dict[key] = scaling * initial_position[key] elif isinstance(initial_position, tuple) and len(initial_position) == 4: - self.initial_position = { - "X": scaling * initial_position[0], - "Y": scaling * initial_position[1], - "Z": scaling * initial_position[2], - "E": scaling * initial_position[3], - } + self.setup_dict.update( + { + "X": scaling * initial_position[0], + "Y": scaling * initial_position[1], + "Z": scaling * initial_position[2], + "E": scaling * initial_position[3], + } + ) elif initial_position == "first": # use first GCode position - self.initial_position = {"X": None, "Y": None, "Z": None, "E": None} + self.setup_dict.update({"X": None, "Y": None, "Z": None, "E": None}) custom_print("Initial position set to first GCode position.", lvl=3) else: raise ValueError("Set initial position through dict with keys: {X, Y, Z, E} or as tuple with length 4.") @@ -614,10 +643,7 @@ def set_property(self, property_dict: dict): ``` """ - if self.printer_select is not None: - self.setup_dict[self.printer_select].update(property_dict) - else: - raise ValueError("No printer is selected. Select printer through select_printer() beforehand.") + self.setup_dict.update(property_dict) def get_dict(self) -> dict: """Return the setup for the selected printer. @@ -625,12 +651,7 @@ def get_dict(self) -> dict: Returns: return_dict: (dict) setup dictionary """ - return_dict = self.setup_dict[self.printer_select] # create dict - return_dict.update(self.initial_position) # add initial position - if self.layer_cue is not None: - return_dict.update({"layer_cue": self.layer_cue}) # add layer cue - return_dict.update({"printer_name": self.printer_select}) # add printer name - + return_dict = self.setup_dict return return_dict def get_scaling_factor(self, input_unit_system: str = None) -> float: diff --git a/tests/data/test_printer_setup_single.yaml b/tests/data/test_printer_setup_single.yaml new file mode 100644 index 0000000..5d40fbe --- /dev/null +++ b/tests/data/test_printer_setup_single.yaml @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +debugging: + # general properties + nozzle_diam: 0.4 + filament_diam: 1.75 + # default settings + p_vel: 85 + p_acc: 100 + jerk: 8 + # axis max speeds + vX: 180 + vY: 170 + vZ: 12 + vE: 80 + firmware: marlin diff --git a/tests/test_gcode_interpreter.py b/tests/test_gcode_interpreter.py index 822a778..6f41586 100644 --- a/tests/test_gcode_interpreter.py +++ b/tests/test_gcode_interpreter.py @@ -42,10 +42,82 @@ def test_setup(): assert sim_dict["Z"] == 3 assert sim_dict["E"] == 4 - # change printer afterwards - simulation_setup.select_printer("prusa_mini") + # # change printer afterwards + # simulation_setup.select_printer("prusa_mini") + # sim_dict = simulation_setup.get_dict() + # assert sim_dict["p_acc"] == 1250 + + +def test_setup_extended(): + """Test for the simulation setup class.""" + from pyGCodeDecode.gcode_interpreter import setup + + # test for specific printer is selected + simulation_setup = setup(presets_file=pathlib.Path("./tests/data/test_printer_setups.yaml"), printer="test") + assert simulation_setup.printer == "test" + assert simulation_setup.firmware == "junction_deviation" + + # test for multiple printers in file and none has been selected -> error expected + try: + simulation_setup = setup( + presets_file=pathlib.Path("./tests/data/test_printer_setups.yaml"), + ) + assert False, "Expected ValueError was not raised." + except ValueError as e: + assert str(e) == "Multiple printers found but none has been selected." + + # test for single printer in file and none has been selected -> auto select + simulation_setup = setup( + presets_file=pathlib.Path("./tests/data/test_printer_setup_single.yaml"), verbosity_level=4 + ) + assert simulation_setup.printer == "debugging" + + # test for custom properties setting + simulation_setup = setup( + presets_file=pathlib.Path("./tests/data/test_printer_setup_single.yaml"), + verbosity_level=4, + extra_property=12, + ) + assert simulation_setup.printer == "debugging" + + # test the accessors + assert simulation_setup.extra_property == 12 + assert simulation_setup.get_dict()["extra_property"] == 12 + + # test the setters + simulation_setup.extra_property = 14 + simulation_setup.set_property({"special_property": 9}) + + assert simulation_setup.extra_property == 14 + assert simulation_setup.get_dict()["extra_property"] == 14 + assert simulation_setup.get_dict()["special_property"] == 9 + assert simulation_setup.special_property == 9 + + # test if printer settings are correctly loaded sim_dict = simulation_setup.get_dict() - assert sim_dict["p_acc"] == 1250 + assert sim_dict["p_vel"] == 85 + assert sim_dict["p_acc"] == 100 + assert sim_dict["jerk"] == 8 + + assert sim_dict["vX"] == 180 + assert sim_dict["vY"] == 170 + assert sim_dict["vZ"] == 12 + assert sim_dict["vE"] == 80 + + assert sim_dict["nozzle_diam"] == 0.4 + assert sim_dict["filament_diam"] == 1.75 + + assert sim_dict["X"] == 0 + assert sim_dict["Y"] == 0 + assert sim_dict["Z"] == 0 + assert sim_dict["E"] == 0 + + simulation_setup.set_initial_position((1, 2, 3, 4)) + sim_dict = simulation_setup.get_dict() + assert sim_dict["X"] == 1 + assert sim_dict["Y"] == 2 + assert sim_dict["Z"] == 3 + assert sim_dict["E"] == 4 def test_simulation_class(): From 58487d6d6228a7da340dc655d35cea98ccaf6b77 Mon Sep 17 00:00:00 2001 From: usmfi Date: Wed, 20 Aug 2025 14:42:31 +0200 Subject: [PATCH 45/68] rename var --- pyGCodeDecode/gcode_interpreter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyGCodeDecode/gcode_interpreter.py b/pyGCodeDecode/gcode_interpreter.py index be26bb1..ec90f9a 100644 --- a/pyGCodeDecode/gcode_interpreter.py +++ b/pyGCodeDecode/gcode_interpreter.py @@ -163,7 +163,7 @@ def __init__( ``` """ simulation_start_time = time.time() - self.last_index = None # used to optimize search in segment list + self._last_index = None # used to optimize search in segment list self.filename = Path(gcode_path) self.firmware = None set_verbosity_level(verbosity_level) @@ -297,7 +297,7 @@ def get_values(self, t: float, output_unit_system: str = None) -> Tuple[List[flo list: [pos_x, pos_y, pos_z, pos_e] position """ segments = unpack_blocklist(blocklist=self.blocklist) - segm, self.last_index = find_current_segment(path=segments, t=t, last_index=self.last_index) + segm, self._last_index = find_current_segment(path=segments, t=t, last_index=self._last_index) tmp_vel = segm.get_velocity(t=t).get_vec(withExtrusion=True) tmp_pos = segm.get_position(t=t).get_vec(withExtrusion=True) From bf839544da9727ce6da759fe21ead786e82b4976 Mon Sep 17 00:00:00 2001 From: usmfi Date: Wed, 20 Aug 2025 15:06:39 +0200 Subject: [PATCH 46/68] implicit position setting when using M92 before any pos is defined --- pyGCodeDecode/state_generator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyGCodeDecode/state_generator.py b/pyGCodeDecode/state_generator.py index b8e7da1..3292350 100644 --- a/pyGCodeDecode/state_generator.py +++ b/pyGCodeDecode/state_generator.py @@ -355,8 +355,10 @@ def apply_extrusion(line_dict: dict, virtual_machine: dict, command: str) -> dic if "G92" in line_dict: for key in line_dict["G92"]: if key in known_commands["G92"]: - virtual_machine["_" + key] = ( - virtual_machine[key] + line_dict["G92"][key] + virtual_machine["_" + key] + if virtual_machine[key] is None: + virtual_machine[key] = 0 # initialize to 0 if no position is set beforehand + virtual_machine[f"_{key}"] = ( + virtual_machine[key] + line_dict["G92"][key] + virtual_machine[f"_{key}"] ) virtual_machine[key] = line_dict["G92"][key] From a224577b62404b01ef1053adf8dfef702a3444f7 Mon Sep 17 00:00:00 2001 From: usmfi Date: Wed, 20 Aug 2025 15:24:03 +0200 Subject: [PATCH 47/68] removed deprc func hints --- README.md | 8 +------- pyGCodeDecode/gcode_interpreter.py | 2 -- tests/test_gcode_interpreter.py | 5 ----- 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/README.md b/README.md index 1b479aa..ab8f19e 100644 --- a/README.md +++ b/README.md @@ -104,13 +104,7 @@ from pyGCodeDecode import gcode_interpreter 1. Load your setup `.yaml` file through: ```python -setup = gcode_interpreter.setup(filename=r"./pyGCodeDecode/data/default_printer_presets.yaml") -``` - -1. Select your printer from the setup by name: - -```python -setup.select_printer("prusa_mini") +setup = gcode_interpreter.setup(filename=r"./pyGCodeDecode/data/default_printer_presets.yaml", printer="prusa_mini") ``` 1. You can optionally set or modify custom properties after loading the setup: diff --git a/pyGCodeDecode/gcode_interpreter.py b/pyGCodeDecode/gcode_interpreter.py index ec90f9a..bed83ba 100644 --- a/pyGCodeDecode/gcode_interpreter.py +++ b/pyGCodeDecode/gcode_interpreter.py @@ -632,8 +632,6 @@ def set_initial_position(self, initial_position: Union[tuple, dict], input_unit_ def set_property(self, property_dict: dict): """Overwrite or add a property to the printer dictionary. - Printer has to be selected through select_printer() beforehand. - Args: property_dict: (dict) set or add property to the setup diff --git a/tests/test_gcode_interpreter.py b/tests/test_gcode_interpreter.py index 6f41586..ae4b93a 100644 --- a/tests/test_gcode_interpreter.py +++ b/tests/test_gcode_interpreter.py @@ -42,11 +42,6 @@ def test_setup(): assert sim_dict["Z"] == 3 assert sim_dict["E"] == 4 - # # change printer afterwards - # simulation_setup.select_printer("prusa_mini") - # sim_dict = simulation_setup.get_dict() - # assert sim_dict["p_acc"] == 1250 - def test_setup_extended(): """Test for the simulation setup class.""" From 0b9f726d1941e12a79d49168ef5983764a4c578e Mon Sep 17 00:00:00 2001 From: usmfi Date: Wed, 20 Aug 2025 15:34:46 +0200 Subject: [PATCH 48/68] doc updates --- doc.md | 702 +++++++++++++++++++++++++++++++++++++++++---- pydoc-markdown.yml | 2 - 2 files changed, 641 insertions(+), 63 deletions(-) diff --git a/doc.md b/doc.md index c8a8134..e453cb9 100644 --- a/doc.md +++ b/doc.md @@ -139,6 +139,49 @@ class simulation() Simulation of .gcode with given machine parameters. + + +#### simulation.\_\_init\_\_ + +```python +def __init__(gcode_path: Path, + machine_name: str = None, + initial_machine_setup: "setup" = None, + output_unit_system: str = "SI (mm)", + verbosity_level: Optional[int] = None) +``` + +Initialize the Simulation of a given G-code with initial machine setup or default machine. + +- Generate all states from GCode. +- Connect states with planner blocks, consisting of segments +- Self correct inconsistencies. + +**Arguments**: + +- `gcode_path` - (Path) path to GCode +- `machine_name` - (string, default = None) name of the default machine to use +- `initial_machine_setup` - (setup, default = None) setup instance +- `output_unit_system` - (string, default = "SI (mm)") available unit systems: SI, SI (mm) & inch +- `verbosity_level` - (int, default = None) set verbosity level (0: no output, 1: warnings, 2: info, 3: debug) + + +**Example**: + +```python +gcode_interpreter.simulation(gcode_path=r"path/to/part.gcode", initial_machine_setup=printer_setup) +``` + + + +#### simulation.\_\_getattr\_\_ + +```python +def __getattr__(name) +``` + +Get result by name. + #### simulation.trajectory\_self\_correct @@ -293,14 +336,14 @@ Return scaled maximum velocity while extruding. #### simulation.save\_summary ```python -def save_summary(filepath: Union[pathlib.Path, str]) +def save_summary(filepath: Union[Path, str]) ``` Save summary to .yaml file. **Arguments**: -- `filepath` _pathlib.Path | str_ - path to summary file +- `filepath` _Path | str_ - path to summary file Saved data keys: - filename (string, filename) @@ -338,43 +381,74 @@ class setup() Setup for printing simulation. - + -#### setup.load\_setup +#### setup.\_\_init\_\_ ```python -def load_setup(filepath) +def __init__(presets_file: str, + printer: str = None, + verbosity_level: Optional[int] = None, + **kwargs) ``` -Load setup from file. +Initialize the setup for the printing simulation. **Arguments**: -- `filepath` - (string) specify path to setup file +- `presets_file` _str_ - Path to the YAML file containing printer presets. +- `printer` _str, optional_ - Name of the printer to select from the preset file. Defaults to None. +- `verbosity_level` _int, optional_ - Verbosity level for logging (0: no output, 1: warnings, 2: info, 3: debug). Defaults to None. +- `**kwargs` - Additional properties to set or override in the setup. - -#### setup.check\_initial\_setup +**Raises**: + +- `ValueError` - If multiple printers are found in the preset file but none is selected. + + + +#### setup.\_\_getattr\_\_ ```python -def check_initial_setup() +def __getattr__(name) ``` -Check the printer Dict for typos or missing parameters and raise errors if invalid. +Access to setup_dict content. - + -#### setup.select\_printer +#### setup.\_\_setattr\_\_ ```python -def select_printer(printer_name) +def __setattr__(name, value) ``` -Select printer by name. +Set setup_dict keys. + + + +#### setup.load\_setup + +```python +def load_setup(filepath, printer=None) +``` + +Load setup from file. **Arguments**: -- `printer_name` - (string) select printer by name +- `filepath` - (string) specify path to setup file + + + +#### setup.check\_initial\_setup + +```python +def check_initial_setup() +``` + +Check the printer Dict for typos or missing parameters and raise errors if invalid. @@ -390,7 +464,7 @@ Set initial Position. **Arguments**: - `initial_position` - (tuple or dict) set initial position as tuple of len(4) - or dictionary with keys: {X, Y, Z, E}. + or dictionary with keys: {X, Y, Z, E} or "first" to use first occuring absolute position in GCode. - `input_unit_system` _str, optional_ - Wanted input unit system. Uses the one specified for the setup if None is specified. @@ -400,6 +474,7 @@ Set initial Position. ```python setup.set_initial_position((1, 2, 3, 4)) setup.set_initial_position({"X": 1, "Y": 2, "Z": 3, "E": 4}) +setup.set_initial_position("first") # use first GCode position ``` @@ -412,8 +487,6 @@ def set_property(property_dict: dict) Overwrite or add a property to the printer dictionary. -Printer has to be selected through select_printer() beforehand. - **Arguments**: - `property_dict` - (dict) set or add property to the setup @@ -517,6 +590,18 @@ class ProgressBar() A simple progress bar for the console. + + +#### ProgressBar.\_\_init\_\_ + +```python +def __init__(name: str = "Percent", + barLength: int = 4, + verbosity_level: int = 2) +``` + +Initialize a progress bar. + #### ProgressBar.update @@ -547,6 +632,21 @@ class junction_handling() Junction handling super class. + + +#### junction\_handling.\_\_init\_\_ + +```python +def __init__(state_A: state, state_B: state) +``` + +Initialize the junction handling. + +**Arguments**: + +- `state_A` - (state) start state +- `state_B` - (state) end state + #### junction\_handling.connect\_state @@ -652,6 +752,21 @@ Prusa specific classic jerk junction handling (validated on Prusa Mini). // ... ``` + + +#### prusa.\_\_init\_\_ + +```python +def __init__(state_A: state, state_B: state) +``` + +Marlin classic jerk specific junction velocity calculation. + +**Arguments**: + +- `state_A` - (state) start state +- `state_B` - (state) end state + #### prusa.calc\_j\_vel @@ -700,6 +815,21 @@ vmax_junction_sqr = sq(vmax_junction * v_factor); // ... ``` + + +#### marlin.\_\_init\_\_ + +```python +def __init__(state_A: state, state_B: state) +``` + +Marlin classic jerk specific junction velocity calculation. + +**Arguments**: + +- `state_A` - (state) start state +- `state_B` - (state) end state + #### marlin.calc\_j\_vel @@ -768,6 +898,21 @@ block->max_entry_speed = vmax_junction; // ... ``` + + +#### ultimaker.\_\_init\_\_ + +```python +def __init__(state_A: state, state_B: state) +``` + +Ultimaker specific junction velocity calculation. + +**Arguments**: + +- `state_A` - (state) start state +- `state_B` - (state) end state + #### ultimaker.calc\_j\_vel @@ -867,6 +1012,21 @@ Calculate junction deviation velocity from 2 velocities. - `velocity` - (float) velocity abs value + + +#### junction\_deviation.\_\_init\_\_ + +```python +def __init__(state_A: state, state_B: state) +``` + +Marlin specific junction velocity calculation with Junction Deviation. + +**Arguments**: + +- `state_A` - (state) start state +- `state_B` - (state) end state + #### junction\_deviation.get\_junction\_vel @@ -979,6 +1139,22 @@ def calc_results(*additional_calculators: abstract_result) Calculate the result of the planner block. + + +#### planner\_block.\_\_init\_\_ + +```python +def __init__(state: state, prev_block: "planner_block", firmware=None) +``` + +Calculate and store planner block consisting of one or multiple segments. + +**Arguments**: + +- `state` - (state) the current state +- `prev_block` - (planner_block) previous planner block +- `firmware` - (string, default = None) firmware selection for junction + #### planner\_block.prev\_block @@ -1001,6 +1177,26 @@ def next_block() Define next_block as property. + + +#### planner\_block.\_\_str\_\_ + +```python +def __str__() -> str +``` + +Create string from planner block. + + + +#### planner\_block.\_\_repr\_\_ + +```python +def __repr__() -> str +``` + +Represent planner block. + #### planner\_block.get\_segments @@ -1326,6 +1522,63 @@ class p_settings() Store Printing Settings. + + +#### p\_settings.\_\_init\_\_ + +```python +def __init__(p_acc, jerk, vX, vY, vZ, vE, speed, units="SI (mm)") +``` + +Initialize printing settings. + +**Arguments**: + +- `p_acc` - (float) printing acceleration +- `jerk` - (float) jerk or similar +- `vX` - (float) max x velocity +- `vY` - (float) max y velocity +- `vZ` - (float) max z velocity +- `vE` - (float) max e velocity +- `speed` - (float) default target velocity +- `units` - (string, default = "SI (mm)") unit settings + + + +#### p\_settings.\_\_str\_\_ + +```python +def __str__() -> str +``` + +Create summary string for p_settings. + + + +#### p\_settings.\_\_repr\_\_ + +```python +def __repr__() -> str +``` + +Define representation. + + + +#### state.\_\_init\_\_ + +```python +def __init__(state_position: position = None, + state_p_settings: p_settings = None) +``` + +Initialize a state. + +**Arguments**: + +- `state_position` - (position) state position +- `state_p_settings` - (p_settings) state printing settings + #### state.state\_position @@ -1426,6 +1679,26 @@ Set previous state. - `state` - (state) previous state + + +#### state.\_\_str\_\_ + +```python +def __str__() -> str +``` + +Generate string for representation. + + + +#### state.\_\_repr\_\_ + +```python +def __repr__() -> str +``` + +Call __str__() for representation. + ## pyGCodeDecode.state\_generator @@ -1511,7 +1784,7 @@ Write the submodel entry and exit times to a yaml file. ## pyGCodeDecode.utils -Utilitys. +Utilities. Utils for the GCode Reader contains: - vector 4D @@ -1543,13 +1816,63 @@ A float subclass representing a time duration in seconds. 5.0 ``` + + +#### seconds.\_\_new\_\_ + +```python +def __new__(cls, value) +``` + +Create a new instance of seconds. + + + +#### seconds.\_\_str\_\_ + +```python +def __str__() -> str +``` + +Return string representation of the time in seconds. + + + +#### seconds.\_\_sub\_\_ + +```python +def __sub__(other) -> "seconds" +``` + +Subtract seconds or float and return a new seconds instance. + + + +#### seconds.\_\_add\_\_ + +```python +def __add__(other) -> "seconds" +``` + +Add seconds or float and return a new seconds instance. + + + +#### seconds.\_\_repr\_\_ + +```python +def __repr__() -> str +``` + +Return a string representation of the seconds object. + #### seconds.seconds ```python @property -def seconds() +def seconds() -> float ``` Return the float value of the seconds instance. @@ -1572,12 +1895,160 @@ The vector_4D class stores 4D vector in x,y,z,e. - truediv (scalar) - eq + + +#### vector\_4D.\_\_init\_\_ + +```python +def __init__(*args) +``` + +Store 3D position + extrusion axis. + +**Arguments**: + +- `args` - coordinates as arguments x,y,z,e or (tuple or list) [x,y,z,e] + + + +#### vector\_4D.\_\_str\_\_ + +```python +def __str__() -> str +``` + +Return string representation. + + + +#### vector\_4D.\_\_repr\_\_ + +```python +def __repr__() +``` + +Return a string representation of the 4D vector. + + + +#### vector\_4D.\_\_add\_\_ + +```python +def __add__(other) +``` + +Add functionality for 4D vectors. + +**Arguments**: + +- `other` - (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') + + +**Returns**: + +- `add` - (self) component wise addition + + + +#### vector\_4D.\_\_sub\_\_ + +```python +def __sub__(other) +``` + +Sub functionality for 4D vectors. + +**Arguments**: + +- `other` - (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') + + +**Returns**: + +- `sub` - (self) component wise subtraction + + + +#### vector\_4D.\_\_mul\_\_ + +```python +def __mul__(other) +``` + +Scalar multiplication functionality for 4D vectors. + +**Arguments**: + +- `other` - (float or int) + + +**Returns**: + +- `mul` - (self) scalar multiplication, scaling + + + +#### vector\_4D.\_\_truediv\_\_ + +```python +def __truediv__(other) +``` + +Scalar division functionality for 4D Vectors. + +**Arguments**: + +- `other` - (float or int) + + +**Returns**: + +- `div` - (self) scalar division, scaling + + + +#### vector\_4D.\_\_eq\_\_ + +```python +def __eq__(other) -> bool +``` + +Check for equality and return True if equal (with tolerance). + +**Arguments**: + +- `other` - (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') + + +**Returns**: + +- `eq` - (bool) true if equal (with tolerance) + + + +#### vector\_4D.\_\_gt\_\_ + +```python +def __gt__(other) -> bool +``` + +Check for greater than and return True if greater. + +**Arguments**: + +- `other` - (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') + + +**Returns**: + +- `gt` - (bool) true if greater + #### vector\_4D.get\_vec ```python -def get_vec(withExtrusion=False) -> List[float] +def get_vec(withExtrusion: bool = False) -> List[float] ``` Return the 4D vector, optionally with extrusion. @@ -1596,7 +2067,7 @@ Return the 4D vector, optionally with extrusion. #### vector\_4D.get\_norm ```python -def get_norm(withExtrusion=False) -> float +def get_norm(withExtrusion: bool = False) -> float ``` Return the 4D vector norm. Optional with extrusion. @@ -1620,6 +2091,16 @@ class position(vector_4D) 4D - Position object for (Cartesian) 3D printer. + + +#### position.\_\_str\_\_ + +```python +def __str__() -> str +``` + +Print out position. + #### position.is\_travel @@ -1664,7 +2145,7 @@ Return True if there is extrusion between self and other position. #### position.get\_t\_distance ```python -def get_t_distance(other=None, withExtrusion=False) -> float +def get_t_distance(other=None, withExtrusion: bool = False) -> float ``` Calculate the travel distance between self and other position. If none is provided, zero will be used. @@ -1679,60 +2160,56 @@ Calculate the travel distance between self and other position. If none is provid - `travel` - (float) travel or extrusion and travel distance - + -### velocity Objects +#### position.\_\_truediv\_\_ ```python -class velocity(vector_4D) +def __truediv__(other) ``` -4D - Velocity object for (Cartesian) 3D printer. +Divide position by seconds to get velocity. - + -#### velocity.get\_norm\_dir +### velocity Objects ```python -def get_norm_dir(withExtrusion=False) +class velocity(vector_4D) ``` -Get normalized vector (regarding travel distance), if only extrusion occurs, normalize to extrusion length. - -**Arguments**: +4D - Velocity object for (Cartesian) 3D printer. -- `withExtrusion` - (bool, default = False) choose if norm dir contains extrusion + +#### velocity.\_\_str\_\_ -**Returns**: +```python +def __str__() -> str +``` -- `dir` - (list[3 or 4]) normalized direction vector as list +Print out velocity. - + -#### velocity.avoid\_overspeed +#### velocity.get\_norm\_dir ```python -def avoid_overspeed(p_settings) +def get_norm_dir(withExtrusion: bool = False) -> Optional[np.ndarray] ``` -Return velocity without any axis overspeed. - -**Arguments**: +Get normalized direction vector as numpy array. -- `p_settings` - (p_settings) printing settings +If only extrusion occurs and withExtrusion=True, normalize to the extrusion length. - -**Returns**: - -- `vel` - (velocity) constrained by max velocity +Returns None if both travel and extrusion are zero. #### velocity.not\_zero ```python -def not_zero() +def not_zero() -> bool ``` Return True if velocity is not zero. @@ -1746,15 +2223,35 @@ Return True if velocity is not zero. #### velocity.is\_extruding ```python -def is_extruding() +def is_extruding() -> bool ``` -Return True if extrusion velocity is not zero. +Return True if extrusion velocity is greater than zero. **Returns**: - `is_extruding` - (bool) true if positive extrusion velocity + + +#### velocity.\_\_mul\_\_ + +```python +def __mul__(other) +``` + +Multiply velocity by a time to get position, or by scalar. + + + +#### velocity.\_\_truediv\_\_ + +```python +def __truediv__(other) +``` + +Divide velocity by scalar. + ### acceleration Objects @@ -1765,6 +2262,36 @@ class acceleration(vector_4D) 4D - Acceleration object for (Cartesian) 3D printer. + + +#### acceleration.\_\_str\_\_ + +```python +def __str__() -> str +``` + +Print out acceleration. + + + +#### acceleration.\_\_mul\_\_ + +```python +def __mul__(other) +``` + +Multiply acceleration by a time to get velocity, or by scalar. + + + +#### acceleration.\_\_truediv\_\_ + +```python +def __truediv__(other) +``` + +Divide acceleration by scalar. + ### segment Objects @@ -1789,12 +2316,56 @@ contains: time, position, velocity - create_initial: returns the artificial initial segment where everything is at standstill, intervall length = 0 - self_check: returns True if all self checks have been successfull + + +#### segment.\_\_init\_\_ + +```python +def __init__(t_begin: Union[float, seconds], + t_end: Union[float, seconds], + pos_begin: position, + vel_begin: velocity, + pos_end: position = None, + vel_end: velocity = None) +``` + +Initialize a segment. + +**Arguments**: + +- `t_begin` - (float) begin of segment +- `t_end` - (float) end of segment +- `pos_begin` - (position) beginning position of segment +- `vel_begin` - (velocity) beginning velocity of segment +- `pos_end` - (position, default = None) ending position of segment +- `vel_end` - (velocity, default = None) ending velocity of segment + + + +#### segment.\_\_str\_\_ + +```python +def __str__() -> str +``` + +Create string from segment. + + + +#### segment.\_\_repr\_\_ + +```python +def __repr__() +``` + +Segment representation. + #### segment.move\_segment\_time ```python -def move_segment_time(delta_t: float) +def move_segment_time(delta_t: Union[float, seconds]) -> None ``` Move segment in time. @@ -1808,7 +2379,7 @@ Move segment in time. #### segment.get\_velocity ```python -def get_velocity(t: float) -> velocity +def get_velocity(t: Union[float, seconds]) -> velocity ``` Get current velocity of segment at a certain time. @@ -1827,17 +2398,21 @@ Get current velocity of segment at a certain time. #### segment.get\_velocity\_by\_dist ```python -def get_velocity_by_dist(dist) +def get_velocity_by_dist(dist: float) -> float ``` -Return the velocity at a certain local segment distance. +Return the velocity magnitude at a certain local segment distance. + +**Arguments**: + +- `dist` - (float) distance from segment start #### segment.get\_position ```python -def get_position(t: float) -> position +def get_position(t: Union[float, seconds]) -> position ``` Get current position of segment at a certain time. @@ -1856,7 +2431,7 @@ Get current position of segment at a certain time. #### segment.get\_segm\_len ```python -def get_segm_len() +def get_segm_len() -> float ``` Return the length of the segment. @@ -1866,7 +2441,7 @@ Return the length of the segment. #### segment.get\_segm\_duration ```python -def get_segm_duration() +def get_segm_duration() -> seconds ``` Return the duration of the segment. @@ -1876,7 +2451,7 @@ Return the duration of the segment. #### segment.self\_check ```python -def self_check(p_settings: "state.p_settings" = None) +def self_check(p_settings: "state.p_settings" = None) -> bool ``` Check the segment for self consistency. @@ -1889,6 +2464,10 @@ Check the segment for self consistency. - `p_settings` - (p_settings, default = None) printing settings to verify +**Returns**: + + True if all checks pass + #### segment.is\_extruding @@ -1908,7 +2487,7 @@ Return true if the segment is pos. extruding. #### segment.get\_result ```python -def get_result(key) +def get_result(key: str) ``` Return the requested result. @@ -1928,7 +2507,8 @@ Return the requested result. ```python @classmethod -def create_initial(cls, initial_position: position = None) +def create_initial(cls, + initial_position: Optional[position] = None) -> "segment" ``` Create initial static segment with (optionally) initial position else start from Zero. diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml index 76c0f00..2e1744e 100644 --- a/pydoc-markdown.yml +++ b/pydoc-markdown.yml @@ -4,8 +4,6 @@ loaders: packages: ["pyGCodeDecode"] processors: - - type: filter - expression: not name.startswith('_') # Skip private members - type: filter expression: not '.examples.' in name # Skip example modules - type: filter From edf9e6b5933c04e57217af2b51b0606937999b7f Mon Sep 17 00:00:00 2001 From: usmfi Date: Thu, 21 Aug 2025 13:53:57 +0200 Subject: [PATCH 49/68] faster eq and more efficient check in junct handling addressing #27 --- pyGCodeDecode/junction_handling.py | 3 ++- pyGCodeDecode/utils.py | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pyGCodeDecode/junction_handling.py b/pyGCodeDecode/junction_handling.py index 68bb493..3dbd408 100644 --- a/pyGCodeDecode/junction_handling.py +++ b/pyGCodeDecode/junction_handling.py @@ -58,7 +58,8 @@ def _calc_vel_next(self): while True: if ( next_next_state.next_state is None - or self.state_B.state_position.get_t_distance(next_next_state.state_position, withExtrusion=True) > 0 + or self.state_B.state_position != next_next_state.state_position + # or self.state_B.state_position.get_t_distance(next_next_state.state_position, withExtrusion=True) > 0 # inefficient check ): vel_next = self.connect_state( state_A=self.state_B, state_B=next_next_state diff --git a/pyGCodeDecode/utils.py b/pyGCodeDecode/utils.py index e77b3d2..4722b83 100644 --- a/pyGCodeDecode/utils.py +++ b/pyGCodeDecode/utils.py @@ -191,13 +191,13 @@ def __truediv__(self, other): return self.__class__(x, y, z, e) def __eq__(self, other) -> bool: - """Check for equality and return True if equal (with tolerance). + """Check for equality and return True if equal. Args: other: (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') Returns: - eq: (bool) true if equal (with tolerance) + eq: (bool) true if equal """ if isinstance(other, type(self)): other_vec = [other.x, other.y, other.z, other.e] @@ -207,7 +207,8 @@ def __eq__(self, other) -> bool: return False self_vec = [self.x, self.y, self.z, self.e] - return np.allclose(self_vec, other_vec) + return other_vec == self_vec + # return np.allclose(self_vec, other_vec) def __gt__(self, other) -> bool: """Check for greater than and return True if greater. From dcdcf999944dc52a4c39f8b935340f34f5392538 Mon Sep 17 00:00:00 2001 From: usmfi Date: Thu, 21 Aug 2025 14:36:42 +0200 Subject: [PATCH 50/68] wip test added and test fixed --- pyGCodeDecode/planner_block.py | 51 +++++++++++++++++++++++++++++---- tests/test_junction_handling.py | 26 +++++++++++++++++ tests/test_planner_block.py | 27 +++++++++++++---- 3 files changed, 93 insertions(+), 11 deletions(-) diff --git a/pyGCodeDecode/planner_block.py b/pyGCodeDecode/planner_block.py index 82d9552..e0333f1 100644 --- a/pyGCodeDecode/planner_block.py +++ b/pyGCodeDecode/planner_block.py @@ -362,15 +362,56 @@ def next_block(self, block: "planner_block"): self._next_block = block def __str__(self) -> str: - """Create string from planner block.""" + """Create a visually aligned ASCII art string for planner block.""" + # Get positions as strings + pos_A = str(self.state_A.state_position) + pos_B = str(self.state_B.state_position) + + # Get velocities + v_begin = self.segments[0].vel_begin.get_norm() if self.segments else 0 + v_end = self.segments[-1].vel_end.get_norm() if self.segments else 0 + v_max = max(segm.vel_begin.get_norm() for segm in self.segments) if self.segments else 0 + + # Pad positions for alignment + padsize = 40 + pos_A_pad = f"{pos_A:<{padsize}}" + pos_B_pad = f"{pos_B:>{padsize}}" + + # Velocity profile + profile_width = 24 if len(self.segments) == 3: - return "{:-^40}".format("Trapezoid Planner Block") + # Trapezoid: ramp up, constant, ramp down + profile = f"/{'‾'*(profile_width-2)}\\" + block_type = "Trapezoid" elif len(self.segments) == 2: - return "{:-^40}".format("Triangular Planner Block") + # Triangle: ramp up, ramp down + profile = "/\\".center(profile_width) + block_type = "Triangle" elif len(self.segments) == 1: - return "{:-^40}".format("Singular Planner Block") + # Single: ramp up or down only + # Center the single segment profile + if v_begin < v_end: + profile = "/".center(profile_width) + else: + profile = "\\".center(profile_width) + block_type = "Single" else: - return "{:#^40}".format("Invalid Planner Block") + profile = "{invalid block}" + block_type = "Invalid" + v_begin = f"{v_begin:.2f} mm/s" + v_beg_str = f"{v_begin:>8}" + v_end = f"{v_end:.2f} mm/s" + v_end_str = f"{v_end:<8}" + tot_len = len(pos_A_pad) + len(profile) + len(pos_B_pad) + + lines = [ + f"{block_type} Planner Block".center(tot_len, "-"), + "", + f"{v_max:.2f} mm/s".center(tot_len), + f"{' '*(len(pos_A_pad)-len(v_beg_str))}{v_beg_str}{profile}{v_end_str}", + f"{pos_A_pad}{' '*(profile_width)}{pos_B_pad}", + ] + return "\n".join(lines) + "\n" def __repr__(self) -> str: """Represent planner block.""" diff --git a/tests/test_junction_handling.py b/tests/test_junction_handling.py index 9380c42..2513c30 100644 --- a/tests/test_junction_handling.py +++ b/tests/test_junction_handling.py @@ -3,18 +3,21 @@ import copy import math import os +import pathlib import sys import matplotlib.pyplot as plt import numpy as np import pandas as pd +from pyGCodeDecode.gcode_interpreter import generate_planner_blocks from pyGCodeDecode.junction_handling import ( _get_handler_names, get_handler, junction_handling, ) from pyGCodeDecode.state import state +from pyGCodeDecode.state_generator import generate_states from pyGCodeDecode.utils import position @@ -216,6 +219,29 @@ def test_junction_handlings_rotating_COS(): # plt.show() +def test_junction_handling_state_connect(): + """Test for the state connect method in the junction handling.""" + from pyGCodeDecode.gcode_interpreter import setup + + test_setup = setup( + presets_file=pathlib.Path("./tests/data/test_printer_setups.yaml"), + printer="prusa_mini", + layer_cue="LAYER cue", + ) + test_setup.set_property({"p_acc": 10}) + + print(test_setup.firmware) + states = generate_states( + filepath=pathlib.Path("./tests/data/test_state_generator.gcode"), + initial_machine_setup=test_setup.get_dict(), + ) + + blocks = generate_planner_blocks(states, firmware=test_setup.firmware) + + print(blocks) + # TODO assert states are connected correctly + + if __name__ == "__main__": test_junction_handlings() plt.show() diff --git a/tests/test_planner_block.py b/tests/test_planner_block.py index 12460e5..1085c9d 100644 --- a/tests/test_planner_block.py +++ b/tests/test_planner_block.py @@ -32,8 +32,13 @@ def test_planner_block(): # trapezoid block test assert block_1.blocktype == "trapezoid" assert block_1.valid is True - assert block_1.get_segments()[0].get_position(t=0) == pos_0 - assert block_1.get_segments()[-1].get_position(t=block_1.get_segments()[-1].t_end) == pos_1 + assert np.allclose( + block_1.get_segments()[0].get_position(t=0).get_vec(withExtrusion=True), pos_0.get_vec(withExtrusion=True) + ) + assert np.allclose( + block_1.get_segments()[-1].get_position(t=block_1.get_segments()[-1].t_end).get_vec(withExtrusion=True), + pos_1.get_vec(withExtrusion=True), + ) t_end = block_1.get_segments()[-1].t_end assert t_end == (settings.speed * settings.speed + dist * settings.p_acc) / ( @@ -54,8 +59,13 @@ def test_planner_block(): # triangle block test assert block_2.blocktype == "triangle" assert block_2.valid is True - assert block_2.get_segments()[0].get_position(t=0) == pos_0 - assert block_2.get_segments()[-1].get_position(t=block_2.get_segments()[-1].t_end) == pos_1 + assert np.allclose( + block_2.get_segments()[0].get_position(t=0).get_vec(withExtrusion=True), pos_0.get_vec(withExtrusion=True) + ) + assert np.allclose( + block_2.get_segments()[-1].get_position(t=block_2.get_segments()[-1].t_end).get_vec(withExtrusion=True), + pos_1.get_vec(withExtrusion=True), + ) assert np.isclose(block_2.get_segments()[0].t_end, np.sqrt(4 * dist * settings.p_acc) / (2 * settings.p_acc)) assert np.isclose( block_2.get_segments()[0].vel_end.get_norm(), (settings.p_acc * block_2.get_segments()[0].t_end) @@ -78,8 +88,13 @@ def test_planner_block(): # single block test assert block_3.blocktype == "single" assert block_3.valid is True - assert block_3.get_segments()[0].get_position(t=0) == pos_0 - assert block_3.get_segments()[-1].get_position(t=block_3.get_segments()[-1].t_end) == pos_1 + assert np.allclose( + block_3.get_segments()[0].get_position(t=0).get_vec(withExtrusion=True), pos_0.get_vec(withExtrusion=True) + ) + assert np.allclose( + block_3.get_segments()[-1].get_position(t=block_3.get_segments()[-1].t_end).get_vec(withExtrusion=True), + pos_1.get_vec(withExtrusion=True), + ) assert np.isclose(block_3.get_segments()[0].t_end, np.sqrt(2 * dist * settings.p_acc) / (settings.p_acc)) assert np.isclose( block_3.get_segments()[0].vel_end.get_norm(), (settings.p_acc * block_3.get_segments()[0].t_end) From 891e5fd8bf7322368bf7d6fe76b974a58fe678f7 Mon Sep 17 00:00:00 2001 From: usmfi Date: Thu, 21 Aug 2025 15:17:22 +0200 Subject: [PATCH 51/68] ghdeply --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index de9af59..b108258 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -46,6 +46,7 @@ doc-build: - pip install -e .[DOCS] - pydoc-markdown - cd docs && mkdocs build + - mkdocs gh-deploy artifacts: untracked: false paths: From bfc6e23cfc561850505c673c1df6fd1c21896a30 Mon Sep 17 00:00:00 2001 From: Lukas Hof Date: Thu, 21 Aug 2025 15:45:40 +0200 Subject: [PATCH 52/68] automatic deploy to gh-pages --- .github/workflows/docs.yml | 43 - .gitlab-ci.yml | 5 +- doc.md | 2523 ------------------------------------ 3 files changed, 4 insertions(+), 2567 deletions(-) delete mode 100644 .github/workflows/docs.yml delete mode 100644 doc.md diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index ef77b89..0000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Build and Deploy Documentation - -on: - push: - branches: [ main, public_pre_main ] - pull_request: - branches: [ main, public_pre_main ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - name: Upgrade pip - run: pip install --upgrade pip - - name: Install dependencies - run: pip install -e .[DOCS] - - name: Build docs with pydoc-markdown - run: pydoc-markdown - - name: Build site with mkdocs - run: mkdocs build - - name: Upload site artifact - uses: actions/upload-artifact@v4 - with: - name: site - path: docs/site - - deploy: - needs: build - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/public_pre_main' - steps: - - uses: actions/checkout@v4 - - name: Download site artifact - uses: actions/download-artifact@v4 - with: - name: site - path: public - # Add your deployment steps here (e.g., GitHub Pages, FTP, etc.) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b108258..a946b55 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -38,7 +38,7 @@ test-package: doc-build: stage: doc - image: "python:3.11" + image: "python:3.13" needs: [] before_script: - pip install --upgrade pip @@ -46,6 +46,9 @@ doc-build: - pip install -e .[DOCS] - pydoc-markdown - cd docs && mkdocs build + - git config user.email "lt-github@fast.kit.edu" + - git config user.name "ci-bot" + - git remote add gitlab_origin https://oauth2:${pyGCD_access_token}@gitlab.kit.edu/kit/fast/lb/collaboration/additive-manufacturing/pygcodedecode.git - mkdocs gh-deploy artifacts: untracked: false diff --git a/doc.md b/doc.md deleted file mode 100644 index e453cb9..0000000 --- a/doc.md +++ /dev/null @@ -1,2523 +0,0 @@ -# pyGCodeDecode API Reference - - - -## pyGCodeDecode.abaqus\_file\_generator - -Module for generating Abaqus .inp files for AMSIM. - - - -#### generate\_abaqus\_event\_series - -```python -def generate_abaqus_event_series( - simulation: gcode_interpreter.simulation, - filepath: str = "pyGcodeDecode_abaqus_events.inp", - tolerance: float = 1e-12, - output_unit_system: str = None, - return_tuple: bool = False) -> tuple -``` - -Generate abaqus event series. - -**Arguments**: - -- `simulation` _gcode_interpreter.simulation_ - simulation instance -- `filepath` _string, default = "pyGcodeDecode_abaqus_events.inp"_ - output file path -- `tolerance` _float, default = 1e-12_ - tolerance to determine whether extrusion is happening -- `output_unit_system` _str, optional_ - Unit system for the output. - The one from the simulation is used, in None is specified. -- `return_tuple` _bool, default = False_ - return the event series as tuple. - - -**Returns**: - - (optional) tuple: the event series as a tuple for use in ABAQUS-Python - - - -## pyGCodeDecode.cli - -The pyGCodeDecode CLI Module. - -Interact with pyGCodeDecode via the command line to run examples and plot GCode files. - -Features: - -- Run built-in examples: `brace`, `benchy` -- Plot GCode files with printer presets and output options -- Save simulation summaries, metrics, screenshots, and VTK files - -Usage Examples: - -- `pygcd --help` -- `pygcd run_example brace` -- `pygcd plot -g myfile.gcode` -- `pygcd plot -g myfile.gcode -p presets.yaml -pn my_printer` -- `pygcd plot -g myfile.gcode -o ./outputs -lc ";LAYER"` - - - -## pyGCodeDecode.gcode\_interpreter - -GCode Interpreter Module. - - - -#### generate\_planner\_blocks - -```python -def generate_planner_blocks(states: List[state], firmware=None) -``` - -Convert list of states to trajectory repr. by planner blocks. - -**Arguments**: - -- `states` - (list[state]) list of states -- `firmware` - (string, default = None) select firmware by name - - -**Returns**: - - block_list (list[planner_block]) list of all planner blocks to complete travel between all states - - - -#### find\_current\_segment - -```python -def find_current_segment(path: List[segment], - t: float, - last_index: int = None, - keep_position: bool = False) -``` - -Find the current segment. - -**Arguments**: - -- `path` - (list[segment]) all segments to be searched -- `t` - (float) time of search -- `last_index` - (int) last found index for optimizing search -- `keep_position` - (bool) keeps position of last segment, use this when working with - gaps of no movement between segments - - -**Returns**: - -- `segment` - (segment) the segment which defines movement at that point in time -- `last_index` - (int) last index where something was found, search speed optimization possible - - - -#### unpack\_blocklist - -```python -def unpack_blocklist(blocklist: List[planner_block]) -> List[segment] -``` - -Return list of segments by unpacking list of planner blocks. - -**Arguments**: - -- `blocklist` - (list[planner_block]) list of planner blocks - - -**Returns**: - -- `path` - (list[segment]) list of all segments - - - -### simulation Objects - -```python -class simulation() -``` - -Simulation of .gcode with given machine parameters. - - - -#### simulation.\_\_init\_\_ - -```python -def __init__(gcode_path: Path, - machine_name: str = None, - initial_machine_setup: "setup" = None, - output_unit_system: str = "SI (mm)", - verbosity_level: Optional[int] = None) -``` - -Initialize the Simulation of a given G-code with initial machine setup or default machine. - -- Generate all states from GCode. -- Connect states with planner blocks, consisting of segments -- Self correct inconsistencies. - -**Arguments**: - -- `gcode_path` - (Path) path to GCode -- `machine_name` - (string, default = None) name of the default machine to use -- `initial_machine_setup` - (setup, default = None) setup instance -- `output_unit_system` - (string, default = "SI (mm)") available unit systems: SI, SI (mm) & inch -- `verbosity_level` - (int, default = None) set verbosity level (0: no output, 1: warnings, 2: info, 3: debug) - - -**Example**: - -```python -gcode_interpreter.simulation(gcode_path=r"path/to/part.gcode", initial_machine_setup=printer_setup) -``` - - - -#### simulation.\_\_getattr\_\_ - -```python -def __getattr__(name) -``` - -Get result by name. - - - -#### simulation.trajectory\_self\_correct - -```python -def trajectory_self_correct() -``` - -Self correct all blocks in the blocklist with self_correction() method. - - - -#### simulation.calc\_results - -```python -def calc_results() -``` - -Calculate the results. - - - -#### simulation.calculate\_averages - -```python -def calculate_averages() -``` - -Calculate averages for averageable results. - - - -#### simulation.get\_values - -```python -def get_values(t: float, output_unit_system: str = None) -> Tuple[List[float]] -``` - -Return unit system scaled values for vel and pos. - -**Arguments**: - -- `t` - (float) time -- `output_unit_system` _str, optional_ - Unit system for the output. - The one from the simulation is used, in None is specified. - - -**Returns**: - -- `list` - [vel_x, vel_y, vel_z, vel_e] velocity -- `list` - [pos_x, pos_y, pos_z, pos_e] position - - - -#### simulation.get\_width - -```python -def get_width(t: float, - extrusion_h: float, - filament_dia: Optional[float] = None) -> float -``` - -Return the extrusion width for a certain extrusion height at time. - -**Arguments**: - -- `t` _float_ - time -- `extrusion_h` _float_ - extrusion height / layer height -- `filament_dia` _float_ - filament_diameter - - -**Returns**: - -- `float` - width - - - -#### simulation.print\_summary - -```python -def print_summary(start_time: float) -``` - -Print simulation summary to console. - -**Arguments**: - -- `start_time` _float_ - time when the simulation run was started - - - -#### simulation.refresh - -```python -def refresh(new_state_list: List[state] = None) -``` - -Refresh simulation. Either through new state list or by rerunning the self.states as input. - -**Arguments**: - -- `new_state_list` - (list[state], default = None) new list of states, - if None is provided, existing states get resimulated - - - -#### simulation.extrusion\_extent - -```python -def extrusion_extent(output_unit_system: str = None) -> np.ndarray -``` - -Return scaled xyz min & max while extruding. - -**Arguments**: - -- `output_unit_system` _str, optional_ - Unit system for the output. - The one from the simulation is used, in None is specified. - - -**Raises**: - -- `ValueError` - if nothing is extruded - - -**Returns**: - -- `np.ndarray` - extent of extruding positions - - - -#### simulation.extrusion\_max\_vel - -```python -def extrusion_max_vel(output_unit_system: str = None) -> np.float64 -``` - -Return scaled maximum velocity while extruding. - -**Arguments**: - -- `output_unit_system` _str, optional_ - Unit system for the output. - The one from the simulation is used, in None is specified. - - -**Returns**: - -- `max_vel` - (np.float64) maximum travel velocity while extruding - - - -#### simulation.save\_summary - -```python -def save_summary(filepath: Union[Path, str]) -``` - -Save summary to .yaml file. - -**Arguments**: - -- `filepath` _Path | str_ - path to summary file - - Saved data keys: - - filename (string, filename) - - t_end (float, end time) - - x/y/z _min/_max (float, extent where positive extrusion) - - max_extrusion_travel_velocity (float, maximum travel velocity where positive extrusion) - - - -#### simulation.get\_scaling\_factor - -```python -def get_scaling_factor(output_unit_system: str = None) -> float -``` - -Get a scaling factor to convert lengths from mm to another supported unit system. - -**Arguments**: - -- `output_unit_system` _str, optional_ - Wanted output unit system. - Uses the one specified for the simulation on None is specified. - - -**Returns**: - -- `float` - scaling factor - - - -### setup Objects - -```python -class setup() -``` - -Setup for printing simulation. - - - -#### setup.\_\_init\_\_ - -```python -def __init__(presets_file: str, - printer: str = None, - verbosity_level: Optional[int] = None, - **kwargs) -``` - -Initialize the setup for the printing simulation. - -**Arguments**: - -- `presets_file` _str_ - Path to the YAML file containing printer presets. -- `printer` _str, optional_ - Name of the printer to select from the preset file. Defaults to None. -- `verbosity_level` _int, optional_ - Verbosity level for logging (0: no output, 1: warnings, 2: info, 3: debug). Defaults to None. -- `**kwargs` - Additional properties to set or override in the setup. - - -**Raises**: - -- `ValueError` - If multiple printers are found in the preset file but none is selected. - - - -#### setup.\_\_getattr\_\_ - -```python -def __getattr__(name) -``` - -Access to setup_dict content. - - - -#### setup.\_\_setattr\_\_ - -```python -def __setattr__(name, value) -``` - -Set setup_dict keys. - - - -#### setup.load\_setup - -```python -def load_setup(filepath, printer=None) -``` - -Load setup from file. - -**Arguments**: - -- `filepath` - (string) specify path to setup file - - - -#### setup.check\_initial\_setup - -```python -def check_initial_setup() -``` - -Check the printer Dict for typos or missing parameters and raise errors if invalid. - - - -#### setup.set\_initial\_position - -```python -def set_initial_position(initial_position: Union[tuple, dict], - input_unit_system: str = None) -``` - -Set initial Position. - -**Arguments**: - -- `initial_position` - (tuple or dict) set initial position as tuple of len(4) - or dictionary with keys: {X, Y, Z, E} or "first" to use first occuring absolute position in GCode. -- `input_unit_system` _str, optional_ - Wanted input unit system. - Uses the one specified for the setup if None is specified. - - -**Example**: - -```python -setup.set_initial_position((1, 2, 3, 4)) -setup.set_initial_position({"X": 1, "Y": 2, "Z": 3, "E": 4}) -setup.set_initial_position("first") # use first GCode position -``` - - - -#### setup.set\_property - -```python -def set_property(property_dict: dict) -``` - -Overwrite or add a property to the printer dictionary. - -**Arguments**: - -- `property_dict` - (dict) set or add property to the setup - - -**Example**: - -```python -setup.set_property({"layer_cue": "LAYER_CHANGE"}) -``` - - - -#### setup.get\_dict - -```python -def get_dict() -> dict -``` - -Return the setup for the selected printer. - -**Returns**: - -- `return_dict` - (dict) setup dictionary - - - -#### setup.get\_scaling\_factor - -```python -def get_scaling_factor(input_unit_system: str = None) -> float -``` - -Get a scaling factor to convert lengths from mm to another supported unit system. - -**Arguments**: - -- `input_unit_system` _str, optional_ - Wanted input unit system. - Uses the one specified for the setup if None is specified. - - -**Returns**: - -- `float` - scaling factor - - - -## pyGCodeDecode.helpers - -Helper functions. - - - -#### pyGCodeDecode.helpers.VERBOSITY\_LEVEL - -default to INFO - - - -#### set\_verbosity\_level - -```python -def set_verbosity_level(level: Optional[int]) -> None -``` - -Set the global verbosity level. - - - -#### get\_verbosity\_level - -```python -def get_verbosity_level() -> int -``` - -Get the current global verbosity level. - - - -#### custom\_print - -```python -def custom_print(*args, lvl=2, **kwargs) -> None -``` - -Sanitize outputs for ABAQUS and print them if the log level is high enough. Takes all arguments for print. - -**Arguments**: - -- `*args` - arguments to be printed -- `lvl` - verbosity level of the print (1 = WARNING, 2 = INFO, 3 = DEBUG) -- `**kwargs` - keyword arguments to be passed to print - - - -### ProgressBar Objects - -```python -class ProgressBar() -``` - -A simple progress bar for the console. - - - -#### ProgressBar.\_\_init\_\_ - -```python -def __init__(name: str = "Percent", - barLength: int = 4, - verbosity_level: int = 2) -``` - -Initialize a progress bar. - - - -#### ProgressBar.update - -```python -def update(progress: float) -> None -``` - -Display or update a console progress bar. - -**Arguments**: - -- `progress` - float between 0 and 1 for percentage, < 0 represents a 'halt', > 1 represents 100% - - - -## pyGCodeDecode.junction\_handling - -Junction handling module for calculating the velocity at junctions. - - - -### junction\_handling Objects - -```python -class junction_handling() -``` - -Junction handling super class. - - - -#### junction\_handling.\_\_init\_\_ - -```python -def __init__(state_A: state, state_B: state) -``` - -Initialize the junction handling. - -**Arguments**: - -- `state_A` - (state) start state -- `state_B` - (state) end state - - - -#### junction\_handling.connect\_state - -```python -def connect_state(state_A: state, state_B: state) -``` - -Connect two states and generates the velocity for the move from state_A to state_B. - -**Arguments**: - -- `state_A` - (state) start state -- `state_B` - (state) end state - - -**Returns**: - -- `velocity` - (float) the target velocity for that travel move - - - -#### junction\_handling.get\_target\_vel - -```python -def get_target_vel() -``` - -Return target velocity. - - - -#### junction\_handling.get\_junction\_vel - -```python -def get_junction_vel() -``` - -Return default junction velocity of zero. - -**Returns**: - -- `0` - zero for default full stop junction handling - - - -### prusa Objects - -```python -class prusa(junction_handling) -``` - -Prusa specific classic jerk junction handling (validated on Prusa Mini). - -**Code reference:** -[Prusa-Firmware-Buddy/lib/Marlin/Marlin/src/module/planner.cpp](https://github.com/prusa3d/Prusa-Firmware-Buddy/blob/818d812f954802903ea0ff39bf44376fb0b35dd2/lib/Marlin/Marlin/src/module/planner.cpp#L1951) - -```cpp -// ... -// Factor to multiply the previous / current nominal velocities to get componentwise limited velocities. - float v_factor = 1; - limited = 0; - - // The junction velocity will be shared between successive segments. Limit the junction velocity to their minimum. - // Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting. - vmax_junction = _MIN(block->nominal_speed, previous_nominal_speed); - - // Now limit the jerk in all axes. - const float smaller_speed_factor = vmax_junction / previous_nominal_speed; - `if` HAS_LINEAR_E_JERK - LOOP_XYZ(axis) - `else` - LOOP_XYZE(axis) - `endif` - { - // Limit an axis. We have to differentiate: coasting, reversal of an axis, full stop. - float v_exit = previous_speed[axis] * smaller_speed_factor, - v_entry = current_speed[axis]; - if (limited) { - v_exit *= v_factor; - v_entry *= v_factor; - } - - // Calculate jerk depending on whether the axis is coasting in the same direction or reversing. - const float jerk = (v_exit > v_entry) - ? // coasting axis reversal - ( (v_entry > 0 || v_exit < 0) ? (v_exit - v_entry) : _MAX(v_exit, -v_entry) ) - : // v_exit <= v_entry coasting axis reversal - ( (v_entry < 0 || v_exit > 0) ? (v_entry - v_exit) : _MAX(-v_exit, v_entry) ); - - if (jerk > settings.max_jerk[axis]) { - v_factor *= settings.max_jerk[axis] / jerk; - ++limited; - } - } - if (limited) vmax_junction *= v_factor; - // Now the transition velocity is known, which maximizes the shared exit / entry velocity while - // respecting the jerk factors, it may be possible, that applying separate safe exit / entry velocities will achieve faster prints. - const float vmax_junction_threshold = vmax_junction * 0.99f; - if (previous_safe_speed > vmax_junction_threshold && safe_speed > vmax_junction_threshold) - vmax_junction = safe_speed; -} -// ... -``` - - - -#### prusa.\_\_init\_\_ - -```python -def __init__(state_A: state, state_B: state) -``` - -Marlin classic jerk specific junction velocity calculation. - -**Arguments**: - -- `state_A` - (state) start state -- `state_B` - (state) end state - - - -#### prusa.calc\_j\_vel - -```python -def calc_j_vel() -``` - -Calculate the junction velocity. - - - -#### prusa.get\_junction\_vel - -```python -def get_junction_vel() -``` - -Return the calculated junction velocity. - -**Returns**: - -- `junction_vel` - (float) junction velocity - - - -### marlin Objects - -```python -class marlin(junction_handling) -``` - -Marlin classic jerk specific junction handling. - -**Code reference:** -[Marlin/src/module/planner.cpp](https://github.com/MarlinFirmware/Marlin/blob/8ec9c379405bb9962aff170d305ddd0725bd64e2/Marlin/src/module/planner.cpp#L2762) -```cpp -// ... -float v_factor = 1.0f; -LOOP_LOGICAL_AXES(i) { - // Jerk is the per-axis velocity difference. - const float jerk = ABS(speed_diff[i]), maxj = max_j[i]; - if (jerk * v_factor > maxj) v_factor = maxj / jerk; -} -vmax_junction_sqr = sq(vmax_junction * v_factor); -// ... -``` - - - -#### marlin.\_\_init\_\_ - -```python -def __init__(state_A: state, state_B: state) -``` - -Marlin classic jerk specific junction velocity calculation. - -**Arguments**: - -- `state_A` - (state) start state -- `state_B` - (state) end state - - - -#### marlin.calc\_j\_vel - -```python -def calc_j_vel() -``` - -Calculate the junction velocity. - - - -#### marlin.get\_junction\_vel - -```python -def get_junction_vel() -``` - -Return the calculated junction velocity. - -**Returns**: - -- `junction_vel` - (float) junction velocity - - - -### ultimaker Objects - -```python -class ultimaker(junction_handling) -``` - -Ultimaker specific junction handling. - -**Code reference:** -[UM2.1-Firmware/Marlin/planner.cpp](https://github.com/Ultimaker/UM2.1-Firmware/blob/f6e69344c00d7f300dace730990652ba614a2105/Marlin/planner.cpp#L840) -```cpp -// ... -float vmax_junction = max_xy_jerk/2; -float vmax_junction_factor = 1.0; -if(fabs(current_speed[Z_AXIS]) > max_z_jerk/2) - vmax_junction = min(vmax_junction, max_z_jerk/2); -if(fabs(current_speed[E_AXIS]) > max_e_jerk/2) - vmax_junction = min(vmax_junction, max_e_jerk/2); -vmax_junction = min(vmax_junction, block->nominal_speed); -float safe_speed = vmax_junction; - -if ((moves_queued > 1) && (previous_nominal_speed > 0.0001)) { - float xy_jerk = sqrt(square(current_speed[X_AXIS]-previous_speed[X_AXIS])+square(current_speed[Y_AXIS]-previous_speed[Y_AXIS])); - // if((fabs(previous_speed[X_AXIS]) > 0.0001) || (fabs(previous_speed[Y_AXIS]) > 0.0001)) { - vmax_junction = block->nominal_speed; - // } - if (xy_jerk > max_xy_jerk) { - vmax_junction_factor = (max_xy_jerk / xy_jerk); - } - if(fabs(current_speed[Z_AXIS] - previous_speed[Z_AXIS]) > max_z_jerk) { - vmax_junction_factor= min(vmax_junction_factor, (max_z_jerk/fabs(current_speed[Z_AXIS] - previous_speed[Z_AXIS]))); - } - if(fabs(current_speed[E_AXIS] - previous_speed[E_AXIS]) > max_e_jerk) { - vmax_junction_factor = min(vmax_junction_factor, (max_e_jerk/fabs(current_speed[E_AXIS] - previous_speed[E_AXIS]))); - } - vmax_junction = min(previous_nominal_speed, vmax_junction * vmax_junction_factor); // Limit speed to max previous speed -} -// Max entry speed of this block equals the max exit speed of the previous block. -block->max_entry_speed = vmax_junction; -// ... -``` - - - -#### ultimaker.\_\_init\_\_ - -```python -def __init__(state_A: state, state_B: state) -``` - -Ultimaker specific junction velocity calculation. - -**Arguments**: - -- `state_A` - (state) start state -- `state_B` - (state) end state - - - -#### ultimaker.calc\_j\_vel - -```python -def calc_j_vel() -``` - -Calculate the junction velocity. - - - -#### ultimaker.get\_junction\_vel - -```python -def get_junction_vel() -``` - -Return the calculated junction velocity. - -**Returns**: - -- `junction_vel` - (float) junction velocity - - - -### mka Objects - -```python -class mka(prusa) -``` - -Anisoprint Composer models using MKA Firmware junction handling. - -The MKA firmware uses a similar approach to Prusa's classic jerk handling. - -**Code reference:** -[anisoprint/MKA-firmware/src/core/planner/planner.cpp](https://github.com/anisoprint/MKA-firmware/blob/6e02973b1b8f325040cc3dbf66ac545ffc5c06b3/src/core/planner/planner.cpp#L1830) -```cpp -// ... -float v_exit = previous_speed[axis] * smaller_speed_factor, - v_entry = current_speed[axis]; - if (limited) { - v_exit *= v_factor; - v_entry *= v_factor; - } - - // Calculate jerk depending on whether the axis is coasting in the same direction or reversing. - const float jerk = (v_exit > v_entry) - ? // coasting axis reversal - ( (v_entry > 0 || v_exit < 0) ? (v_exit - v_entry) : max(v_exit, -v_entry) ) - : // v_exit <= v_entry coasting axis reversal - ( (v_entry < 0 || v_exit > 0) ? (v_entry - v_exit) : max(-v_exit, v_entry) ); - - const float maxj = mechanics.max_jerk[axis]; - if (jerk > maxj) { - v_factor *= maxj / jerk; - ++limited; - } -} -if (limited) vmax_junction *= v_factor; -// ... -``` - - - -### junction\_deviation Objects - -```python -class junction_deviation(junction_handling) -``` - -Marlin specific junction handling with Junction Deviation. - -**Reference:** -1: [Developer Blog](https://onehossshay.wordpress.com/2011/09/24/improving_grbl_cornering_algorithm/) -2: [Kynetic CNC Blog](http://blog.kyneticcnc.com/2018/10/computing-junction-deviation-for-marlin.html) - - - -#### junction\_deviation.calc\_JD - -```python -def calc_JD(vel_0: velocity, vel_1: velocity, p_settings: state.p_settings) -``` - -Calculate junction deviation velocity from 2 velocities. - -**Arguments**: - -- `vel_0` - (velocity) entry -- `vel_1` - (velocity) exit -- `p_settings` - (state.p_settings) print settings - - -**Returns**: - -- `velocity` - (float) velocity abs value - - - -#### junction\_deviation.\_\_init\_\_ - -```python -def __init__(state_A: state, state_B: state) -``` - -Marlin specific junction velocity calculation with Junction Deviation. - -**Arguments**: - -- `state_A` - (state) start state -- `state_B` - (state) end state - - - -#### junction\_deviation.get\_junction\_vel - -```python -def get_junction_vel() -``` - -Return junction velocity. - -**Returns**: - -- `junction_vel` - (float) junction velocity - - - -#### get\_handler - -```python -def get_handler(firmware_name: str) -> type[junction_handling] -``` - -Get the junction handling class for the given firmware name. - -**Arguments**: - -- `firmware_name` - (str) name of the firmware - - -**Returns**: - -- `junction_handling` - (type[junction_handling]) junction handling class - - - -## pyGCodeDecode.planner\_block - -Planner block Module. - - - -### planner\_block Objects - -```python -class planner_block() -``` - -Planner Block Class. - - - -#### planner\_block.move\_maker - -```python -def move_maker(v_end) -``` - -Calculate the correct move type (trapezoidal,triangular or singular) and generate the corresponding segments. - -**Arguments**: - -- `v_end` - (velocity) target velocity for end of move - - - -#### planner\_block.self\_correction - -```python -def self_correction(tolerance=float("1e-12")) -``` - -Check for interfacing vel and self correct. - - - -#### planner\_block.timeshift - -```python -def timeshift(delta_t: float) -``` - -Shift planner block in time. - -**Arguments**: - -- `delta_t` - (float) time to be shifted - - - -#### planner\_block.extrusion\_block\_max\_vel - -```python -def extrusion_block_max_vel() -> Union[np.ndarray, None] -``` - -Return max vel from planner block while extruding. - -**Returns**: - -- `block_max_vel` - (np.ndarray 1x4) maximum axis velocity while extruding in block or None - if no extrusion is happening - - - -#### planner\_block.calc\_results - -```python -def calc_results(*additional_calculators: abstract_result) -``` - -Calculate the result of the planner block. - - - -#### planner\_block.\_\_init\_\_ - -```python -def __init__(state: state, prev_block: "planner_block", firmware=None) -``` - -Calculate and store planner block consisting of one or multiple segments. - -**Arguments**: - -- `state` - (state) the current state -- `prev_block` - (planner_block) previous planner block -- `firmware` - (string, default = None) firmware selection for junction - - - -#### planner\_block.prev\_block - -```python -@property -def prev_block() -``` - -Define prev_block as property. - - - -#### planner\_block.next\_block - -```python -@property -def next_block() -``` - -Define next_block as property. - - - -#### planner\_block.\_\_str\_\_ - -```python -def __str__() -> str -``` - -Create string from planner block. - - - -#### planner\_block.\_\_repr\_\_ - -```python -def __repr__() -> str -``` - -Represent planner block. - - - -#### planner\_block.get\_segments - -```python -def get_segments() -``` - -Return segments, contained by the planner block. - - - -#### planner\_block.get\_block\_travel - -```python -def get_block_travel() -``` - -Return the travel length of the planner block. - - - -#### planner\_block.inverse\_time\_at\_pos - -```python -def inverse_time_at_pos(dist_local) -``` - -Get the global time, at which the local length is reached. - -**Arguments**: - -- `dist_local` - (float) local (relative to planner block start) distance - - -**Returns**: - -- `time_global` - (float) global time when the point will be reached. - - - -## pyGCodeDecode.plotter - -This module provides functionality for 3D plotting of G-code simulation data using PyVista. - - - -#### plot\_3d - -```python -def plot_3d(sim: simulation, - extrusion_only: bool = True, - scalar_value: str = "velocity", - screenshot_path: pathlib.Path = None, - camera_settings: dict = None, - vtk_path: pathlib.Path = None, - mesh: pv.MultiBlock = None, - layer_select: int = None, - z_scaler: float = None, - window_size: tuple = (2048, 1536), - mpl_subplot: bool = False, - mpl_rcParams: Union[dict, None] = None, - solid_color: str = "black", - transparent_background: bool = True, - parallel_projection: bool = False, - lighting: bool = True, - block_colorbar: bool = False, - extra_plotting: callable = None, - overwrite_labels: Union[dict, None] = None, - scalar_value_bounds: Union[Tuple[float, float], None] = None, - return_type: str = "mesh") -> pv.MultiBlock -``` - -Plot a 3D visualization of G-code simulation data using PyVista. - -**Arguments**: - -- `sim` _simulation_ - The simulation object containing blocklist and segment data. -- `extrusion_only` _bool, optional_ - If True, plot only segments where extrusion occurs. Defaults to True. -- `scalar_value` _str, optional_ - Scalar value to color the plot. Options: "velocity", "rel_vel_err", "acceleration", or None. Defaults to "velocity". -- `screenshot_path` _pathlib.Path, optional_ - If provided, saves a screenshot to this path and disables interactive plotting. Defaults to None. -- `camera_settings` _dict, optional_ - Camera settings for the plotter. Keys: "camera_position", "elevation", "azimuth", "roll". Defaults to None. -- `vtk_path` _pathlib.Path, optional_ - If provided, saves the mesh as a VTK file to this path. Defaults to None. -- `mesh` _pv.MultiBlock, optional_ - Precomputed PyVista mesh to use instead of generating a new one. Defaults to None. -- `layer_select` _int, optional_ - If provided, only plot the specified layer. Defaults to None (all layers). -- `z_scaler` _float, optional_ - Scaling factor for the z-axis layer squishing (z_scaler = width/height of extrusion). Defaults to None (automatic scaling). -- `window_size` _tuple, optional_ - Size of the plot window in pixels. Defaults to (2048, 1536). -- `mpl_subplot` _bool, optional_ - If True, use matplotlib for screenshot and colorbar. Defaults to False. -- `mpl_rcParams` _dict or None, optional_ - Custom matplotlib rcParams for styling. Defaults to None. -- `solid_color` _str, optional_ - Background color for the plot. Defaults to "black". -- `transparent_background` _bool, optional_ - If True, screenshot background is transparent. Defaults to True. -- `parallel_projection` _bool, optional_ - If True, enables parallel projection in PyVista. Defaults to False. -- `lighting` _bool, optional_ - If True, enables lighting in the plot. Defaults to True. -- `block_colorbar` _bool, optional_ - If True, removes the scalar colorbar from the plot. Defaults to False. -- `extra_plotting` _callable, optional_ - Function to add extra plotting to the PyVista plotter. Signature: (plotter, mesh). Defaults to None. -- `overwrite_labels` _dict or None, optional_ - Dictionary to overwrite colorbar labels. Defaults to None. -- `scalar_value_bounds` _tuple or None, optional_ - Tuple (min, max) to set scalar colorbar range. Defaults to None. -- `return_type` _str, optional_ - Return type, "mesh" or "image". Defaults to "mesh". - - -**Returns**: - -- `pv.MultiBlock` - The PyVista mesh used for plotting. - or -- `np.ndarray` - The screenshot image if `screenshot_path` is provided and `return_type` is "image". - - - -#### plot\_2d - -```python -def plot_2d(sim: simulation, - filepath: pathlib.Path = pathlib.Path("trajectory_2D.png"), - colvar="Velocity", - show_points=False, - colvar_spatial_resolution=1, - dpi=400, - scaled=True, - show=False) -``` - -Plot 2D position (XY plane) with matplotlib (unmaintained). - - - -#### plot\_vel - -```python -def plot_vel(sim: simulation, - axis: Tuple[str] = ("x", "y", "z", "e"), - show: bool = True, - show_planner_blocks: bool = True, - show_segments: bool = False, - show_jv: bool = False, - time_steps: Union[int, str] = "constrained", - filepath: pathlib.Path = None, - dpi: int = 400) -> Figure -``` - -Plot axis velocity with matplotlib. - -**Arguments**: - -- `axis` - (tuple(string), default = ("x", "y", "z", "e")) select plot axis -- `show` - (bool, default = True) show plot and return plot figure -- `show_planner_blocks` - (bool, default = True) show planner_blocks as vertical lines -- `show_segments` - (bool, default = False) show segments as vertical lines -- `show_jv` - (bool, default = False) show junction velocity as x -- `time_steps` - (int or string, default = "constrained") number of time steps or constrain plot - vertices to segment vertices -- `filepath` - (Path, default = None) save fig as image if filepath is provided -- `dpi` - (int, default = 400) select dpi - - -**Returns**: - - (optionally) -- `fig` - (figure) - - - -## pyGCodeDecode.result - -Result calculation for segments and planner blocks. - - - -### abstract\_result Objects - -```python -class abstract_result(ABC) -``` - -Abstract class for result calculation. - - - -#### abstract\_result.name - -```python -@property -@abstractmethod -def name() -``` - -Name of the result. Has to be set in the derived class. - - - -#### abstract\_result.calc\_pblock - -```python -@abstractmethod -def calc_pblock(pblock: "planner_block", **kwargs) -``` - -Calculate the result for a planner block. - - - -#### abstract\_result.calc\_segm - -```python -@abstractmethod -def calc_segm(segm: "segment", **kwargs) -``` - -Calculate the result for a segment. - - - -### acceleration\_result Objects - -```python -class acceleration_result(abstract_result) -``` - -The acceleration. - - - -#### acceleration\_result.calc\_segm - -```python -def calc_segm(segm: "segment", **kwargs) -``` - -Calculate the acceleration for a segment. - - - -#### acceleration\_result.calc\_pblock - -```python -def calc_pblock(pblock, **kwargs) -``` - -Calculate the acceleration for a planner block. - - - -### velocity\_result Objects - -```python -class velocity_result(abstract_result) -``` - -The velocity. - - - -#### velocity\_result.calc\_segm - -```python -def calc_segm(segm: "segment", **kwargs) -``` - -Calculate the velocity for a segment. - - - -#### velocity\_result.calc\_pblock - -```python -def calc_pblock(pblock: "planner_block", **kwargs) -``` - -Calculate the velocity for a planner block. - - - -#### get\_all\_result\_calculators - -```python -def get_all_result_calculators() -``` - -Get all results. - - - -#### has\_private\_results - -```python -def has_private_results() -``` - -Check if private results are available. - - - -#### get\_result\_info - -```python -def get_result_info() -``` - -Get information about available result calculators. - - - -## pyGCodeDecode.state - -State module with state. - - - -### state Objects - -```python -class state() -``` - -State contains a Position and Printing Settings (p_settings) to apply for the corresponding move to this State. - - - -### p\_settings Objects - -```python -class p_settings() -``` - -Store Printing Settings. - - - -#### p\_settings.\_\_init\_\_ - -```python -def __init__(p_acc, jerk, vX, vY, vZ, vE, speed, units="SI (mm)") -``` - -Initialize printing settings. - -**Arguments**: - -- `p_acc` - (float) printing acceleration -- `jerk` - (float) jerk or similar -- `vX` - (float) max x velocity -- `vY` - (float) max y velocity -- `vZ` - (float) max z velocity -- `vE` - (float) max e velocity -- `speed` - (float) default target velocity -- `units` - (string, default = "SI (mm)") unit settings - - - -#### p\_settings.\_\_str\_\_ - -```python -def __str__() -> str -``` - -Create summary string for p_settings. - - - -#### p\_settings.\_\_repr\_\_ - -```python -def __repr__() -> str -``` - -Define representation. - - - -#### state.\_\_init\_\_ - -```python -def __init__(state_position: position = None, - state_p_settings: p_settings = None) -``` - -Initialize a state. - -**Arguments**: - -- `state_position` - (position) state position -- `state_p_settings` - (p_settings) state printing settings - - - -#### state.state\_position - -```python -@property -def state_position() -``` - -Define property state_position. - - - -#### state.state\_p\_settings - -```python -@property -def state_p_settings() -``` - -Define property state_p_settings. - - - -#### state.line\_number - -```python -@property -def line_number() -``` - -Define property line_number. - - - -#### state.line\_number - -```python -@line_number.setter -def line_number(nmbr) -``` - -Set line number. - -**Arguments**: - -- `nmbr` - (int) line number - - - -#### state.next\_state - -```python -@property -def next_state() -``` - -Define property next_state. - - - -#### state.next\_state - -```python -@next_state.setter -def next_state(state: "state") -``` - -Set next state. - -**Arguments**: - -- `state` - (state) next state - - - -#### state.prev\_state - -```python -@property -def prev_state() -``` - -Define property prev_state. - - - -#### state.prev\_state - -```python -@prev_state.setter -def prev_state(state: "state") -``` - -Set previous state. - -**Arguments**: - -- `state` - (state) previous state - - - -#### state.\_\_str\_\_ - -```python -def __str__() -> str -``` - -Generate string for representation. - - - -#### state.\_\_repr\_\_ - -```python -def __repr__() -> str -``` - -Call __str__() for representation. - - - -## pyGCodeDecode.state\_generator - -State generator module. - - - -#### generate\_states - -```python -def generate_states(filepath: pathlib.Path, - initial_machine_setup: dict) -> List[state] -``` - -Generate state list from GCode file. - -**Arguments**: - -- `filepath` - (Path) filepath to GCode -- `initial_machine_setup` - (dict) dictionary with machine setup - - -**Returns**: - -- `states` - (list[states]) all states in a list - - - -## pyGCodeDecode.tools - -Tools for pyGCD. - - - -#### save\_layer\_metrics - -```python -def save_layer_metrics( - simulation: simulation, - filepath: Optional[pathlib.Path] = pathlib.Path("./layer_metrics.csv"), - locale: str = None, - delimiter: str = ";") -> Optional[tuple[list, list, list, list]] -``` - -Print out print times, distance traveled and the average travel speed to a csv-file. - -**Arguments**: - -- `simulation` - (simulation) simulation instance -- `filepath` - (Path , default = "./layer_metrics.csv") file name -- `locale` - (string, default = None) select locale settings, e.g. "en_US.utf8", None = use system locale -- `delimiter` - (string, default = ";") select delimiter - - Layers are detected using the given layer cue. - - - -#### write\_submodel\_times - -```python -def write_submodel_times(simulation: simulation, - sub_orig: list, - sub_side_x_len: float, - sub_side_y_len: float, - sub_side_z_len: float, - filename: Optional[pathlib.Path] = pathlib.Path( - "submodel_times.yaml"), - **kwargs) -> dict -``` - -Write the submodel entry and exit times to a yaml file. - -**Arguments**: - -- `simulation` - (simulation) the simulation instance to analyze -- `sub_orig` - (list with [xcoord, ycoord, zcoord]) the origin of the submodel control volume -- `sub_side_len` - (float) the side length of the submodel control volume -- `filename` - (string) yaml filename -- `**kwargs` - (any) provide additional info to write into the yaml file - - - -## pyGCodeDecode.utils - -Utilities. - -Utils for the GCode Reader contains: -- vector 4D - - velocity - - position - - - -### seconds Objects - -```python -class seconds(float) -``` - -A float subclass representing a time duration in seconds. - -**Arguments**: - -- `value` _float or int_ - The time duration in seconds. - -**Examples**: - -```python ->>> from pyGCodeDecode.utils import seconds ->>> t = seconds(5) ->>> str(t) -'5.0 s' ->>> t.seconds -5.0 -``` - - - -#### seconds.\_\_new\_\_ - -```python -def __new__(cls, value) -``` - -Create a new instance of seconds. - - - -#### seconds.\_\_str\_\_ - -```python -def __str__() -> str -``` - -Return string representation of the time in seconds. - - - -#### seconds.\_\_sub\_\_ - -```python -def __sub__(other) -> "seconds" -``` - -Subtract seconds or float and return a new seconds instance. - - - -#### seconds.\_\_add\_\_ - -```python -def __add__(other) -> "seconds" -``` - -Add seconds or float and return a new seconds instance. - - - -#### seconds.\_\_repr\_\_ - -```python -def __repr__() -> str -``` - -Return a string representation of the seconds object. - - - -#### seconds.seconds - -```python -@property -def seconds() -> float -``` - -Return the float value of the seconds instance. - - - -### vector\_4D Objects - -```python -class vector_4D() -``` - -The vector_4D class stores 4D vector in x,y,z,e. - -**Supports:** -- str -- add -- sub -- mul (scalar) -- truediv (scalar) -- eq - - - -#### vector\_4D.\_\_init\_\_ - -```python -def __init__(*args) -``` - -Store 3D position + extrusion axis. - -**Arguments**: - -- `args` - coordinates as arguments x,y,z,e or (tuple or list) [x,y,z,e] - - - -#### vector\_4D.\_\_str\_\_ - -```python -def __str__() -> str -``` - -Return string representation. - - - -#### vector\_4D.\_\_repr\_\_ - -```python -def __repr__() -``` - -Return a string representation of the 4D vector. - - - -#### vector\_4D.\_\_add\_\_ - -```python -def __add__(other) -``` - -Add functionality for 4D vectors. - -**Arguments**: - -- `other` - (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') - - -**Returns**: - -- `add` - (self) component wise addition - - - -#### vector\_4D.\_\_sub\_\_ - -```python -def __sub__(other) -``` - -Sub functionality for 4D vectors. - -**Arguments**: - -- `other` - (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') - - -**Returns**: - -- `sub` - (self) component wise subtraction - - - -#### vector\_4D.\_\_mul\_\_ - -```python -def __mul__(other) -``` - -Scalar multiplication functionality for 4D vectors. - -**Arguments**: - -- `other` - (float or int) - - -**Returns**: - -- `mul` - (self) scalar multiplication, scaling - - - -#### vector\_4D.\_\_truediv\_\_ - -```python -def __truediv__(other) -``` - -Scalar division functionality for 4D Vectors. - -**Arguments**: - -- `other` - (float or int) - - -**Returns**: - -- `div` - (self) scalar division, scaling - - - -#### vector\_4D.\_\_eq\_\_ - -```python -def __eq__(other) -> bool -``` - -Check for equality and return True if equal (with tolerance). - -**Arguments**: - -- `other` - (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') - - -**Returns**: - -- `eq` - (bool) true if equal (with tolerance) - - - -#### vector\_4D.\_\_gt\_\_ - -```python -def __gt__(other) -> bool -``` - -Check for greater than and return True if greater. - -**Arguments**: - -- `other` - (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') - - -**Returns**: - -- `gt` - (bool) true if greater - - - -#### vector\_4D.get\_vec - -```python -def get_vec(withExtrusion: bool = False) -> List[float] -``` - -Return the 4D vector, optionally with extrusion. - -**Arguments**: - -- `withExtrusion` - (bool, default = False) choose if vec repr contains extrusion - - -**Returns**: - -- `vec` - (list[3 or 4]) with (x,y,z,(optionally e)) - - - -#### vector\_4D.get\_norm - -```python -def get_norm(withExtrusion: bool = False) -> float -``` - -Return the 4D vector norm. Optional with extrusion. - -**Arguments**: - -- `withExtrusion` - (bool, default = False) choose if norm contains extrusion - - -**Returns**: - -- `norm` - (float) length/norm of 3D or 4D vector - - - -### position Objects - -```python -class position(vector_4D) -``` - -4D - Position object for (Cartesian) 3D printer. - - - -#### position.\_\_str\_\_ - -```python -def __str__() -> str -``` - -Print out position. - - - -#### position.is\_travel - -```python -def is_travel(other) -> bool -``` - -Return True if there is travel between self and other position. - -**Arguments**: - -- `other` - (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') - - -**Returns**: - -- `is_travel` - (bool) true if between self and other is distance - - - -#### position.is\_extruding - -```python -def is_extruding(other: "position", ignore_retract: bool = True) -> bool -``` - -Return True if there is extrusion between self and other position. - -**Arguments**: - -- `other` - (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray') -- `ignore_retract` - (bool, default = True) if true ignore retract movements else retract is also extrusion - - -**Returns**: - -- `is_extruding` - (bool) true if between self and other is extrusion - - - -#### position.get\_t\_distance - -```python -def get_t_distance(other=None, withExtrusion: bool = False) -> float -``` - -Calculate the travel distance between self and other position. If none is provided, zero will be used. - -**Arguments**: - -- `other` - (4D vector, 1x4 'list', 1x4 'tuple' or 1x4 'numpy.ndarray', default = None) -- `withExtrusion` - (bool, default = False) use or ignore extrusion - - -**Returns**: - -- `travel` - (float) travel or extrusion and travel distance - - - -#### position.\_\_truediv\_\_ - -```python -def __truediv__(other) -``` - -Divide position by seconds to get velocity. - - - -### velocity Objects - -```python -class velocity(vector_4D) -``` - -4D - Velocity object for (Cartesian) 3D printer. - - - -#### velocity.\_\_str\_\_ - -```python -def __str__() -> str -``` - -Print out velocity. - - - -#### velocity.get\_norm\_dir - -```python -def get_norm_dir(withExtrusion: bool = False) -> Optional[np.ndarray] -``` - -Get normalized direction vector as numpy array. - -If only extrusion occurs and withExtrusion=True, normalize to the extrusion length. - -Returns None if both travel and extrusion are zero. - - - -#### velocity.not\_zero - -```python -def not_zero() -> bool -``` - -Return True if velocity is not zero. - -**Returns**: - -- `not_zero` - (bool) true if velocity is not zero - - - -#### velocity.is\_extruding - -```python -def is_extruding() -> bool -``` - -Return True if extrusion velocity is greater than zero. - -**Returns**: - -- `is_extruding` - (bool) true if positive extrusion velocity - - - -#### velocity.\_\_mul\_\_ - -```python -def __mul__(other) -``` - -Multiply velocity by a time to get position, or by scalar. - - - -#### velocity.\_\_truediv\_\_ - -```python -def __truediv__(other) -``` - -Divide velocity by scalar. - - - -### acceleration Objects - -```python -class acceleration(vector_4D) -``` - -4D - Acceleration object for (Cartesian) 3D printer. - - - -#### acceleration.\_\_str\_\_ - -```python -def __str__() -> str -``` - -Print out acceleration. - - - -#### acceleration.\_\_mul\_\_ - -```python -def __mul__(other) -``` - -Multiply acceleration by a time to get velocity, or by scalar. - - - -#### acceleration.\_\_truediv\_\_ - -```python -def __truediv__(other) -``` - -Divide acceleration by scalar. - - - -### segment Objects - -```python -class segment() -``` - -Store Segment data for linear 4D Velocity function segment. - -contains: time, position, velocity -**Supports** -- str - -**Additional methods** -- move_segment_time: moves Segment in time by a specified interval -- get_velocity: returns the calculated Velocity for all axis at a given point in time -- get_position: returns the calculated Position for all axis at a given point in time -- get_segm_len: returns the length of the segment. - -**Class method** -- create_initial: returns the artificial initial segment where everything is at standstill, intervall length = 0 -- self_check: returns True if all self checks have been successfull - - - -#### segment.\_\_init\_\_ - -```python -def __init__(t_begin: Union[float, seconds], - t_end: Union[float, seconds], - pos_begin: position, - vel_begin: velocity, - pos_end: position = None, - vel_end: velocity = None) -``` - -Initialize a segment. - -**Arguments**: - -- `t_begin` - (float) begin of segment -- `t_end` - (float) end of segment -- `pos_begin` - (position) beginning position of segment -- `vel_begin` - (velocity) beginning velocity of segment -- `pos_end` - (position, default = None) ending position of segment -- `vel_end` - (velocity, default = None) ending velocity of segment - - - -#### segment.\_\_str\_\_ - -```python -def __str__() -> str -``` - -Create string from segment. - - - -#### segment.\_\_repr\_\_ - -```python -def __repr__() -``` - -Segment representation. - - - -#### segment.move\_segment\_time - -```python -def move_segment_time(delta_t: Union[float, seconds]) -> None -``` - -Move segment in time. - -**Arguments**: - -- `delta_t` - (float) time to be shifted - - - -#### segment.get\_velocity - -```python -def get_velocity(t: Union[float, seconds]) -> velocity -``` - -Get current velocity of segment at a certain time. - -**Arguments**: - -- `t` - (float) time - - -**Returns**: - -- `current_vel` - (velocity) velocity at time t - - - -#### segment.get\_velocity\_by\_dist - -```python -def get_velocity_by_dist(dist: float) -> float -``` - -Return the velocity magnitude at a certain local segment distance. - -**Arguments**: - -- `dist` - (float) distance from segment start - - - -#### segment.get\_position - -```python -def get_position(t: Union[float, seconds]) -> position -``` - -Get current position of segment at a certain time. - -**Arguments**: - -- `t` - (float) time - - -**Returns**: - -- `pos` - (position) position at time t - - - -#### segment.get\_segm\_len - -```python -def get_segm_len() -> float -``` - -Return the length of the segment. - - - -#### segment.get\_segm\_duration - -```python -def get_segm_duration() -> seconds -``` - -Return the duration of the segment. - - - -#### segment.self\_check - -```python -def self_check(p_settings: "state.p_settings" = None) -> bool -``` - -Check the segment for self consistency. - -**Raises**: - -- `ValueError` - if self check fails - -**Arguments**: - -- `p_settings` - (p_settings, default = None) printing settings to verify - -**Returns**: - - True if all checks pass - - - -#### segment.is\_extruding - -```python -def is_extruding() -> bool -``` - -Return true if the segment is pos. extruding. - -**Returns**: - -- `is_extruding` - (bool) true if positive extrusion - - - -#### segment.get\_result - -```python -def get_result(key: str) -``` - -Return the requested result. - -**Arguments**: - -- `key` - (str) choose result - - -**Returns**: - -- `result` - (list) - - - -#### segment.create\_initial - -```python -@classmethod -def create_initial(cls, - initial_position: Optional[position] = None) -> "segment" -``` - -Create initial static segment with (optionally) initial position else start from Zero. - -**Arguments**: - -- `initial_position` - (postion, default = None) position to begin segment series - - -**Returns**: - -- `segment` - (segment) initial beginning segment From 4be75f4867881a46a51f7c2a36d41a79e4028d32 Mon Sep 17 00:00:00 2001 From: usmfi Date: Thu, 21 Aug 2025 15:51:22 +0200 Subject: [PATCH 53/68] var ref --- .gitlab-ci.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a946b55..b556f56 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -48,7 +48,7 @@ doc-build: - cd docs && mkdocs build - git config user.email "lt-github@fast.kit.edu" - git config user.name "ci-bot" - - git remote add gitlab_origin https://oauth2:${pyGCD_access_token}@gitlab.kit.edu/kit/fast/lb/collaboration/additive-manufacturing/pygcodedecode.git + - git remote add gitlab_origin https://oauth2:$pyGCD_access_token@gitlab.kit.edu/kit/fast/lb/collaboration/additive-manufacturing/pygcodedecode.git - mkdocs gh-deploy artifacts: untracked: false diff --git a/README.md b/README.md index ab8f19e..47f7679 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ generate_abaqus_event_series( ) ``` -For more in depth information have a look into the [documentation](https://github.com/FAST-LB/pyGCodeDecode/blob/main/doc.md). +For more in depth information have a look into the [documentation](https://fast-lb.github.io/pyGCodeDecode/). ## Supported GCode commands From c81489bfe912fa8b9988cb5c367f68d664cd58f5 Mon Sep 17 00:00:00 2001 From: usmfi Date: Thu, 21 Aug 2025 15:58:12 +0200 Subject: [PATCH 54/68] trial and errror --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b556f56..a157595 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -48,6 +48,7 @@ doc-build: - cd docs && mkdocs build - git config user.email "lt-github@fast.kit.edu" - git config user.name "ci-bot" + - git remote rm gitlab_origin - git remote add gitlab_origin https://oauth2:$pyGCD_access_token@gitlab.kit.edu/kit/fast/lb/collaboration/additive-manufacturing/pygcodedecode.git - mkdocs gh-deploy artifacts: From b78f12271691495ad7ab9851f0a19beb69cecdd2 Mon Sep 17 00:00:00 2001 From: usmfi Date: Thu, 21 Aug 2025 16:11:41 +0200 Subject: [PATCH 55/68] other auth --- .gitlab-ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a157595..c53a357 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -47,9 +47,8 @@ doc-build: - pydoc-markdown - cd docs && mkdocs build - git config user.email "lt-github@fast.kit.edu" - - git config user.name "ci-bot" - - git remote rm gitlab_origin - - git remote add gitlab_origin https://oauth2:$pyGCD_access_token@gitlab.kit.edu/kit/fast/lb/collaboration/additive-manufacturing/pygcodedecode.git + - git config user.name "root" + - git remote add deploy-origin https://gitlab-ci-token:$pyGCD_access_token@gitlab.kit.edu/kit/fast/lb/collaboration/additive-manufacturing/pygcodedecode.git - mkdocs gh-deploy artifacts: untracked: false From f32fd7ed6dcfc0635c4d4c0b77e75e5da8856b84 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Thu, 21 Aug 2025 16:21:13 +0200 Subject: [PATCH 56/68] trying to echo secrets --- .gitlab-ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c53a357..da71503 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -43,12 +43,14 @@ doc-build: before_script: - pip install --upgrade pip script: + - echo $test_secret + - echo $pyGCD_access_token - pip install -e .[DOCS] - pydoc-markdown - cd docs && mkdocs build - git config user.email "lt-github@fast.kit.edu" - - git config user.name "root" - - git remote add deploy-origin https://gitlab-ci-token:$pyGCD_access_token@gitlab.kit.edu/kit/fast/lb/collaboration/additive-manufacturing/pygcodedecode.git + - git config user.name "ci-bot" + - git remote add deploy-origin https://oauth2:$pyGCD_access_token@gitlab.kit.edu/kit/fast/lb/collaboration/additive-manufacturing/pygcodedecode.git - mkdocs gh-deploy artifacts: untracked: false @@ -70,5 +72,5 @@ doc-compile_paper: untracked: false paths: - ./paper/*.pdf - when: always + when: manual expire_in: "30 days" From d9e6451aa31423b038afb21c736eb978db284fcf Mon Sep 17 00:00:00 2001 From: ci-bot Date: Thu, 21 Aug 2025 16:24:05 +0200 Subject: [PATCH 57/68] removing paper compilation --- .gitlab-ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index da71503..7c36283 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -63,6 +63,8 @@ doc-build: doc-compile_paper: stage: doc needs: [] + rules: + - when: manual tags: - shell script: @@ -72,5 +74,5 @@ doc-compile_paper: untracked: false paths: - ./paper/*.pdf - when: manual + when: always expire_in: "30 days" From 0d9ca0a9647b2c02eb13f50121d05f8e63c10233 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Thu, 21 Aug 2025 16:32:31 +0200 Subject: [PATCH 58/68] another try for the CI --- .gitlab-ci.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7c36283..9d91a85 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -43,14 +43,12 @@ doc-build: before_script: - pip install --upgrade pip script: - - echo $test_secret - - echo $pyGCD_access_token + - git config --global user.name "${GITLAB_USER_NAME}" + - git config --global user.email "${GITLAB_USER_EMAIL}" + - git remote set-url origin https://gitlab-ci-token:${pyGCD_access_token}@gitlab.kit.edu/kit/fast/lb/collaboration/additive-manufacturing/pygcodedecode.git - pip install -e .[DOCS] - - pydoc-markdown - cd docs && mkdocs build - - git config user.email "lt-github@fast.kit.edu" - - git config user.name "ci-bot" - - git remote add deploy-origin https://oauth2:$pyGCD_access_token@gitlab.kit.edu/kit/fast/lb/collaboration/additive-manufacturing/pygcodedecode.git + - pydoc-markdown - mkdocs gh-deploy artifacts: untracked: false From 9c8373657c4f22156ce7a69389a297776a685ba0 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Thu, 21 Aug 2025 16:37:32 +0200 Subject: [PATCH 59/68] correct order in doc deploy --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9d91a85..cd950bb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -47,8 +47,8 @@ doc-build: - git config --global user.email "${GITLAB_USER_EMAIL}" - git remote set-url origin https://gitlab-ci-token:${pyGCD_access_token}@gitlab.kit.edu/kit/fast/lb/collaboration/additive-manufacturing/pygcodedecode.git - pip install -e .[DOCS] - - cd docs && mkdocs build - pydoc-markdown + - cd docs - mkdocs gh-deploy artifacts: untracked: false From a695e9d6b55daee57e083d94a5edbf8bcac0dc0e Mon Sep 17 00:00:00 2001 From: ci-bot Date: Thu, 21 Aug 2025 16:41:21 +0200 Subject: [PATCH 60/68] installing git-lfs in CI --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cd950bb..3da3e23 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -41,6 +41,8 @@ doc-build: image: "python:3.13" needs: [] before_script: + - sudo apt-get install git-lfs + - git lfs install - pip install --upgrade pip script: - git config --global user.name "${GITLAB_USER_NAME}" From 39876ae47e8d708d62cdd10673242610a3dfd65f Mon Sep 17 00:00:00 2001 From: ci-bot Date: Thu, 21 Aug 2025 16:44:47 +0200 Subject: [PATCH 61/68] removing sudo --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3da3e23..dbec8e2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -41,7 +41,7 @@ doc-build: image: "python:3.13" needs: [] before_script: - - sudo apt-get install git-lfs + - apt-get install git-lfs - git lfs install - pip install --upgrade pip script: From cb76963905dc2355bba31d784cfc9f472de835cd Mon Sep 17 00:00:00 2001 From: Lukas Hof Date: Thu, 21 Aug 2025 16:50:22 +0200 Subject: [PATCH 62/68] added apt-get update --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dbec8e2..4ae8dd0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -41,7 +41,7 @@ doc-build: image: "python:3.13" needs: [] before_script: - - apt-get install git-lfs + - apt-get update && apt-get install git-lfs - git lfs install - pip install --upgrade pip script: From 3e44238c879f0c2e6809744aaf48e0312e19fe9b Mon Sep 17 00:00:00 2001 From: Lukas Hof Date: Thu, 21 Aug 2025 16:59:30 +0200 Subject: [PATCH 63/68] force push to gh-pages --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4ae8dd0..400dfb0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -51,7 +51,7 @@ doc-build: - pip install -e .[DOCS] - pydoc-markdown - cd docs - - mkdocs gh-deploy + - mkdocs gh-deploy --force artifacts: untracked: false paths: From c4342975176f7714aa02793996395259c0f6196b Mon Sep 17 00:00:00 2001 From: Lukas Hof Date: Thu, 21 Aug 2025 17:09:17 +0200 Subject: [PATCH 64/68] run doc build only on main --- .gitlab-ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 400dfb0..6353429 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -40,6 +40,12 @@ doc-build: stage: doc image: "python:3.13" needs: [] + only: + refs: + - main + rules: + # Only run on merge requests to the main branch + - if: $CI_PIPELINE_SOURCE == 'merge_request_event' && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main" before_script: - apt-get update && apt-get install git-lfs - git lfs install From 00357e3c85595e2407e869e40bda15cdfe9d1442 Mon Sep 17 00:00:00 2001 From: Lukas Hof Date: Thu, 21 Aug 2025 17:12:10 +0200 Subject: [PATCH 65/68] removing MR restriction in CI --- .gitlab-ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6353429..ccb9252 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -43,9 +43,6 @@ doc-build: only: refs: - main - rules: - # Only run on merge requests to the main branch - - if: $CI_PIPELINE_SOURCE == 'merge_request_event' && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main" before_script: - apt-get update && apt-get install git-lfs - git lfs install From ac66c2d9c5e1c05f21438978df88ed2bf60bbe52 Mon Sep 17 00:00:00 2001 From: Lukas Hof Date: Thu, 21 Aug 2025 17:15:11 +0200 Subject: [PATCH 66/68] replacing "main" with regex --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ccb9252..b49992e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -42,7 +42,7 @@ doc-build: needs: [] only: refs: - - main + - ^main$ before_script: - apt-get update && apt-get install git-lfs - git lfs install From d3a3096ab6eea62fa0c9350f7147f9d969932ebe Mon Sep 17 00:00:00 2001 From: usmfi Date: Thu, 21 Aug 2025 17:29:59 +0200 Subject: [PATCH 67/68] rmv changelog --- whats_new.md | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 whats_new.md diff --git a/whats_new.md b/whats_new.md deleted file mode 100644 index 01dd5fb..0000000 --- a/whats_new.md +++ /dev/null @@ -1,22 +0,0 @@ -## Latest Release Notes -pyGCodeDecode has gotten several updates which are now published with Version XX.XX. -These include QOL improvements and error fixes. - -### Result Calculation -A separate result calculation module is added, which is executed after simulation. User defined results may be calculated using the resulting trajectory and can be mapped to the segments. See current implementations for details. - -### Plotting updates -Plotting methods are moved to be separated from the simulation objects. They now allow for several arguments to be passed to the `pyvista` visualization toolkit. The scalar value plotted may be selected via keys, allowing different results to be displayed. Further individual layer plotting is supported, if a layer cue is provided. Additional arguments enable advanced settings such as transparent background, lighting and rendering options. This includes camera position and orientation. -Further, the plotting methods allow a callable to be passed; the user may modify the `pyvista`-scene through these and add geometry to the plot. -Screenshots are also improved visually by wrapping the pyvista screenshot into a `matplotlib` axis, which allows for nicer colorbars and vector graphics rendering of text. -Individual extrusions now are represented through a squiched cylinder instead of a circular one. This squiching can be set by the user and results from the layer width to height ratio. - -### Junction Handling Bug fixes -Several bug fixes are implemented for junction handling, improving the overall robustness of the simulation and ensuring accurate results. The firmware identifiers have been changed to be more consistent. - -### Prints -Print statements have been improved for better clarity and information. A verbosity control is added to declutter the output, especially when running a large number of simulations. They now include more context about the simulation state and results, making it easier to understand the output. Consistent formatting and different verbosity levels are supported. - -### Testing and Type Hints -More tests have been added to ensure a reliable simulation and framework. Especially the junction handling module is tested more thoroughly. -More type hints have been added throughout the codebase to improve readability and facilitate easier debugging and development. Physical quantities are morphed by mathematical operations to yield the correct units, allowing for more intuitive code and reducing the likelihood of errors. From f4b2eabd2ab57c33731fca6f399e1e491a2b653d Mon Sep 17 00:00:00 2001 From: Lukas Hof Date: Thu, 21 Aug 2025 17:40:07 +0200 Subject: [PATCH 68/68] trying different option for only on main --- .gitlab-ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b49992e..464c7c7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -40,9 +40,8 @@ doc-build: stage: doc image: "python:3.13" needs: [] - only: - refs: - - ^main$ + rules: + - if: $CI_COMMIT_BRANCH == "main" before_script: - apt-get update && apt-get install git-lfs - git lfs install