nc-gcode-interpreter 0.1.10__cp313-cp313-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nc_gcode_interpreter/__init__.py +259 -0
- nc_gcode_interpreter/_internal.cp313-win_amd64.pyd +0 -0
- nc_gcode_interpreter/_internal.pyi +62 -0
- nc_gcode_interpreter/ggroups.json +2922 -0
- nc_gcode_interpreter/py.typed +0 -0
- nc_gcode_interpreter-0.1.10.dist-info/METADATA +191 -0
- nc_gcode_interpreter-0.1.10.dist-info/RECORD +9 -0
- nc_gcode_interpreter-0.1.10.dist-info/WHEEL +4 -0
- nc_gcode_interpreter-0.1.10.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
import polars as pl
|
|
3
|
+
from ._internal import nc_to_dataframe as _nc_to_dataframe
|
|
4
|
+
from ._internal import sanitize_dataframe as _sanitize_dataframe
|
|
5
|
+
from ._internal import __doc__ # noqa: F401
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TypedDict, TypeVar, Any, Generic
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Define TextFileLike Protocol
|
|
13
|
+
class TextFileLike(Protocol):
|
|
14
|
+
def read(self) -> str: ...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__all__ = ["nc_to_dataframe", "sanitize_dataframe", "dataframe_to_nc"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def nc_to_dataframe(
|
|
21
|
+
input: TextFileLike | str,
|
|
22
|
+
initial_state: TextFileLike | str | None = None,
|
|
23
|
+
axis_identifiers: list[str] | None = None,
|
|
24
|
+
extra_axes: list[str] | None = None,
|
|
25
|
+
iteration_limit: int = 10000,
|
|
26
|
+
disable_forward_fill: bool = False,
|
|
27
|
+
) -> tuple[pl.DataFrame, dict]:
|
|
28
|
+
"""
|
|
29
|
+
Parses Sinumerik-flavored NC G-code and converts it into a Polars DataFrame along with the final state.
|
|
30
|
+
|
|
31
|
+
This function processes the provided G-code input, interprets it according to Sinumerik specifications,
|
|
32
|
+
and outputs the movement commands and other relevant data in a structured DataFrame format. It also
|
|
33
|
+
returns a dictionary representing the final state after code execution, which can be useful for further analysis or inspection.
|
|
34
|
+
|
|
35
|
+
Parameters:
|
|
36
|
+
-----------
|
|
37
|
+
input: TextFileLike | str
|
|
38
|
+
The G-code input as a string or a file-like object.
|
|
39
|
+
initial_state: TextFileLike | str | None, optional
|
|
40
|
+
An optional initial state string or a file-like object to initialize the interpreter's state.
|
|
41
|
+
axis_identifiers: list[str] | None, optional
|
|
42
|
+
A list of axis identifiers to override the default axes (e.g., ['X', 'Y', 'Z']).
|
|
43
|
+
extra_axes: list[str] | None, optional
|
|
44
|
+
A list of extra axis identifiers to include in addition to the default ones (e.g., ['A', 'B', 'C']).
|
|
45
|
+
iteration_limit: int, optional
|
|
46
|
+
The maximum number of iterations to process, to prevent infinite loops in the G-code [default: 10000].
|
|
47
|
+
disable_forward_fill: bool, optional
|
|
48
|
+
If True, disables forward-filling of null values in axes columns in the resulting DataFrame.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
--------
|
|
52
|
+
tuple[pl.DataFrame, dict]
|
|
53
|
+
A tuple containing:
|
|
54
|
+
- A Polars DataFrame representing the parsed G-code.
|
|
55
|
+
- A nested dictionary representing the final state after execution.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
-------
|
|
59
|
+
ValueError
|
|
60
|
+
If the input is None or invalid.
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
--------
|
|
64
|
+
>>> df, state = nc_to_dataframe('G1 X10 Y20 Z30')
|
|
65
|
+
>>> print(df)
|
|
66
|
+
shape: (1, 4)
|
|
67
|
+
┌─────────────┬──────┬──────┬──────┐
|
|
68
|
+
│ gg01_motion ┆ X ┆ Y ┆ Z │
|
|
69
|
+
│ --- ┆ --- ┆ --- ┆ --- │
|
|
70
|
+
│ str ┆ f64 ┆ f64 ┆ f64 │
|
|
71
|
+
╞═════════════╪══════╪══════╪══════╡
|
|
72
|
+
│ G1 ┆ 10.0 ┆ 20.0 ┆ 30.0 │
|
|
73
|
+
└─────────────┴──────┴──────┴──────┘
|
|
74
|
+
"""
|
|
75
|
+
if input is None:
|
|
76
|
+
raise ValueError("input cannot be None")
|
|
77
|
+
if not isinstance(input, str):
|
|
78
|
+
input = input.read()
|
|
79
|
+
if initial_state is not None and not isinstance(initial_state, str):
|
|
80
|
+
initial_state = initial_state.read()
|
|
81
|
+
|
|
82
|
+
df, state = _nc_to_dataframe(
|
|
83
|
+
input,
|
|
84
|
+
initial_state,
|
|
85
|
+
axis_identifiers,
|
|
86
|
+
extra_axes,
|
|
87
|
+
iteration_limit,
|
|
88
|
+
disable_forward_fill,
|
|
89
|
+
)
|
|
90
|
+
return df, state
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_T = TypeVar("_T")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class _classproperty(Generic[_T]):
|
|
97
|
+
def __init__(self, fget: Callable[[Any], _T]) -> None:
|
|
98
|
+
self.fget = fget
|
|
99
|
+
|
|
100
|
+
def __get__(self, instance: Any, owner: type[Any]) -> _T:
|
|
101
|
+
return self.fget(owner)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class _GGroupEntry(TypedDict):
|
|
105
|
+
id: str
|
|
106
|
+
nr: int
|
|
107
|
+
description: str
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class _GGroup(TypedDict):
|
|
111
|
+
nr: int
|
|
112
|
+
title: str
|
|
113
|
+
effectiveness: str
|
|
114
|
+
short_name: str
|
|
115
|
+
entries: list[_GGroupEntry]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class GGroups:
|
|
119
|
+
_g_groups: list[_GGroup] | None = None
|
|
120
|
+
_g_group_short_names: set[str] | None = None
|
|
121
|
+
_g_groups_by_short_name: dict[str, _GGroup] | None = None
|
|
122
|
+
|
|
123
|
+
@_classproperty
|
|
124
|
+
def g_groups(cls) -> list[_GGroup]:
|
|
125
|
+
if cls._g_groups is None:
|
|
126
|
+
cls._load_data()
|
|
127
|
+
assert cls._g_groups is not None
|
|
128
|
+
return cls._g_groups
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def _load_data(cls) -> None:
|
|
132
|
+
json_file = Path(__file__).parent / "ggroups.json"
|
|
133
|
+
with open(json_file) as file:
|
|
134
|
+
g_groups = json.load(file)
|
|
135
|
+
cls._g_groups = g_groups
|
|
136
|
+
cls._g_group_short_names = {group["short_name"] for group in g_groups}
|
|
137
|
+
cls._g_groups_by_short_name = {
|
|
138
|
+
group["short_name"]: group for group in g_groups
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def is_g_group(cls, name: str) -> bool:
|
|
143
|
+
if cls._g_group_short_names is None:
|
|
144
|
+
cls._load_data()
|
|
145
|
+
assert cls._g_group_short_names is not None
|
|
146
|
+
return name in cls._g_group_short_names
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def is_modal_g_group(cls, name: str) -> bool:
|
|
150
|
+
if cls._g_groups_by_short_name is None:
|
|
151
|
+
cls._load_data()
|
|
152
|
+
assert cls._g_groups_by_short_name is not None
|
|
153
|
+
return cls._g_groups_by_short_name[name]["effectiveness"] == "modal"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def sanitize_dataframe(
|
|
157
|
+
df: pl.DataFrame, disable_forward_fill: bool = False
|
|
158
|
+
) -> pl.DataFrame:
|
|
159
|
+
"""
|
|
160
|
+
Cleans and preprocesses the DataFrame resulting from G-code interpretation.
|
|
161
|
+
|
|
162
|
+
This function performs necessary sanitization steps on the DataFrame, such as handling null values,
|
|
163
|
+
forward-filling axis positions if enabled, and preparing the DataFrame for further processing or conversion.
|
|
164
|
+
|
|
165
|
+
Parameters:
|
|
166
|
+
-----------
|
|
167
|
+
df: pl.DataFrame
|
|
168
|
+
The DataFrame to sanitize.
|
|
169
|
+
disable_forward_fill: bool, optional
|
|
170
|
+
If True, disables forward-filling of null values in axes columns.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
--------
|
|
174
|
+
pl.DataFrame
|
|
175
|
+
The sanitized DataFrame ready for analysis or conversion back to G-code.
|
|
176
|
+
"""
|
|
177
|
+
# Call the internal Rust function to sanitize the DataFrame
|
|
178
|
+
return _sanitize_dataframe(df, disable_forward_fill)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def dataframe_to_nc(df: pl.DataFrame, file_path: str | Path):
|
|
182
|
+
"""
|
|
183
|
+
Converts a Polars DataFrame back into NC G-code and writes it to a file.
|
|
184
|
+
|
|
185
|
+
This function takes a DataFrame representing G-code commands (as produced by `nc_to_dataframe`)
|
|
186
|
+
and reconstructs the G-code, writing the output to the specified file path.
|
|
187
|
+
|
|
188
|
+
Parameters:
|
|
189
|
+
-----------
|
|
190
|
+
df: pl.DataFrame
|
|
191
|
+
The DataFrame containing G-code data to be converted back into NC code.
|
|
192
|
+
file_path: str or Path
|
|
193
|
+
The file path where the generated G-code should be written.
|
|
194
|
+
|
|
195
|
+
Notes:
|
|
196
|
+
------
|
|
197
|
+
- Currently, this function is implemented in Python. Future versions may implement this in Rust for performance.
|
|
198
|
+
- The function ensures that consecutive duplicate values are appropriately handled to generate clean G-code.
|
|
199
|
+
|
|
200
|
+
Example:
|
|
201
|
+
--------
|
|
202
|
+
>>> import polars as pl
|
|
203
|
+
>>> from nc_gcode_interpreter import dataframe_to_nc
|
|
204
|
+
>>> df = pl.DataFrame({'X': [10, 20], 'Y': [0, 0], 'Z': [0, 0]})
|
|
205
|
+
>>> dataframe_to_nc(df, 'output.MPF')
|
|
206
|
+
"""
|
|
207
|
+
df = sanitize_dataframe(df)
|
|
208
|
+
# Python prototype of df to nc conversion code
|
|
209
|
+
float_cols = [col for col in df.columns if df[col].dtype == pl.Float64]
|
|
210
|
+
int_cols = [col for col in df.columns if df[col].dtype == pl.Int64]
|
|
211
|
+
g_group_cols = [col for col in df.columns if GGroups.is_g_group(col)]
|
|
212
|
+
list_of_str_cols = [
|
|
213
|
+
col for col in df.columns if df[col].dtype == pl.List(pl.String)
|
|
214
|
+
]
|
|
215
|
+
string_axes_cols = [col for col in df.columns if col in ["T"]]
|
|
216
|
+
|
|
217
|
+
# Replace consecutive duplicates with null values
|
|
218
|
+
df = df.with_columns(
|
|
219
|
+
[
|
|
220
|
+
pl.when(pl.col(c) == pl.col(c).shift(1))
|
|
221
|
+
.then(None)
|
|
222
|
+
.otherwise(
|
|
223
|
+
pl.lit(f"{c}{'=' if len(c) > 1 else ''}")
|
|
224
|
+
+ pl.col(c).round(3).cast(pl.String)
|
|
225
|
+
)
|
|
226
|
+
.alias(c)
|
|
227
|
+
for c in float_cols
|
|
228
|
+
]
|
|
229
|
+
+ [
|
|
230
|
+
pl.when(pl.col(c) == pl.col(c).shift(1))
|
|
231
|
+
.then(None)
|
|
232
|
+
.otherwise(
|
|
233
|
+
pl.lit(f"{c}{'=' if len(c) > 1 else ''}") + pl.col(c).cast(pl.String)
|
|
234
|
+
)
|
|
235
|
+
.alias(c)
|
|
236
|
+
for c in int_cols
|
|
237
|
+
]
|
|
238
|
+
+ [
|
|
239
|
+
(pl.lit(f'{c}="') + pl.col(c) + pl.lit('"')).alias(c)
|
|
240
|
+
for c in string_axes_cols
|
|
241
|
+
]
|
|
242
|
+
+ [
|
|
243
|
+
pl.when(pl.col(c) == pl.col(c).shift(1))
|
|
244
|
+
.then(None)
|
|
245
|
+
.otherwise(pl.col(c))
|
|
246
|
+
.alias(c)
|
|
247
|
+
for c in g_group_cols
|
|
248
|
+
]
|
|
249
|
+
+ [pl.col(c).list.join(separator=" ").alias(c) for c in list_of_str_cols]
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Define the columns you want to include in the output
|
|
253
|
+
columns_of_interest = df.columns
|
|
254
|
+
df_line = df.with_columns(
|
|
255
|
+
pl.concat_str(
|
|
256
|
+
[pl.col(c) for c in columns_of_interest], ignore_nulls=True, separator=" "
|
|
257
|
+
).alias("line")
|
|
258
|
+
).select("line")
|
|
259
|
+
df_line.write_csv(file_path, include_header=False, quote_style="never")
|
|
Binary file
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from typing import Optional, List, Dict, Tuple
|
|
2
|
+
import polars as pl
|
|
3
|
+
|
|
4
|
+
# Define the type hint for the `nc_to_dataframe` function
|
|
5
|
+
def nc_to_dataframe(
|
|
6
|
+
input: str,
|
|
7
|
+
initial_state: Optional[str] = None,
|
|
8
|
+
axis_identifiers: Optional[List[str]] = None,
|
|
9
|
+
extra_axes: Optional[List[str]] = None,
|
|
10
|
+
iteration_limit: int = 10000,
|
|
11
|
+
disable_forward_fill: bool = False,
|
|
12
|
+
) -> Tuple[pl.DataFrame, Dict[str, Dict[str, float]]]:
|
|
13
|
+
"""
|
|
14
|
+
Convert G-code to a DataFrame representation along with the state information.
|
|
15
|
+
|
|
16
|
+
Parameters:
|
|
17
|
+
-----------
|
|
18
|
+
input: str
|
|
19
|
+
The G-code input as a string.
|
|
20
|
+
initial_state: Optional[str]
|
|
21
|
+
An optional initial state string.
|
|
22
|
+
axis_identifiers: Optional[List[str]]
|
|
23
|
+
A list of axis identifiers.
|
|
24
|
+
extra_axes: Optional[List[str]]
|
|
25
|
+
A list of extra axes to be included.
|
|
26
|
+
iteration_limit: int
|
|
27
|
+
The maximum number of iterations to process.
|
|
28
|
+
disable_forward_fill: bool
|
|
29
|
+
Whether to disable forward-filling of values.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
--------
|
|
33
|
+
Tuple[pl.DataFrame, Dict[str, Dict[str, float]]]
|
|
34
|
+
A tuple containing the resulting DataFrame and a nested dictionary representing the state.
|
|
35
|
+
"""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
def sanitize_dataframe(
|
|
39
|
+
df: pl.DataFrame,
|
|
40
|
+
disable_forward_fill: bool = False,
|
|
41
|
+
) -> pl.DataFrame:
|
|
42
|
+
"""
|
|
43
|
+
Sanitize the given DataFrame by applying the necessary type conversions and optionally filling missing values.
|
|
44
|
+
|
|
45
|
+
Parameters:
|
|
46
|
+
-----------
|
|
47
|
+
df: pl.DataFrame
|
|
48
|
+
The input DataFrame to sanitize.
|
|
49
|
+
disable_forward_fill: bool
|
|
50
|
+
Whether to disable forward-filling of missing values.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
--------
|
|
54
|
+
pl.DataFrame
|
|
55
|
+
The sanitized DataFrame.
|
|
56
|
+
"""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
# Module definition for nc_gcode_interpreter
|
|
60
|
+
# Since this is an auto-generated module, the binding name corresponds to the compiled Rust module.
|
|
61
|
+
|
|
62
|
+
__all__ = ["nc_to_dataframe", "sanitize_dataframe"]
|