Уточнить обработку плотных трендов и восстановление сигналов
This commit is contained in:
@@ -31,7 +31,8 @@ enum set_plot_operation {
|
|||||||
PCAN_ABI_API uint32_t set_plot_abi_version(void);
|
PCAN_ABI_API uint32_t set_plot_abi_version(void);
|
||||||
/** Evaluates one operation. Returns output count, or 0 for invalid arguments.
|
/** Evaluates one operation. Returns output count, or 0 for invalid arguments.
|
||||||
* TRANSFORM rejects malformed viewports; invalid gesture values return the
|
* TRANSFORM rejects malformed viewports; invalid gesture values return the
|
||||||
* unchanged viewport. Zoom is restricted to 1..128, with focus anchoring.
|
* unchanged viewport. Zoom is restricted to 1..10000000 on X and 1..128 on Y,
|
||||||
|
* with focus anchoring.
|
||||||
* Screen fractions increase downwards; set inverted=1 for the Y value axis.
|
* Screen fractions increase downwards; set inverted=1 for the Y value axis.
|
||||||
*/
|
*/
|
||||||
PCAN_ABI_API size_t set_plot_eval(uint32_t operation, const double *input,
|
PCAN_ABI_API size_t set_plot_eval(uint32_t operation, const double *input,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ extern "C" {
|
|||||||
#endif
|
#endif
|
||||||
enum { SET_SIGNAL_POLYNOMIAL, SET_SIGNAL_LINEAR, SET_SIGNAL_PCHIP, SET_SIGNAL_SPLINE };
|
enum { SET_SIGNAL_POLYNOMIAL, SET_SIGNAL_LINEAR, SET_SIGNAL_PCHIP, SET_SIGNAL_SPLINE };
|
||||||
/* Caller-owned workspace: at least 14*count+128 doubles. No heap or globals.
|
/* Caller-owned workspace: at least 14*count+128 doubles. No heap or globals.
|
||||||
|
* Bounds: 2..100000 input points, 2..1000000 output points.
|
||||||
* Inputs may be unsorted; duplicate times are averaged. No extrapolation.
|
* Inputs may be unsorted; duplicate times are averaged. No extrapolation.
|
||||||
* meta[0..2] = original count, unique count, RMSE at original measurements.
|
* meta[0..2] = original count, unique count, RMSE at original measurements.
|
||||||
* endpoint=1 includes the last time; 0 samples a cyclic period [first,last).
|
* endpoint=1 includes the last time; 0 samples a cyclic period [first,last).
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
#include "set_plot.h"
|
#include "set_plot.h"
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
|
/* Allow sample-level X navigation in multi-million-point recordings. */
|
||||||
|
#define MIN_VIEW_WIDTH (1.0 / 10000000)
|
||||||
|
|
||||||
static double clamp(double v, double lo, double hi) { return fmin(hi, fmax(lo, v)); }
|
static double clamp(double v, double lo, double hi) { return fmin(hi, fmax(lo, v)); }
|
||||||
static int finite_values(const double *v, size_t n) {
|
static int finite_values(const double *v, size_t n) {
|
||||||
size_t i;
|
size_t i;
|
||||||
@@ -23,12 +26,12 @@ size_t set_plot_eval(uint32_t op, const double *v, size_t n, double *out, size_t
|
|||||||
cap < (op == SET_PLOT_TRANSFORM || op == SET_PLOT_LIMITS ? 4U : 1U)) return 0;
|
cap < (op == SET_PLOT_TRANSFORM || op == SET_PLOT_LIMITS ? 4U : 1U)) return 0;
|
||||||
if (op == SET_PLOT_TRANSFORM) {
|
if (op == SET_PLOT_TRANSFORM) {
|
||||||
double w, h, fx, fy;
|
double w, h, fx, fy;
|
||||||
if (!finite_values(v, 4) || v[2] < 1.0/128 || v[2] > 1 ||
|
if (!finite_values(v, 4) || v[2] < MIN_VIEW_WIDTH || v[2] > 1 ||
|
||||||
v[3] < 1.0/128 || v[3] > 1 || v[0] < 0 || v[1] < 0 ||
|
v[3] < 1.0/128 || v[3] > 1 || v[0] < 0 || v[1] < 0 ||
|
||||||
v[0] + v[2] > 1.000000000001 || v[1] + v[3] > 1.000000000001) return 0;
|
v[0] + v[2] > 1.000000000001 || v[1] + v[3] > 1.000000000001) return 0;
|
||||||
out[0] = v[0]; out[1] = v[1]; out[2] = v[2]; out[3] = v[3];
|
out[0] = v[0]; out[1] = v[1]; out[2] = v[2]; out[3] = v[3];
|
||||||
if (!finite_values(v + 4, 6) || v[4] <= 0 || v[5] <= 0) return 4;
|
if (!finite_values(v + 4, 6) || v[4] <= 0 || v[5] <= 0) return 4;
|
||||||
w = clamp(v[2] / v[4], 1.0/128, 1);
|
w = clamp(v[2] / v[4], MIN_VIEW_WIDTH, 1);
|
||||||
h = clamp(v[3] / v[5], 1.0/128, 1);
|
h = clamp(v[3] / v[5], 1.0/128, 1);
|
||||||
fx = clamp(v[8], 0, 1); fy = clamp(v[9], 0, 1);
|
fx = clamp(v[8], 0, 1); fy = clamp(v[9], 0, 1);
|
||||||
out[0] = clamp(v[0] + (v[2] - w) * fx - v[6] * w, 0, 1 - w);
|
out[0] = clamp(v[0] + (v[2] - w) * fx - v[6] * w, 0, 1 - w);
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ int set_signal_reconstruct(const double *input_x,const double *input_y,size_t co
|
|||||||
double coef[6]={0},r[6][6]={{0}},origin,span,scale=1,rmse=0;
|
double coef[6]={0},r[6][6]={{0}},origin,span,scale=1,rmse=0;
|
||||||
size_t i,j,k,n=0; unsigned pass;
|
size_t i,j,k,n=0; unsigned pass;
|
||||||
if(!input_x||!input_y||!out_x||!out_y||!meta||!work || count<2||count>100000
|
if(!input_x||!input_y||!out_x||!out_y||!meta||!work || count<2||count>100000
|
||||||
||output_count<2||output_count>10000||work_count<14*count+128
|
||output_count<2||output_count>1000000||work_count<14*count+128
|
||||||
||method<0||method>3||(endpoint!=0&&endpoint!=1)) return 1;
|
||method<0||method>3||(endpoint!=0&&endpoint!=1)) return 1;
|
||||||
pairs=(point *)work; x=work+2*count; y=x+count; d=y+count; h=d+count;
|
pairs=(point *)work; x=work+2*count; y=x+count; d=y+count; h=d+count;
|
||||||
s=h+count; q=s+count; column=q+6*count;
|
s=h+count; q=s+count; column=q+6*count;
|
||||||
|
|||||||
2
c/set-protocol/tests/fixtures/plot-v1.json
vendored
2
c/set-protocol/tests/fixtures/plot-v1.json
vendored
@@ -4,7 +4,7 @@
|
|||||||
{"name":"zoom_x_anchor","op":0,"input":[0,0,1,1,2,1,0,0,0.25,0.5],"output":[0.125,0,0.5,1]},
|
{"name":"zoom_x_anchor","op":0,"input":[0,0,1,1,2,1,0,0,0.25,0.5],"output":[0.125,0,0.5,1]},
|
||||||
{"name":"zoom_y_anchor","op":0,"input":[0,0,1,1,1,4,0,0,0.5,0.75],"output":[0,0.5625,1,0.25]},
|
{"name":"zoom_y_anchor","op":0,"input":[0,0,1,1,1,4,0,0,0.5,0.75],"output":[0,0.5625,1,0.25]},
|
||||||
{"name":"pan_clamp","op":0,"input":[0.25,0.25,0.5,0.5,1,1,20,-20,0.5,0.5],"output":[0,0.5,0.5,0.5]},
|
{"name":"pan_clamp","op":0,"input":[0.25,0.25,0.5,0.5,1,1,20,-20,0.5,0.5],"output":[0,0.5,0.5,0.5]},
|
||||||
{"name":"zoom_limit","op":0,"input":[0,0,1,1,1000,1000,0,0,0.5,0.5],"output":[0.49609375,0.49609375,0.0078125,0.0078125]},
|
{"name":"zoom_limit","op":0,"input":[0,0,1,1,1000,1000,0,0,0.5,0.5],"output":[0.4995,0.49609375,0.001,0.0078125]},
|
||||||
{"name":"ignore_invalid_zoom","op":0,"input":[0.25,0.25,0.5,0.5,0,1,0,0,0.5,0.5],"output":[0.25,0.25,0.5,0.5]},
|
{"name":"ignore_invalid_zoom","op":0,"input":[0.25,0.25,0.5,0.5,0,1,0,0,0.5,0.5],"output":[0.25,0.25,0.5,0.5]},
|
||||||
{"name":"horizontal_spread","op":1,"input":[100,2,8],"output":[1]},
|
{"name":"horizontal_spread","op":1,"input":[100,2,8],"output":[1]},
|
||||||
{"name":"vertical_contract","op":1,"input":[2,-100,8],"output":[2]},
|
{"name":"vertical_contract","op":1,"input":[2,-100,8],"output":[2]},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from dataclasses import dataclass
|
|||||||
from protocan.legacycan import PROJECTS, LegacyCanFormat, decode
|
from protocan.legacycan import PROJECTS, LegacyCanFormat, decode
|
||||||
|
|
||||||
from .can485_board import BoardFrame, SdLogImport, parse_sd_log
|
from .can485_board import BoardFrame, SdLogImport, parse_sd_log
|
||||||
|
from .trend_limits import MAX_TOTAL_POINTS
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -63,7 +64,7 @@ def _values(frame: BoardFrame):
|
|||||||
|
|
||||||
|
|
||||||
def read_can_log(lines, *, reference_timestamp: float, capacity: int,
|
def read_can_log(lines, *, reference_timestamp: float, capacity: int,
|
||||||
max_total_points: int = 1_000_000,
|
max_total_points: int = MAX_TOTAL_POINTS,
|
||||||
frame_limit: int = 100_000) -> tuple[list[CanLogChannel], SdLogImport]:
|
frame_limit: int = 100_000) -> tuple[list[CanLogChannel], SdLogImport]:
|
||||||
imported = parse_sd_log(lines, reference_timestamp, limit=frame_limit)
|
imported = parse_sd_log(lines, reference_timestamp, limit=frame_limit)
|
||||||
channels = {}
|
channels = {}
|
||||||
|
|||||||
@@ -9,11 +9,35 @@ are separate objects, never channels in the acquisition or measurement model.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import csv
|
import csv
|
||||||
|
from bisect import bisect_left, bisect_right
|
||||||
|
from operator import itemgetter
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from .signal_reconstruction import METHODS, reconstruct
|
from .signal_reconstruction import METHODS, reconstruct
|
||||||
|
|
||||||
|
|
||||||
|
class FrozenPoints(tuple):
|
||||||
|
"""Normalized immutable samples, reusable across processing requests."""
|
||||||
|
|
||||||
|
def __new__(cls, points):
|
||||||
|
if isinstance(points, cls):
|
||||||
|
return points
|
||||||
|
result = super().__new__(cls, ((float(x), float(y)) for x, y in points))
|
||||||
|
result.ordered = all(result[i - 1][0] <= result[i][0] for i in range(1, len(result)))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def between(self, left, right):
|
||||||
|
if self.ordered:
|
||||||
|
start = bisect_left(self, left, key=itemgetter(0))
|
||||||
|
stop = bisect_right(self, right, key=itemgetter(0))
|
||||||
|
if start == 0 and stop == len(self):
|
||||||
|
return self
|
||||||
|
result = tuple.__new__(type(self), self[start:stop])
|
||||||
|
result.ordered = True
|
||||||
|
return result
|
||||||
|
return FrozenPoints((x, y) for x, y in self if left <= x <= right)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Axis:
|
class Axis:
|
||||||
label: str = "Время"
|
label: str = "Время"
|
||||||
@@ -36,7 +60,7 @@ class Series:
|
|||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
# A source may reuse mutable buffers immediately after publication.
|
# A source may reuse mutable buffers immediately after publication.
|
||||||
object.__setattr__(self, "points", tuple((float(x), float(y)) for x, y in self.points))
|
object.__setattr__(self, "points", FrozenPoints(self.points))
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -103,7 +127,7 @@ def prepare(snapshot, key, method="pchip", output_count=1000, degree=2):
|
|||||||
if snapshot.x_range is not None:
|
if snapshot.x_range is not None:
|
||||||
left, right = snapshot.x_range
|
left, right = snapshot.x_range
|
||||||
selected = Series(selected.key, selected.label,
|
selected = Series(selected.key, selected.label,
|
||||||
tuple((x, y) for x, y in selected.points if left <= x <= right),
|
selected.points.between(left, right),
|
||||||
y_unit=selected.y_unit)
|
y_unit=selected.y_unit)
|
||||||
return Request(selected, snapshot.axis, snapshot.source, method, output_count, degree, snapshot.x_range)
|
return Request(selected, snapshot.axis, snapshot.source, method, output_count, degree, snapshot.x_range)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ from protocan.plot import Bounds, Viewport
|
|||||||
|
|
||||||
|
|
||||||
def small_amplitude(values, threshold=30):
|
def small_amplitude(values, threshold=30):
|
||||||
|
extent = getattr(values, "finite_extent", None)
|
||||||
|
if extent is not None:
|
||||||
|
return max(abs(extent[0]), abs(extent[1])) <= threshold
|
||||||
magnitude = max((abs(v) for v in values if math.isfinite(v)), default=None)
|
magnitude = max((abs(v) for v in values if math.isfinite(v)), default=None)
|
||||||
return magnitude is not None and magnitude <= threshold
|
return magnitude is not None and magnitude <= threshold
|
||||||
|
|
||||||
@@ -52,7 +55,8 @@ class DualAxisPlot:
|
|||||||
for series in self._series:
|
for series in self._series:
|
||||||
if series.visible and not series.discrete:
|
if series.visible and not series.discrete:
|
||||||
groups[series.key in self.right_axis_keys].extend(
|
groups[series.key in self.right_axis_keys].extend(
|
||||||
v for v in series.values if math.isfinite(v))
|
getattr(series.values, "finite_extent", None) or
|
||||||
|
(v for v in series.values if math.isfinite(v)))
|
||||||
ranges = []
|
ranges = []
|
||||||
for values in groups:
|
for values in groups:
|
||||||
low, high = (min(values), max(values)) if values else (0, 1)
|
low, high = (min(values), max(values)) if values else (0, 1)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ except ImportError:
|
|||||||
QVBoxLayout, QWidget,
|
QVBoxLayout, QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from set_devices.signal_reconstruction import METHODS
|
from set_devices.signal_reconstruction import MAX_OUTPUT_POINTS, METHODS
|
||||||
from set_devices.plot_processing import Snapshot, prepare, process, write_csv
|
from set_devices.plot_processing import Snapshot, prepare, process, write_csv
|
||||||
|
|
||||||
RESULT_COLORS = ("#FF70D0", "#50D5FF", "#FFD166", "#83E377", "#B99AFF", "#FF9870")
|
RESULT_COLORS = ("#FF70D0", "#50D5FF", "#FFD166", "#83E377", "#B99AFF", "#FF9870")
|
||||||
@@ -88,8 +88,8 @@ class SignalProcessingPanel(QWidget):
|
|||||||
row.addWidget(self.degree)
|
row.addWidget(self.degree)
|
||||||
row.addWidget(QLabel("Точек результата"))
|
row.addWidget(QLabel("Точек результата"))
|
||||||
self.count = QSpinBox()
|
self.count = QSpinBox()
|
||||||
self.count.setRange(2, 10000)
|
self.count.setRange(2, MAX_OUTPUT_POINTS)
|
||||||
self.count.setValue(1000)
|
self.count.setValue(10000)
|
||||||
row.addWidget(self.count)
|
row.addWidget(self.count)
|
||||||
self.apply_button = QPushButton("Рассчитать")
|
self.apply_button = QPushButton("Рассчитать")
|
||||||
self.apply_button.clicked.connect(self.calculate)
|
self.apply_button.clicked.connect(self.calculate)
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from dataclasses import dataclass
|
|||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from protocan.native import NativeProtocol
|
from protocan.native import NativeProtocol
|
||||||
|
|
||||||
|
MAX_OUTPUT_POINTS = 1_000_000
|
||||||
|
|
||||||
METHODS = {"polynomial": "Аппроксимация: полином МНК", "linear": "Интерполяция: линейная",
|
METHODS = {"polynomial": "Аппроксимация: полином МНК", "linear": "Интерполяция: линейная",
|
||||||
"pchip": "Интерполяция: PCHIP", "spline": "Восстановление: кубический сплайн"}
|
"pchip": "Интерполяция: PCHIP", "spline": "Восстановление: кубический сплайн"}
|
||||||
|
|
||||||
@@ -35,8 +37,8 @@ def library():
|
|||||||
raise ValueError("Пересоберите SETProtocol с set_signal.c и set_wavegen.c") from error
|
raise ValueError("Пересоберите SETProtocol с set_signal.c и set_wavegen.c") from error
|
||||||
|
|
||||||
def reconstruct(points, method="pchip", output_count=1000, degree=2, *, endpoint=True, with_model=False):
|
def reconstruct(points, method="pchip", output_count=1000, degree=2, *, endpoint=True, with_model=False):
|
||||||
if method not in METHODS or type(output_count) is not int or not 2 <= output_count <= 10000:
|
if method not in METHODS or type(output_count) is not int or not 2 <= output_count <= MAX_OUTPUT_POINTS:
|
||||||
raise ValueError("Неизвестный метод или число выходных точек вне 2…10000")
|
raise ValueError(f"Неизвестный метод или число выходных точек вне 2…{MAX_OUTPUT_POINTS}")
|
||||||
count = len(points)
|
count = len(points)
|
||||||
if not 2 <= count <= 100000 or type(degree) is not int or not 1 <= degree <= 5:
|
if not 2 <= count <= 100000 or type(degree) is not int or not 1 <= degree <= 5:
|
||||||
raise ValueError("Нужно 2…100000 исходных точек, степень 1…5")
|
raise ValueError("Нужно 2…100000 исходных точек, степень 1…5")
|
||||||
|
|||||||
5
python/set_devices/trend_limits.py
Normal file
5
python/set_devices/trend_limits.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Shared limits for host-side trend history buffers."""
|
||||||
|
|
||||||
|
DEFAULT_CAPACITY = 100_000
|
||||||
|
MAX_CAPACITY = 5_000_000
|
||||||
|
MAX_TOTAL_POINTS = 10_000_000
|
||||||
@@ -53,6 +53,21 @@ class PlotTests(unittest.TestCase):
|
|||||||
crossed = markers.move(Marker.A, 1900).move(Marker.B, 1100)
|
crossed = markers.move(Marker.A, 1900).move(Marker.B, 1100)
|
||||||
self.assertEqual(-800, self.core.delta(crossed.a, crossed.b))
|
self.assertEqual(-800, self.core.delta(crossed.a, crossed.b))
|
||||||
|
|
||||||
|
def test_deep_x_zoom_preserves_anchor_and_allows_pan_and_zoom_out(self):
|
||||||
|
view = Viewport()
|
||||||
|
for _ in range(65):
|
||||||
|
view = view.transform(self.core, zoom_x=1.25, focus_x=.3)
|
||||||
|
self.assertAlmostEqual(.3, view.x + .3 * view.width)
|
||||||
|
self.assertLess(view.width * 5_000_000, 3)
|
||||||
|
moved = view.transform(self.core, pan_x=.2)
|
||||||
|
self.assertLess(moved.x, view.x)
|
||||||
|
self.assertEqual(view.width, moved.width)
|
||||||
|
expanded = moved.transform(self.core, zoom_x=.8)
|
||||||
|
self.assertGreater(expanded.width, moved.width)
|
||||||
|
limited = view.transform(self.core, zoom_x=1e10)
|
||||||
|
self.assertEqual(1e-7, limited.width)
|
||||||
|
self.assertEqual(limited, limited.transform(self.core, zoom_x=2))
|
||||||
|
|
||||||
def test_invalid_numeric_inputs_do_not_escape_to_painter(self):
|
def test_invalid_numeric_inputs_do_not_escape_to_painter(self):
|
||||||
for value in (math.nan, math.inf, -math.inf):
|
for value in (math.nan, math.inf, -math.inf):
|
||||||
self.assertEqual(Viewport(), Viewport().transform(self.core, zoom_x=value))
|
self.assertEqual(Viewport(), Viewport().transform(self.core, zoom_x=value))
|
||||||
|
|||||||
@@ -21,6 +21,20 @@ from set_devices.signal_reconstruction import METHODS
|
|||||||
|
|
||||||
|
|
||||||
class PlotProcessingTests(unittest.TestCase):
|
class PlotProcessingTests(unittest.TestCase):
|
||||||
|
def test_million_output_points_for_all_methods(self):
|
||||||
|
snapshot = Snapshot([Series("a", "A", [(0, 1), (1, 3), (2, 5)])])
|
||||||
|
for method in METHODS:
|
||||||
|
with self.subTest(method=method):
|
||||||
|
curve = process(prepare(snapshot, "a", method, 1_000_000, 1))
|
||||||
|
self.assertEqual(1_000_000, len(curve.points))
|
||||||
|
self.assertEqual((0., 2.), (curve.points[0][0], curve.points[-1][0]))
|
||||||
|
for x, y in curve.points[::10000]:
|
||||||
|
self.assertAlmostEqual(1 + 2 * x, y)
|
||||||
|
self.assertAlmostEqual(5., curve.points[-1][1])
|
||||||
|
self.assertTrue(curve.pieces)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
process(prepare(snapshot, "a", "linear", 1_000_001))
|
||||||
|
|
||||||
def test_snapshot_copies_mutable_samples_and_excludes_hidden_and_digital(self):
|
def test_snapshot_copies_mutable_samples_and_excludes_hidden_and_digital(self):
|
||||||
points = [[0, 0], [1, 1]]
|
points = [[0, 0], [1, 1]]
|
||||||
channel = Series("a", "Analog", points)
|
channel = Series("a", "Analog", points)
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class ProcessingQtTests(unittest.TestCase):
|
|||||||
self.panel.method.setCurrentIndex(self.panel.method.findData(method))
|
self.panel.method.setCurrentIndex(self.panel.method.findData(method))
|
||||||
self.calculate()
|
self.calculate()
|
||||||
self.assertEqual(method, self.panel.curve.request.method)
|
self.assertEqual(method, self.panel.curve.request.method)
|
||||||
|
|
||||||
self.panel.count.setValue(self.panel.count.value() + 1)
|
self.panel.count.setValue(self.panel.count.value() + 1)
|
||||||
self.assertEqual(index + 1, len(self.panel.curves))
|
self.assertEqual(index + 1, len(self.panel.curves))
|
||||||
self.assertTrue(self.panel.export_button.isEnabled())
|
self.assertTrue(self.panel.export_button.isEnabled())
|
||||||
@@ -67,6 +68,12 @@ class ProcessingQtTests(unittest.TestCase):
|
|||||||
self.assertEqual((), self.panel.curves)
|
self.assertEqual((), self.panel.curves)
|
||||||
self.assertIsNone(self.panel.curve)
|
self.assertIsNone(self.panel.curve)
|
||||||
|
|
||||||
|
def test_large_output_count_reaches_worker(self):
|
||||||
|
self.assertEqual(10000, self.panel.count.value())
|
||||||
|
self.panel.count.setValue(1_000_000)
|
||||||
|
self.calculate()
|
||||||
|
self.assertEqual(1_000_000, len(self.panel.curve.points))
|
||||||
|
|
||||||
def test_repeated_request_does_not_duplicate_curve(self):
|
def test_repeated_request_does_not_duplicate_curve(self):
|
||||||
self.calculate()
|
self.calculate()
|
||||||
self.calculate()
|
self.calculate()
|
||||||
|
|||||||
Reference in New Issue
Block a user