feat: integrate cascade workflow

This commit is contained in:
ws50529
2026-05-27 08:38:40 +08:00
parent 63382f6cf5
commit b8ec600673
17 changed files with 572 additions and 500096 deletions
+54
View File
@@ -0,0 +1,54 @@
import assert from 'node:assert/strict';
import { computeStageFixedCoeffs, createCascadeFilterPayload, normalizeCascadeStage } from '../../src/cascade-stage.js';
const legacyStage = normalizeCascadeStage({
id: 'legacy-1',
filterType: 'Lowpass (低通)',
b_str: '1, 0',
a_str: '1',
});
assert.equal(legacyStage.id, 'legacy-1');
assert.equal(legacyStage.isActive, true);
assert.equal(legacyStage.isExpanded, false);
assert.equal(legacyStage.shiftBitsB, 14);
assert.deepEqual(Object.keys(legacyStage.fixedOverrides), ['a', 'b']);
const fixedStage = normalizeCascadeStage({
b_str: '0.5, -0.25',
a_str: '1, -0.5',
shiftBitsB: 4,
shiftBitsA: 5,
fixedOverrides: { b: { 1: -9 }, a: {} },
});
const fixed = computeStageFixedCoeffs(fixedStage, (value) => value.split(',').map(Number));
assert.deepEqual(fixed.b, [8, -9]);
assert.deepEqual(fixed.a, [32, -16]);
const stages = [
normalizeCascadeStage({ id: 'a', b_str: '1', a_str: '1', shiftBitsB: 12, shiftBitsA: 13, isActive: true }),
normalizeCascadeStage({ id: 'b', b_str: '2', a_str: '1', shiftBitsB: 10, shiftBitsA: 11, isActive: false }),
];
const payload = createCascadeFilterPayload({
stages,
fs: 100000,
shiftBitsIn: 9,
shiftBitsOut: 8,
useRound: true,
parseCoeffs: (value) => value.split(',').map(Number),
getStageFixedCoeffs: (stage) => ({
b: stage.b_str.split(',').map(Number),
a: stage.a_str.split(',').map(Number),
}),
});
assert.equal(payload.length, 2);
assert.equal(payload[0].b_str, '1');
assert.equal(payload[1].b_str, '2');
assert.equal(payload[0].shift_in, 9);
assert.equal(payload[0].shift_out, 12);
assert.equal(payload[1].shift_in, 12);
assert.equal(payload[1].shift_out, 8);
assert.equal(payload[1].isActive, false);
assert.equal(payload[0].use_round, true);
+47
View File
@@ -1,10 +1,15 @@
import asyncio
import json
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from fastapi import HTTPException
from fastapi.responses import JSONResponse
import dea.csv_processing as csv_processing
from dea.csv_processing import PRESET_FILE_ID
from dea_api import (
BodeCascadeParams,
BodeCompareParams,
@@ -18,6 +23,7 @@ from dea_api import (
design_filter,
filter_csv,
filter_csv_download,
preset_csv,
upload_csv,
write_mcu_command,
)
@@ -159,6 +165,47 @@ class DeaApiTest(unittest.TestCase):
raise AssertionError("Expected invalid MCU command validation to fail")
def test_preset_csv_endpoint_returns_local_ignored_file_metadata(self):
with tempfile.TemporaryDirectory() as tmp:
preset_path = Path(tmp) / "preset_signals.csv"
preset_path.write_text("time,value\n0,1\n1,2\n", encoding="utf-8")
with patch.object(csv_processing, "PRESET_CSV_PATH", str(preset_path)):
body = preset_csv()
self.assertTrue(body["available"])
self.assertEqual(body["file_id"], PRESET_FILE_ID)
self.assertEqual(body["columns"], ["time", "value"])
self.assertEqual(body["default_col_idx"], 1)
self.assertEqual(body["rows"], 2)
def test_filter_can_use_preset_file_id_and_skip_blank_signal_cells(self):
with tempfile.TemporaryDirectory() as tmp:
preset_path = Path(tmp) / "preset_signals.csv"
preset_path.write_text("time,value\n0,1\n1,\n2,3\n", encoding="utf-8")
with patch.object(csv_processing, "PRESET_CSV_PATH", str(preset_path)):
body = asyncio.run(
filter_csv(
file_id=PRESET_FILE_ID,
b="1",
a="1",
col_idx=1,
b_int=None,
a_int=None,
shift_in=14,
shift_out=14,
shift_b=14,
shift_a=14,
use_round=False,
)
)
self.assertEqual(body["total_points"], 2)
self.assertEqual(body["index"], [0, 2])
self.assertEqual(body["original"], [1.0, 3.0])
def test_filter_downsamples_plot_response_for_large_csv(self):
rows = ["value"] + [str(i) for i in range(6001)]
upload = InMemoryUpload(("\n".join(rows) + "\n").encode("utf-8"), "input.csv")