nc-gcode-interpreter 0.2.0__cp314-cp314-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,530 @@
1
+ from typing import Protocol, Iterator
2
+ import os
3
+ import polars as pl
4
+ from ._internal import nc_to_rows as _nc_to_rows
5
+ from ._internal import nc_to_batches as _nc_to_batches
6
+ from ._internal import __doc__ # noqa: F401
7
+ import json
8
+ from pathlib import Path
9
+ from typing import TypedDict, TypeVar, Any, Generic
10
+ from collections.abc import Callable
11
+
12
+
13
+ # Define TextFileLike Protocol
14
+ class TextFileLike(Protocol):
15
+ def read(self) -> str: ...
16
+
17
+
18
+ __all__ = [
19
+ "nc_to_dataframe",
20
+ "nc_to_rows",
21
+ "nc_to_batches",
22
+ "sanitize_dataframe",
23
+ "dataframe_to_nc",
24
+ ]
25
+
26
+ # Batch size nc_to_dataframe uses internally when draining the batch stream it
27
+ # concatenates. A 1.1 GB / 22M-row sweep put the optimum at 250k-1M (~33 s, a
28
+ # flat minimum); it degrades outside that - 100k pays per-batch overhead, and
29
+ # batches >=2M starve the pipeline (the worker blocks building a huge batch
30
+ # before the consumer sees one) and add first-output latency. 500k measured
31
+ # best and matches the nc_to_batches default, so both paths share one size.
32
+ _COLLECT_BATCH_SIZE = 500_000
33
+
34
+
35
+ def _normalize_input(input: "TextFileLike | str | os.PathLike") -> tuple[str, bool]:
36
+ """Normalize the ``input`` argument into ``(text_or_path, is_path)``.
37
+
38
+ Backwards compatible with the historical contract: a ``str`` is the
39
+ program text and a file-like object is read in Python. A ``pathlib.Path``
40
+ (or any :class:`os.PathLike`) is passed to Rust as a *path* so the file is
41
+ read there once, avoiding a Python-side read and the str->String copy over
42
+ the PyO3 boundary for large programs.
43
+ """
44
+ if input is None:
45
+ raise ValueError("input cannot be None")
46
+ if isinstance(input, str):
47
+ return input, False
48
+ if isinstance(input, os.PathLike):
49
+ return os.fspath(input), True
50
+ return input.read(), False
51
+
52
+
53
+ def nc_to_dataframe(
54
+ input: "TextFileLike | str | os.PathLike",
55
+ initial_state: TextFileLike | str | None = None,
56
+ axis_identifiers: list[str] | None = None,
57
+ extra_axes: list[str] | None = None,
58
+ iteration_limit: int = 10000,
59
+ disable_forward_fill: bool = False,
60
+ axis_index_map: dict[str, int] | None = None,
61
+ allow_undefined_variables: bool = False,
62
+ flatten_tolerance: float | None = None,
63
+ ) -> tuple[pl.DataFrame, dict]:
64
+ """
65
+ Parses Sinumerik-flavored NC G-code and converts it into a Polars DataFrame along with the final state.
66
+
67
+ This function processes the provided G-code input, interprets it according to Sinumerik specifications,
68
+ and outputs the movement commands and other relevant data in a structured DataFrame format. It also
69
+ returns a dictionary representing the final state after code execution, which can be useful for further analysis or inspection.
70
+
71
+ Parameters:
72
+ -----------
73
+ input: TextFileLike | str
74
+ The G-code input as a string or a file-like object.
75
+ initial_state: TextFileLike | str | None, optional
76
+ An optional initial state string or a file-like object to initialize the interpreter's state.
77
+ axis_identifiers: list[str] | None, optional
78
+ A list of axis identifiers to override the default axes (e.g., ['X', 'Y', 'Z']).
79
+ extra_axes: list[str] | None, optional
80
+ A list of extra axis identifiers to include in addition to the default ones (e.g., ['A', 'B', 'C']).
81
+ iteration_limit: int, optional
82
+ The maximum number of iterations to process, to prevent infinite loops in the G-code [default: 10000].
83
+ disable_forward_fill: bool, optional
84
+ If True, disables forward-filling of null values in axes columns in the resulting DataFrame.
85
+ axis_index_map: dict[str, int] | None, optional
86
+ A mapping from axis identifiers (e.g., 'E') to numeric indices (e.g., 4) for array assignments like FL[E]=10.
87
+ This allows user-configurable mapping of axis names to indices. Example: {'E': 4, 'X': 0}.
88
+ allow_undefined_variables: bool, optional
89
+ If True, allows undefined variables to be used in expressions with a value of 0 [default: False].
90
+ flatten_tolerance: float | None, optional
91
+ When set, flatten curved motions (G2/G3 arcs and ASPLINE/BSPLINE/CSPLINE
92
+ splines) into runs of G1 moves whose polyline stays within this maximum
93
+ deviation (in path units, i.e. mm) of the true curve. The interpolation
94
+ parameters (I/J/K/CR, PW/SD/PL) are consumed and do not appear in the
95
+ output. Default None (curves pass through untouched).
96
+
97
+ Returns:
98
+ --------
99
+ tuple[pl.DataFrame, dict]
100
+ A tuple containing:
101
+ - A Polars DataFrame representing the parsed G-code.
102
+ - A nested dictionary representing the final state after execution.
103
+
104
+ Raises:
105
+ -------
106
+ ValueError
107
+ If the input is None or invalid.
108
+
109
+ Example:
110
+ --------
111
+ >>> df, state = nc_to_dataframe('G1 X10 Y20 Z30')
112
+ >>> print(df)
113
+ shape: (1, 4)
114
+ ┌─────────────┬──────┬──────┬──────┐
115
+ │ gg01_motion ┆ X ┆ Y ┆ Z │
116
+ │ --- ┆ --- ┆ --- ┆ --- │
117
+ │ str ┆ f64 ┆ f64 ┆ f64 │
118
+ ╞═════════════╪══════╪══════╪══════╡
119
+ │ G1 ┆ 10.0 ┆ 20.0 ┆ 30.0 │
120
+ └─────────────┴──────┴──────┴──────┘
121
+ """
122
+ program, input_is_path = _normalize_input(input)
123
+ if initial_state is not None and not isinstance(initial_state, str):
124
+ initial_state = initial_state.read()
125
+
126
+ # The whole table is the concatenation of the batch stream: the interpreter
127
+ # runs on a worker thread, building columnar Arrow batches (each handed over
128
+ # zero-copy through the Arrow PyCapsule interface, no Python-list
129
+ # materialization) that this thread wraps with pl.DataFrame and concatenates.
130
+ # Streaming the batches keeps only one batch of intermediate rows alive at a
131
+ # time - far less peak memory than collecting all rows up front - and
132
+ # overlaps interpretation with DataFrame assembly.
133
+ it = _nc_to_batches(
134
+ program,
135
+ _COLLECT_BATCH_SIZE,
136
+ initial_state,
137
+ axis_identifiers,
138
+ extra_axes,
139
+ iteration_limit,
140
+ disable_forward_fill,
141
+ axis_index_map,
142
+ allow_undefined_variables,
143
+ input_is_path,
144
+ flatten_tolerance,
145
+ )
146
+ # pl.DataFrame wraps each Arrow record batch via __arrow_c_array__ (polars
147
+ # >= 1.3), no pyarrow needed. Exhaust the iterator before reading .state.
148
+ frames: list[pl.DataFrame] = [pl.DataFrame(batch) for batch in it]
149
+ state = it.state
150
+ if not frames:
151
+ # A program with no output rows (e.g. only variable assignments) yields
152
+ # no batches; return an empty frame with the final state, as before.
153
+ return pl.DataFrame(), state
154
+ if len(frames) == 1:
155
+ return frames[0], state
156
+ # Later batches may introduce a column a header line did not (e.g. `M`
157
+ # first appears mid-program): a diagonal concat unions them (absent -> null),
158
+ # and the last batch's columns are the canonical order (the batch builder
159
+ # applies canonical_order cumulatively, so the final batch has every column
160
+ # in the same order the whole-table path produced).
161
+ return pl.concat(frames, how="diagonal").select(frames[-1].columns), state
162
+
163
+
164
+ _T = TypeVar("_T")
165
+
166
+
167
+ class _classproperty(Generic[_T]):
168
+ def __init__(self, fget: Callable[[Any], _T]) -> None:
169
+ self.fget = fget
170
+
171
+ def __get__(self, instance: Any, owner: type[Any]) -> _T:
172
+ return self.fget(owner)
173
+
174
+
175
+ class _GGroupEntry(TypedDict):
176
+ id: str
177
+ nr: int
178
+ description: str
179
+
180
+
181
+ class _GGroup(TypedDict):
182
+ nr: int
183
+ title: str
184
+ effectiveness: str
185
+ short_name: str
186
+ entries: list[_GGroupEntry]
187
+
188
+
189
+ class GGroups:
190
+ _g_groups: list[_GGroup] | None = None
191
+ _g_group_short_names: set[str] | None = None
192
+ _g_groups_by_short_name: dict[str, _GGroup] | None = None
193
+
194
+ @_classproperty
195
+ def g_groups(cls) -> list[_GGroup]:
196
+ if cls._g_groups is None:
197
+ cls._load_data()
198
+ assert cls._g_groups is not None
199
+ return cls._g_groups
200
+
201
+ @classmethod
202
+ def _load_data(cls) -> None:
203
+ json_file = Path(__file__).parent / "ggroups.json"
204
+ with open(json_file) as file:
205
+ g_groups = json.load(file)
206
+ cls._g_groups = g_groups
207
+ cls._g_group_short_names = {group["short_name"] for group in g_groups}
208
+ cls._g_groups_by_short_name = {
209
+ group["short_name"]: group for group in g_groups
210
+ }
211
+
212
+ @classmethod
213
+ def is_g_group(cls, name: str) -> bool:
214
+ if cls._g_group_short_names is None:
215
+ cls._load_data()
216
+ assert cls._g_group_short_names is not None
217
+ return name in cls._g_group_short_names
218
+
219
+ @classmethod
220
+ def is_modal_g_group(cls, name: str) -> bool:
221
+ if cls._g_groups_by_short_name is None:
222
+ cls._load_data()
223
+ assert cls._g_groups_by_short_name is not None
224
+ return cls._g_groups_by_short_name[name]["effectiveness"] == "modal"
225
+
226
+
227
+ def sanitize_dataframe(
228
+ df: pl.DataFrame, disable_forward_fill: bool = False
229
+ ) -> pl.DataFrame:
230
+ """
231
+ Cleans and preprocesses the DataFrame resulting from G-code interpretation.
232
+
233
+ This function performs necessary sanitization steps on the DataFrame, such as handling null values,
234
+ forward-filling axis positions if enabled, and preparing the DataFrame for further processing or conversion.
235
+
236
+ Parameters:
237
+ -----------
238
+ df: pl.DataFrame
239
+ The DataFrame to sanitize.
240
+ disable_forward_fill: bool, optional
241
+ If True, disables forward-filling of null values in axes columns.
242
+
243
+ Returns:
244
+ --------
245
+ pl.DataFrame
246
+ The sanitized DataFrame ready for analysis or conversion back to G-code.
247
+ """
248
+ modal = [g["short_name"] for g in GGroups.g_groups if g["effectiveness"] == "modal"]
249
+ non_modal = [g["short_name"] for g in GGroups.g_groups if g["effectiveness"] != "modal"]
250
+ known_axes = [
251
+ "X", "Y", "Z", "A", "B", "C", "D", "E", "F", "S", "U", "V",
252
+ "RA1", "RA2", "RA3", "RA4", "RA5", "RA6",
253
+ ]
254
+ string_columns = {*modal, *non_modal, "T", "non_returning_function_call", "comment"}
255
+ # Block addresses: value columns, but never forward-filled. These are the
256
+ # circular/helical interpolation parameters (I/J/K arc-centre offsets and
257
+ # the CR radius form) and the spline programming addresses (PW/SD/PL).
258
+ # Each value belongs only to the block that programs it, so it must not be
259
+ # carried forward. Keep this list identical (and in the same order) to the
260
+ # Rust BLOCK_ADDRESSES constant so both output layers agree on columns.
261
+ block_addresses = ["I", "J", "K", "CR", "PW", "SD", "PL"]
262
+
263
+ # Value columns: anything that is not a known string/list column.
264
+ value_columns = [
265
+ c for c in df.columns if c not in string_columns and c not in ("M", "N")
266
+ ]
267
+
268
+ # Cast to the canonical dtypes.
269
+ casts = []
270
+ if "N" in df.columns:
271
+ casts.append(pl.col("N").cast(pl.Int64))
272
+ casts += [pl.col(c).cast(pl.String) for c in df.columns if c in string_columns]
273
+ casts += [pl.col(c).cast(pl.Float64) for c in value_columns]
274
+ if "M" in df.columns:
275
+ casts.append(pl.col("M").cast(pl.List(pl.String)))
276
+ df = df.with_columns(casts)
277
+
278
+ # Canonical column order.
279
+ order: list[str] = []
280
+ for name in (
281
+ ["N"] + modal + non_modal + known_axes
282
+ + sorted(c for c in value_columns if c not in known_axes and c not in block_addresses)
283
+ + block_addresses
284
+ + ["T", "M", "non_returning_function_call", "comment"]
285
+ ):
286
+ if name in df.columns and name not in order:
287
+ order.append(name)
288
+ df = df.select(order)
289
+
290
+ # Forward-fill value columns and modal G-group columns.
291
+ if not disable_forward_fill:
292
+ fill = [
293
+ c
294
+ for c in df.columns
295
+ if (c in value_columns and c not in block_addresses) or c == "N" or c in modal
296
+ ]
297
+ df = df.with_columns([pl.col(c).fill_null(strategy="forward") for c in fill])
298
+ return df
299
+
300
+
301
+ def dataframe_to_nc(df: pl.DataFrame, file_path: str | Path):
302
+ """
303
+ Converts a Polars DataFrame back into NC G-code and writes it to a file.
304
+
305
+ This function takes a DataFrame representing G-code commands (as produced by `nc_to_dataframe`)
306
+ and reconstructs the G-code, writing the output to the specified file path.
307
+
308
+ Parameters:
309
+ -----------
310
+ df: pl.DataFrame
311
+ The DataFrame containing G-code data to be converted back into NC code.
312
+ file_path: str or Path
313
+ The file path where the generated G-code should be written.
314
+
315
+ Notes:
316
+ ------
317
+ - Currently, this function is implemented in Python. Future versions may implement this in Rust for performance.
318
+ - The function ensures that consecutive duplicate values are appropriately handled to generate clean G-code.
319
+
320
+ Example:
321
+ --------
322
+ >>> import polars as pl
323
+ >>> from nc_gcode_interpreter import dataframe_to_nc
324
+ >>> df = pl.DataFrame({'X': [10, 20], 'Y': [0, 0], 'Z': [0, 0]})
325
+ >>> dataframe_to_nc(df, 'output.MPF')
326
+ """
327
+ df = sanitize_dataframe(df)
328
+ # Python prototype of df to nc conversion code
329
+ float_cols = [col for col in df.columns if df[col].dtype == pl.Float64]
330
+ int_cols = [col for col in df.columns if df[col].dtype == pl.Int64]
331
+ g_group_cols = [col for col in df.columns if GGroups.is_g_group(col)]
332
+ list_of_str_cols = [
333
+ col for col in df.columns if df[col].dtype == pl.List(pl.String)
334
+ ]
335
+ string_axes_cols = [col for col in df.columns if col in ["T"]]
336
+
337
+ # Replace consecutive duplicates with null values
338
+ df = df.with_columns(
339
+ [
340
+ pl.when(pl.col(c) == pl.col(c).shift(1))
341
+ .then(None)
342
+ .otherwise(
343
+ pl.lit(f"{c}{'=' if len(c) > 1 else ''}")
344
+ + pl.col(c).round(3).cast(pl.String)
345
+ )
346
+ .alias(c)
347
+ for c in float_cols
348
+ ]
349
+ + [
350
+ pl.when(pl.col(c) == pl.col(c).shift(1))
351
+ .then(None)
352
+ .otherwise(
353
+ pl.lit(f"{c}{'=' if len(c) > 1 else ''}") + pl.col(c).cast(pl.String)
354
+ )
355
+ .alias(c)
356
+ for c in int_cols
357
+ ]
358
+ + [
359
+ (pl.lit(f'{c}="') + pl.col(c) + pl.lit('"')).alias(c)
360
+ for c in string_axes_cols
361
+ ]
362
+ + [
363
+ pl.when(pl.col(c) == pl.col(c).shift(1))
364
+ .then(None)
365
+ .otherwise(pl.col(c))
366
+ .alias(c)
367
+ for c in g_group_cols
368
+ ]
369
+ + [pl.col(c).list.join(separator=" ").alias(c) for c in list_of_str_cols]
370
+ )
371
+
372
+ # Define the columns you want to include in the output
373
+ columns_of_interest = df.columns
374
+ df_line = df.with_columns(
375
+ pl.concat_str(
376
+ [pl.col(c) for c in columns_of_interest], ignore_nulls=True, separator=" "
377
+ ).alias("line")
378
+ ).select("line")
379
+ df_line.write_csv(file_path, include_header=False, quote_style="never")
380
+
381
+
382
+ def nc_to_rows(
383
+ input: "TextFileLike | str | os.PathLike",
384
+ initial_state: TextFileLike | str | None = None,
385
+ axis_identifiers: list[str] | None = None,
386
+ extra_axes: list[str] | None = None,
387
+ iteration_limit: int = 10000,
388
+ forward_fill: bool = True,
389
+ include_variables: bool = False,
390
+ axis_index_map: dict[str, int] | None = None,
391
+ allow_undefined_variables: bool = False,
392
+ flatten_tolerance: float | None = None,
393
+ ):
394
+ """Interpret an NC program lazily, yielding one row at a time.
395
+
396
+ Returns an iterator of ``(line_no, row)`` tuples: the 1-based source
397
+ line the block came from (loops and jumps repeat / reorder line
398
+ numbers), and a dict of column values typed like the batch DataFrame
399
+ (``N``: int, ``M``: list[str], G-group/T/comment columns: str, axes and
400
+ other value columns: float). Rows are forward-filled like
401
+ :func:`nc_to_dataframe` unless ``forward_fill`` is False.
402
+
403
+ With ``include_variables=True`` the iterator yields ``(line_no, row,
404
+ variables)`` instead, where ``variables`` maps each variable the block
405
+ assigned (``R1=R1+1``, ``DEF REAL Q=5``, FOR counters) to its new float
406
+ value. Blocks that only assign variables — invisible in the batch
407
+ DataFrame and in the default stream — are then also yielded, with an
408
+ empty (never forward-filled) ``row``. Accumulating the ``variables``
409
+ dicts with ``dict.update`` reconstructs the full variable state at any
410
+ point of the stream.
411
+
412
+ The interpreter runs on a background thread behind a bounded channel:
413
+ rows arrive as they are produced, memory use is constant, and dropping
414
+ the iterator aborts interpretation. Breaking out of a ``for`` loop over
415
+ an anonymous iterator drops it; a stored iterator keeps the worker
416
+ alive (parked on the bounded channel) until it is deleted or
417
+ garbage-collected.
418
+ Errors raise ``ValueError`` from ``next()`` when reached. After the
419
+ iterator is exhausted, its ``state`` attribute holds the final
420
+ interpreter state (axes, symbol_table, translation).
421
+
422
+ Example:
423
+ --------
424
+ >>> for line_no, row in nc_to_rows("G1 X10\nX20 Y5"):
425
+ ... print(line_no, row["X"])
426
+ 1 10.0
427
+ 2 20.0
428
+ >>> for line_no, row, variables in nc_to_rows("R1=5\nX=R1", include_variables=True):
429
+ ... print(line_no, row.get("X"), variables)
430
+ 1 None {'R1': 5.0}
431
+ 2 5.0 {}
432
+ """
433
+ program, input_is_path = _normalize_input(input)
434
+ if initial_state is not None and not isinstance(initial_state, str):
435
+ initial_state = initial_state.read()
436
+ return _nc_to_rows(
437
+ program,
438
+ initial_state,
439
+ axis_identifiers,
440
+ extra_axes,
441
+ iteration_limit,
442
+ forward_fill,
443
+ include_variables,
444
+ axis_index_map,
445
+ allow_undefined_variables,
446
+ input_is_path,
447
+ flatten_tolerance,
448
+ )
449
+
450
+
451
+ class _BatchIterator:
452
+ """Iterator of polars DataFrames returned by :func:`nc_to_batches`.
453
+
454
+ Wraps the Rust batch iterator, which yields each batch as an Arrow record
455
+ batch built column-wise in Rust and handed over the Arrow PyCapsule
456
+ interface (``__arrow_c_array__``, via pyo3-arrow) - a zero-copy transfer
457
+ with no Python list of primitives materialized. ``pl.DataFrame`` wraps that
458
+ capsule directly (polars >= 1.3), so no ``pyarrow`` is involved. After
459
+ exhaustion its ``state`` attribute holds the final interpreter state (axes,
460
+ symbol_table, translation), like the iterator returned by :func:`nc_to_rows`.
461
+ """
462
+
463
+ def __init__(self, inner: Any) -> None:
464
+ self._inner = inner
465
+
466
+ def __iter__(self) -> "Iterator[pl.DataFrame]":
467
+ return self
468
+
469
+ def __next__(self) -> pl.DataFrame:
470
+ return pl.DataFrame(next(self._inner))
471
+
472
+ @property
473
+ def state(self) -> dict | None:
474
+ return self._inner.state
475
+
476
+
477
+ def nc_to_batches(
478
+ input: "TextFileLike | str | os.PathLike",
479
+ batch_size: int = 500_000,
480
+ initial_state: TextFileLike | str | None = None,
481
+ axis_identifiers: list[str] | None = None,
482
+ extra_axes: list[str] | None = None,
483
+ iteration_limit: int = 10000,
484
+ disable_forward_fill: bool = False,
485
+ axis_index_map: dict[str, int] | None = None,
486
+ allow_undefined_variables: bool = False,
487
+ flatten_tolerance: float | None = None,
488
+ ) -> _BatchIterator:
489
+ """Interpret an NC program into a stream of columnar polars DataFrames.
490
+
491
+ Yields one :class:`polars.DataFrame` per batch of at most ``batch_size``
492
+ output rows. Each batch is built column-wise in Rust (the same machinery
493
+ :func:`nc_to_dataframe` uses), so memory stays bounded by the batch size
494
+ rather than materializing the whole program at once - the way to process a
495
+ program too large to fit in one DataFrame.
496
+
497
+ Forward-filling (axes, value columns and modal G-groups) carries across
498
+ batch boundaries, and the canonical column set grows as new columns appear,
499
+ so concatenating the batches reconstructs :func:`nc_to_dataframe` exactly.
500
+ When every column already appears in the first batch (the usual case) the
501
+ batches share one schema and ``pl.concat`` suffices; otherwise concatenate
502
+ with ``how="diagonal"`` and select the final column order.
503
+
504
+ The returned iterator exposes a ``state`` attribute holding the final
505
+ interpreter state once it is exhausted, like :func:`nc_to_rows`.
506
+
507
+ Example:
508
+ --------
509
+ >>> batches = nc_to_batches("G1 X10\\nX20 Y5", batch_size=1)
510
+ >>> frames = list(batches)
511
+ >>> pl.concat(frames, how="diagonal").height
512
+ 2
513
+ """
514
+ program, input_is_path = _normalize_input(input)
515
+ if initial_state is not None and not isinstance(initial_state, str):
516
+ initial_state = initial_state.read()
517
+ inner = _nc_to_batches(
518
+ program,
519
+ batch_size,
520
+ initial_state,
521
+ axis_identifiers,
522
+ extra_axes,
523
+ iteration_limit,
524
+ disable_forward_fill,
525
+ axis_index_map,
526
+ allow_undefined_variables,
527
+ input_is_path,
528
+ flatten_tolerance,
529
+ )
530
+ return _BatchIterator(inner)
@@ -0,0 +1,40 @@
1
+ from typing import Any, Iterator, Optional, List, Dict, Tuple
2
+
3
+ # Type stubs for the compiled Rust extension `nc_gcode_interpreter._internal`.
4
+ # mypy cannot introspect the pyo3 module, so the public entry points the Python
5
+ # wrapper imports are declared here. Signatures mirror the `#[pyo3(signature)]`
6
+ # defaults in src/lib.rs.
7
+
8
+ def nc_to_rows(
9
+ input: str,
10
+ initial_state: Optional[str] = None,
11
+ axis_identifiers: Optional[List[str]] = None,
12
+ extra_axes: Optional[List[str]] = None,
13
+ iteration_limit: int = 10000,
14
+ forward_fill: bool = True,
15
+ include_variables: bool = False,
16
+ axis_index_map: Optional[Dict[str, int]] = None,
17
+ allow_undefined_variables: bool = False,
18
+ input_is_path: bool = False,
19
+ flatten_tolerance: Optional[float] = None,
20
+ ) -> Iterator[Tuple[Any, ...]]:
21
+ """Interpret an NC program lazily into ``(line_no, row[, variables])`` tuples."""
22
+ ...
23
+
24
+ def nc_to_batches(
25
+ input: str,
26
+ batch_size: int = 500_000,
27
+ initial_state: Optional[str] = None,
28
+ axis_identifiers: Optional[List[str]] = None,
29
+ extra_axes: Optional[List[str]] = None,
30
+ iteration_limit: int = 10000,
31
+ disable_forward_fill: bool = False,
32
+ axis_index_map: Optional[Dict[str, int]] = None,
33
+ allow_undefined_variables: bool = False,
34
+ input_is_path: bool = False,
35
+ flatten_tolerance: Optional[float] = None,
36
+ ) -> Any:
37
+ """Interpret an NC program into an iterator of columnar polars DataFrames."""
38
+ ...
39
+
40
+ __all__ = ["nc_to_rows", "nc_to_batches"]