GeckoCIRCUITS 二次开发实战:工具组扩展

一、开篇引言

在储能 PCS 的控制器开发中,我经常需要在仿真阶段验证控制算法——比如 SOGI-PLL 的锁相精度、PR 控制器的谐波抑制效果。MATLAB/Simulink 当然可以做,但它的仿真速度和电路级建模能力有限。GeckoCIRCUITS 是一个开源的电力电子仿真软件,电路仿真能力很强,同时开源,可以按自己的需要和使用习惯定制化自己的仿真工具。

但它的控制库只提供了基础的 PI、s 域传递函数,没有 z 域传递函数、矩阵运算,也没有重复控制、LQR等这些现代控制算法模块。于是我决定给它写一套。这篇文章记录我在这个过程中真正踩过的坑、做过的设计决策,以及最后验证的结果。

1.1 开发目标

  • 矩阵工具组:提供完整的矩阵运算链路,支持现代控制理论(LQR、状态空间、观测器等)的图形化建模
  • DSP 工具组:补齐数字信号处理常用算法模块,实现从仿真到嵌入式代码的无缝衔接

1.2 本文导航

本文将从架构剖析入手,详细讲述两大工具组的设计思路、实现细节及验证案例。


二、GeckoCIRCUITS 架构剖析(二次开发前置知识)

2.1 整体架构分层

┌─────────────────────────────────────────────────────┐
│  GUI 层(SchematischeEingabe2 / ToolBar 等)        │
├─────────────────────────────────────────────────────┤
│  组件注册层(ControlTyp / CircuitTyp 枚举)         │
├─────────────────────────────────────────────────────┤
│  控制模块层(RegelBlock 子类 + Calculator 计算器)  │
├─────────────────────────────────────────────────────┤
│  仿真内核层(SimulationsKern / LKMatrices)         │
├─────────────────────────────────────────────────────┤
│  数学基础库(Matrix / LUDecomposition / Polynom)   │
└─────────────────────────────────────────────────────┘

2.2 控制模块(Control Block)的核心设计模式

  • 抽象基类体系:RegelBlock.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/RegelBlock.java) → AbstractReglerSingleInputSingleOutput / AbstractReglerVariableInputs
  • Calculator 分离模式:UI 参数配置(ReglerXxx.java)与仿真计算(XxxCalculator.java)解耦
    • 关键接口:AbstractControlCalculatable.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/calculators/AbstractControlCalculatable.java) 的 berechneYOUT(double deltaT) 方法
  • 终端注册机制:TerminalControlInput / TerminalControlOutput 的动态数量管理
  • 组件注册入口:ControlTyp.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ControlTyp.java#L23-L137) 枚举(新增模块必须在此注册 ID)

2.3 矩阵数据传输协议

关键洞察:GeckoCIRCUITS 原有信号以标量为主,矩阵数据需要自定义序列化协议

矩阵输出信号(double[] 数组编码):
[0] = 行数 m
[1] = 列数 n
[2] = a₁₁, [3] = a₁₂, ..., [2+m*n] = aₘₙ

参考实现:ReglerMatrix.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerMatrix.java#L50-L92) 中的 MatrixOutputCalculator


三、矩阵工具组扩展设计与实现

matrix-tool

3.1 工具组功能全景

模块编号 类名 功能 输入 输出
96 ReglerMatrix 矩阵常量输入 0 矩阵 M
97 ReglerMatrixAdd 矩阵加法 A+B 矩阵×2 矩阵
98 ReglerMatrixGain 矩阵数乘 k·A 矩阵×1, 标量×1 矩阵
99 ReglerMatrixMult 矩阵乘法 A·B 矩阵×2 矩阵
100 ReglerMatrixView 矩阵可视化查看 矩阵×1 0
101 ReglerMatrixInverse 矩阵求逆 A⁻¹ 矩阵×1 矩阵
102 ReglerMatrixTranspose 矩阵转置 Aᵀ 矩阵×1 矩阵
103 ReglerMatrixDeterminant 行列式 det(A) 矩阵×1 标量
104 ReglerMatrixRank 矩阵秩 rank(A) 矩阵×1 标量
105 ReglerMatrixEigenvalues 特征值分解 矩阵×1 特征值向量
106 ReglerVectorInput 向量输入 0 列向量

3.2 核心功能以及坑点详解

3.2.1 组件注册与元数据定义(扩展机制)

亮点:

  • 枚举 + 静态 tinfo 声明式注册,新增模块只需追加一行枚举值
  • 模块 ID 96~106 与官方 0~95 物理隔离,避免版本升级冲突
  • ControlTypeInfo 捆绑类名 / 显示符号 / 国际化键,元信息单一来源
3.2.1.1 枚举批量注册(11 个矩阵模块一次性接入)

ControlTyp.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ControlTyp.java#L127-L137)

// —— 矩阵工具组模块 ID 规划:96~106 区间预留,与官方 0~95 解耦 ——
C_MATRIX(96,           ReglerMatrix.tinfo),           // 矩阵常量输入
C_MATRIX_ADD(97,       ReglerMatrixAdd.tinfo),        // 矩阵加法 A+B
C_MATRIX_GAIN(98,      ReglerMatrixGain.tinfo),       // 矩阵数乘 k·A
C_MATRIX_MULT(99,      ReglerMatrixMult.tinfo),       // 矩阵乘法 A·B
C_MATRIX_VIEW(100,     ReglerMatrixView.tinfo),       // 矩阵可视化
C_MATRIX_INV(101,      ReglerMatrixInverse.tinfo),    // 矩阵求逆 A⁻¹
C_MATRIX_TRANS(102,    ReglerMatrixTranspose.tinfo),  // 矩阵转置 Aᵀ
C_MATRIX_DET(103,      ReglerMatrixDeterminant.tinfo),// 行列式 det(A)
C_MATRIX_RANK(104,     ReglerMatrixRank.tinfo),       // 秩 rank(A)
C_MATRIX_EIG(105,      ReglerMatrixEigenvalues.tinfo),// 特征值分解
C_VECTOR_INPUT(106,    ReglerVectorInput.tinfo);      // 列向量输入

设计意图:矩阵模块 ID 从 96 起步,与原作者的 0~95 物理隔离,避免版本升级时的 ID 冲突,同时支持旧 .ipes 文件的 TokenMap 反向映射(getFromIntNumber())。


3.2.1.2 单个模块的元信息声明模式

ReglerMatrixMult.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerMatrixMult.java#L10-L19)

public final class ReglerMatrixMult extends RegelBlock {

    public static final ControlTypeInfo tinfo = new ControlTypeInfo(
        ReglerMatrixMult.class, "M*", I18nKeys.MATRIX_MULTIPLICATION
    );

    public ReglerMatrixMult() {
        super(0, 0);
        XIN.add(new TerminalControlInputWithLabel(this, -2, -XIN.size(), "M1"));
        XIN.add(new TerminalControlInputWithLabel(this, -2, -XIN.size(), "M2"));
        YOUT.add(new TerminalControlOutputWithLabel(this, 3, -YOUT.size(), "M"));
    }
}

设计模式:RegelBlock(UI 层)+ 内部 Calculator(仿真层)的解耦结构,ControlTypeInfo 绑定类名、显示符号、国际化键。


3.2.2 矩阵信号编码协议(自定义序列化)

亮点:GeckoCIRCUITS 原生只传标量 double[1],矩阵需要在 double[] 上自定义协议,这是整个矩阵工具组能跑通的"地基"。

优点:

  • 不改动仿真内核,仅在原生 double[] 通道上扩展出矩阵传输能力
  • 统一 [rows, cols, data…] 协议,模块间可直接级联,零类型转换
  • 定长缓冲 + 尾部置零,内存布局确定,适合实时仿真步进
3.2.2.1 协议定义 + 编码输出(以矩阵常量源为例)

ReglerMatrix.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerMatrix.java#L74-L91)

private void updateBufferContent() {
    int rows = mat.getRowDimension();
    int cols = mat.getColumnDimension();
    int dataSize = rows * cols;
    int totalSize = 2 + dataSize;
    // ———— 协议头部:[0]=行数, [1]=列数 ————
    _outputBuffer[0] = rows;
    _outputBuffer[1] = cols;
    // ———— 协议载荷:[2..2+m*n-1] = 行优先展开的矩阵元素 ————
    double[][] data = mat.getArray();
    int idx = 2;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            _outputBuffer[idx++] = data[i][j];
        }
    }
    // 尾部置零,保证与固定缓冲区 MAX_BUFFER_SIZE 对齐
    for (int i = totalSize; i < _outputBuffer.length; i++) {
        _outputBuffer[i] = 0;
    }
}

3.2.2.2 协议解码 + 维度校验 + 执行运算(以矩阵乘法为例)

核心设计思想:矩阵运算模块全部遵守同一套 [rows, cols, a₁₁, a₁₂, …, aₘₙ] 协议,模块间可直接级联搭建 LQR / 状态空间模型,无需任何类型转换。

ReglerMatrixMult.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerMatrixMult.java#L44-L127)

@Override
public void berechneYOUT(final double deltaT) {
    double[] input1 = _inputSignal[0];
    double[] input2 = _inputSignal[1];
    // ========== Step 1:输入有效性防御 ==========
    if (input1 == null || input2 == null || input1.length < 2 || input2.length < 2) {
        setFallbackOutput(_outputBuffer); return;
    }
    int rows1 = (int) input1[0]; int cols1 = (int) input1[1];
    int rows2 = (int) input2[0]; int cols2 = (int) input2[1];
    // ========== Step 2:维度兼容性断言(A.cols == B.rows) ==========
    if (cols1 != rows2 || rows1 <= 0 || cols1 <= 0 || cols2 <= 0) {
        setFallbackOutput(_outputBuffer); return;
    }
    if (rows1 > 10 || cols1 > 10 || rows2 > 10 || cols2 > 10) {
        setFallbackOutput(_outputBuffer); return;
    }
    // ========== Step 3:从 double[] 解码为 double[][] ==========
    double[][] data1 = new double[rows1][cols1];
    double[][] data2 = new double[rows2][cols2];
    int idx = 2;
    for (int i = 0; i < rows1; i++)
        for (int j = 0; j < cols1; j++) data1[i][j] = input1[idx++];
    idx = 2;
    for (int i = 0; i < rows2; i++)
        for (int j = 0; j < cols2; j++) data2[i][j] = input2[idx++];
    // ========== Step 4:调用数学库执行乘法 ==========
    Matrix mat1 = new Matrix(data1);
    Matrix mat2 = new Matrix(data2);
    Matrix result = mat1.times(mat2);
    // ========== Step 5:结果重新编码为 double[] ==========
    int resultRows = result.getRowDimension();
    int resultCols = result.getColumnDimension();
    _outputBuffer[0] = resultRows;
    _outputBuffer[1] = resultCols;
    double[][] resultData = result.getArray();
    idx = 2;
    for (int i = 0; i < resultRows; i++)
        for (int j = 0; j < resultCols; j++) _outputBuffer[idx++] = resultData[i][j];
}

3.2.3 参数持久化与工程坑点解决(ASCII 导入/导出 + 复制)

踩坑记录:这是二次开发最容易踩坑的部分——矩阵文本带换行符会被 ASCII 解析器截断,copyAdditionalParameters 忘记重写会导致复制后数据丢失。

应对策略:

  • 换行符转义保证多行矩阵文本在 .ipes 文件中读写往返不丢失
  • copyAdditionalParameters 深拷贝裸成员变量,复制模块不共享引用
  • 导入解析失败自动回退单位矩阵,容错性好
3.2.3.1 换行符转义编码(.ipes 文件读写)

ReglerMatrix.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerMatrix.java#L187-L213)

// ===== 导出:将矩阵文本中的 \n 编码为 \\n,防止 TokenMap 跨行截断 =====
@Override
protected void exportAsciiIndividual(final StringBuffer ascii) {
    super.exportAsciiIndividual(ascii);
    String encodedMatrixText = matrixText;
    if (encodedMatrixText.contains("\n")) {
        encodedMatrixText = encodedMatrixText.replaceAll("\n", "\\\\n");
    }
    DatenSpeicher.appendAsString(ascii.append("\nmatrixText"), encodedMatrixText);
}

// ===== 导入:反向解码 \\n → \n,还原用户编辑的多行格式 =====
@Override
protected void importIndividual(final TokenMap tokenMap) {
    super.importIndividual(tokenMap);
    if (tokenMap.containsToken("matrixText")) {
        String loadedMatrixText = tokenMap.readDataLine("matrixText", matrixText);
        String decodedMatrixText = loadedMatrixText;
        if (loadedMatrixText.contains("\\n") && !loadedMatrixText.contains("\n")) {
            decodedMatrixText = loadedMatrixText.replaceAll("\\\\n", "\n");
        }
        matrixText = decodedMatrixText;
        try { mat = parseMatrix(matrixText); }
        catch (Exception e) { mat = Matrix.identity(2, 2); }
    }
}
3.2.3.2 模块复制时的深拷贝(Ctrl-C 拖拽复制的数据同步)

踩坑记录:基类 copyFabric() → copyLKBlockPars() 默认只复制注册的 UserParameter,matrixText 和 mat 作为裸成员变量必须在 copyAdditionalParameters 中手动克隆,否则新粘贴的模块会回退到 "1,0;0,1" 单位矩阵。

ReglerMatrix.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerMatrix.java#L215-L229)

@Override
public void copyAdditionalParameters(final AbstractBlockInterface originalBlock) {
    super.copyAdditionalParameters(originalBlock);
    if (originalBlock instanceof ReglerMatrix) {
        ReglerMatrix orig = (ReglerMatrix) originalBlock;
        // 1. 复制原始文本(用户编辑内容)
        this.matrixText = orig.matrixText;
        // 2. 重新解析生成 Matrix 对象(避免引用共享)
        try { this.mat = orig.parseMatrix(this.matrixText); }
        catch (Exception e) { this.mat = Matrix.identity(2, 2); }
        // 3. 同步显示参数(行数列数)
        _displayRows.setUserValue(this.mat.getRowDimension());
        _displayCols.setUserValue(this.mat.getColumnDimension());
    }
}

3.2.4 高性能策略(输入变化检测 + 缓存复用)

踩坑记录:矩阵求逆 / 特征值是 O(n³) 运算,在仿真步长 1µs 级下每步重算会严重拖慢。采用"输入未变直接跳过"的增量计算策略。

应对策略:

  • 输入指纹逐元素比对,未变化则整帧跳过 O(n³) 运算
  • 时不变矩阵场景下长仿真只计算一次,加速效果显著
  • 同一模式在求逆 / 特征值模块间直接复用
3.2.4.1 输入指纹比较 + 缓存命中跳过(矩阵求逆)

ReglerMatrixInverse.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerMatrixInverse.java#L48-L79)

// 逐元素比较上一帧输入,指纹未变则整帧跳过
private boolean inputChanged(double[] newInput, double[] oldInput) {
    if (newInput == null || newInput.length != oldInput.length) return true;
    for (int i = 0; i < newInput.length; i++) {
        if (newInput[i] != oldInput[i]) return true;
    }
    return false;
}

@Override
public void berechneYOUT(final double deltaT) {
    // ... 输入拷贝到 _inputMatrix ...
    if (inputChanged(_inputMatrix, _lastInput)) {
        _needsRecalculation = true;
        System.arraycopy(_inputMatrix, 0, _lastInput, 0, _inputMatrix.length);
    }
    if (!_needsRecalculation) return; // 缓存命中,直接 exit
    // ... 执行 inverse() ...
    _needsRecalculation = false;       // 标记缓存有效
}

性能收益:在 LQR 控制中状态矩阵 A 通常时不变,10 秒仿真(步长 1µs)下,逆运算从 10⁷ 次缩减为 1 次,对 O(n³) 复杂度收益尤为显著。同样模式也在 ReglerMatrixEigenvalues.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerMatrixEigenvalues.java#L48-L79) 中复用。


3.2.5 动态端子 + 可配置向量(VariableTerminalNumber 接口)

亮点:向量输入模块支持用户动态增减输入维度(1~100),体现"可配置拓扑"的设计能力。

优点:

  • 端子数量随用户参数动态增删,模块拓扑可配置
  • 参数列表与物理端子成对扩缩容,状态始终一致
  • 复制 / 导入时先扩容再写数据,避免索引越界

ReglerVectorInput.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerVectorInput.java#L61-L77)

// 根据用户设置的 count,动态增删"默认值参数 + 是否连输入信号标志 + 物理端子"
private void updateInputTerminals(int count) {
    // —— 扩容:补齐参数列表与 XIN 端子 ——
    while (_defaultValues.size() < count) {
        int idx = _defaultValues.size();
        _defaultValues.add(UserParameter.Builder.<Double>start("default" + idx, 0.0).shortName("d" + idx).build());
        _useInputSignals.add(UserParameter.Builder.<Boolean>start("useInput" + idx, false).shortName("u" + idx).build());
        XIN.add(new TerminalControlInputWithLabelAndVisibility(this, -3, -XIN.size(), "x" + idx));
    }
    // —— 缩容:从尾部移除 ——
    while (_defaultValues.size() > count) {
        int idx = _defaultValues.size() - 1;
        _defaultValues.remove(idx);
        _useInputSignals.remove(idx);
        XIN.pop();
    }
    updateInputTerminalVisibility();
}

配套的复制/导入安全:在 copyAdditionalParameters 和 importIndividual 中必须先调用 updateInputTerminals(origCount) 扩容列表,再按索引写入数据,否则会触发 IndexOutOfBoundsException(实际踩坑点)。


3.2.6 矩阵对话框(Dialog)的交互设计

设计思想:

  • 文本解析(逗号+分号格式)
  • 实时预览 + 语法校验
  • 撤销/重做支持(AbstractUndoGenericModel)

ReglerMatrixDialog.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerMatrixDialog.java)

private void updatePreview() {
        String text = matrixTextArea.getText().trim();
        Matrix mat;
        try {
            mat = element.parseMatrix(text);
        } catch (Exception e) {
            matrixPreviewArea.setText("Invalid format!\nUse: row1;row2\nExample: 1,0;0,1");
            return;
        }
        // 格式化输出矩阵
        StringBuilder sb = new StringBuilder();
        sb.append("Size: ").append(mat.getRowDimension())
          .append(" x ").append(mat.getColumnDimension()).append("\n\n");
        // 逐行输出矩阵元素
        double[][] data = mat.getArray();
        for (int i = 0; i < mat.getRowDimension(); i++) {
            sb.append("[ ");
            for (int j = 0; j < mat.getColumnDimension(); j++) {
                sb.append(df.format(data[i][j]));
                if (j < mat.getColumnDimension() - 1) {
                    sb.append(", ");
                }
            }
            sb.append(" ]\n");
        }

        matrixPreviewArea.setText(sb.toString());
    }

四、DSP 工具组扩展设计与实现

dsp-tool

4.1 工具组功能全景

模块编号 类名 算法类型 典型应用
92 ReglerDPRCONTROL 数字比例谐振控制 并网逆变器电流控制
93 ReglerDiscreteTransferFunction 离散传递函数 G(z⁻¹) 数字滤波器设计
94 Regler1P1ZCompensator 一极一零补偿器 电压/电流环补偿
95 ReglerRepetitiveController 重复控制器 周期性误差抑制
107 ReglerSOGI 二阶广义积分器 电网同步(SOGI-PLL)
108 ReglerPark Park/反Park 变换 电机 dq 解耦控制
109 ReglerFrequencyDiscriminator 鉴频器 频率测量
110 ReglerZeroOrderHold 零阶保持 离散化建模
112 ReglerQuantizer 量化器 ADC/DAC 建模
113 ReglerPWM PWM 调制器 脉冲宽度调制

4.2 数字控制器设计(以 DPR 为例)

  • 准比例谐振(QPR)传递函数离散化(双线性变换 / 零极点匹配)
  • DPRControlCalculator.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/calculators/DPRControlCalculator.java) 差分方程实现
  • 参数整定界面:谐振频率、比例增益、谐振增益、截止带宽

4.3 DSP 工具组核心功能以及坑点详解

4.3.1 参数暴露方式的三种范式:对话框 / 引脚 / 混合

设计思想:DSP 工具组内部同时存在三种参数暴露范式,由仿真需求动态选择。

  • 静态结构参数(采样率、阻尼系数)走 UserParameter + 对话框,改参数不必重新连线
  • 运行时变化的量(谐振频率、控制器增益)走引脚,支持在线整定与外部闭环
  • 混合范式(SOGI)兼顾两者,是推荐默认
4.3.1.1 混合范式(以 SOGI 为例:会被闭环修改的参数是信号)

设计意图:f0 会跟随 PLL 输出实时波动(频率自适应 SOGI-PLL 的前提),必须作为信号参与闭环;而采样率、阻尼系数 K(默认 √2)在一次运行内不变,走 UserParameter + ReglerSOGIDialog。

ReglerSOGI.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerSOGI.java#L39-L47)

public ReglerSOGI() {
    super();

    XIN.add(new TerminalControlInputWithLabel(this, -3, -XIN.size(), "input"));
    XIN.add(new TerminalControlInputWithLabel(this, -3, -XIN.size(), "f0"));    // 谐振频率走引脚

    YOUT.add(new TerminalControlOutputWithLabel(this, 3, -YOUT.size(), "vd")); // 正交双输出
    YOUT.add(new TerminalControlOutputWithLabel(this, 3, -YOUT.size(), "vq"));
}
4.3.1.2 全引脚范式的极端案例(重复控制器:10 输入 3 输出)

踩坑记录 1(无对话框的代价):gain / vrms / work freq / ... 九个参数全部引脚化,虽然支持运行时在线整定(自适应控制的刚需),但每次修改参数都要重新连线,模块在原理图上占半个屏幕。Regler1P1ZCompensator 的 7 引脚是折中,但同样没有参数对话框(DialogWindowWithoutInput)。
踩坑记录 2(模块与其他模块节奏失调):YOUT 有 3 个端子(output/x0/y0),其中 x0/y0 用于外接数字滤波器,滤波输出又灌回模块的 x1/y1 ,模块间的仿真步频容易失调,并且仿真机制造成一拍仿真延迟。后来滤波器改为内置,由用户输入差分方程,避免了以上问题。

ReglerRepetitiveController.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerRepetitiveController.java#L20-L37)

XIN.add(new TerminalControlInputWithLabel(this, -3, -XIN.size(), "input"));
XIN.add(new TerminalControlInputWithLabel(this, -3, -XIN.size(), "gain"));
XIN.add(new TerminalControlInputWithLabel(this, -3, -XIN.size(), "vrms"));
XIN.add(new TerminalControlInputWithLabel(this, -3, -XIN.size(), "work freq"));
XIN.add(new TerminalControlInputWithLabel(this, -3, -XIN.size(), "sample freq"));
// ... lead num / x1 / y1 / max / min 共 10 个输入端子

4.3.2 PWM 动态输出端子:预创建 + 折叠栈 + 参数监听兜底

亮点:PWM 模块的输出端子数随模式在 1/2/4 间切换(单路 / 互补 / 移相互补),但没有沿用矩阵组"动态增删端子"的路线,而是构造时一次建满 4 个,再折叠收纳,从根本上避开了导入/复制时的索引越界。

  • 端子对象永不销毁,折叠栈(Stack)只挪引用,展开后连线关系可恢复
  • 折叠 / 展开 / 重排三个入口(setFolded / setExpanded / updateTerminalCount)收敛到同一个 getTargetTerminalCount() 计算
  • 用参数监听器兜住"从 .ipes 文件恢复"这条隐秘路径
4.3.2.1 构造时建满端子 + 参数监听器

ReglerPWM.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerPWM.java#L114-L133)

public ReglerPWM() {
    super();
    XIN.add(new TerminalControlInputWithLabel(this, -3, -XIN.size(), "duty"));
    YOUT.add(new TerminalControlOutputWithLabel(this, 3, -YOUT.size(), "pwm1"));
    YOUT.add(new TerminalControlOutputWithLabel(this, 3, -YOUT.size(), "pwm2"));
    YOUT.add(new TerminalControlOutputWithLabel(this, 3, -YOUT.size(), "pwm3"));
    YOUT.add(new TerminalControlOutputWithLabel(this, 3, -YOUT.size(), "pwm4"));
    // start in SINGLE mode: fold extra terminals
    setFolded();
    // when the mode parameter changes (user picks another mode in the dialog,
    // or the value is restored from a saved file) re-apply the terminal count.
    // Without this, reopening a file restores the mode value but leaves the
    // terminals folded to the default (SINGLE) count.
    _mode.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(final ActionEvent e) {
            updateTerminalCount();
        }
    });
}
4.3.2.2 折叠 / 展开的对称实现

ReglerPWM.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerPWM.java#L200-L233)

@Override
public void setFolded() {
    final int targetCount = getTargetTerminalCount();
    while (YOUT.size() > targetCount) {
        _terminalStack.push((TerminalControlOutput) YOUT.pop());
    }
}

@Override
public void setExpanded() {
    final int targetCount = getTargetTerminalCount();
    while (YOUT.size() < targetCount && !_terminalStack.isEmpty()) {
        YOUT.add(_terminalStack.pop());
    }
}

// updateTerminalCount():先按目标数折叠、再按目标数展开,两种方向一段代码全覆盖

踩坑记录(文件恢复端子不同步):注释原文就是事故现场——.ipes 重新打开时 mode 值能正确恢复,但端子仍停留在默认 SINGLE 的折叠数,pwm2~pwm4 的连线全部丢失。修复方式是给 UserParameter _mode 挂 ActionListener,让"参数值变化"(无论来自对话框还是文件导入)统一触发 updateTerminalCount()。凡是端子拓扑依赖参数的模块,参数监听器是必选项而非可选项。
对话框侧的同步:ReglerPWMDialog.processInputs() 除了写回 mode/carrier 两个参数,还必须显式调用 element.updateTerminalCount(),保证对话框"确认"后端子数立即跟上。


4.3.3 离散传递函数持久化:token 拼写即契约 + 定长数组防御

亮点:离散传递函数(DTF)的 ASCII 导出代码里藏着一个"将错就错"的兼容性智慧——token 命名是错的,但发布之后就是持久化契约,谁都不能改。

优点:

  • 读写 token 字符串严格一致,旧 .ipes 文件永远可读
  • 定长系数数组(MAX_ARRAY_SIZE = 20)让导出格式恒定、仿真期零 GC
  • 所有写入路径统一"清零 + 截断",杜绝旧系数残留
4.3.3.1 错位命名的 token(发布即冻结)

ReglerDiscreteTransferFunction.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerDiscreteTransferFunction.java#L121-L146)

@Override
protected void exportAsciiIndividual(final StringBuffer ascii) {
    ascii.append("\nnominatorPoles ");         // ← 拼写错误:应为 numerator
    for (int i = 0; i < _poles.length; i++) {  // ← 且存的确实是"极点" _poles
        ascii.append(_poles[i]);
        ascii.append(' ');
    }

    ascii.append("\ndenominatorZeros ");       // ← 零点 _zeros 挂在 denominator 前缀下
    for (int i = 0; i < _zeros.length; i++) {
        ascii.append(_zeros[i]);
        ascii.append(' ');
    }
    // nominatorPolynom / denominatorPolynom 同理
}

踩坑记录(token 是持久化契约):nominator 应为 numerator,且 nominatorPoles 里存的是极点、denominatorZeros 里存的是零点——双重错位。但 importIndividual() 用完全相同的字符串读取(tokenMap.readDataLine("nominatorPoles", _poles)),读写自洽,历史文件全部可用。二次开发时若"顺手修正"拼写,所有旧 .ipes 会静默读回默认值。对照矩阵组 3.2.3 的经验:持久化字符串发布即冻结,错了也只能将错就错,最多新增一个别名 token 做迁移。

4.3.3.2 定长数组的写入防御(清零 + 截断)

ReglerDiscreteTransferFunction.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerDiscreteTransferFunction.java#L168-L186)

public void setNumeratorPolynom(final List<Double> values) {
    for (int i = 0; i < _numeratorPolynom.length; i++) {
        _numeratorPolynom[i] = 0;              // 1. 先整段清零(清掉上一次的系数)
    }
    for (int i = 0; i < Math.min(values.size(), _numeratorPolynom.length); i++) {
        _numeratorPolynom[i] = values.get(i);  // 2. min() 截断,超长输入静默丢弃
    }
}

设计意图:定长数组换来确定性的内存布局与文件格式,代价是所有写入路径必须"清零 + 截断",否则上一次设置的 10 阶系数会残留在 3 阶新传递函数里,出现"明明改了参数波形却没变"的灵异问题。copyAdditionalParameters() 中对 4 个系数数组逐一 new + arraycopy 深拷贝——与矩阵组 3.2.3.2 是同一个坑。
脚本接口的类型防御:经 getOperationEnumInterfaces() 暴露给脚本引擎的 setNumeratorPolynom 等操作,入参可能是 double[] 也可能是 Double[](取决于调用方),入口处 checkParameterType() 两种都接并显式转换,其余类型抛 IllegalArgumentException——对外暴露的 Operation 接口不能假设调用方类型。


4.3.4 双模态对话框:初始化时序与交错存储

亮点:DTF 对话框支持"零极点增益"与"多项式系数"双模态互转,内部用实部/虚部交错的定长数组做统一存储,两种表示的互换全部收敛在对话框层。

4.3.4.1 零极点的交错存储(2i / 2i+1)

DialogDiscreteTransferFunction.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/DialogDiscreteTransferFunction.java#L153-L167)

private void updateReglerPolesZeros() {
    _reglerDTF.clearPolesAndZeros();
    for (int i = 0; i < _deNomModel.getSize(); i++) {
        // 复数极点:实部存偶数位,虚部存奇数位(2i / 2i+1 交错)
        _reglerDTF.setPole(((ComplexPrinter) _deNomModel.get(i))._value.getRe(), 2 * i);
        _reglerDTF.setPole(((ComplexPrinter) _deNomModel.get(i))._value.getIm(), 2 * i + 1);
    }
    // 零点 _zeros 同理
}
4.3.4.2 构造期初始化:强制走一次模态切换 + _initDone 门闩

DialogDiscreteTransferFunction.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/DialogDiscreteTransferFunction.java#L42-L72)

jRadButtPoly.setSelected(_reglerDTF._inPolynomMode.getValue());
_inPolynomialMode = !_reglerDTF._inPolynomMode.getValue(); // 强制制造状态差异
toggleMode();                                               // 骗过幂等检查,执行一次初始化
_inPolynomialMode = _reglerDTF._inPolynomMode.getValue();   // 恢复真实状态
// ...
_initDone = true;   // 此后监听器才允许写回模块
this.setVisible(true);

踩坑记录 1(构造期监听器误触发):toggleMode() 开头的幂等检查 if (_inPolynomialMode == jRadButtPoly.isSelected()) return; 会把"初始状态恰好一致"的首次初始化也拦掉。作者的解法是先人为取反 _inPolynomialMode,强制走一遍模态切换逻辑(读系数、刷视图、设可见性),再恢复。属于丑但有效的 UI 初始化惯用法。
踩坑记录 2(_initDone 门闩):构造过程中 setSelected / setVisible 会触发已注册的监听器,jCheckBoxEnabledActionPerformed 里用 if(_initDone) 挡住,避免对话框还没显示就把模块状态改掉。任何"监听器写回模块"的对话框都需要这层门闩。
踩坑记录 3(幽灵依赖兄弟类):零极点列表的容量上限实际引用的是连续域版本 ReglerTransferFunction.MAX_ARRAY_SIZE,而不是本类的同名常量——复制粘贴残留。两个常量目前恰好相等所以没爆雷,一旦单独改其中一个,对话框上限就与持久化数组容量脱钩。

对话框的顺序耦合(PWM 对话框):ReglerPWMDialog.updateParameterVisibility() 通过 parameterPanel.getComponents()[4..7] 硬编码索引切换死区时间 / 移相参数的可见性——createParameterPanel() 的传参顺序成了隐式契约,中间插一个新参数,可见性切换就整体错位。声明式参数面板省了代码,也埋了顺序耦合,新增参数必须检查所有按索引取组件的地方。


4.3.5 Calculator 的唯一入口与"空壳类"残留

亮点:所有 DSP 模块的仿真计算只从 getInternalControlCalculatableForSimulationStart() 一个口子交给内核;UI 类里持有的 Calculator 引用一概不参与仿真。

ReglerDiscreteTransferFunction.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerDiscreteTransferFunction.java#L110-L113)

@Override
public AbstractControlCalculatable getInternalControlCalculatableForSimulationStart() {
    return new DifferenceEquationCalculator(_sampleRate.getValue(), _denomPolynom, _numeratorPolynom);
}

踩坑记录(空壳内部类):文件里同时存在一个内部类 TransferFunctionCalculator(实现了 InitializableAtSimulationStart、IsDtChangeSensitive,但三个方法全空),它不注册给仿真内核、不被任何路径调用——是从早期 StateSpace 方案迁移到差分方程方案后的残留死代码(旁边还有成片注释掉的 _stateSpaceCalc / _savedState)。教训:UI 类内的任何 Calculator 成员都容易造成"改了它却没生效"的错觉,重构后要彻底删除空壳;反过来,想确认某段计算逻辑是否真的在跑,就搜它有没有从 getInternalControlCalculatableForSimulationStart() 返回。

posted @ 2026-10-04 19:12  JustKevinShen  阅读(7)  评论(0)    收藏  举报