Skip to content

feat: arithmetic of signals, and sources relative to the first - #16

Merged
icyspirit merged 2 commits into
mainfrom
feat/path-arithmetic
Sep 29, 2026
Merged

icyspirit merged 2 commits into
mainfrom
feat/path-arithmetic

Conversation

@icyspirit

@icyspirit icyspirit commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Summary

Arithmetic of signals. A signal can be computed from other signals and from numbers:

  • the operators +, -, * and /, with the usual precedence and parentheses;
  • deriv(x): d/dt for time traces, d/dx for profiles (in the order of x);
  • abs(x), and norm(x), which now also takes an expression.
equilibrium/time_slice/global_quantities/ip - summary/global_quantities/ip/value
core_profiles/profiles_1d/t_i_average / core_profiles/profiles_1d/electrons/temperature
core_profiles/profiles_1d/electrons/temperature / core_profiles/profiles_1d/electrons/temperature[0]
deriv(summary/global_quantities/ip/value) * 1e-6
abs(pf_active/coil[:]/current/data)
  • Division: / also separates the nodes of a path, so a division has spaces around it (a / b). A / right after a path, with no space, belongs to the path. Plain paths parse as before, including norm(), transpose(), Fortran indices, [-1] and odd spacing.
  • Alignment: two signals are combined on the times and the grid of the first.
    • For traces, the second is interpolated in time, and is NaN outside its range. A single sample counts only at its own time.
    • For profiles and maps, the second is taken at its closest time slice within its time range and interpolated onto the grid of the first. Maps use a separable bilinear interpolation.
    • A time trace applies to profiles and maps at their times, e.g. a profile divided by its central value.
    • A static profile or map combined with a time trace becomes time-dependent.
    • Profiles against different coordinates are an error.
  • Wildcards: signals combine member by member, by the names both have, or in order when all members are renamed; or each member is combined with the same signal.
  • Labels and units: the result is labeled with the expression. The units follow:
    • those of a sum or difference;
    • none for a ratio of like quantities;
    • a/b and a.b for other quotients and products;
    • per second for deriv of a trace.
  • Evaluation: a data source evaluates an expression from its cached signals and caches the result.
    • Missing data in any operand is still a gray note, not an error.
    • Alternatives (a - b | c) can be expressions.

Sources relative to the first. A panel can show every source but the first relative to the first, signal by signal:

  • relative: difference shows B − A, and relative: ratio shows B / A, using the same arithmetic.
  • It is set in three places: the configuration, a sources choice in the setup dialog, and a Sources submenu in the panel's context menu (undoable).
  • The first source is then not drawn, and the legend says what is compared ([B − A]). The y and colorbar titles say Δ … or … ratio.
  • The scale of a signal still applies to a difference. The offset cancels out, so it does not apply, and a ratio has neither.
  • Shapes cannot be compared: a map of B − A keeps each source's boundary, and the line-style legend then names those sources (A, B).
  • A single source is shown as it is.
  • The crosshair of the Point mode reads what is drawn, so it gives B − A or B / A.

Docs: the README (signal paths, panel comparison, configuration keys), the path tooltip in the setup dialog, and the module documentation.

Notes

  • The expression layer (idscope.data.expressions) sits on top of SignalPath:
    • parse_alternatives returns SignalPaths for plain paths, as before, and Expressions otherwise.
    • An Expression stands wherever a path does (text, label, name, canonical()).
    • DataSource.signal evaluates it.
  • Parsing tries the expression grammar first. If that fails, it falls back to the old path parser, so paths written with spaces inside still work. When both fail, the more relevant error is reported: the expression's when the text looks like an expression, e.g. "unknown function 'sqrt' (available: …)".
  • transpose() must still take a path: it reorders dimensions before the data is extracted.
  • No new dependency: interpolation uses numpy only.
  • Main merged in (the crosshair of feat: a crosshair with the values under the mouse in Point mode #14 and the device template of feat: a device template, with vessel layers and conductors as shapes #15). The conflicts were only side by side: imports, the panel's attributes, and tests appended at the same place.

Test plan

  • QT_QPA_PLATFORM=offscreen pytest: 228 passed in the dev environment, a clean PySide6 6.11 environment and a PyQt5 5.15 environment, after merging main. The clean installs were rebuilt without a stale build/ directory.
  • New tests:
    • parsing:
      • plain paths stay paths, including norm(), transpose(), Fortran indices and spaces;
      • precedence and parentheses in the labels;
      • a/b vs a / b;
      • [-1] is not a subtraction;
      • canonical forms;
      • alternatives;
      • errors: trailing operator, unbalanced brackets, unknown function, transpose of an expression, **, a bare IDS name, a number alone;
    • on the demo: traces, units, deriv, abs, the cache, profiles (ratio, over the central value, deriv, norm of an expression, different coordinates), maps, members, shapes refused, and what a source does not have (note, and fallback to the next alternative);
    • arithmetic, on synthetic data: interpolation and time ranges, constants and single samples, profiles on grids and closest slices, a static profile times a trace, maps on another grid and at the times of a trace, members by name, renamed or disjoint, derivatives with repeated or NaN times and decreasing x, relative_to;
    • configuration: round trip and invalid value;
    • GUI:
      • expressions in a panel, with the red error for an unknown function;
      • difference, ratio and map difference, each with its names, values, scale and offset, legend and colorbar title;
      • the crosshair on a difference (Ip (equilibrium) [B − A] = -0.2);
      • the Sources menu, and undo;
      • a single source shown as it is;
      • the setup dialog choice.
  • Screenshot checked: B − A, B / A, the equilibrium Ip minus the summary Ip, dIp/dt, Te/Te(0), and a psi map difference with both boundaries.
  • pyflakes src tests reports nothing.

🤖 Generated with Claude Code

https://claude.ai/code/session_018aVekYzCRLPE71Mm6UH8Zp


Generated by Claude Code

A signal can be computed from others and from numbers: +, -, * and / with the
usual precedence and parentheses, and deriv(), abs() and norm() of an
expression. Since "/" also separates the nodes of a path, a division has spaces
around it ("a / b"); a "/" right after a path, with no space, is the path's.

- Two signals are combined on the times and the grid of the first: the second
  is interpolated in time for traces, and taken at its closest time slice
  (within its time range, a single one only at its time) and interpolated on
  the grid of the first for profiles and maps. A time trace applies to profiles
  and maps at their times (a profile over its central value); a static profile
  or map times a time trace depends on time. Profiles against different
  coordinates cannot be combined.
- Signals with wildcards combine member by member, by name (those both have),
  in order when all are renamed, or each member with the same signal.
- deriv() is d/dt for traces and d/dx for profiles, in the order of x.
- The result is labeled with the expression, with units that follow: those of
  a difference, none for a ratio of like quantities, a/b, a.b, per second.
- A data source evaluates an expression from its cached signals, and caches the
  result; what a source does not have is still a note, not an error.
  Alternatives can be expressions. Plain paths parse as before.

A panel can show every source but the first relative to the first, signal by
signal: its difference (B − A) or its ratio (B / A), with the same arithmetic.
It is `relative:` in the configuration, "sources" in the setup dialog, and a
"Sources" menu of the panel (undoable). The first source is then not drawn and
the legend says what is compared ("[B − A]"); the scale still applies to a
difference but not the offset, and a ratio has neither. Shapes are drawn as
they are, and a single source is shown as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018aVekYzCRLPE71Mm6UH8Zp
Main now has the crosshair (#14) and the device template (#15). The
crosshair reads the curves a panel draws, so with sources relative to
the first it gives B − A or B / A; a test checks it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018aVekYzCRLPE71Mm6UH8Zp
@icyspirit
icyspirit merged commit 141c9bf into main Sep 29, 2026
2 checks passed
@icyspirit
icyspirit deleted the feat/path-arithmetic branch September 29, 2026 23:39
icyspirit pushed a commit that referenced this pull request Sep 29, 2026
Main now has the arithmetic of signals and the sources relative to the
first (#16). The conflicts were side by side: imports, helpers of the
panel, and tests appended at the same place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018aVekYzCRLPE71Mm6UH8Zp
icyspirit pushed a commit that referenced this pull request Sep 29, 2026
With the arithmetic of signals (#16) on main, values on the cells of an
unstructured grid can now be computed with:

- numbers and abs(), cell by cell;
- values on the same cells (the same points and cells, e.g. two runs of
  an edge code), at the closest slice of the second within its time
  range, and NaN for a slice on another grid; grids of different cells
  are an error;
- a time trace, at the time of each slice, and a static grid at the
  times of a trace.

deriv() and other maps or profiles are refused with an error. A panel
showing sources relative to the first thus gives B − A or B / A of such
values, where it failed with an AttributeError. Its line-style legend
now counts these values as an image, as it does maps, so the boundaries
over them are named after their sources (A, B), not "B − A".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018aVekYzCRLPE71Mm6UH8Zp
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