diff --git a/docs/claude/invariants-math-channels.md b/docs/claude/invariants-math-channels.md index 36510ae..f8f3faa 100644 --- a/docs/claude/invariants-math-channels.md +++ b/docs/claude/invariants-math-channels.md @@ -291,3 +291,12 @@ the assertion: `finite` must be about `M` minus the hole share, and `finite=0` b which is the "modifying one expression invalidates all the others" report. `[math-probe] graph edit: stored_slots N -> M` reads it: M must be at least N -- the store keeps filling while the edit runs -- and never 0. + +- **A math channel whose source is switched off is switched off with it.** `Source` resolves to a *channel*, so + a derived channel cannot be computed from a signal nobody acquires: left enabled it would silently compute + nothing, which is what "all math signals depending on it do not compute anymore" looked like from outside. + `on_signal_switch()` disables the channels that read that signal -- **transitively**, since one math channel + may name another -- through the same path the pane's own switch uses (`MathChannel::enabled` and + `apply_math_channels()`), and takes their traces off the plots along with the signal's. The channels that do + not read it are untouched: `[enable-all-probe]`'s `math_on=[…]` is the half that reads this. Re-enabling the + signal does **not** switch them back on, for the same reason re-enabling does not restore the traces. diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index 4bda204..f962fc3 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -1256,6 +1256,15 @@ switches one off **through its own acquire tick in the signal list** -- the tick is the shape `signal_tick_visible()` walks -- and reads the plots back. `traces_left=0` is the rule: a disabled signal leaves every plot rather than staying as a slot that draws nothing, and the notice is what says so. +The switch step runs late, so that a math probe armed *behind* this one has installed its graphs by then; the +two halves after it are the other rule. `math_off` is the derived channels that read this signal -- they cannot +be computed from a signal nobody acquires, so they go off with it -- and `math_on` is the ones that do not, +which must be untouched: + +``` +[enable-all-probe] switched 'sine' off: traces_left=0 (want 0) math_off=[math x2,math3] math_on=[math lp] notice=''sine' is off: 1 trace left the plots; math off: math x2 and 1 more' +``` + ## `[math-probe] graph edit:` ``` diff --git a/src/ui.cpp b/src/ui.cpp index c2de9da..1dd7d63 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -2534,6 +2534,42 @@ void remove_trace(App& app, int row, int slot) { app.rows->assign(row, slot, std::string(), std::string(), 0); } +/// Disables every math channel that (transitively) reads `name`, and answers with their names. +/// +/// A derived channel cannot be computed from a signal nobody acquires: `Source` resolves to a *channel*, and a +/// disabled signal has none -- so a channel left enabled would silently compute nothing. The rule is that the +/// channels which depend on a signal follow it off, which is also what the pane's own switch would do by hand. +/// The dependency is transitive: one math channel may name another, so a channel reading a disabled one goes +/// too, and the loop runs to a fixpoint for that. +std::vector disable_dependent_math_channels(App& app, const std::string& name) { + std::vector off; + if (name.empty()) return off; + const auto math_index_of = [&app](const std::string& n) -> int { + for (std::size_t k = 0; k < app.math.size(); ++k) { + if (app.math[k].name == n) return static_cast(k); + } + return -1; + }; + for (bool moved = true; moved;) { + moved = false; + for (MathChannel& channel : app.math) { + if (!channel.enabled) continue; + for (const MathNode& node : channel.nodes) { + if (node.op != MathOp::Source) continue; + const int k = math_index_of(node.source); + const bool reads_it = node.source == name; + const bool reads_a_disabled_one = k >= 0 && !app.math[static_cast(k)].enabled; + if (!reads_it && !reads_a_disabled_one) continue; + channel.enabled = false; + off.push_back(channel.name); + moved = true; + break; + } + } + } + return off; +} + /// Takes a signal off every plot, and says how many traces went. /// /// Switching a signal off means "do not acquire this", so a trace of it left on a plot draws nothing: a slot diff --git a/src/ui_app.hpp b/src/ui_app.hpp index 9b4e0cd..a461af7 100644 --- a/src/ui_app.hpp +++ b/src/ui_app.hpp @@ -1251,6 +1251,7 @@ void on_fixed_range_apply(GtkButton*, gpointer user); void on_y_fixed_clicked(GtkButton* button, gpointer user); void remove_trace(App& app, int row, int slot); int remove_signal_from_plots(App& app, const std::string& name); +std::vector disable_dependent_math_channels(App& app, const std::string& name); void on_remove_clicked(GtkButton* button, gpointer user); GtkWidget* menu_icon_button(const char* icon, const char* label); void on_row_menu_show_remove(GtkButton*, gpointer user); diff --git a/src/ui_probe.cpp b/src/ui_probe.cpp index c7a9e57..8b12b8c 100644 --- a/src/ui_probe.cpp +++ b/src/ui_probe.cpp @@ -1873,8 +1873,9 @@ gboolean enable_all_probe(gpointer user) { a->model->channel_count()); std::fflush(stderr); // Then switch one off through its own switch in the signal list: a disabled signal must leave the plots, - // not sit there drawing nothing in a slot a live one could take. - g_timeout_add(250, +[](gpointer u) -> gboolean { + // not sit there drawing nothing in a slot a live one could take. Late enough that a math probe armed behind + // this one has had its graphs installed -- the dependants are the point of the second line. + g_timeout_add(2000, +[](gpointer u) -> gboolean { App* app = static_cast(u); if (app->model->channels().empty() || app->signal_list == nullptr) return G_SOURCE_REMOVE; const std::size_t signal = app->model->channels().front().signal; @@ -1907,8 +1908,25 @@ gboolean enable_all_probe(gpointer user) { if (app->rows->row(r).signal_name[static_cast(s)] == name) ++left; } } - std::fprintf(stderr, "[enable-all-probe] switched '%s' off: traces_left=%d (want 0) notice='%s'\n", - name.c_str(), left, app->notice.c_str()); + // Which *math* channels the disabled signal took with it: a derived channel cannot be computed from a + // signal nobody acquires, so the ones that read it follow it off -- and their traces go too. + std::string off_list; + for (const MathChannel& m : app->math) { + if (m.enabled) continue; + if (!off_list.empty()) off_list += ","; + off_list += m.name; + } + // Both halves: the channels that read it went off, and the ones that do not are still on. + std::string on_list; + for (const MathChannel& m : app->math) { + if (!m.enabled) continue; + if (!on_list.empty()) on_list += ","; + on_list += m.name; + } + std::fprintf(stderr, + "[enable-all-probe] switched '%s' off: traces_left=%d (want 0) math_off=[%s] " + "math_on=[%s] notice='%s'\n", + name.c_str(), left, off_list.c_str(), on_list.c_str(), app->notice.c_str()); std::fflush(stderr); return G_SOURCE_REMOVE; }, a); diff --git a/src/ui_signals.cpp b/src/ui_signals.cpp index a34dd4a..38f4730 100644 --- a/src/ui_signals.cpp +++ b/src/ui_signals.cpp @@ -27,13 +27,23 @@ void on_signal_switch(GObject* obj, GParamSpec*, gpointer user) { app->model->set_signal_enabled(index, on); if (!on) { // A trace of a signal nobody acquires draws nothing, and holds the slot a live one could take. - const int gone = remove_signal_from_plots(*app, name); - if (gone > 0) { + int gone = remove_signal_from_plots(*app, name); + // ... and the derived channels that read it cannot be computed at all, so they follow it off, and their + // traces leave with them. + const std::vector off = disable_dependent_math_channels(*app, name); + for (const std::string& channel : off) gone += remove_signal_from_plots(*app, channel); + if (!off.empty()) apply_math_channels(*app); // the single writer: the pane's switches follow it + if (gone > 0 || !off.empty()) { app->relayout_pending = true; rebuild_signal_panels(*app); // the chips of the signals that are still plotted - set_notice(*app, "'" + name + "' is off: " + std::to_string(gone) + - (gone == 1 ? " trace left the plots" : " traces left the plots"), - 4); + std::string what = "'" + name + "' is off: " + std::to_string(gone) + + (gone == 1 ? " trace left the plots" : " traces left the plots"); + if (!off.empty()) { + // The channel names already read as what they are ("math x2"), so the sentence does not prefix them. + what += "; math off: " + off.front() + + (off.size() == 1 ? "" : " and " + std::to_string(off.size() - 1) + " more"); + } + set_notice(*app, what, 5); } } // The enable state is part of the session (a signal can be on only as a math channel's source).