fix: a readout keeps its metric prefix when the stream declares no unit
The SI prefix ladder in format_measure() was gated on a non-empty unit, so two decimals of a small unitless value printed `0.00` -- the reading gone, not merely coarse. That is a whole class of channels rather than a corner: a CONFIG need not declare a unit, and a math channel never has one. Below 1e-12 the same loss happened even with a unit, so that range falls back to three significant digits. With no unit the prefix goes tight against the number (`585.24m`), since the space exists to separate the number from its unit and a spaced `m` alone reads as metres. The mock declared `unit="V"` for every data signal, which is why no probe could see this; it now takes `--unit ''`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
3d7e214295
commit
b505706d92
6 files changed
+66
-25
No files matched your search
@@ -792,6 +792,19 @@ far the largest file.
|
|||||||
`crosshair_interpolated_mode_blends_the_two_samples` and
|
`crosshair_interpolated_mode_blends_the_two_samples` and
|
||||||
`the_crosshair_mark_is_bigger_than_a_sample_dot`, plus `[cursors-probe]`'s `first mark at x=..
|
`the_crosshair_mark_is_bigger_than_a_sample_dot`, plus `[cursors-probe]`'s `first mark at x=..
|
||||||
y=..` agreeing with the pane's first value.
|
y=..` agreeing with the pane's first value.
|
||||||
|
- **`format_measure()` is the one formatter for both panes, and its SI prefix does not depend on
|
||||||
|
there being a unit.** The cursors pane's values and the measurements pane's `text=` go through
|
||||||
|
it, so a change reaches both. The prefix ladder used to be inside `if (!unit.empty())`, which
|
||||||
|
made two decimals of a small unitless value read `0.00` — the reading gone, not merely coarse —
|
||||||
|
and that is a whole class of channels, not a corner: `measure_unit()` returns `""` for any signal
|
||||||
|
whose CONFIG declares no unit, and a **math channel never has one**. The regression net could not
|
||||||
|
see it because `tools/mock_udpstreamer.py` declared `unit="V"` for every data signal; it now takes
|
||||||
|
**`--unit ''`**, and that is the flag to run the measures and cursors probes under when touching
|
||||||
|
this (`text=[636.62m]`, `● sine=585.24m`). With no unit the prefix is written **tight** against
|
||||||
|
the number — `1.34m`, not `1.34 m` — because the space exists to separate the number from its
|
||||||
|
unit and a spaced `m` alone reads as metres; with one it stays spaced and the prefix joins the
|
||||||
|
unit (`636.62 mV`). Below `1e-12` the ladder runs out, and `%.2f` would print `0.00` again, so
|
||||||
|
that range falls back to `%.3g`.
|
||||||
- **The cursors are read back from the plots only while a gesture is on that row.** `Cursor::x`
|
- **The cursors are read back from the plots only while a gesture is on that row.** `Cursor::x`
|
||||||
is the *free* pointer position while a drag runs and the snapped value otherwise -- which is
|
is the *free* pointer position while a drag runs and the snapped value otherwise -- which is
|
||||||
what the app itself wrote a frame earlier, or the snap of it. A tick that reads a row on
|
what the app itself wrote a frame earlier, or the snap of it. A tick that reads a row on
|
||||||
|
|||||||
@@ -1185,7 +1185,16 @@ Every readout is a **measurement**, not a raw number: at most two decimals, scal
|
|||||||
SI prefix so the digits sit in `1..999`, and the unit the stream declared appended —
|
SI prefix so the digits sit in `1..999`, and the unit the stream declared appended —
|
||||||
`10.52 mV`, `-9.8 V`, `dT -398.67 ms`, `1.06 pV`. Two decimals is the resolution the
|
`10.52 mV`, `-9.8 V`, `dT -398.67 ms`, `1.06 pV`. Two decimals is the resolution the
|
||||||
instruments can claim (a marker lands on a sample, and the sample is an `f32`), and a
|
instruments can claim (a marker lands on a sample, and the sample is an `f32`), and a
|
||||||
trailing zero is dropped, so `1.50 V` reads `1.5 V`. A row's rulers use its signals' unit
|
trailing zero is dropped, so `1.50 V` reads `1.5 V`. The prefix is applied **whether or not
|
||||||
|
the stream declared a unit**, and that is not a detail: a signal whose CONFIG declares none is
|
||||||
|
ordinary, a *math* channel never has one, and two decimals of an unprefixed small value is
|
||||||
|
`0.00` — the reading was simply gone for an entire class of channels, which went unnoticed
|
||||||
|
because the mock declares `V` for everything (it now takes `--unit ''`, which is the only way
|
||||||
|
a probe can see this). With no unit the prefix is written *tight* against the number —
|
||||||
|
`585.24m`, not `585.24 m` — because the space is there to separate the number from its unit,
|
||||||
|
and a spaced `m` with nothing after it reads as metres. Below `1e-12`, where no prefix can
|
||||||
|
help, the value falls back to three significant digits in whatever form fits rather than to
|
||||||
|
`0.00`. A row's rulers use its signals' unit
|
||||||
when they agree on one and print bare digits when they do not: a ruler is on the row's y
|
when they agree on one and print bare digits when they do not: a ruler is on the row's y
|
||||||
axis, and two units on one axis make "the" unit a guess. A trace's line carries the
|
axis, and two units on one axis make "the" unit a guess. A trace's line carries the
|
||||||
signal's **colour chip and its name** — `● sine 616.49 mV -24.57 mV -641.06 mV` — and the
|
signal's **colour chip and its name** — `● sine 616.49 mV -24.57 mV -641.06 mV` — and the
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ owns a <b>Reset</b> button. Drag a marker to move it. The mode dropdown offers f
|
|||||||
• A value is the <i>nearest stored sample's</i>, never a number between two samples. Zoom in past
|
• A value is the <i>nearest stored sample's</i>, never a number between two samples. Zoom in past
|
||||||
the sample spacing and the line between two samples is something the plot drew, not data —
|
the sample spacing and the line between two samples is something the plot drew, not data —
|
||||||
so a reading off it would be the drawing. The hover crosshair reads the same way.
|
so a reading off it would be the drawing. The hover crosshair reads the same way.
|
||||||
|
• Every reading carries a <i>metric prefix</i> so the digits sit in <tt>1..999</tt>:
|
||||||
|
<tt>10.52 mV</tt>, not <tt>0.01 V</tt>. Two decimals is all an instrument landing on an
|
||||||
|
<tt>f32</tt> sample can claim, so without the prefix a small value would read <tt>0.00</tt>
|
||||||
|
and the reading would be gone. A stream that declares no unit gets the prefix anyway, written
|
||||||
|
tight against the number (<tt>585.24m</tt>) — the space separates the number from its unit,
|
||||||
|
and a spaced <tt>m</tt> with nothing after it would read as metres.
|
||||||
• A marker past the newest sample reads <tt>--</tt>. With only one marker over data the row
|
• A marker past the newest sample reads <tt>--</tt>. With only one marker over data the row
|
||||||
reads <tt>partial</tt>: a difference between a number and nothing is not a number.
|
reads <tt>partial</tt>: a difference between a number and nothing is not a number.
|
||||||
• In <tt>mix</tt> each marker also places a crossing annotation per trace, which is what lets
|
• In <tt>mix</tt> each marker also places a crossing annotation per trace, which is what lets
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ set, with <b>Save</b> and <b>Delete</b>.
|
|||||||
• A window with too few crossings reads <tt>invalid</tt> / <tt>--</tt> rather than a plausible
|
• A window with too few crossings reads <tt>invalid</tt> / <tt>--</tt> rather than a plausible
|
||||||
invented number. Frequency over a window holding one edge is the usual case.
|
invented number. Frequency over a window holding one edge is the usual case.
|
||||||
• <tt>frequency</tt> is reported in Hz and <tt>period</tt>/<tt>pulse duration</tt> in seconds;
|
• <tt>frequency</tt> is reported in Hz and <tt>period</tt>/<tt>pulse duration</tt> in seconds;
|
||||||
every other type carries the channel's own unit.
|
every other type carries the channel's own unit. A value is written with a <i>metric prefix</i>
|
||||||
|
so its digits sit in <tt>1..999</tt> — <tt>636.62 mV</tt>, not <tt>0.64 V</tt> — including when
|
||||||
|
the stream declares no unit at all, which is always the case for a math channel: there the
|
||||||
|
prefix goes tight against the number (<tt>636.62m</tt>), since two decimals of an unprefixed
|
||||||
|
small value is <tt>0.00</tt>.
|
||||||
• Cost is bounded by reading the summary pyramid the producer already maintains, not the raw
|
• Cost is bounded by reading the summary pyramid the producer already maintains, not the raw
|
||||||
window. The values are still exact: a bucket's sum is a sum.
|
window. The values are still exact: a bucket's sum is a sum.
|
||||||
|
|
||||||
|
|||||||
+23
-18
@@ -922,6 +922,13 @@ const char* badge_markup(BadgePhase phase) {
|
|||||||
/// sample is an `f32` of a value the CONFIG may scale arbitrarily, so more digits would be
|
/// sample is an `f32` of a value the CONFIG may scale arbitrarily, so more digits would be
|
||||||
/// noise dressed as precision. Trailing zeros go, so `1.50 V` reads `1.5 V` -- "at most
|
/// noise dressed as precision. Trailing zeros go, so `1.50 V` reads `1.5 V` -- "at most
|
||||||
/// two decimals", not exactly two.
|
/// two decimals", not exactly two.
|
||||||
|
///
|
||||||
|
/// The prefix is applied **whether or not a unit is declared**, and that is the point: two
|
||||||
|
/// decimals of an unprefixed small value is `0.00`, i.e. the reading is simply gone. A signal
|
||||||
|
/// whose CONFIG declares no unit is ordinary (and a *math* channel never has one), so the pane
|
||||||
|
/// showed `0.00` for every value under 5 mV of an entire class of channels. A bare prefix is
|
||||||
|
/// written tight against the number -- `1.34m`, not `1.34 m` -- because the space is there to
|
||||||
|
/// separate the number from its unit, and with no unit to separate, a spaced `m` reads as metres.
|
||||||
std::string format_measure(double v, const std::string& unit) {
|
std::string format_measure(double v, const std::string& unit) {
|
||||||
if (!std::isfinite(v)) return "--";
|
if (!std::isfinite(v)) return "--";
|
||||||
static const struct {
|
static const struct {
|
||||||
@@ -934,22 +941,22 @@ std::string format_measure(double v, const std::string& unit) {
|
|||||||
const double mag = std::fabs(v);
|
const double mag = std::fabs(v);
|
||||||
const char* prefix = "";
|
const char* prefix = "";
|
||||||
double scale = 1.0;
|
double scale = 1.0;
|
||||||
// No unit, no prefix: `10.52 m` is a prefix with nothing to prefix, which reads as
|
for (const auto& p : kPrefixes) {
|
||||||
// metres. A bare number stays a bare number and simply keeps its decimals.
|
if (mag >= p.scale) {
|
||||||
if (!unit.empty()) {
|
prefix = p.prefix;
|
||||||
for (const auto& p : kPrefixes) {
|
scale = p.scale;
|
||||||
if (mag >= p.scale) {
|
break;
|
||||||
prefix = p.prefix;
|
|
||||||
scale = p.scale;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (mag > 0.0 && mag < 1.0e-12) { // below the ladder: keep it in plain digits
|
|
||||||
prefix = "";
|
|
||||||
scale = 1.0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
char buf[64];
|
char buf[64];
|
||||||
|
if (mag > 0.0 && mag < 1.0e-12) {
|
||||||
|
// Below the ladder, where scaling cannot help and `%.2f` would print `0.00` again. Three
|
||||||
|
// significant digits in whatever form fits, so the reading survives as a reading.
|
||||||
|
std::snprintf(buf, sizeof(buf), "%.3g", v);
|
||||||
|
std::string out = buf;
|
||||||
|
if (!unit.empty()) out += " " + unit;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
std::snprintf(buf, sizeof(buf), "%.2f", v / scale);
|
std::snprintf(buf, sizeof(buf), "%.2f", v / scale);
|
||||||
std::string num = buf;
|
std::string num = buf;
|
||||||
if (num.find('.') != std::string::npos) { // at most two decimals, not exactly two
|
if (num.find('.') != std::string::npos) { // at most two decimals, not exactly two
|
||||||
@@ -958,11 +965,9 @@ std::string format_measure(double v, const std::string& unit) {
|
|||||||
}
|
}
|
||||||
if (num == "-0") num = "0";
|
if (num == "-0") num = "0";
|
||||||
std::string out = num;
|
std::string out = num;
|
||||||
if (prefix[0] != '\0') out += std::string(" ") + prefix;
|
if (!unit.empty()) out += " "; // the space separates the number from its unit, nothing else
|
||||||
if (!unit.empty()) {
|
out += prefix;
|
||||||
if (prefix[0] == '\0') out += " ";
|
out += unit;
|
||||||
out += unit;
|
|
||||||
}
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,16 +50,16 @@ def signal_desc(name, *, rate, time_mode, time_idx, unit, rows=1, cols=1, dims=0
|
|||||||
return bytes(d)
|
return bytes(d)
|
||||||
|
|
||||||
|
|
||||||
def build_config(rate, time_mode=TIME_FULL_ARRAY):
|
def build_config(rate, time_mode=TIME_FULL_ARRAY, unit="V"):
|
||||||
sigs = [
|
sigs = [
|
||||||
# The stream's clock: referenced by every data signal, so udpscope treats
|
# The stream's clock: referenced by every data signal, so udpscope treats
|
||||||
# it as the time base rather than a plottable channel. Its *own* time_mode
|
# it as the time base rather than a plottable channel. Its *own* time_mode
|
||||||
# is what udpscope switches on, so `--time-mode first` has to land here.
|
# is what udpscope switches on, so `--time-mode first` has to land here.
|
||||||
signal_desc("time", rate=0.0, time_mode=time_mode,
|
signal_desc("time", rate=0.0, time_mode=time_mode,
|
||||||
time_idx=NO_TIME_SIGNAL, unit="s"),
|
time_idx=NO_TIME_SIGNAL, unit="s"),
|
||||||
signal_desc("sine", rate=rate, time_mode=time_mode, time_idx=0, unit="V"),
|
signal_desc("sine", rate=rate, time_mode=time_mode, time_idx=0, unit=unit),
|
||||||
signal_desc("square", rate=rate, time_mode=time_mode, time_idx=0, unit="V"),
|
signal_desc("square", rate=rate, time_mode=time_mode, time_idx=0, unit=unit),
|
||||||
signal_desc("ramp", rate=rate, time_mode=time_mode, time_idx=0, unit="V"),
|
signal_desc("ramp", rate=rate, time_mode=time_mode, time_idx=0, unit=unit),
|
||||||
]
|
]
|
||||||
payload = struct.pack("<I", len(sigs)) + b"".join(sigs)
|
payload = struct.pack("<I", len(sigs)) + b"".join(sigs)
|
||||||
payload += bytes([PUBLISH_ACCUMULATE])
|
payload += bytes([PUBLISH_ACCUMULATE])
|
||||||
@@ -101,6 +101,10 @@ def main():
|
|||||||
"sample (UDPS_TIME_FIRST_SAMPLE) and leaves the viewer to space the "
|
"sample (UDPS_TIME_FIRST_SAMPLE) and leaves the viewer to space the "
|
||||||
"rest by the declared cadence -- the shape a real MARTe2-style "
|
"rest by the declared cadence -- the shape a real MARTe2-style "
|
||||||
"producer with a single u64 clock has")
|
"producer with a single u64 clock has")
|
||||||
|
ap.add_argument("--unit", default="V",
|
||||||
|
help="the unit the data signals declare in CONFIG; '' declares none, which "
|
||||||
|
"is an ordinary stream (and what every math channel looks like) and is "
|
||||||
|
"the only way a probe can see the unitless readout formatting")
|
||||||
ap.add_argument("--clock-jitter", type=float, default=0.0,
|
ap.add_argument("--clock-jitter", type=float, default=0.0,
|
||||||
help="per-packet stamp jitter, as a fraction of one packet's span: a device "
|
help="per-packet stamp jitter, as a fraction of one packet's span: a device "
|
||||||
"that stamps each batch from its own clock does not land on a perfect "
|
"that stamps each batch from its own clock does not land on a perfect "
|
||||||
@@ -138,7 +142,7 @@ def main():
|
|||||||
t_stream = 0.0
|
t_stream = 0.0
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
next_send = started
|
next_send = started
|
||||||
sock.sendto(build_config(declared, time_mode), peer)
|
sock.sendto(build_config(declared, time_mode, args.unit), peer)
|
||||||
print("mock udpstreamer: CONNECT from %s:%d, CONFIG sent" % peer, flush=True)
|
print("mock udpstreamer: CONNECT from %s:%d, CONFIG sent" % peer, flush=True)
|
||||||
elif magic == MAGIC and ptype == PKT_DISCONNECT:
|
elif magic == MAGIC and ptype == PKT_DISCONNECT:
|
||||||
print("mock udpstreamer: DISCONNECT", flush=True)
|
print("mock udpstreamer: DISCONNECT", flush=True)
|
||||||
|
|||||||
Reference in new issue
Block a user