统一力值阈值处理并保留原始采样

移除自动显示基线,使逐轴阈值统一作用于曲线、读数与记录输出,标定拟合继续使用原始采样。
共享层全量测试和相关 Studio 测试通过。
This commit is contained in:
ykcheungab
2026-10-07 12:25:12 +08:00
parent 6194778a6f
commit 86996694b4
27 changed files with 507 additions and 1512 deletions
+3 -11
View File
@@ -286,28 +286,20 @@ def test_a_partial_plot_settings_mapping_leaves_the_rest_as_stored():
assert parts["charts"].deadband_floors[-1]["raw"] == 900.0
def test_the_display_baseline_is_not_a_finger_only_setting():
"""Every sensor type and both units: an idle newton offset is the same problem."""
def test_force_threshold_policy_is_available_for_each_sensor_and_unit():
for sensor, units in (("box", "newton"), ("box", "raw"),
("finger", "newton"), ("finger", "raw")):
controller, parts = _controller(prefs={"raw_auto_zero": True},
sensor_type=sensor, units=units)
controller, parts = _controller(sensor_type=sensor, units=units)
assert parts["charts"].presentation_enabled[-1] is True, (sensor, units)
assert parts["charts"].presentation[-1]["unit"] == (
"N" if units == "newton" else "raw"
)
controller, parts = _controller(prefs={"raw_auto_zero": False}, sensor_type="box",
units="newton")
assert parts["charts"].presentation_enabled[-1] is False
def test_the_deadband_floors_reach_the_charts_from_the_preferences():
controller, parts = _controller(prefs={"plot_deadband_n": 0.5, "plot_deadband_raw": 2000.0})
assert parts["charts"].deadband_floors[-1] == {"newton": 0.5, "raw": 2000.0}
controller.set_raw_auto_zero(True) # re-applies the whole policy
assert parts["charts"].deadband_floors[-1] == {"newton": 0.5, "raw": 2000.0}
assert parts["charts"].presentation_enabled[-1] is True
def test_the_sample_rate_prefers_the_measured_rate_over_the_stored_one():
+21 -199
View File
@@ -1,25 +1,12 @@
"""The force charts as the operator sees them: baseline, deadband and ranges.
The display policy is a *view* concern: an idle channel must read zero and draw a
flat line, while the session store and ``forces.csv`` keep the measurement. These
tests drive the panel on its own (no acquisition, no window) and read back what
the curves and the value cards were actually given.
"""
"""Force cards, curves, thresholds, precision and chart ranges."""
from __future__ import annotations
import numpy as np
from orisys_app_shared.force.session import SessionSample, SessionStore
from orisys_studio.ui.force_plots import ForcePlotsPanel, _nice_ticks, format_force
#: The two idle triplets from the customer's screenshots: a raw FN of ~9555 and
#: an FX whose bias sits ~13k counts off zero.
RAW_BIAS = np.array([9555.0, -13368.0, -353.0])
NEWTON_BIAS = np.array([0.4, -0.25, 0.1])
from orisys_studio.ui.force_plots import ForcePlotsPanel, _nice_ticks
def _panel(unit: str = "raw") -> ForcePlotsPanel:
"""A chart in one unit, with the display baseline switched on."""
panel = ForcePlotsPanel(theme="dark")
panel.set_units(unit)
panel.configure_presentation(sensor_type="box", unit=unit)
@@ -28,29 +15,6 @@ def _panel(unit: str = "raw") -> ForcePlotsPanel:
return panel
def _rest(
panel: ForcePlotsPanel,
bias: np.ndarray,
*,
unit: str = "raw",
samples: int = 80,
spread: float = 0.0,
sigma: float = 0.0,
seed: int = 0,
) -> None:
"""Feed a quiet startup window, the way a live session would.
``spread`` is a bounded wander (the device really does sit still) and
``sigma`` is the random jitter of a `noisier` product.
"""
rng = np.random.default_rng(seed)
for index in range(samples):
sample = bias + ((index % 5) - 2) * spread
if sigma:
sample = sample + rng.normal(0.0, sigma, size=3)
panel.push(index / 30.0, *sample, unit=unit)
def _ticks(panel: ForcePlotsPanel, channel: int) -> list[float]:
"""The Y tick values actually installed on one axis."""
levels = panel.plots[channel].getAxis("left")._tickLevels
@@ -63,174 +27,32 @@ def _curve(panel: ForcePlotsPanel, channel: int) -> np.ndarray:
return np.asarray(values, dtype=float)
# ------------------------------------------------------------ resting channel
def test_the_cards_read_zero_from_the_first_samples_of_a_session(qt_app):
"""The reported defect: FN 1118 raw, FX 34.16, FY -186.1 on the cards.
The screenshot was taken as the session started, so the aid has to be
effective from the first samples — not once the whole quiet window closed.
"""
# ------------------------------------------------------------- force output
def test_raw_threshold_and_precision_match_cards_and_curve(qt_app):
panel = _panel("raw")
for index in range(4):
panel.push(index / 30.0, *RAW_BIAS, unit="raw")
if index < 2:
# Nothing has been estimated from fewer than three samples yet; those
# first frames honestly show the measurement.
continue
assert [label.text() for label in panel.value_labels] == ["0"] * 3, index
assert not panel.presentation.ready, "the whole three second window was needed"
assert panel.presentation.display_quality == "provisional"
for channel in (0, 1, 2):
assert set(np.unique(_curve(panel, channel))) == {0.0}, channel
panel.set_deadband_floors(raw=50.0)
panel.push(0.0, 49.0, -50.0, 51.2, unit="raw")
assert [label.text() for label in panel.value_labels] == ["0", "0", "51"]
assert [_curve(panel, axis)[-1] for axis in range(3)] == [0.0, 0.0, 51.2]
panel.set_value_decimals(raw=1, newton=2)
assert panel.value_labels[2].text() == "51.2"
def test_the_newton_cards_read_zero_from_the_first_samples_too(qt_app):
"""Both units: a newton offset at rest is the same display problem."""
def test_newton_threshold_and_precision_match_cards_and_curve(qt_app):
panel = _panel("N")
for index in range(4):
panel.push(index / 30.0, *NEWTON_BIAS, unit="N")
if index < 2:
continue
assert [label.text() for label in panel.value_labels] == ["0.000"] * 3, index
assert not panel.presentation.ready
panel.set_deadband_floors(newton=0.5)
panel.push(0.0, 0.49, -0.5, 0.51, unit="N")
assert [label.text() for label in panel.value_labels] == ["0.00", "0.00", "0.51"]
assert [_curve(panel, axis)[-1] for axis in range(3)] == [0.0, 0.0, 0.51]
def test_a_card_and_its_own_curve_are_the_same_value(qt_app):
"""A card may never read a number its curve does not carry.
The extra "the estimate is ready" gate in ``_render_value_cards`` is what
used to let the card show the raw measurement while the curve was already
drawn through the applied estimate.
"""
def test_threshold_has_no_estimated_baseline(qt_app):
panel = _panel("raw")
for index in range(4):
panel.push(index / 30.0, *RAW_BIAS, unit="raw")
# A real contact, flagged as one so it does not enter the idle estimate.
panel.push(1.0, *(RAW_BIAS + [4000.0, 0.0, 0.0]), unit="raw", contact_found=True)
for channel in range(3):
drawn = float(_curve(panel, channel)[-1])
assert panel.value_labels[channel].text() == format_force(
drawn, "raw", presentation=panel.presentation
), channel
assert panel.value_labels[0].text() not in ("0", "1118"), "the contact was lost"
def test_a_resting_raw_channel_reads_zero_and_draws_a_flat_curve(qt_app):
"""FN 9555 / FX -13368 at rest must read 0 and draw a line on zero."""
panel = _panel("raw")
_rest(panel, RAW_BIAS, spread=20.0)
assert panel.presentation.ready, "the quiet window never closed"
for index, label in enumerate(panel.value_labels):
assert label.text() == "0", (index, label.text())
for channel in (0, 1, 2):
values = _curve(panel, channel)
assert values.size, channel
assert set(np.unique(values)) == {0.0}, (channel, np.unique(values))
def test_a_resting_newton_channel_is_baselined_too(qt_app):
"""The same aid in newtons: it is a display problem in either unit."""
panel = _panel("N")
_rest(panel, NEWTON_BIAS, unit="N")
assert panel.presentation.ready
assert [label.text() for label in panel.value_labels] == ["0.000"] * 3
assert set(np.unique(_curve(panel, 0))) == {0.0}
def test_noise_outside_the_estimated_band_is_still_rare(qt_app):
"""A p99 band, so a resting *noisy* channel is flat but not rigid.
The offset removal is what fixes a 13k-count bias; the percentile band holds
the remaining jitter at zero. A few idle samples may sit just outside it —
that is what the operator's deadband floor in the dialog is for.
"""
panel = _panel("raw")
_rest(panel, RAW_BIAS, sigma=30.0)
assert panel.presentation.ready
values = _curve(panel, 1)
zeroed = float(np.mean(values == 0.0))
assert zeroed >= 0.95, f"only {zeroed:.2%} of a resting channel was zeroed"
assert abs(float(np.median(values))) < 1.0
def test_the_record_keeps_the_measurement_while_the_display_is_zeroed(qt_app):
"""The deadband is a display copy: the buffer and the store see the truth."""
panel = _panel("raw")
store = SessionStore()
panel.set_store(store)
for index in range(6):
panel.push(index / 30.0, *RAW_BIAS, unit="raw")
store.append(
SessionSample(
index,
index / 30.0,
fn_raw=RAW_BIAS[0],
fx_raw=RAW_BIAS[1],
fy_raw=RAW_BIAS[2],
fn_n=NEWTON_BIAS[0],
fx_n=NEWTON_BIAS[1],
fy_n=NEWTON_BIAS[2],
valid=True,
)
)
# The very values the customer complained about are still measured, from the
# first sample on — in the live buffer...
state = panel.history.state()
assert state is not None
assert abs(float(np.median(state.values[:, 0])) - RAW_BIAS[0]) < 100.0
assert abs(float(np.median(state.values[:, 1])) - RAW_BIAS[1]) < 100.0
# ...and in the session store (which is what forces.csv is written from).
stored = store.data().raw
assert stored.shape[0] == 6
assert float(np.median(stored[:, 0])) == RAW_BIAS[0]
assert float(np.median(stored[:, 1])) == RAW_BIAS[1]
assert float(np.median(stored[:, 2])) == RAW_BIAS[2]
# ...while the cards and the curve drawn from it are already flat on zero.
assert [label.text() for label in panel.value_labels] == ["0"] * 3
assert set(np.unique(_curve(panel, 1))) == {0.0}
# ------------------------------------------------------------------ deadband
def test_a_small_excursion_is_suppressed_and_a_larger_one_is_not(qt_app):
panel = _panel("raw")
_rest(panel, RAW_BIAS, spread=20.0)
estimate = panel.presentation.last_estimate
assert estimate is not None and estimate.quality == "ok"
deadband = float(estimate.deadbands[0])
assert deadband > 0.0
panel.push(3.0, *(RAW_BIAS + [0.5 * deadband, 0.0, 0.0]), unit="raw")
assert panel.value_labels[0].text() == "0", "half the deadband was drawn"
panel.push(3.1, *(RAW_BIAS + [3.0 * deadband, 0.0, 0.0]), unit="raw")
assert panel.value_labels[0].text() != "0", (
"an excursion well outside the deadband was swallowed"
)
assert abs(_curve(panel, 0)[-1]) > 2.0 * deadband
# ...and the other axes, which stayed quiet, are still zero.
assert panel.value_labels[1].text() == "0"
def test_the_operator_floor_widens_the_deadband_at_once(qt_app):
panel = _panel("raw")
_rest(panel, RAW_BIAS, spread=20.0)
panel.set_deadband_floors(raw=250000.0)
panel.push(3.0, *(RAW_BIAS + [50000.0, 0.0, 0.0]), unit="raw")
assert panel.value_labels[0].text() == "0", (
"an excursion inside the operator's floor was drawn"
)
panel.set_deadband_floors(raw=0.0) # "auto": the layer's own floor returns
assert panel.presentation.min_deadband_for("raw") == 50.0
panel.set_deadband_floors(raw=50.0)
panel.push(0.0, 9555.0, -13368.0, 20.0, unit="raw")
assert [label.text() for label in panel.value_labels] == ["9555", "-13368", "0"]
panel.set_deadband_floors(raw=0.0)
assert panel.presentation.min_deadband_for("raw") == 0.0
# -------------------------------------------------------------------- ranges
def test_the_configured_ranges_reach_every_axis(qt_app):
+35 -41
View File
@@ -634,6 +634,8 @@ def test_unit_switch_rebuilds_from_the_session_store(make_window):
import math
window, stuck = _running_window(make_window)
window.recording.set_force_thresholds(raw=0.0, newton=0.0)
window.force_plots.set_deadband_floors(raw=0.0, newton=0.0)
window.toolbar.units_combo.setCurrentIndex(1) # raw
window.force_plots.reset()
for index in range(60):
@@ -1700,7 +1702,7 @@ def test_the_replay_view_loads_a_recording_and_scrubs_it(make_window, replay_rec
window.brand_bar.btn_playback.click()
QApplication.processEvents()
assert window._load_recording(replay_recording)
assert window.replay_ctl.load_recording(replay_recording)
QApplication.processEvents()
panel = window.force_plots
assert panel.is_recording_source(), "the charts did not switch to the recording"
@@ -1787,15 +1789,12 @@ def test_a_late_calibration_does_not_plot_raw_counts_on_the_newton_axis(make_win
# The sample that caused the switch still reaches the readouts; the early
# return used to leave the cards showing the previous sample. The number on
# the card is the *display* copy of it: this fake FN is a slow 2.0…2.4 N ramp
# with no contact flagged, which is exactly the resting drift the display
# baseline holds at zero. What matters here is that the card is not a raw
# count, not the previous sample and not a dash.
# the card uses the newton sample, with only the configured absolute threshold.
assert abs(plots._last_values[0] - (2.0 + 39 * 0.01)) < 1e-9
assert plots.presentation.display_quality != "none", (
assert plots.presentation.display_quality == "threshold", (
"the display policy was not applied to the freshly shown newton unit"
)
assert plots.value_labels[0].text() == "0.000", plots.value_labels[0].text()
assert plots.value_labels[0].text() == "2.39", plots.value_labels[0].text()
assert plots.value_labels[0].text() != t("chart.value_unavailable")
_finish(window, stuck)
@@ -1940,9 +1939,9 @@ def test_a_replay_keeps_the_2d_pane_from_the_live_camera(
window, stuck = _running_window(make_window)
window.brand_bar.btn_playback.click()
QApplication.processEvents()
assert window._load_recording(replay_recording), "the replay could not open it"
assert window.replay_ctl.load_recording(replay_recording), "the replay could not open it"
QApplication.processEvents()
assert window._replay_owns_image(), "the replay does not own the 2D pane"
assert window.replay_ctl.owns_image(), "the replay does not own the 2D pane"
label = window.arrow_panel.image_label
# Park the playhead at the end of the recording and remember what the pane and
@@ -1971,7 +1970,7 @@ def test_a_replay_keeps_the_2d_pane_from_the_live_camera(
# meaningful rather than a dead pane.
window.brand_bar.btn_live.click()
QApplication.processEvents()
assert not window._replay_owns_image()
assert not window.replay_ctl.owns_image()
stuck.emit(ui_check._fake_packet(9, mono_t=9 / 30.0, frame=live))
window._on_run_tick()
QApplication.processEvents()
@@ -1993,7 +1992,7 @@ def test_the_readout_describes_the_point_the_cursor_is_on(
window = _show(make_window(), (1440, 880))
window.brand_bar.btn_playback.click()
QApplication.processEvents()
assert window._load_recording(replay_recording)
assert window.replay_ctl.load_recording(replay_recording)
QApplication.processEvents()
plots = window.force_plots
assert plots.mode() == "browse", "opening a recording did not browse it"
@@ -2028,7 +2027,7 @@ def test_a_raw_only_recording_is_never_shown_in_newton(make_window, replay_recor
QApplication.processEvents()
window.brand_bar.btn_playback.click()
QApplication.processEvents()
assert window._load_recording(replay_recording)
assert window.replay_ctl.load_recording(replay_recording)
QApplication.processEvents()
panel = window.force_plots
@@ -2051,7 +2050,7 @@ def test_the_replay_transport_plays_and_pauses(make_window, replay_recording):
window = _show(make_window(), (1440, 880))
window.brand_bar.btn_playback.click()
QApplication.processEvents()
assert window._load_recording(replay_recording)
assert window.replay_ctl.load_recording(replay_recording)
QApplication.processEvents()
panel = window.force_plots
@@ -2091,7 +2090,7 @@ def test_the_recompute_switch_asks_for_analysis_not_an_acquire_chain(
window = _show(make_window(), (1440, 880))
window.brand_bar.btn_playback.click()
QApplication.processEvents()
assert window._load_recording(replay_recording)
assert window.replay_ctl.load_recording(replay_recording)
QApplication.processEvents()
panel = window.force_plots
assert panel.is_recording_source(), "the charts do not read the recording"
@@ -2140,7 +2139,7 @@ def test_the_replay_view_offers_a_stop_that_always_works(make_window, replay_rec
window = _show(make_window(), (1440, 880))
window.brand_bar.btn_playback.click()
QApplication.processEvents()
assert window._load_recording(replay_recording)
assert window.replay_ctl.load_recording(replay_recording)
QApplication.processEvents()
assert window.toolbar.btn_reset.isEnabled(), (
@@ -2197,7 +2196,7 @@ def test_escape_stops_the_replay_too(make_window, replay_recording):
window = _show(make_window(), (1440, 880))
window.brand_bar.btn_playback.click()
QApplication.processEvents()
assert window._load_recording(replay_recording)
assert window.replay_ctl.load_recording(replay_recording)
QApplication.processEvents()
window.toolbar.btn_run.setChecked(True)
QApplication.processEvents()
@@ -2252,7 +2251,7 @@ def test_the_replay_reset_is_the_display_reset_and_never_the_device(
window._finger_worker = stuck
window.brand_bar.btn_playback.click()
QApplication.processEvents()
assert window._load_recording(replay_recording)
assert window.replay_ctl.load_recording(replay_recording)
QApplication.processEvents()
assert window.toolbar.reset_target() == "display"
@@ -2298,9 +2297,8 @@ def test_the_replay_reset_is_the_display_reset_and_never_the_device(
window.close()
def test_the_live_reset_zeroes_the_device_and_restarts_the_display_baseline(make_window):
"""The live half of the same control: the device is asked, and the tip's side
effect (the display baseline restarting) really happens."""
def test_the_live_reset_zeroes_the_device_without_changing_threshold(make_window):
"""The device reset must not erase or re-estimate the configured threshold."""
from orisys_app_shared.i18n import t
from PySide6.QtWidgets import QApplication
@@ -2322,14 +2320,11 @@ def test_the_live_reset_zeroes_the_device_and_restarts_the_display_baseline(make
assert window.toolbar.btn_reset.isEnabled()
assert window.toolbar.btn_reset.text() == t("toolbar.reset_sensor")
assert window.toolbar.btn_reset.toolTip() == t("toolbar.reset_sensor_tip")
restarts: list = []
window.force_plots.request_presentation_reestimate = lambda: restarts.append(True)
threshold = window.force_plots.presentation.min_deadband_for("raw")
window.toolbar.btn_reset.click()
assert stuck.reset_requests == 1, "the live reset did not reach the device"
assert restarts == [True], (
"the display baseline was not restarted with the zero (the tooltip's side effect)"
)
assert window.force_plots.presentation.min_deadband_for("raw") == threshold
assert window.statusBar().currentMessage() == t("status.reset_pending"), (
window.statusBar().currentMessage()
)
@@ -2460,7 +2455,7 @@ def test_a_bare_video_plays_even_though_it_is_not_a_recording(
window.brand_bar.btn_playback.click()
QApplication.processEvents()
assert not window.replay.has_video()
assert window._load_replay_video(bare) is True, "a playable video was refused"
assert window.replay_ctl.load_video(bare) is True, "a playable video was refused"
QApplication.processEvents()
assert window.replay.has_video(), "nothing is playable"
assert window.replay.is_video_only(), "a bare video was taken for a recording"
@@ -2534,8 +2529,8 @@ def test_the_replay_and_the_live_paths_share_one_image_backend(
app_module.render_image = spy
try:
window.layout_ctl.apply_preset("replay")
assert window._load_replay_video(bare) is True
window._render_replay_frame()
assert window.replay_ctl.load_video(bare) is True
window.replay_ctl.render_frame()
QApplication.processEvents()
assert window.arrow_panel.plan_modes() == ("recorded",), (
"a recording must not offer a derived view"
@@ -2623,7 +2618,7 @@ def test_the_replay_entries_exist_only_in_the_replay_view(
assert not window.toolbar.btn_recompute.isEnabled()
# Programmatic use is refused too, and leaves nothing half-changed.
assert window._load_replay_video(bare) is False
assert window.replay_ctl.load_video(bare) is False
QApplication.processEvents()
assert not window.replay.has_video()
assert window.toolbar.source_mode() == "camera"
@@ -2637,7 +2632,7 @@ def test_the_replay_entries_exist_only_in_the_replay_view(
QApplication.processEvents()
for action in (window.act_open_video, window.act_playback, window.act_playback_latest):
assert action.isEnabled()
assert window._load_replay_video(bare) is True
assert window.replay_ctl.load_video(bare) is True
QApplication.processEvents()
assert window.arrow_panel.plan_modes() == ("recorded",)
assert window.toolbar.btn_recompute.isEnabled(), (
@@ -2670,7 +2665,7 @@ def test_a_file_that_is_not_a_video_at_all_is_refused_with_a_reason(
try:
window.brand_bar.btn_playback.click()
QApplication.processEvents()
assert window._load_replay_video(broken) is False, (
assert window.replay_ctl.load_video(broken) is False, (
"a file that cannot be decoded was accepted for playback"
)
QApplication.processEvents()
@@ -2700,7 +2695,7 @@ def test_a_video_inside_a_recording_opens_the_recording_and_plays(
QApplication.processEvents()
segments = sorted(Path(replay_recording).glob("raw_*.mp4"))
assert segments, "the fixture wrote no video segment"
assert window._load_replay_video(segments[0]) is True
assert window.replay_ctl.load_video(segments[0]) is True
QApplication.processEvents()
assert window.replay.is_loaded()
assert window.toolbar.btn_run.isEnabled(), "the transport is not offered"
@@ -2736,11 +2731,11 @@ def test_a_recording_without_a_video_says_it_cannot_be_played(make_window, recor
window = _show(make_window(), (1440, 880))
window.brand_bar.btn_playback.click()
QApplication.processEvents()
if not window._load_recording(directory):
if not window.replay_ctl.load_recording(directory):
pytest.skip("this recording layout was refused before it could be played")
QApplication.processEvents()
assert window.replay.duration_s() <= 0.0
window._start_playback()
window.replay_ctl.start_playback()
QApplication.processEvents()
assert not window._play_timer.isActive()
assert t("playback.empty_recording")[:8] in window.statusBar().currentMessage(), (
@@ -2784,7 +2779,7 @@ def test_a_selection_moves_its_statistics_onto_the_value_cards(make_window):
), summary
# The numbers a reader cannot fit in the strip are on the cards instead.
expected_peak = float(plots._selection.peaks[0])
expected = f"{expected_peak:.3f}"
expected = f"{expected_peak:.2f}"
assert plots.value_labels[0].text() == expected, (
f"the FN card is not the selection's peak: {plots.value_labels[0].text()!r}"
)
@@ -3074,9 +3069,9 @@ def test_the_plot_dialog_carries_the_ranges_and_the_zero_threshold(qt_app):
assert raw.settings()["fn_raw_min"] == 0.0
assert raw.settings()["fn_min"] == 0.0
assert raw.settings()["window_sec"] == 4.0
assert raw.deadbands() == {"deadband_n": 0.0, "deadband_raw": 0.0}, (
"the shipped deadband answer is 'auto'"
)
assert raw.deadbands() == {"deadband_n": 0.0, "deadband_raw": 50.0}
assert raw.settings()["newton_decimals"] == 2
assert raw.settings()["raw_decimals"] == 0
def test_the_plot_dialog_values_reach_the_chart(make_window):
@@ -3113,12 +3108,11 @@ def test_the_plot_dialog_values_reach_the_chart(make_window):
window.close()
def test_the_display_baseline_is_on_for_every_sensor_by_default(make_window):
"""It used to be Finger-raw only; an idle box sensor drifts just as much."""
def test_force_threshold_is_on_for_every_sensor_by_default(make_window):
window, stuck = _running_window(make_window) # sensor_type="box"
plots = window.force_plots
assert plots._presentation_enabled is True, "a box sensor lost its baseline"
assert plots._presentation_enabled is True
assert plots.presentation.enabled is True
# The per-unit floors came from the preferences, not from a sensor type.
assert plots.presentation.min_deadband_for("raw") == 50.0