Skip to content

Add unified root-solving batch workflow#53

Merged
yilibinbin merged 10 commits into
mainfrom
codex/root-solving-module
Jun 2, 2026
Merged

Add unified root-solving batch workflow#53
yilibinbin merged 10 commits into
mainfrom
codex/root-solving-module

Conversation

@yilibinbin

@yilibinbin yilibinbin commented Jun 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add the unified root-solving batch path on top of the shared computation input layer.
  • Connect desktop root solving to active data rows, result snapshots, CSV output, and workspace restore behavior.
  • Harden scalar and multi-root solving with scan configuration, high-precision data-cell preservation, and shared symbol validation.

Test plan

  • PYTHONPATH=. pytest -q tests/test_shared_computation_inputs.py tests/test_root_solving_normalization.py tests/test_root_solving_expression.py tests/test_root_solving_solver.py tests/test_root_solving_uncertainty.py tests/test_root_solving_batch.py tests/test_root_solving_formatting.py
  • QT_QPA_PLATFORM=offscreen PYTHONPATH=. pytest -q tests/test_app_desktop_workers_core.py tests/test_desktop_root_solving_ui.py tests/test_desktop_implicit_model_ui.py
  • ruff check shared/computation_inputs.py root_solving app_desktop/panels.py app_desktop/window.py app_desktop/window_extrapolation_mixin.py app_desktop/workers_core.py app_desktop/workers_qt.py tests/test_shared_computation_inputs.py tests/test_root_solving_normalization.py tests/test_root_solving_expression.py tests/test_root_solving_solver.py tests/test_root_solving_uncertainty.py tests/test_root_solving_batch.py tests/test_root_solving_formatting.py tests/test_app_desktop_workers_core.py tests/test_desktop_root_solving_ui.py tests/test_desktop_implicit_model_ui.py
  • python3 -m compileall -q shared root_solving app_desktop tests
  • QT_QPA_PLATFORM=offscreen PYTHONPATH=. pytest -q -x $(git ls-files 'tests/*.py') -> 1348 passed, 26 skipped, 1 warning

Reviews

  • Local implementation review: PASS
  • Gemini focused adversarial re-review: PASS
  • Claude focused adversarial re-review: PASS

Summary by CodeRabbit

New Features

  • Added root-solving mode for solving equations with unknowns across single or batch data rows
  • Results rendered as markdown tables with CSV export capability
  • Linear uncertainty propagation for computed roots
  • Workspace save and restore for root-solving configurations

Tests

  • Comprehensive test coverage added for root-solving functionality

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@yilibinbin, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 29 minutes and 20 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0211359b-4635-46d4-afbe-32181921f98f

📥 Commits

Reviewing files that changed from the base of the PR and between 46d4f82 and 6013c68.

📒 Files selected for processing (13)
  • app_desktop/window.py
  • app_desktop/window_extrapolation_mixin.py
  • app_desktop/workspace_controller.py
  • root_solving/normalization.py
  • root_solving/solver.py
  • shared/computation_inputs.py
  • tests/test_desktop_root_solving_ui.py
  • tests/test_root_solving_expression.py
  • tests/test_root_solving_normalization.py
  • tests/test_root_solving_solver.py
  • tests/test_root_solving_uncertainty.py
  • tests/test_shared_computation_inputs.py
  • tests/test_workspace_controller.py
📝 Walkthrough

Walkthrough

This PR introduces a complete root-solving feature to the DataLab application. It adds immutable data models for root problems and results, comprehensive input normalization with symbol classification, symbolic/numeric expression evaluation supporting SciPy/mpmath solvers across multiple modes, linear uncertainty propagation, batch row processing, reusable Qt DetectedRowsTable components, end-to-end desktop UI integration, worker thread orchestration with subprocess execution, and workspace persistence. Tests provide extensive coverage of all major code paths.

Changes

Root-Solving Feature Implementation

Layer / File(s) Summary
Data Models and Input Validation
root_solving/models.py, root_solving/normalization.py, shared/computation_inputs.py, shared/expression_names.py, shared/input_normalization.py
Frozen dataclasses for root problems, unknowns, results, and scan configuration; normalization functions validate equations, unknowns, and constants with identifier/reserved-name checks; symbol classification detects name collisions and missing symbols.
Expression System and Root-Solving Algorithms
root_solving/expression.py, root_solving/solver.py
Symbolic/numeric expression evaluation with finite-result validation and derivative computation; solvers use SciPy (precision ≤ 16), mpmath (high precision), polynomial mode (coefficient extraction via SymPy), and scan-multiple (interval sampling with sign-change bracketing and absolute-minimum refinement).
Uncertainty Propagation and Batch Processing
root_solving/uncertainty.py, root_solving/batch.py, root_solving/formatting.py
Linear uncertainty attachment via Jacobian-based sensitivity analysis for scalar and system modes; batch orchestration normalizes and solves per-row problems, accumulating results and warnings; markdown and CSV rendering with source-header collision detection.
Qt Detected-Rows Widget
app_desktop/detected_rows_table.py
Reusable DetectedRowsController and DetectedRowsTable components manage detected vs manual row entries with source tracking, change signaling, and orphan handling via set_detected_names(keep_orphans).
Parameter Table Integration
app_desktop/parameter_table.py
Integrates DetectedRowsController for unified row-source tracking; detected-row refresh delegates to controller with orphan removal; UI item changes mark rows as manual.
Root-Solving UI Panel
app_desktop/panels.py
New left-panel section with root equations editor, solve-mode combo, unknown detection controls, DetectedRowsTable, and constants editor; adds _open_root_formula_preview, _add_detected_rows_table_row, and _remove_detected_rows_table_rows helpers.
Main Window Job Building
app_desktop/window.py
Constructs RootSolvingJob from UI state; extracts data headers, detects unknown symbols excluding constants/data, parses root data sources (CSV/whitespace), builds constants configuration; displays results with markdown rendering and CSV export; workspace dirty tracking for root controls.
Workspace Persistence
app_desktop/workspace_controller.py
Captures root equations, mode, unknown rows with source, and constants editor state; restores configuration to UI from manifest.
Worker Execution and Orchestration
app_desktop/workers_core.py, app_desktop/workers_qt.py, app_desktop/window_extrapolation_mixin.py
RootSolvingJob serialization with legacy payload migration; subprocess execution via KillableProcessTaskRunner with timeout and cancellation; RootSolvingWorker Qt thread emits success/failure/cancel signals; _run_root_solving_mode orchestrates job construction, worker lifecycle, result display, and worker cleanup on completion.
Package Configuration
pyproject.toml, root_solving/__init__.py
Build system includes root_solving* in package discovery; package exports core types and solver functions via __all__.
Comprehensive Tests
tests/test_root_solving_*.py, tests/test_shared_computation_inputs.py, tests/test_desktop_*.py, tests/test_workspace_controller.py
100+ test cases covering model serialization, expression evaluation, solver modes/backends, uncertainty propagation, batch processing, formatting, UI integration, and workspace round-trip persistence.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • yilibinbin/DataLab#47: Modifies root-worker lifecycle handling in WindowExtrapolationMixin, complementary to worker management changes in this PR.

Poem

🐰 With roots to solve and workers to thread,
Through matrices of symbol-spread,
We batch, we solve, we format neat—
A mathematical feat
DataLab now dances to the beat!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/root-solving-module

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (3)
root_solving/solver.py (1)

203-212: 💤 Low value

Minor: Unused variable center_x.

The unpacked variable center_x is never used. Consider prefixing with underscore to indicate it's intentionally unused.

♻️ Suggested fix
-        for left, center, right in zip(samples, samples[1:], samples[2:], strict=False):
-            left_x, left_y = left
-            center_x, center_y = center
-            right_x, right_y = right
+        for left, center, right in zip(samples, samples[1:], samples[2:], strict=False):
+            left_x, left_y = left
+            _center_x, center_y = center
+            right_x, right_y = right
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@root_solving/solver.py` around lines 203 - 212, The loop unpacks center_x
which is never used; change the unused variable name to _center_x in the for
left, center, right in zip(...) unpack (i.e., replace center_x with _center_x)
to signal intentional unused value and avoid linter warnings, keeping the rest
of the logic (center_y, left_x, left_y, right_x, right_y) unchanged and ensuring
no other references to center_x exist in this scope.
tests/test_workspace_controller.py (1)

253-267: ⚡ Quick win

Assert the restored constants numeric mode too.

The manifest assertion covers "numeric_mode": "uncertainty", but the round-trip check never verifies that root_constants_editor restored that mode. A restore regression here would still pass while changing how later constant edits are parsed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_workspace_controller.py` around lines 253 - 267, Add an assertion
that the constants numeric mode from the manifest ("numeric_mode":
"uncertainty") was restored on the UI: after the existing root_constants_editor
checks in the test (around restore_workspace and
root_constants_editor.raw_text()), assert the editor's numeric-mode accessor
(e.g., root_constants_editor.numeric_mode(),
root_constants_editor.get_numeric_mode(), or
root_constants_editor.is_uncertainty_mode()) indicates "uncertainty"/uncertainty
mode so the round-trip verifies the numeric mode is preserved.
tests/test_root_solving_uncertainty.py (1)

160-164: ⚡ Quick win

Avoid claiming boundary “just above/below” flakiness from mp.nstr(..., 60) here
_diagonal_condition_problem truncates alpha and initial_y to 60 digits (lines 162-163), but the two threshold tests choose alpha so the resulting jacobian_condition lands at 0.5 * (1/sqrt(mp.eps)) vs 2 * (1/sqrt(mp.eps)) (large separation). With that gap, 60-digit formatting won’t realistically flip the </> assertions, so the major flakiness risk described isn’t supported for this setup. If these tests were tightened to a truly “near-boundary” construction, increasing the emitted digits (e.g., based on precision) would be worthwhile.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_root_solving_uncertainty.py` around lines 160 - 164, The test
helper _diagonal_condition_problem currently truncates alpha and initial_y with
a fixed 60-digit mp.nstr which the reviewer says is misleading for
“near-boundary” flakiness; change the formatting to emit digits based on the
test precision (e.g. use mp.nstr(alpha, precision+10) and mp.nstr(initial_y,
precision+10) or max(60, precision+10)) so the stringified values reflect the
requested precision when constructing the RootProblem (update the two
mp.nstr(...) calls in _diagonal_condition_problem).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app_desktop/window_extrapolation_mixin.py`:
- Around line 473-498: _on_root_solving_finished currently only replaces
markdown/CSV and can leave previous plots and export state visible; update it to
clear any stale plot/display/export artifacts when showing root-solving results
by invoking the appropriate cleanup before setting the result (e.g. call a
plot/image cleanup helper such as self._reset_plot_state() or
self._clear_image_tab(), and clear any cached export state like
self._reset_export_state()); ensure these cleanup calls occur alongside the
existing self._set_result_text(...) / self._reset_csv_data() paths so the image
tab and export cache cannot show previous run artifacts.

In `@app_desktop/window.py`:
- Around line 1385-1387: The parser currently returns (), () when there are no
data rows which drops headers and causes header-only columns to be misclassified
as unknowns; change the no-rows branch to return the preserved headers and an
empty rows tuple (i.e., return headers, () rather than return (), ()) so callers
like _refresh_root_unknown_rows() keep column names while treating rows as
empty; locate the branch that checks "if not rows:" and update its return
accordingly.

In `@app_desktop/workspace_controller.py`:
- Around line 569-590: The early return in _restore_root_config when config is
not a dict leaves stale UI state; replace it with logic that explicitly clears
the root-solving controls before returning: call _set_text on getattr(window,
"root_equations_edit", None) with "", call _set_combo_data on getattr(window,
"root_mode_combo", None) with "auto", clear rows on getattr(window,
"root_unknowns_table", None) by calling set_rows([]) if the table has set_rows,
and call _restore_constants_editor_state(getattr(window,
"root_constants_editor", None), None); then return. Ensure these actions are
applied only when window attributes exist (use getattr checks) and keep the rest
of _restore_root_config behavior unchanged for valid dict configs.

In `@root_solving/normalization.py`:
- Line 57: The call in normalize_root_problem only invokes
_validate_scope(unknowns=unknowns, known_values=known_values,
constants=constants) and thus misses validating clean_equations for undeclared
symbols; update normalize_root_problem to validate the cleaned/normalized
equations the same way the context-based branch does: pass clean_equations (or
its normalized form) into the scope validation routine or call the same
validation helper used in the Lines 93-101 branch so that duplicate/reserved
names and undeclared symbols referenced in clean_equations are caught during
normalization (refer to _validate_scope, normalize_root_problem, and the
variable clean_equations to locate and apply the change).

In `@shared/computation_inputs.py`:
- Around line 206-210: build_data_rows currently accepts any non-empty header
but later extract_expression_symbols only recognizes identifier-shaped tokens,
causing headers like "mass (kg)" to later fail; update the header validation
where clean_headers is created to reject any header that does not match the
identifier rule used for unknowns/constants (e.g. the same isidentifier or regex
test used elsewhere), raising the existing ValueError message on invalid
headers; reference build_data_rows(), clean_headers, and
extract_expression_symbols in the change so the validation logic matches the
tokenization rules.

In `@tests/test_desktop_root_solving_ui.py`:
- Around line 8-13: The test imports from PySide6 (from
PySide6.QtCore/QtWidgets) before calling pytest.importorskip("PySide6"), causing
import-time errors in environments without PySide6; move the
pytest.importorskip("PySide6") call (and keep pytest.importorskip("pytestqt")
before any related imports) to the top of the file so the importorskip checks
run before any `from PySide6...` imports, ensuring the test is skipped instead
of failing during collection.

In `@tests/test_root_solving_expression.py`:
- Line 155: The test uses pytest.raises(...) with a regex in the match argument
that contains an unescaped metacharacter; update the pytest.raises call (the
match= argument in the with pytest.raises(...) context) to supply a raw string
literal for the pattern by prefixing the existing string with r so the regex is
treated correctly (e.g., change the match argument on the pytest.raises call to
use a raw string).

In `@tests/test_root_solving_normalization.py`:
- Line 96: Change the three pytest.raises(...) calls that pass regex strings to
use raw string literals so Ruff RUF043 is satisfied; e.g. replace the call
containing with pytest.raises(ValueError, match="Duplicate unknown|未知量重复") with
with pytest.raises(ValueError, match=r"Duplicate unknown|未知量重复"), and do the
same conversion to raw strings for the other two occurrences in the same file
(the pytest.raises(...) at the other reported locations).

In `@tests/test_root_solving_solver.py`:
- Line 174: The pytest assertion uses a regex pattern containing '|' so update
the pytest.raises call to pass a raw string for the match argument (i.e., change
match="scan|single|scalar" to match=r"scan|single|scalar") to avoid RUF043;
locate the pytest.raises(...) invocation in tests/test_root_solving_solver.py
(the test using pytest.raises(ValueError, match=...)) and prefix the match
string with r.

In `@tests/test_shared_computation_inputs.py`:
- Line 28: Update the pytest.raises calls that pass regex patterns as plain
strings to use raw string literals; specifically change the four occurrences in
tests/test_shared_computation_inputs.py (the pytest.raises(..., match=...) calls
at the locations flagged) to use match=r"collision|冲突" (and similarly prefix the
other three pattern strings with r) so the backslashes and regex metacharacters
are interpreted correctly by pytest; locate these in the test functions calling
pytest.raises and replace the match argument values to raw-string form.

---

Nitpick comments:
In `@root_solving/solver.py`:
- Around line 203-212: The loop unpacks center_x which is never used; change the
unused variable name to _center_x in the for left, center, right in zip(...)
unpack (i.e., replace center_x with _center_x) to signal intentional unused
value and avoid linter warnings, keeping the rest of the logic (center_y,
left_x, left_y, right_x, right_y) unchanged and ensuring no other references to
center_x exist in this scope.

In `@tests/test_root_solving_uncertainty.py`:
- Around line 160-164: The test helper _diagonal_condition_problem currently
truncates alpha and initial_y with a fixed 60-digit mp.nstr which the reviewer
says is misleading for “near-boundary” flakiness; change the formatting to emit
digits based on the test precision (e.g. use mp.nstr(alpha, precision+10) and
mp.nstr(initial_y, precision+10) or max(60, precision+10)) so the stringified
values reflect the requested precision when constructing the RootProblem (update
the two mp.nstr(...) calls in _diagonal_condition_problem).

In `@tests/test_workspace_controller.py`:
- Around line 253-267: Add an assertion that the constants numeric mode from the
manifest ("numeric_mode": "uncertainty") was restored on the UI: after the
existing root_constants_editor checks in the test (around restore_workspace and
root_constants_editor.raw_text()), assert the editor's numeric-mode accessor
(e.g., root_constants_editor.numeric_mode(),
root_constants_editor.get_numeric_mode(), or
root_constants_editor.is_uncertainty_mode()) indicates "uncertainty"/uncertainty
mode so the round-trip verifies the numeric mode is preserved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 082a6eae-a033-44e1-923a-ef5e98a73c60

📥 Commits

Reviewing files that changed from the base of the PR and between 39c9630 and 46d4f82.

📒 Files selected for processing (31)
  • app_desktop/detected_rows_table.py
  • app_desktop/panels.py
  • app_desktop/parameter_table.py
  • app_desktop/window.py
  • app_desktop/window_extrapolation_mixin.py
  • app_desktop/workers_core.py
  • app_desktop/workers_qt.py
  • app_desktop/workspace_controller.py
  • pyproject.toml
  • root_solving/__init__.py
  • root_solving/batch.py
  • root_solving/expression.py
  • root_solving/formatting.py
  • root_solving/models.py
  • root_solving/normalization.py
  • root_solving/solver.py
  • root_solving/uncertainty.py
  • shared/computation_inputs.py
  • shared/expression_names.py
  • shared/input_normalization.py
  • tests/test_app_desktop_workers_core.py
  • tests/test_desktop_implicit_model_ui.py
  • tests/test_desktop_root_solving_ui.py
  • tests/test_root_solving_batch.py
  • tests/test_root_solving_expression.py
  • tests/test_root_solving_formatting.py
  • tests/test_root_solving_normalization.py
  • tests/test_root_solving_solver.py
  • tests/test_root_solving_uncertainty.py
  • tests/test_shared_computation_inputs.py
  • tests/test_workspace_controller.py

Comment thread app_desktop/window_extrapolation_mixin.py
Comment thread app_desktop/window.py
Comment thread app_desktop/workspace_controller.py
Comment thread root_solving/normalization.py Outdated
Comment thread shared/computation_inputs.py
Comment thread tests/test_desktop_root_solving_ui.py
Comment thread tests/test_root_solving_expression.py Outdated
Comment thread tests/test_root_solving_normalization.py Outdated
Comment thread tests/test_root_solving_solver.py Outdated
Comment thread tests/test_shared_computation_inputs.py Outdated
@yilibinbin
yilibinbin merged commit 811b6b2 into main Jun 2, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants