Files
udpscope/help/C/org.udpscope.app/math.page
T
Martino Ferrari e140642ed0 fix: the manual's figures carry a description, and the check that would have caught them
Three pages opened in nothing in Yelp -- overview (twice), math, trigger -- while every check
passed. They are exactly the pages whose <figure> had a sentence for a <title> and no <desc>:
a Mallard <figure> is title?, desc?, media, and the page is refused without the description. The
idiomatic shape, as the shipped sets on this machine write it, moves the sentence into <desc> and
leaves the <title> a name. Note trigger-capture.png is referenced from both overview and trigger,
which is why those two pages are the ones that were noticed.

The gap was in the check, not in its reach: tools/check_help.sh ran links/media/orphans/ids and an
HTML render, and all four are silent about a page that is well linked, well parsed and
unrenderable. `yelp-check validate` is the one that would have caught it, and it cannot run here:
it generates the RelaxNG schema with xsltproc, but the schema includes the base grammar over the
network and this machine's libxml cannot fetch. So the structural rules this project has actually
needed are checked with xmllint's XPath, offline, each written after a page needed it.

One trap, paid for while writing it: a Mallard page declares
xmlns="http://projectmallard.org/1.0/", XPath 1.0 has no default namespace, so `//figure` matches
nothing and every rule passes -- the first version of this check reported "ok" for the very defect
it was written for. Every expression uses local-name() for that reason, and the script says so
where the next reader will be.

Verified: on the committed pages the rules exit 1 and name the three figures, which is the
failing-before case kept as the regression test; on the fixed pages all five rules pass and
yelp-build renders all 15; make -C build test-help goes from red to green.
2026-10-14 17:15:31 +02:00

111 lines
5.6 KiB
Plaintext

<page xmlns="http://projectmallard.org/1.0/"
type="topic" style="task"
id="math">
<info>
<link type="guide" xref="index#analysis"/>
<link type="seealso" xref="signals"/>
<desc>Building a derived channel from a graph of source nodes, operations and one output,
and how the graph is evaluated.</desc>
<credit type="author copyright">
<name>Martino Ferrari</name>
</credit>
</info>
<title>Math channels</title>
<p>A math channel is a signal computed from other signals. You build it as a small graph —
source nodes, operations, one output — and udpscope then treats the result as an ordinary
channel.</p>
<section id="reach">
<title>How to reach it</title>
<steps>
<item>
<p>Press the math button in the toolbar, or <key>g</key>.</p>
</item>
<item>
<p>The pane's <gui>+</gui> adds a channel, and opening one shows the graph editor:
nodes as boxes, click a box to select it and edit its parameters beside the canvas.</p>
</item>
<item>
<p>Inside the editor, <gui>add node</gui> is a menu button whose popover picks the op,
so the node arrives as the operation you asked for.</p>
</item>
</steps>
</section>
<section id="gestures">
<title>Graph gestures</title>
<p><gui>drag</gui> from an output port to an input port to wire them; drag a wired input
port off its connection to detach it; click a wire and press <key>Delete</key> to remove
it; drag a box to move it. <gui>Wheel</gui> zooms about the pointer. <gui>Drag the
background</gui> to pan. <key>f</key> fits all nodes into view;
<keyseq><key>Ctrl</key><key>0</key></keyseq> resets the view. <gui>tidy</gui>, the button
beside <gui>fit</gui> in the editor, lays the graph out in topological columns and frames
it. The orange dot marks the output node; click <gui>set as output</gui> to move it.</p>
</section>
<section id="knowing">
<title>Worth knowing</title>
<list>
<item><p>The graph is evaluated as an ordered list, so a node may only read ones
<em>above</em> it. A wire drawn right to left re-sorts the list silently so the graph
stays valid. A cycle is unrepresentable: the connection is refused rather than created,
and the line beneath the form says why — a loop, or a node asked to read itself.</p></item>
<item><p>A derived channel <em>is</em> a channel. It appears in the signal panel, can be
plotted, measured, exported, and used as a source for another math channel.</p></item>
<item><p>The math runs on the <em>stored</em> samples — decimated and averaged — so a
cutoff you type in Hz means the rate the filter will actually run at. Change the window
across a decimation boundary and the coefficients are rebuilt for the new rate.</p></item>
<item><p>A source may only name an <em>acquired</em> channel. The stream's timestamp
signal is not one: a graph sourced from it validates and then reads NaN for ever, so the
dropdown does not offer it.</p></item>
<item><p>The output defaults to the <em>last</em> node, and a node added to a linear
graph becomes the output. Choosing an earlier node explicitly is respected.</p></item>
<item><p>Every numeric field starts at its own identity value — gain 1, offset 0 — not
at the bottom of its range.</p></item>
</list>
</section>
<section id="why">
<title>Why an operation sometimes does nothing</title>
<p>A graph's <em>output</em> is the node whose value becomes the channel, and it is drawn
as the orange dot. A new graph is built source, then operation, and the last node added
becomes the output — but a graph whose output is left on the source is valid and simply
passes its input through, which is the usual reason a gain or a filter appears to have no
effect. <gui>Set as output</gui> moves the dot.</p>
<p><gui>Apply</gui> is insensitive while the graph is incomplete. A line beneath the form
says why: an input that is not connected, no output chosen, or a <em>channel</em> node
naming a signal the scope is not acquiring. Only the channels being acquired are offered,
because a source that is not acquired reads as no data for ever — and a graph that reads
NaN is harder to diagnose than one that would not save.</p>
<p>The filters are real filters. A math channel's samples are produced as the stream
arrives, once per stored slot, so a low pass carries the history a filter needs. A cutoff
typed in hertz is against the <em>stored</em> rate, which is the wire rate until a wide
window makes the buffer decimate — the status line's rate is that number, and the filters
are rebuilt whenever it changes.</p>
<figure>
<title>The math channel editor</title>
<desc>Nodes as boxes on a canvas, the orange dot marking the output, and the selected
node's parameters beside it.</desc>
<media type="image" mime="image/png" src="figures/math-editor.png" width="938" height="420">
<p>A sine source wired through a gain into a filter, with the output on the last
node.</p>
</media>
</figure>
</section>
<section id="example">
<title>Example</title>
<p><code>[sine] → [gain x2]</code> gives a channel reading <code>y=[-2, 2]</code>. A
100 Hz low pass on the same source passes 50 Hz at 0.97 and 2 kHz at 0.002; a 1 kHz band
pass with Q=2 passes 200 Hz at 0.05, 1 kHz at 0.95 and 4 kHz at 0.05. Each derived channel
must hold exactly as many samples as the shared clock has time slots.</p>
</section>
</page>