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.
@@ -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")
@@ -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"]