nc-gcode-interpreter 0.2.0__tar.gz → 0.2.2__tar.gz
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-0.2.0 → nc_gcode_interpreter-0.2.2}/.github/workflows/build-and-release.yml +7 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/CHANGELOG.md +60 -12
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/Cargo.lock +1 -1
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/Cargo.toml +1 -1
- nc_gcode_interpreter-0.2.2/Development.md +61 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/PKG-INFO +1 -1
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/README.md +2 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/nc_gcode_interpreter/__init__.py +71 -2
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/nc_gcode_interpreter/_internal.pyi +18 -1
- nc_gcode_interpreter-0.2.2/python/tests/test_batch_variables.py +205 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_batches.py +53 -0
- nc_gcode_interpreter-0.2.2/python/tests/test_structured_errors.py +50 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/errors.rs +156 -32
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/flatten.rs +318 -69
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/interpret_rules.rs +208 -103
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/interpreter.rs +220 -59
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/lib.rs +166 -55
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/line_driver.rs +98 -71
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/main.rs +16 -23
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/modal_groups.rs +41 -10
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/output.rs +160 -12
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/state.rs +40 -27
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/structure_scan.rs +3 -8
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/tests/cli.rs +4 -1
- nc_gcode_interpreter-0.2.0/Development.md +0 -46
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/.gitignore +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/CONTRIBUTING.md +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/LICENSE +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/TODO.md +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/docs/sinumerik-execution-model.md +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/actual_position.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/actual_position.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/arc.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/arc.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/arrays.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/arrays.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/axis_index_assignment.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/axis_index_assignment.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/basic_math.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/basic_math.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/case.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/case.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/case_insensitive_variables.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/case_insensitive_variables.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/custom_vars.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/custom_vars.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/def_string.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/def_string.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/defaults.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/defaults.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/edge_cases.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/edge_cases.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/flattening/flatten_demo.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/flattening/flatten_demo_flattened.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/flattening/flatten_demo_raw.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/for_loop.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/for_loop.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/function_calls.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/function_calls.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/goto.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/goto.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/if_statement.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/if_statement.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/increment.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/increment.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/logic_operators.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/logic_operators.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/loop.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/loop.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/multiple_m_codes.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/multiple_m_codes.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/precedence.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/precedence.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/r_param.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/r_param.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/repeat.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/repeat.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/simple.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/simple.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/spline.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/spline.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/tool.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/tool.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trans.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trans.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trans2.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trans2.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trans_ic.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trans_ic.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trans_reset.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trans_reset.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trig.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/trig.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/variables.csv +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/examples/variables.mpf +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/ggroups/generate_g_commands.py +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/pyproject.toml +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/example/minimal.py +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/example/streaming.py +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/nc_gcode_interpreter/cli.py +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/nc_gcode_interpreter/ggroups.json +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/nc_gcode_interpreter/py.typed +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/nc_gcode_interpreter/viz.py +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_arithmetic_functions.py +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_diagnostics.py +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_expected_output.py +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_field_defects.py +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_flags.py +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_flatten.py +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_g_groups.py +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_g_vocabulary.py +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_jumps.py +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_real_comparisons.py +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_stage1.py +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_streaming.py +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/tests/test_unsupported.py +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/rustfmt.toml +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/grammar.pest +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/src/types.rs +0 -0
- {nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/uv.lock +0 -0
{nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/.github/workflows/build-and-release.yml
RENAMED
|
@@ -43,7 +43,14 @@ jobs:
|
|
|
43
43
|
runs-on: ubuntu-latest
|
|
44
44
|
steps:
|
|
45
45
|
- uses: actions/checkout@v5
|
|
46
|
+
# Pin the toolchain and explicitly pull in rustfmt so `cargo fmt --check`
|
|
47
|
+
# does not depend on whatever components the runner image ships with.
|
|
48
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
49
|
+
with:
|
|
50
|
+
components: rustfmt
|
|
46
51
|
- uses: Swatinem/rust-cache@v2
|
|
52
|
+
- name: Check Rust formatting
|
|
53
|
+
run: cargo fmt --check
|
|
47
54
|
- name: Run Rust unit and integration tests
|
|
48
55
|
run: cargo test
|
|
49
56
|
|
|
@@ -4,20 +4,58 @@ Notable changes to **nc-gcode-interpreter**. The format loosely follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); versions are git tags,
|
|
5
5
|
released to PyPI.
|
|
6
6
|
|
|
7
|
-
## [v0.2.
|
|
7
|
+
## [v0.2.2] - 2026-07-07
|
|
8
8
|
|
|
9
|
-
###
|
|
9
|
+
### Added
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
- Variable-change events on the batch path: `nc_to_batches(...,
|
|
12
|
+
include_variables=True)` exposes, once exhausted, a sparse `variable_events`
|
|
13
|
+
DataFrame (`row_idx` / `name_id` / `value`) plus a `variable_names` list -
|
|
14
|
+
the batch-path twin of the per-row `variables` dict `nc_to_rows` already
|
|
15
|
+
yields. `row_idx` is the output-row index a change is seen at (a change on a
|
|
16
|
+
variable-only block is attributed to the next output row), so replaying the
|
|
17
|
+
events reconstructs the symbol table at any row. Off by default (no cost).
|
|
18
|
+
- `string_table` in the interpreter state dict: `DEF STRING` variables now
|
|
19
|
+
round-trip into `.state` / the `nc_to_dataframe` state tuple as
|
|
20
|
+
`state["string_table"]` (`dict[str, str]`), alongside the existing `axes` /
|
|
21
|
+
`symbol_table` / `translation` numeric tables (previously omitted).
|
|
22
|
+
|
|
23
|
+
## [v0.2.1] - 2026-07-07
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Optional `line_no` output column on the batch/dataframe path, enabled with
|
|
28
|
+
`include_line_numbers=True` on `nc_to_dataframe` / `nc_to_batches` (default
|
|
29
|
+
`False`, so the output schema is unchanged unless you ask for it). When
|
|
30
|
+
enabled it prepends a leading `Int64` column giving the 1-based source line
|
|
31
|
+
each output row came from - previously only the streaming `nc_to_rows`
|
|
32
|
+
exposed it. Loops repeat the value and jumps make it non-monotonic, matching
|
|
33
|
+
`nc_to_rows` row-for-row; flatten-generated samples keep the originating
|
|
34
|
+
block's line number; `dataframe_to_nc` ignores it (source provenance, not an
|
|
35
|
+
emittable word). Concatenating batches reconstructs the same per-row
|
|
36
|
+
`line_no` as the whole-file dataframe (#45)
|
|
37
|
+
- Structured error locations: parse/interpret failures now raise
|
|
38
|
+
`nc_gcode_interpreter.NcError` (a `ValueError` subclass, so existing
|
|
39
|
+
`except ValueError` keeps working) carrying the position as data - `.line`,
|
|
40
|
+
`.column` (syntax errors), `.context`, and `.line_text` attributes (each an
|
|
41
|
+
int / str or `None`) - so a caller (e.g. an editor) can locate the offending
|
|
42
|
+
token without regex-parsing the message. `str(err)` is unchanged
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- Arithmetic-function arity: added a regression test pinning that every
|
|
47
|
+
`SIN`/`COS`/`ATAN2`/`BOUND`/... call already validates its argument count
|
|
48
|
+
(`check_args`) before indexing `args[..]`, returning
|
|
49
|
+
`ParsingError::InvalidFunctionArity` instead of panicking on a wrong-arity
|
|
50
|
+
call (#16).
|
|
51
|
+
|
|
52
|
+
### Documented
|
|
53
|
+
|
|
54
|
+
- Pinned iterator-drop -> `ParsingError::StreamClosed` as the supported
|
|
55
|
+
cancel contract for `nc_to_rows`/`nc_to_batches` (README, docstrings) and
|
|
56
|
+
noted the release-profile `strip`/`lto` tradeoff in `Development.md` (#49).
|
|
57
|
+
|
|
58
|
+
## [v0.2.0] - 2026-07-07
|
|
21
59
|
|
|
22
60
|
### Added
|
|
23
61
|
|
|
@@ -95,6 +133,16 @@ released to PyPI.
|
|
|
95
133
|
|
|
96
134
|
### Changed
|
|
97
135
|
|
|
136
|
+
- Rust-side polars is gone. The Table -> Python DataFrame handoff no longer
|
|
137
|
+
builds a polars DataFrame in Rust (via `pyo3-polars`); it builds an Arrow
|
|
138
|
+
record batch with the minimal `arrow-array`/`arrow-schema`/`arrow-data`
|
|
139
|
+
crates and hands it to Python zero-copy through the Arrow PyCapsule
|
|
140
|
+
interface (`__arrow_c_array__`), where `pl.DataFrame(...)` wraps it. The
|
|
141
|
+
Python API is unchanged (still returns `polars.DataFrame`), needs no
|
|
142
|
+
`pyarrow`, and performance is unchanged. This drops ~60 crates from the
|
|
143
|
+
`python`-feature build (127 -> 64), cutting a clean release build ~4x
|
|
144
|
+
(83s -> 21s), and bumps PyO3 0.28 -> 0.29 (resolving the RUSTSEC pyo3
|
|
145
|
+
advisories).
|
|
98
146
|
- **Breaking:** `I`/`J`/`K`/`CR` are now treated as arc interpolation-parameter
|
|
99
147
|
block addresses (output columns), so they can no longer be used as user
|
|
100
148
|
variable names: `I=5` followed by `X=I+1` was a variable read before and is
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Development
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
Use Cargo to build and run the CLi tool:
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
cargo run -- --help
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
To compile the python module:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
maturin develop
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Setup python environment
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv venv -p 3.12
|
|
21
|
+
uv synv --all-extras
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## Release
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cargo build --release
|
|
30
|
+
maturin develop --release --uv
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Release profile
|
|
34
|
+
|
|
35
|
+
`[profile.release]` currently sets no `strip`/`lto` overrides. `strip = true` was
|
|
36
|
+
tried and dropped (`e1ff63a`): cargo runs the *host* `strip` binary even on a
|
|
37
|
+
cross-compiled artifact, so stripping the cross-built macOS x86_64 dylib with
|
|
38
|
+
the arm64 host `strip` produced a wheel PyPI rejected as "not a zipfile" -
|
|
39
|
+
stripping was reverted to unblock the release. It was a secondary size win
|
|
40
|
+
(mostly orthogonal to the polars removal that motivated it) and can be
|
|
41
|
+
revisited via maturin's cross-aware `--strip` flag instead of a Cargo
|
|
42
|
+
profile setting.
|
|
43
|
+
|
|
44
|
+
`lto` remains an untried, plausible speed knob (doesn't invoke an external
|
|
45
|
+
tool, so it isn't blocked by the cross-compile issue above) - just not yet
|
|
46
|
+
evaluated for its extra build time across the wheel matrix.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
## Super simple test
|
|
50
|
+
|
|
51
|
+
There are a bunch of csv files in the examples directory. To test the tool on all of them (use git to check changes)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
rm **/*.csv && cargo build --release && find examples -name "*.mpf" -type f -print0 | xargs -0 -I {} sh -c './target/release/nc-gcode-interpreter --axis-index-map E:4 --initial_state=examples/defaults.mpf "$1" || echo "Failed to process $1" >&2' sh {}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## python test
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
maturin develop --release --uv && pytest
|
|
61
|
+
```
|
|
@@ -161,6 +161,8 @@ for line_no, row, variables in nc_to_rows(program, include_variables=True):
|
|
|
161
161
|
|
|
162
162
|
Rows are typed and forward-filled like the batch DataFrame (disable with `forward_fill=False`); errors raise from `next()` at the offending row; after exhaustion the iterator's `state` attribute holds the final interpreter state. See `python/example/streaming.py` for a runnable version.
|
|
163
163
|
|
|
164
|
+
**Cancellation.** Dropping the iterator is the supported way to cancel a `nc_to_rows`/`nc_to_batches` run — leaving a `for` loop early or `del`-ing it drops it immediately; relying on garbage collection works too but only whenever the runtime happens to collect it, so it is not prompt on all interpreters: the background interpreter thread's next attempt to send a row hits a closed channel and returns internally via `ParsingError::StreamClosed`, so the thread unwinds promptly instead of running the rest of the program to completion. No explicit `.close()`/`.cancel()` call is needed or provided — drop is the whole contract, and downstreams (e.g. ribweaver's `/sim` live-edit reinterpret) rely on it for latest-wins cancellation.
|
|
165
|
+
|
|
164
166
|
Additionally, conversion from a Polars DataFrame back to an MPF (NC) program is also supported:
|
|
165
167
|
|
|
166
168
|
```bash
|
{nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/nc_gcode_interpreter/__init__.py
RENAMED
|
@@ -3,6 +3,7 @@ import os
|
|
|
3
3
|
import polars as pl
|
|
4
4
|
from ._internal import nc_to_rows as _nc_to_rows
|
|
5
5
|
from ._internal import nc_to_batches as _nc_to_batches
|
|
6
|
+
from ._internal import NcError
|
|
6
7
|
from ._internal import __doc__ # noqa: F401
|
|
7
8
|
import json
|
|
8
9
|
from pathlib import Path
|
|
@@ -21,6 +22,7 @@ __all__ = [
|
|
|
21
22
|
"nc_to_batches",
|
|
22
23
|
"sanitize_dataframe",
|
|
23
24
|
"dataframe_to_nc",
|
|
25
|
+
"NcError",
|
|
24
26
|
]
|
|
25
27
|
|
|
26
28
|
# Batch size nc_to_dataframe uses internally when draining the batch stream it
|
|
@@ -60,6 +62,7 @@ def nc_to_dataframe(
|
|
|
60
62
|
axis_index_map: dict[str, int] | None = None,
|
|
61
63
|
allow_undefined_variables: bool = False,
|
|
62
64
|
flatten_tolerance: float | None = None,
|
|
65
|
+
include_line_numbers: bool = False,
|
|
63
66
|
) -> tuple[pl.DataFrame, dict]:
|
|
64
67
|
"""
|
|
65
68
|
Parses Sinumerik-flavored NC G-code and converts it into a Polars DataFrame along with the final state.
|
|
@@ -93,6 +96,12 @@ def nc_to_dataframe(
|
|
|
93
96
|
deviation (in path units, i.e. mm) of the true curve. The interpolation
|
|
94
97
|
parameters (I/J/K/CR, PW/SD/PL) are consumed and do not appear in the
|
|
95
98
|
output. Default None (curves pass through untouched).
|
|
99
|
+
include_line_numbers: bool, optional
|
|
100
|
+
If True, prepend a `line_no` (Int64) column giving the 1-based source
|
|
101
|
+
line each output row came from - repeated under a loop, non-monotonic
|
|
102
|
+
across a jump, matching the streaming `nc_to_rows`. Never
|
|
103
|
+
forward-filled; `dataframe_to_nc` ignores it. Default False (column
|
|
104
|
+
absent, output schema unchanged).
|
|
96
105
|
|
|
97
106
|
Returns:
|
|
98
107
|
--------
|
|
@@ -142,6 +151,7 @@ def nc_to_dataframe(
|
|
|
142
151
|
allow_undefined_variables,
|
|
143
152
|
input_is_path,
|
|
144
153
|
flatten_tolerance,
|
|
154
|
+
include_line_numbers,
|
|
145
155
|
)
|
|
146
156
|
# pl.DataFrame wraps each Arrow record batch via __arrow_c_array__ (polars
|
|
147
157
|
# >= 1.3), no pyarrow needed. Exhaust the iterator before reading .state.
|
|
@@ -245,6 +255,13 @@ def sanitize_dataframe(
|
|
|
245
255
|
pl.DataFrame
|
|
246
256
|
The sanitized DataFrame ready for analysis or conversion back to G-code.
|
|
247
257
|
"""
|
|
258
|
+
# `line_no` (present only when interpreted with include_line_numbers=True)
|
|
259
|
+
# is source-provenance metadata, not an emittable G-code word; drop it up
|
|
260
|
+
# front so it is never cast/forward-filled or written back out by
|
|
261
|
+
# dataframe_to_nc.
|
|
262
|
+
if "line_no" in df.columns:
|
|
263
|
+
df = df.drop("line_no")
|
|
264
|
+
|
|
248
265
|
modal = [g["short_name"] for g in GGroups.g_groups if g["effectiveness"] == "modal"]
|
|
249
266
|
non_modal = [g["short_name"] for g in GGroups.g_groups if g["effectiveness"] != "modal"]
|
|
250
267
|
known_axes = [
|
|
@@ -415,6 +432,13 @@ def nc_to_rows(
|
|
|
415
432
|
an anonymous iterator drops it; a stored iterator keeps the worker
|
|
416
433
|
alive (parked on the bounded channel) until it is deleted or
|
|
417
434
|
garbage-collected.
|
|
435
|
+
|
|
436
|
+
Dropping the iterator is the **supported cancel contract**: the
|
|
437
|
+
background thread's next attempted send hits the now-closed channel and
|
|
438
|
+
returns internally via ``ParsingError::StreamClosed``, so it unwinds
|
|
439
|
+
promptly instead of finishing the rest of the program. There is no
|
|
440
|
+
separate ``.cancel()``/``.close()`` - drop is the whole mechanism.
|
|
441
|
+
|
|
418
442
|
Errors raise ``ValueError`` from ``next()`` when reached. After the
|
|
419
443
|
iterator is exhausted, its ``state`` attribute holds the final
|
|
420
444
|
interpreter state (axes, symbol_table, translation).
|
|
@@ -453,8 +477,9 @@ class _BatchIterator:
|
|
|
453
477
|
|
|
454
478
|
Wraps the Rust batch iterator, which yields each batch as an Arrow record
|
|
455
479
|
batch built column-wise in Rust and handed over the Arrow PyCapsule
|
|
456
|
-
interface (``__arrow_c_array__``,
|
|
457
|
-
|
|
480
|
+
interface (``__arrow_c_array__``, exported directly with the minimal
|
|
481
|
+
arrow-rs crates - no pyo3-arrow/pyo3-polars) - a zero-copy transfer with
|
|
482
|
+
no Python list of primitives materialized. ``pl.DataFrame`` wraps that
|
|
458
483
|
capsule directly (polars >= 1.3), so no ``pyarrow`` is involved. After
|
|
459
484
|
exhaustion its ``state`` attribute holds the final interpreter state (axes,
|
|
460
485
|
symbol_table, translation), like the iterator returned by :func:`nc_to_rows`.
|
|
@@ -473,6 +498,29 @@ class _BatchIterator:
|
|
|
473
498
|
def state(self) -> dict | None:
|
|
474
499
|
return self._inner.state
|
|
475
500
|
|
|
501
|
+
@property
|
|
502
|
+
def variable_events(self) -> "pl.DataFrame | None":
|
|
503
|
+
"""Sparse variable-change events (``include_variables=True``).
|
|
504
|
+
|
|
505
|
+
A :class:`polars.DataFrame` with columns ``row_idx`` (the output-row
|
|
506
|
+
index the change is seen at), ``name_id`` (index into
|
|
507
|
+
:attr:`variable_names`) and ``value``. ``None`` unless the iterator was
|
|
508
|
+
created with ``include_variables=True``. Available once exhausted.
|
|
509
|
+
Replaying the events in order (decoding ``name_id`` via
|
|
510
|
+
:attr:`variable_names`) reconstructs the symbol table at any row, the
|
|
511
|
+
batch-path twin of the per-row ``variables`` dict ``nc_to_rows`` yields.
|
|
512
|
+
"""
|
|
513
|
+
batch = self._inner.variable_events
|
|
514
|
+
return None if batch is None else pl.DataFrame(batch)
|
|
515
|
+
|
|
516
|
+
@property
|
|
517
|
+
def variable_names(self) -> list[str] | None:
|
|
518
|
+
"""Variable names ``variable_events.name_id`` indexes into.
|
|
519
|
+
|
|
520
|
+
``None`` unless ``include_variables=True``; available once exhausted.
|
|
521
|
+
"""
|
|
522
|
+
return self._inner.variable_names
|
|
523
|
+
|
|
476
524
|
|
|
477
525
|
def nc_to_batches(
|
|
478
526
|
input: "TextFileLike | str | os.PathLike",
|
|
@@ -485,6 +533,8 @@ def nc_to_batches(
|
|
|
485
533
|
axis_index_map: dict[str, int] | None = None,
|
|
486
534
|
allow_undefined_variables: bool = False,
|
|
487
535
|
flatten_tolerance: float | None = None,
|
|
536
|
+
include_line_numbers: bool = False,
|
|
537
|
+
include_variables: bool = False,
|
|
488
538
|
) -> _BatchIterator:
|
|
489
539
|
"""Interpret an NC program into a stream of columnar polars DataFrames.
|
|
490
540
|
|
|
@@ -504,6 +554,23 @@ def nc_to_batches(
|
|
|
504
554
|
The returned iterator exposes a ``state`` attribute holding the final
|
|
505
555
|
interpreter state once it is exhausted, like :func:`nc_to_rows`.
|
|
506
556
|
|
|
557
|
+
Like :func:`nc_to_rows`, this runs on a background thread behind a
|
|
558
|
+
bounded channel, and dropping the iterator is the supported way to
|
|
559
|
+
cancel: the next attempted send hits the closed channel and returns
|
|
560
|
+
internally via ``ParsingError::StreamClosed``, unwinding the thread
|
|
561
|
+
promptly instead of finishing the rest of the program.
|
|
562
|
+
|
|
563
|
+
With ``include_line_numbers=True`` every batch gains a leading ``line_no``
|
|
564
|
+
(Int64) column: the 1-based source line each row came from (repeated under
|
|
565
|
+
a loop, non-monotonic across a jump), never forward-filled. Default False
|
|
566
|
+
leaves the schema unchanged.
|
|
567
|
+
|
|
568
|
+
With ``include_variables=True`` the iterator additionally exposes
|
|
569
|
+
``variable_events`` (a sparse :class:`polars.DataFrame` of ``row_idx`` /
|
|
570
|
+
``name_id`` / ``value``) and ``variable_names`` once exhausted — the
|
|
571
|
+
batch-path equivalent of the per-row ``variables`` dict :func:`nc_to_rows`
|
|
572
|
+
yields; replaying the events reconstructs the symbol table at any row.
|
|
573
|
+
|
|
507
574
|
Example:
|
|
508
575
|
--------
|
|
509
576
|
>>> batches = nc_to_batches("G1 X10\\nX20 Y5", batch_size=1)
|
|
@@ -526,5 +593,7 @@ def nc_to_batches(
|
|
|
526
593
|
allow_undefined_variables,
|
|
527
594
|
input_is_path,
|
|
528
595
|
flatten_tolerance,
|
|
596
|
+
include_line_numbers,
|
|
597
|
+
include_variables,
|
|
529
598
|
)
|
|
530
599
|
return _BatchIterator(inner)
|
{nc_gcode_interpreter-0.2.0 → nc_gcode_interpreter-0.2.2}/python/nc_gcode_interpreter/_internal.pyi
RENAMED
|
@@ -5,6 +5,15 @@ from typing import Any, Iterator, Optional, List, Dict, Tuple
|
|
|
5
5
|
# wrapper imports are declared here. Signatures mirror the `#[pyo3(signature)]`
|
|
6
6
|
# defaults in src/lib.rs.
|
|
7
7
|
|
|
8
|
+
class NcError(ValueError):
|
|
9
|
+
"""NC parse/interpret error carrying structured location data. Subclasses
|
|
10
|
+
ValueError so `except ValueError` keeps working."""
|
|
11
|
+
|
|
12
|
+
line: Optional[int]
|
|
13
|
+
column: Optional[int]
|
|
14
|
+
context: Optional[str]
|
|
15
|
+
line_text: Optional[str]
|
|
16
|
+
|
|
8
17
|
def nc_to_rows(
|
|
9
18
|
input: str,
|
|
10
19
|
initial_state: Optional[str] = None,
|
|
@@ -33,8 +42,16 @@ def nc_to_batches(
|
|
|
33
42
|
allow_undefined_variables: bool = False,
|
|
34
43
|
input_is_path: bool = False,
|
|
35
44
|
flatten_tolerance: Optional[float] = None,
|
|
45
|
+
include_line_numbers: bool = False,
|
|
46
|
+
include_variables: bool = False,
|
|
36
47
|
) -> Any:
|
|
37
|
-
"""Interpret an NC program into an iterator of columnar polars DataFrames.
|
|
48
|
+
"""Interpret an NC program into an iterator of columnar polars DataFrames.
|
|
49
|
+
|
|
50
|
+
The returned iterator exposes ``state`` (dict with ``axes``,
|
|
51
|
+
``symbol_table``, ``translation`` and ``string_table``) once exhausted, and
|
|
52
|
+
- when ``include_variables`` is set - ``variable_events`` (an Arrow batch of
|
|
53
|
+
``row_idx`` / ``name_id`` / ``value``) and ``variable_names`` (list[str]).
|
|
54
|
+
"""
|
|
38
55
|
...
|
|
39
56
|
|
|
40
57
|
__all__ = ["nc_to_rows", "nc_to_batches"]
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Batch-path variable-change events (#46) + `string_table` in the state dict.
|
|
2
|
+
|
|
3
|
+
The streaming `nc_to_rows(include_variables=True)` path yields a per-row
|
|
4
|
+
`variables` dict; the batch path (`nc_to_batches` / `nc_to_dataframe`) now
|
|
5
|
+
surfaces the same information as a sparse side-table (`row_idx`, `name_id`,
|
|
6
|
+
`value`) plus a `variable_names` list. These tests pin (a) parity of the
|
|
7
|
+
ordered assignment sequence between the two paths and (b) that `DEF STRING`
|
|
8
|
+
variables round-trip into the returned state dict.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import polars as pl
|
|
12
|
+
|
|
13
|
+
from nc_gcode_interpreter import nc_to_batches, nc_to_dataframe, nc_to_rows
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _stream_sequence(program: str) -> list[tuple[str, float]]:
|
|
17
|
+
"""Flat, ordered `(name, value)` assignment sequence from the stream."""
|
|
18
|
+
return [
|
|
19
|
+
(name, value)
|
|
20
|
+
for _line, _row, variables in nc_to_rows(program, include_variables=True)
|
|
21
|
+
for name, value in variables.items()
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _batch_sequence(it) -> list[tuple[str, float]]:
|
|
26
|
+
"""Flat, ordered `(name, value)` sequence decoded from the batch events."""
|
|
27
|
+
events = it.variable_events
|
|
28
|
+
names = it.variable_names
|
|
29
|
+
return [
|
|
30
|
+
(names[name_id], value)
|
|
31
|
+
for name_id, value in zip(events["name_id"], events["value"])
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_batch_variable_events_match_stream_simple():
|
|
36
|
+
program = "\n".join(
|
|
37
|
+
[
|
|
38
|
+
"DEF REAL Q=2.5", # variable-only
|
|
39
|
+
"R1=0", # variable-only
|
|
40
|
+
"X=R1", # output row, no change
|
|
41
|
+
"X=Q", # output row, no change
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
it = nc_to_batches(program, include_variables=True)
|
|
45
|
+
list(it) # exhaust to populate variable_events / state
|
|
46
|
+
assert _batch_sequence(it) == _stream_sequence(program)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_batch_variable_events_match_stream_while_loop():
|
|
50
|
+
# A WHILE loop where R1 changes once per iteration: the per-iteration
|
|
51
|
+
# counter events must appear on the batch path exactly as they do on the
|
|
52
|
+
# stream, in the same order.
|
|
53
|
+
program = "\n".join(
|
|
54
|
+
[
|
|
55
|
+
"DEF REAL Q=2.5",
|
|
56
|
+
"R1=0",
|
|
57
|
+
"WHILE R1<3",
|
|
58
|
+
"X=R1 Q=Q*2", # output row that ALSO assigns a variable
|
|
59
|
+
"R1=R1+1", # variable-only, per iteration
|
|
60
|
+
"ENDWHILE",
|
|
61
|
+
"X=Q",
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
it = nc_to_batches(program, include_variables=True)
|
|
65
|
+
list(it)
|
|
66
|
+
assert _batch_sequence(it) == _stream_sequence(program)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_batch_variable_events_row_idx_reconstructs_symbol_table():
|
|
70
|
+
# Replaying every event reconstructs the final symbol table (minus the
|
|
71
|
+
# built-in TRUE/FALSE), mirroring the streaming accumulation invariant.
|
|
72
|
+
program = "\n".join(
|
|
73
|
+
[
|
|
74
|
+
"DEF REAL Q=1",
|
|
75
|
+
"R1=0",
|
|
76
|
+
"WHILE R1<3",
|
|
77
|
+
"X=R1 Q=Q*2",
|
|
78
|
+
"R1=R1+1",
|
|
79
|
+
"ENDWHILE",
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
it = nc_to_batches(program, include_variables=True)
|
|
83
|
+
list(it)
|
|
84
|
+
accumulated: dict[str, float] = {}
|
|
85
|
+
for name, value in _batch_sequence(it):
|
|
86
|
+
accumulated[name] = value
|
|
87
|
+
symbol_table = dict(it.state["symbol_table"])
|
|
88
|
+
for builtin in ("TRUE", "FALSE"):
|
|
89
|
+
symbol_table.pop(builtin)
|
|
90
|
+
assert accumulated == symbol_table
|
|
91
|
+
|
|
92
|
+
# row_idx is monotonic non-decreasing (events arrive in program order) and
|
|
93
|
+
# bounded by the number of output rows.
|
|
94
|
+
events = it.variable_events
|
|
95
|
+
row_idx = events["row_idx"].to_list()
|
|
96
|
+
assert row_idx == sorted(row_idx)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_batch_variable_events_row_idx_aligns_with_output_rows():
|
|
100
|
+
# A change on a variable-only block is attributed to the NEXT output row;
|
|
101
|
+
# a change on an output row gets that row's own index.
|
|
102
|
+
program = "\n".join(
|
|
103
|
+
[
|
|
104
|
+
"R1=0", # variable-only, before output row 0
|
|
105
|
+
"X=R1", # output row 0
|
|
106
|
+
"R1=1", # variable-only, before output row 1
|
|
107
|
+
"X=R1", # output row 1
|
|
108
|
+
]
|
|
109
|
+
)
|
|
110
|
+
it = nc_to_batches(program, include_variables=True)
|
|
111
|
+
list(it)
|
|
112
|
+
events = it.variable_events
|
|
113
|
+
names = it.variable_names
|
|
114
|
+
decoded = [
|
|
115
|
+
(row_idx, names[name_id], value)
|
|
116
|
+
for row_idx, name_id, value in zip(
|
|
117
|
+
events["row_idx"], events["name_id"], events["value"]
|
|
118
|
+
)
|
|
119
|
+
]
|
|
120
|
+
assert decoded == [(0, "R1", 0.0), (1, "R1", 1.0)]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_batch_variable_events_absent_without_flag():
|
|
124
|
+
it = nc_to_batches("R1=5\nX=R1")
|
|
125
|
+
list(it)
|
|
126
|
+
assert it.variable_events is None
|
|
127
|
+
assert it.variable_names is None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_batch_variable_events_empty_when_no_assignments():
|
|
131
|
+
it = nc_to_batches("G1 X10\nX20 Y5", include_variables=True)
|
|
132
|
+
list(it)
|
|
133
|
+
assert isinstance(it.variable_events, pl.DataFrame)
|
|
134
|
+
assert it.variable_events.height == 0
|
|
135
|
+
assert it.variable_names == []
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_string_table_round_trips_through_state_dict():
|
|
139
|
+
program = "\n".join(
|
|
140
|
+
[
|
|
141
|
+
"DEF STRING[16] MSG",
|
|
142
|
+
'MSG="HELLO WORLD"',
|
|
143
|
+
"DEF STRING[8] TAG",
|
|
144
|
+
'TAG="ABC"',
|
|
145
|
+
"G1 X1",
|
|
146
|
+
]
|
|
147
|
+
)
|
|
148
|
+
_df, state = nc_to_dataframe(program)
|
|
149
|
+
assert state["string_table"] == {"MSG": "HELLO WORLD", "TAG": "ABC"}
|
|
150
|
+
# The existing numeric sub-tables are untouched by the new key.
|
|
151
|
+
assert set(state) == {"axes", "symbol_table", "translation", "string_table"}
|
|
152
|
+
# The string variable never leaks into the numeric symbol table.
|
|
153
|
+
assert "MSG" not in state["symbol_table"]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_string_table_on_streaming_and_batch_iterators():
|
|
157
|
+
program = 'DEF STRING[8] MSG\nMSG="HI"\nG1 X1'
|
|
158
|
+
rows = nc_to_rows(program)
|
|
159
|
+
list(rows)
|
|
160
|
+
assert rows.state["string_table"] == {"MSG": "HI"}
|
|
161
|
+
|
|
162
|
+
it = nc_to_batches(program)
|
|
163
|
+
list(it)
|
|
164
|
+
assert it.state["string_table"] == {"MSG": "HI"}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_line_numbers_and_variables_compose_on_the_batch_path():
|
|
168
|
+
# include_line_numbers (#45) and include_variables (#46) share the batch
|
|
169
|
+
# path and were merged independently; requesting both at once must give the
|
|
170
|
+
# leading `line_no` column AND the sparse variable-event side-table, with
|
|
171
|
+
# `row_idx` still aligned to the emitted (line_no-carrying) output rows.
|
|
172
|
+
program = "\n".join(
|
|
173
|
+
[
|
|
174
|
+
"R1=0", # variable-only, before output row 0
|
|
175
|
+
"WHILE R1<3",
|
|
176
|
+
"X=R1", # output row (loop body, source line 3)
|
|
177
|
+
"R1=R1+1", # variable-only
|
|
178
|
+
"ENDWHILE",
|
|
179
|
+
"X9", # output row, source line 6
|
|
180
|
+
]
|
|
181
|
+
)
|
|
182
|
+
it = nc_to_batches(
|
|
183
|
+
program,
|
|
184
|
+
batch_size=1_000_000,
|
|
185
|
+
include_line_numbers=True,
|
|
186
|
+
include_variables=True,
|
|
187
|
+
)
|
|
188
|
+
df = pl.concat(list(it), how="diagonal")
|
|
189
|
+
|
|
190
|
+
# line_no leads and tracks source lines (loop body repeats line 3).
|
|
191
|
+
assert df.columns[0] == "line_no"
|
|
192
|
+
assert df["line_no"].to_list() == [3, 3, 3, 6]
|
|
193
|
+
assert df["X"].to_list() == [0.0, 1.0, 2.0, 9.0]
|
|
194
|
+
|
|
195
|
+
# Variable events still decode independently of the line_no column.
|
|
196
|
+
names = it.variable_names
|
|
197
|
+
decoded = [
|
|
198
|
+
(row_idx, names[name_id], value)
|
|
199
|
+
for row_idx, name_id, value in zip(
|
|
200
|
+
it.variable_events["row_idx"],
|
|
201
|
+
it.variable_events["name_id"],
|
|
202
|
+
it.variable_events["value"],
|
|
203
|
+
)
|
|
204
|
+
]
|
|
205
|
+
assert decoded == [(0, "R1", 0.0), (1, "R1", 1.0), (2, "R1", 2.0), (3, "R1", 3.0)]
|
|
@@ -155,3 +155,56 @@ def test_path_input_batches(tmp_path):
|
|
|
155
155
|
df, _ = nc_to_dataframe(program)
|
|
156
156
|
frames = list(nc_to_batches(mpf, batch_size=2))
|
|
157
157
|
assert_frame_equal(pl.concat(frames, how="diagonal").select(df.columns), df)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_include_line_numbers_is_opt_in():
|
|
161
|
+
"""`line_no` is off by default and, when enabled, is a leading Int64 column
|
|
162
|
+
that repeats under a loop and is non-monotonic across a jump - matching the
|
|
163
|
+
streaming `nc_to_rows` row-for-row and reconstructible across batches."""
|
|
164
|
+
prog = "R1=0\nWHILE R1<2\nX=R1 F100\nR1=R1+1\nENDWHILE\nX9\n"
|
|
165
|
+
|
|
166
|
+
# Default off: schema unchanged, no line_no column anywhere.
|
|
167
|
+
df_off, _ = nc_to_dataframe(prog)
|
|
168
|
+
assert "line_no" not in df_off.columns
|
|
169
|
+
off_batches = list(nc_to_batches(prog, batch_size=1))
|
|
170
|
+
assert all("line_no" not in f.columns for f in off_batches)
|
|
171
|
+
|
|
172
|
+
# Opt-in: leading Int64 line_no with the loop's repeated source lines.
|
|
173
|
+
df_on, _ = nc_to_dataframe(prog, include_line_numbers=True)
|
|
174
|
+
assert df_on.columns[0] == "line_no"
|
|
175
|
+
assert df_on["line_no"].dtype == pl.Int64
|
|
176
|
+
assert df_on["line_no"].to_list() == [3, 3, 6]
|
|
177
|
+
# Dropping line_no reproduces the default-off frame exactly.
|
|
178
|
+
assert_frame_equal(df_on.drop("line_no"), df_off)
|
|
179
|
+
|
|
180
|
+
# Batches carry it too and concatenate back to the whole-file column.
|
|
181
|
+
on_batches = list(nc_to_batches(prog, batch_size=1, include_line_numbers=True))
|
|
182
|
+
assert pl.concat(on_batches, how="diagonal")["line_no"].to_list() == [3, 3, 6]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_include_line_numbers_matches_streaming_under_jumps():
|
|
186
|
+
"""A GOTO reorders execution; the batch `line_no` column must match what
|
|
187
|
+
the streaming `nc_to_rows` yields, line for line."""
|
|
188
|
+
from nc_gcode_interpreter import nc_to_rows
|
|
189
|
+
|
|
190
|
+
prog = "N10 X1\nGOTOF 40\nN30 X999\nN40 X4\n"
|
|
191
|
+
df, _ = nc_to_dataframe(prog, include_line_numbers=True)
|
|
192
|
+
streamed = [line_no for line_no, _row in nc_to_rows(prog)]
|
|
193
|
+
assert df["line_no"].to_list() == streamed == [1, 4]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_include_line_numbers_survives_flattening():
|
|
197
|
+
"""Flatten-generated samples keep the originating block's source line, so a
|
|
198
|
+
flattened arc/spline block yields many rows all carrying its line_no - and
|
|
199
|
+
still matches the streaming path row-for-row."""
|
|
200
|
+
from nc_gcode_interpreter import nc_to_rows
|
|
201
|
+
|
|
202
|
+
program = "G1 X0 Y0 F1000\nG2 X100 Y0 I50 J0\nG1 X100 Y50\n"
|
|
203
|
+
df, _ = nc_to_dataframe(program, flatten_tolerance=0.1, include_line_numbers=True)
|
|
204
|
+
assert df.columns[0] == "line_no"
|
|
205
|
+
assert df["line_no"].null_count() == 0
|
|
206
|
+
per_line = df["line_no"].to_list()
|
|
207
|
+
# The arc on line 2 expands to many G1 samples, every one tagged line 2.
|
|
208
|
+
assert per_line.count(2) > 5
|
|
209
|
+
stream_lines = [line for line, *_ in nc_to_rows(program, flatten_tolerance=0.1)]
|
|
210
|
+
assert per_line == stream_lines
|