summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-06-09 22:35:40 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-06-09 22:35:40 -0600
commitd396692de571445537e617ac31627d7bd7ab84aa (patch)
treec60570493f03addf770b7df5b49e69363fc69b80
parentf8b5e47ffe9a52bd28ad5d953a551ccb92cbbb09 (diff)
Fix custom_script execution + add templates for expression/function kinds
- custom_script now uses exec-style (assign to `result`) matching the built-in templates — previously auto-wrapped in def compute() which had no return statement, causing state and channel vars to silently fail - function kind unchanged: auto-wraps body as def compute(x,t); use return - Add SCRIPT_TEMPLATES for expression, function, custom_script kinds; shown automatically when user switches to a script kind (replacing stale code from previous kind) - New channels default to template when no saved code exists Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--core/signal_processor.py27
-rw-r--r--ui/code_templates.py43
-rw-r--r--ui/windows/channels_window.py19
3 files changed, 70 insertions, 19 deletions
diff --git a/core/signal_processor.py b/core/signal_processor.py
index 8ac4d8c..e2752cd 100644
--- a/core/signal_processor.py
+++ b/core/signal_processor.py
@@ -286,7 +286,8 @@ class DerivedChannel:
self._fn = _expr_fn
- elif self.kind in ("function", "custom_script"):
+ elif self.kind == "function":
+ # Auto-wrap body as def compute(x, t): ... return <value>
src = self.script
if not src.strip().startswith("def compute"):
src = "def compute(x, t):\n" + "\n".join(
@@ -297,9 +298,8 @@ class DerivedChannel:
_fn_ref = _exec_ns["compute"]
_state = self._exec_state
- def _script_fn(inputs, t, sv,
- _f=_fn_ref, _ns=_exec_ns, _st=_state, _names=src_names):
- # refresh mutable globals before each call so scripts see live values
+ def _fn_fn(inputs, t, sv,
+ _f=_fn_ref, _ns=_exec_ns, _st=_state, _names=src_names):
_ns["vars"] = sv
_ns["state"] = _st
for i, name in enumerate(_names):
@@ -307,7 +307,24 @@ class DerivedChannel:
_ns[name] = inputs[i]
return float(_f(inputs, t))
- self._fn = _script_fn
+ self._fn = _fn_fn
+
+ elif self.kind == "custom_script":
+ # Exec-style: script assigns to `result`
+ code = compile(self.script, "<custom_script>", "exec")
+ _state = self._exec_state
+
+ def _cs_fn(inputs, t, sv, _code=code, _names=src_names, _st=_state):
+ ns = {"x": inputs, "t": t, "math": math, "vars": sv, "state": _st}
+ for i, name in enumerate(_names):
+ if i < len(inputs):
+ ns[name] = inputs[i]
+ exec(_code, ns)
+ if "result" not in ns:
+ raise NameError("custom_script must assign to 'result'")
+ return float(ns["result"])
+
+ self._fn = _cs_fn
else:
# Built-in kinds handled in SignalProcessor._compute_derived
diff --git a/ui/code_templates.py b/ui/code_templates.py
index 0863e62..32332e9 100644
--- a/ui/code_templates.py
+++ b/ui/code_templates.py
@@ -1,18 +1,24 @@
"""
ui/code_templates.py
-Python code templates for built-in derived channel kinds.
-Shown as read-only previews in the UI; clicking "Edit as Custom Script"
-converts the channel to custom_script kind with the template pre-filled.
+Code templates for derived channel kinds.
-Templates use the enhanced expression namespace:
- x — list of source values in order
- <name> — each source's channel_id as a named variable
+BUILTIN_TEMPLATES — shown as read-only previews for built-in kinds.
+ "Edit as Custom Script" copies the template into custom_script.
+ Uses exec-style: assign to `result`.
+
+SCRIPT_TEMPLATES — default starter code shown when user picks a script kind.
+ expression → single Python expression (no assignment)
+ function → auto-wrapped as def compute(x, t): ... use return
+ custom_script → exec-style block, assign to `result`
+
+Namespace available in all kinds:
+ x — list of source values in source order
+ <name> — each source's channel_id as a named variable (same as x[i])
t — elapsed time (seconds)
math — Python math module
vars — shared SignalProcessor._script_vars dict (read/write)
state — per-channel persistent dict (survives between evaluations)
- result — assign the computed value here (expression-style exec)
"""
BUILTIN_TEMPLATES: dict = {
@@ -68,3 +74,26 @@ result = x[0] - x[1]
result = sum(x)
""",
}
+
+
+# Default starter code shown when user selects a script kind
+SCRIPT_TEMPLATES: dict = {
+
+ "expression": "x[0]",
+
+ "function": """\
+# Function body — use 'return' to output the value.
+# Named source vars, t, math, vars, state all available.
+offset = vars.get("offset", 0)
+return x[0] - offset
+""",
+
+ "custom_script": """\
+# Assign computed value to 'result'.
+# Named source vars, t, math, vars, state all available.
+
+# Example: running zero-offset
+zero = vars.get("zero", 0)
+result = x[0] - zero
+""",
+}
diff --git a/ui/windows/channels_window.py b/ui/windows/channels_window.py
index 266432b..597b0b2 100644
--- a/ui/windows/channels_window.py
+++ b/ui/windows/channels_window.py
@@ -618,8 +618,8 @@ _KIND_HELP = {
"Example:\n offset = vars.get('zero', 0)\n return x[0] - offset"
),
"custom_script": (
- "Full Python. Must define: def compute(x, t): ...\n"
- "Available globals: x, signal names, t, math, vars, state"
+ "Multi-line Python block. Assign computed value to result.\n"
+ "Available: x, signal names, t, math, vars, state"
),
}
@@ -758,9 +758,11 @@ class DerivedBlock(QFrame):
from ui.code_templates import BUILTIN_TEMPLATES
txt = BUILTIN_TEMPLATES.get(self.dc.kind, "")
elif self.dc.kind == "expression":
- txt = self.dc.expression
+ from ui.code_templates import SCRIPT_TEMPLATES
+ txt = self.dc.expression or SCRIPT_TEMPLATES.get("expression", "")
else:
- txt = self.dc.script
+ from ui.code_templates import SCRIPT_TEMPLATES
+ txt = self.dc.script or SCRIPT_TEMPLATES.get(self.dc.kind, "")
self._code.setPlainText(txt)
code_lay.addWidget(self._code)
# "Edit as Custom Script" button — only visible for built-in kinds
@@ -875,16 +877,19 @@ class DerivedBlock(QFrame):
# expression kind: label-only, no code injection (single-line expr)
def _on_kind(self, idx: int):
- from ui.code_templates import BUILTIN_TEMPLATES
+ from ui.code_templates import BUILTIN_TEMPLATES, SCRIPT_TEMPLATES
k = _ALL_KINDS[idx]
self.dc.kind = k
self._help.setText(_KIND_HELP.get(k, ""))
if k in ("derivative", "second_derivative") and not self._src_combos:
self._add_src()
if k in _BUILTIN:
- # Reset to bare template first; _update_vars_hint will prepend comment
+ # Reset to bare template; _update_vars_hint will prepend variable comment
self._code.setPlainText(BUILTIN_TEMPLATES.get(k, ""))
- self._update_vars_hint() # also injects variable comment into code
+ elif k in _SCRIPT:
+ # Show starter template for script kinds (always replace on kind switch)
+ self._code.setPlainText(SCRIPT_TEMPLATES.get(k, ""))
+ self._update_vars_hint()
self._update_visibility()
def _on_wrt_changed(self, idx: int):