我编程的主要场景是在研发过程中使用,注意我的程序不是我最终研发的产品,这一点非常重要。如果你的程序是你最终的产品,氛围编程很可能不合适

相当多的时候,我只是需要一个程序快速完成一些任务,验证一些概念。我用的这些程序通常只是一次性或者一段时间内使用,由我个人或者团队内使用,所以不用特别考虑各种极端边界问题,也不必特别顾虑安全问题。所以,我经常使用AI帮我写程序,即所谓“氛围编程”。

用公理设计进行氛围编程:镜片对比度测量系统实战

本教程将通过开发“镜片成像对比度测量系统”的完整过程,介绍如何将公理设计方法应用于科研仪器软件的开发。我们将以公理设计的理论为指导,从科研目标出发,逐步将需求转化为功能需求(FR)、设计参数(DP)和过程变量(PV),构建设计矩阵并规划模块化的软件结构。通过这一实战案例,读者将学会如何在编程中遵循公理设计的两大原则(独立性公理和信息公理),实现逻辑清晰、易于测试和扩展的科研软件。

1. 公理设计简介

公理设计(Axiomatic Design, AD) 由 MIT Suh教授于 1990 年提出,是指导设计过程的一套理论框架。公理设计的核心是两条设计公理 (公理设计笔记(4) | GoldenGrape's Blog):

  • 独立性公理(Independence Axiom) : 最大化功能模块的独立性 (公理设计笔记(4) | GoldenGrape's Blog)。即设计应确保各个功能需求(Functional Requirements, FRs)彼此独立,由独立的设计参数(Design Parameters, DPs)来满足。如果每个FR都能通过调整某一个DP来实现且不影响其他FR,则满足独立性公理。独立性越高,意味着设计改动时彼此影响越小,因而后续修改需求时不会推倒重来,已有工作的浪费也越少 (公理设计笔记(4) | GoldenGrape's Blog)。

  • 信息公理(Information Axiom):最小化实现设计所需的信息量 (公理设计笔记(4) | GoldenGrape's Blog)。在满足独立性公理的所有有效设计中,包含信息量(复杂度)最少的设计是最优的 (公理化设计 - 中国大百科全书)。信息量越小,表示设计越简单明确,成功满足需求的概率就越高 (公理化设计 - 中国大百科全书)。因此在多个可行方案中,应选择信息量最少(即实现复杂度最低、参数不确定性最小)的方案。

公理设计将设计过程划分为四个领域(4-domain framework)

  • 用户域(Customer Domain):定义客户需求或科研目标,即我们“想要实现什么”。
  • 功能域(Functional Domain):将用户需求转化为功能需求(FR)及约束条件(Constraints),明确系统需要具备的功能。
  • 物理域(Physical Domain):提出满足功能需求的设计参数(DP),即系统的物理组成和方案,“如何实现”功能需求。
  • 过程域(Process Domain):确定实现设计参数的过程变量(PV),包括实现方案所需的工艺、算法和操作等细节。

相邻域之间通过映射建立对应关系:用户域到功能域回答的是“需要实现什么功能?”,功能域到物理域回答的是“用什么方案实现?”,物理域到过程域回答“如何具体实施方案?”。设计者通常采用“之字形”迭代,在FR和DP之间不断分解和映射,逐层推进设计。这一过程会产生设计矩阵来表征FR-DP间的关系,并以两条公理为准则评估设计优劣。

2. 将科研目标转化为 FR / DP / PV

An image to describe post

在公理设计方法中,从用户需求到功能需求的明确是第一步。对于本实例,“镜片成像对比度测量系统”的科研目标可以表述为:需要一种系统来测量光学镜头对不同空间频率图像的对比度传递特性。这是用户域的问题描述,我们需要将其转化为功能域的功能需求FR集合。

根据公理设计方法,我们首先列出系统必须满足的基本功能需求和约束,并确保不遗漏重要需求。为了满足独立性公理,功能需求的数量应尽可能少且相互独立。经过分析,我们确定本系统的主要FR如下:

  • FR1:提供可调的图像测试图案。 系统应能够产生不同空间频率的标准图案(如条纹或正弦光栅),作为镜头成像的输入。
  • FR2:采集镜头成像输出。 系统需要通过相机采集镜头对图案成像后的图像。
  • FR3:计算成像的对比度。 对于采集的图像,系统应计算图案的实际对比度(例如采用RMS对比度或其他指标)。
  • FR4:将结果转换为有意义的度量。 把图像对比度结果关联到图案的空间频率,转换为每角度周期(cycles/deg)的对比度曲线数据。
  • FR5:呈现和保存结果。 将测量得到的对比度随频率变化关系呈现给用户(曲线图),并保存数据和相关图像以供分析。

以上5个FR构成了完整的功能需求集,涵盖了从提供输入到输出结果的全过程。接下来进入物理域,为每个FR制定相应的设计参数DP。DP是实现该FR的设计手段或组成模块:

  • DP1:图案发生器。 提供测试图案的方案。本系统采用计算机屏幕显示合成的条纹图像作为测试图案源。
  • DP2:成像采集装置。 获取图像的方案,我们选择使用数码相机/摄像头对镜头成像进行采集,连接至软件读取帧。
  • DP3:对比度计算算法。 实现对比度计算的方法,例如基于像素灰度统计计算RMS对比度的函数。
  • DP4:校准与转换模块。 将像素尺度转换为空间频率的方案,例如利用已知的像素尺寸和成像距离进行计算。
  • DP5:用户界面与数据记录模块。 展示结果并保存数据的方案,例如图形界面显示曲线,文件保存CSV及图像。

进一步,我们在过程域为每个DP确定过程变量PV,即实现该设计参数的具体手段和技术细节:

  • PV1:显示屏幕全屏显示测试图案图像 (软件使用GUI窗口全屏呈现条纹图案)。
  • PV2:使用OpenCV库通过摄像头采集图像帧 (设置分辨率、ROI区域等参数)。
  • PV3:使用Python算法计算RMS对比度 (如利用NumPy计算选定区域像素的均值和标准差来得到对比度)。
  • PV4:从校准文件读取像素尺寸(px/mm)和用户输入距离,按照公式转换为每度周期数(cpd)。
  • PV5:基于Tkinter库构建GUI界面,Matplotlib绘制曲线,保存CSV数据和图像文件

通过上述FR-DP-PV的逐层映射,我们将模糊的科研目标细化为了明确的功能需求和对应的实现方案。在实际设计中,这一过程往往需要多次迭代和调整。例如在初步方案中可能会发现某些FR之间存在耦合,需要重新划分或增加FR以维护独立性。这也是下一步要讨论的设计矩阵分析内容。

3. 构建设计矩阵与解耦性分析

有了FR和DP列表后,我们可以构建设计矩阵A来表示它们之间的映射关系。设计矩阵是一个以FR为行、DP为列的矩阵,矩阵中的元素$a_{ij}$表示DP_j对FR_i的影响关系。若某DP需要调整才能满足某FR,则对应元素标记为X(非零);反之如果DP_j不影响FR_i则记0。理想情况下,设计矩阵应该是对角矩阵或上(三角)或下三角矩阵,从而满足独立性公理。

根据前述FR和DP,本案例的设计矩阵如下表所示:

FR\DP DP1 图案发生器 DP2 成像采集装置 DP3 对比度算法 DP4 校准转换 DP5 界面与存储
FR1 提供测试图案 X 0 0 0 0
FR2 采集成像图像 X X 0 0 0
FR3 计算图像对比度 0 0 X 0 0
FR4 转换频率单位 0 0 0 X 0
FR5 展示与保存结果 0 0 0 0 X

从表中可以看到,大部分FR由各自对应的DP独立实现,设计矩阵接近对角阵结构。其中FR1、FR3、FR4、FR5各自仅对应一个DP(矩阵对角线上为X),彼此独立,不会相互影响。例如,无需更改图案发生模块(DP1)就可以单独修改对比度计算算法(DP3)而不影响其他功能,这符合独立性公理的要求。

唯一存在耦合的是FR2和FR1之间:为了采集镜头成像,必须先有测试图案输入(FR1)。在设计矩阵中这表现为FR2对应的列DP1和DP2均为X(DP1对FR2有影响)。所幸,这种耦合属于上三角阵形式,可以通过适当的顺序解耦——即先满足FR1(显示图案)再调整DP2满足FR2(采集图像)。这种设计称为准耦合设计(decoupled design),通过按顺序设置DP,同样满足独立性公理要求。因此,本系统的整体设计矩阵是一个按功能流程解耦的三角阵,设计在可接受范围内。

在实际开发中,我们据此将系统划分为多个模块,每个模块对应一个主要的DP,实现特定的功能需求:

  • 图案生成模块(对应FR1,DP1):负责生成不同频率的条纹图案图像。实现上采用一个函数,根据所需的像素周期生成numpy数组图像并在屏幕上显示。
  • 图像采集模块(对应FR2,DP2):负责相机初始化和图像捕获。使用OpenCV接口打开摄像头,调整分辨率,捕获帧,并将图像截取ROI区域用于分析。
  • 对比度计算模块(对应FR3,DP3):负责对获取的ROI图像计算对比度。实现为一个独立的函数,输入灰度像素数组,输出对比度值。
  • 校准及频率转换模块(对应FR4,DP4):负责读取校准参数(例如每毫米像素数)和获取拍摄距离,将像素周期转换为空间频率(cyc/deg)单位。
  • 用户界面与数据处理模块(对应FR5,DP5):负责与用户交互、流程控制以及结果的呈现和保存。包括启动测试、显示实时曲线、保存结果CSV和图像文件等功能。

通过模块化分解,我们将复杂问题分成了相对独立的子问题,各模块边界清晰,接口简单。例如GUI模块通过调用图案生成、相机采集等模块提供的接口来执行测量流程,而对比度计算和频率转换则作为独立的工具函数使用。这种模块结构直接来源于设计矩阵对FR-DP独立性的分析,体现了高内聚、低耦合的设计思想。正如公理设计理论所述,如果各功能模块一开始就独立良好,那么需求变化时我们只需调整相关模块,其他部分仍可重复利用 (公理设计笔记(4) | GoldenGrape's Blog)。

4. 模块实现与信息公理的应用

在确定模块划分和主要实现方案后,我们进入具体编码实现阶段。此时仍需遵循信息公理的指导,即在满足功能需求的前提下尽量简化实现、降低系统的信息量与复杂度。这体现为以下实践策略:

  • 选择简单可靠的算法实现: 对于对比度计算(FR3),我们有多种可选方案,例如傅里叶分析计算MTF曲线或直接计算图像亮暗区域差异。考虑到实现复杂度和稳定性,我们采用了RMS对比度这一简单指标。RMS对比度计算非常直接:对ROI像素计算均值和标准差,其比值即为对比度。下面的代码片段展示了这一功能的实现,函数短小且不依赖外部状态,方便测试和重用:
import numpy as np

def rms_contrast(pixels: np.ndarray) -> float:
    """计算像素矩阵的RMS对比度"""
    mean_val = pixels.mean()
    return float(pixels.std() / mean_val)

上述实现对应DP3的功能,用不到几行代码就完成了主要计算,明显是信息量低的方案。相比之下,更复杂的频域算法虽然理论严谨,但需要更多参数校准和调试,不符合信息公理下优先简化的原则。

  • 使用成熟库简化开发: 针对相机采集(FR2),我们直接采用OpenCV的VideoCapture接口配合NumPy处理图像。借助成熟库,可以减少自己处理底层细节的代码量,降低出错概率。比如ROI裁剪、图像格式转换等都可利用现有函数,一方面降低了实现信息量,另一方面提高了可靠性。

  • 封装可测试的函数单元: 为提高模块独立性和可测试性,我们将核心计算逻辑从界面代码中剥离。例如DP3和DP4相关的计算(对比度、cpd转换)实现为独立的contrast_core.py模块,使其不依赖任何GUI状态或文件I/O。这种函数式实现方便编写单元测试来验证正确性。事实上,我们为这些函数编写了简单的测试用例,模拟输入已知图案的数据,检查输出结果是否符合预期(如对纯黑白相间图像的对比度计算应接近1等),从而在开发早期就验证算法正确性。这体现了信息公理在测试友好性上的另一个好处:设计越简洁、模块边界越清晰,越容易对其进行数学分析和单元测试,降低调试信息的不确定性。

  • 简化用户交互流程: 界面设计遵循朴素直观的准则,不引入多余选项以减少用户犯错机会。例如,本系统GUI上只有必要的输入(加载校准文件、ROI文件、距离输入等)和开始/停止按钮,流程按照预定顺序引导,不需要用户进行复杂设置。这种简化交互的设计降低了用户使用过程的信息量负担,同样符合信息公理“简单即最佳”的精神。

通过以上措施,我们在实现阶段确保了各模块按预期功能最小化地实现,没有额外的耦合与冗余功能,从而满足信息公理的要求。在满足独立性公理的前提下进一步减少了系统的复杂度,使设计更趋于最优。

5. 收获与建议

通过本案例实践,我们深刻体会到公理设计在科研软件开发中的作用和优势:

  • 需求分析条理清晰: 采用4域框架和FR分解,使我们在动手编码前就对系统做了全局规划。哪些功能是必须的、彼此关系如何,一张设计矩阵表就一目了然。这种清晰的蓝图极大减少了开发过程中的反复修改。

  • 模块独立性高,协作效率高: 独立性公理指导下的模块划分,使各部分职责单一明确。不同开发人员可各自专注于某个模块,最终集成时轻松对接。即使后期需求变更或增加新功能,也只需在相关模块上改动,不影响其他部分,大大提高了维护性 (公理设计笔记(4) | GoldenGrape's Blog)。

  • 代码可测试、性能可靠: 信息公理促使我们选择了简单可行的实现方案,并避免过度设计。每个核心算法都可以用少量测试用例验证。在本案例中,我们编写的自动化测试确保了对比度计算、单位转换等模块的正确性,这使整个系统在集成时几乎未遇到算法方面的问题,后期测量得到的数据也被证明是可信的。

  • 工程优雅感: 最终完成的系统,其代码结构和逻辑非常贴近最初设计。GUI主程序就像一个流程 orchestrator,通过调用各独立模块完成任务;各模块内部实现简洁高效。这样的代码既具备“Pythonic”的优雅,又蕴含着公理设计的科学理念,令人对科研编程也能如此井井有条深受鼓舞。

建议: 在科研型软件开发中,尤其是仪器控制、数据处理这类复杂系统,强烈推荐在编码前尝试公理设计的方法。花一些时间理清FR/DP,不但不会减慢进度,反而能避免走错路、大重构等浪费。在设计过程中,坚持检视是否违反独立性公理,及时解耦;在实现过程中,多用信息公理来权衡方案优劣,选择更简单可靠的路径。久而久之,这种“先设计后编码,先逻辑后细节”的思维方式将大大提升科研软件开发的效率和质量。

公理设计将工程设计提升到方法学层面,在本案例中我们见证了其威力。希望通过本教程的讲解和示范,科研人员和学生读者能将公理设计思想融会贯通,应用到自己的课题和项目中。在实际编程实践中不断打磨,最终写出既满足科研严谨要求又体现工程优雅之美的优秀软件作品。祝各位在实践中取得丰硕成果!

参考文献:

  1. Suh N. P. The Principles of Design. Oxford University Press, 1990. (公理设计理论奠基著作)
  2. 吴克寿, 刘丽娟. 基于公理设计的软件构件设计优化. 厦门大学学报(自然科学版), 2012, 51(1): 41-45.
  3. GoldenGrape. 公理设计笔记(系列) (公理设计笔记(4) | GoldenGrape's Blog) (公理设计笔记(4) | GoldenGrape's Blog), 2019. (博客文章)
  4. 中国大百科全书第三版网络版. 公理化设计 (公理化设计 - 中国大百科全书), 2023. (对公理设计的概念和公理的阐述)

真实场景记录

上面的只是我让ChatGPT根据对话过程写出的教程。有一些美化,下面是一些实际场景的记录。

system prompt

我在ChatGPT个性化中,设定了系统提示:

当撰写程序代码时,你应当使用公理设计的思考原则,使用函数式编程的范式进行编码,并且应当输出具有生产级别的完善代码,而不仅仅是提供简单的示例。在生成完成代码后,你应当同时撰写编程笔记,其中记录此程序或算法的运行逻辑要点,界面和绘图细节要点。在回答时,你应当保持友好严谨谦逊的态度,不可阿谀奉承也不可盛气凌人。在检索和深度研究时,你应当引用权威的内容,对商业宣传保持谨慎。

所以,从一开始我就一直保持着公理设计的要求。即使只是简单写一个小程序,ChatGPT也会尽量依照解耦合的思路去写代码。函数式编程的范式比较有助于拆分程序和debug,而且我习惯于使用Jupyter一个函数一个函数写程序,逐渐组装成形,而不是使用大型的IDE跟踪断点debug。

AI其实相当见多识广,它知道公理设计是怎么回事。

聊需求

其实最开始的过程,就是聊需求,而且要抑制AI直接编程的冲动。

我们来讨论一下,我想做一个自动测量镜片成像对比度的程序,用摄像头通过镜片拍摄显示器上显示的正弦条纹画面,然后计算对比度。为了计算不同空间频率的成像对比度,我还需要让拍摄和显示同步。我大致的设想如下:
图像显示部分:
显示出一个条纹图像,条纹的亮度按照正弦变化,方向为垂直方向。
条纹的亮度范围为0-255,均值为128。条纹的宽度为T。用户给出一个条纹宽度的列表,依次显示出这些条纹。
图像拍摄部分:
用opencv控制1号摄像头去拍摄图像,每拍摄一张,就计算感兴趣区内的对比度。完成计算以后,显示下一张条纹。
用户控制部分,用户指定一个条纹宽度的列表,然后开始拍摄,直到条纹列表用尽。
最终输出得到拍摄到的对比度数据。
请帮我细化和优化,有必要的话可以问我

就是简单的聊天,但“请帮我细化和优化,有必要的话可以问我”这句我我认为很重要。通常甲方都不太清楚自己到底想要什么,所以让AI帮忙澄清需求是很重要的。

ChatGPT:

下面先把 整体方案 拆成 4 个可独立测试的功能模块,说明各自要做什么、为什么这样设计,以及它们之间的数据流。随后给出 关键实现要点(同步策略、对比度算法、硬件与显示校正等),最后列出 我还需要确认的 6 个细节。等您回答后,我再输出生产级完整代码和编程笔记(会完全采用函数式、可组合的风格)。
(略)

抑制AI编程冲动

ChatGPT不断想直接开始编写完整代码。但如果需求没搞清楚的时候贸然开始,后续改动就很麻烦。所以我在澄清一些细节后,要求它暂时不要开始写代码,而是先从公理设计的角度把问题继续分析清楚

  1. 条纹周期的单位我打算使用像素。因为显示器只能显示整数倍的像素数。……
  2. ROI位置和大小需要一个交互界面……
  3. 摄像头的分辨率与帧率固定,……
  4. 参考1.
  5. 没有现成的gamma矫正
  6. 我需要GUI,因为有多个用户交互场景。
  7. 关于对比度计算,Michelson对极值太敏感,我打算使用RMS对比度,这样更robust一些。
    我们仍然暂时先不开始写代码,先从公理设计的角度将问题分析清楚

前面描述的公理设计分析虽然很复杂,描述好需求以后,就完成了用户域,而从用户域向FP、DP的转换,其实是由AI来完成的,如前所述,AI相当见多识广,它知道公理设计应该怎么做,我们只需要审核即可。

  • 用户域(Customer Domain):定义客户需求或科研目标,即我们“想要实现什么”。【聊需求时完成】

后续由AI完成:

  • 功能域(Functional Domain):将用户需求转化为功能需求(FR)及约束条件(Constraints),明确系统需要具备的功能。
  • 物理域(Physical Domain):提出满足功能需求的设计参数(DP),即系统的物理组成和方案,“如何实现”功能需求。
  • 过程域(Process Domain):确定实现设计参数的过程变量(PV),包括实现方案所需的工艺、算法和操作等细节。

记录规划文档

没有草稿纸的思考是肤浅的。 对AI也是如此,即使它可以有超长的上下文,但是一旦开始写程序,ChatGPT 128k的上下文很快就不够用,而且还可能有各种意外(降智力)发生。另外,在编程中,我不一定只用ChatGPT,还经常会在Trae中使用Claude或者Gemini来辅助。所以我要求ChatGPT要生成一个规划文档。

可以,我们选择方案一:完全外部流程。仍然暂时先不写代码。请先把整个程序的公理设计规划写好。我们后续将依照此进行程序的架构设计和撰写

在规划文档中,ChatGPT记录了上面提到的用户需求,功能需求,设计参数,过程变量等。还贴心地给出了一份实施路线图。

实施路线图
阶段 0 – 搭建仓库、CI、pytest 框架,完成 DP-1/2/5/6 纯函数 + 单测。
阶段 1 – 实现 tkinter CalibratorWindow 与 ROISelectorWindow,JSON 配置持久化。
阶段 2 – 完成测量主控台与结果导出 (DP-7),通过 CLI 与 GUI 双入口验收。
阶段 3 – 编写用户文档,明确 “先用 Anker Utility 对焦” 的操作步骤。
阶段 4 (可选) – 性能优化、摄像头控制面板、跨平台打包 (PyInstaller)。

写代码

审核好规划文档,把各种需求都明确了以后,就可以开始写了,其实这部分到没什么特殊了,就让它开始就行了。

很好,请开始

在写代码过程中,还是要不断地微调。所以其实“氛围编程”并不是一个完全不会写代码的人可以完成的事情,你得知道每段代码的大致含义,甚至是一些细节。有时候,你还会发现有可能需要修改需求,那么这种时候还是应该记录在规划文档里。

有一些需要修正的部分:

  • ROI应当是圆区域
  • 既然已经使用numpy了,就都统一到numpy上,不必再使用math了
  • 有可能需要选择摄像头,通常笔记本电脑自带了摄像头,那么外接的摄像头是1号。(这个需求是否应当更新公理设计文档?)

debug

一方面应该是写test,把核心函数测一遍。一方面还应该输出一些可视化的东西,眼见为实。当然此时也就又在增加一些需求。

很好,
.........[100%]
9 passed in 0.55s
但我想看看生成的图片。目测检查
接下来我们应该做什么了?请继续

定期回顾公理设计规划文档

这是从ChatGPT 3.5时代遗留下来的习惯,当时还只有很短的上下文,所以我经常会要求AI回顾一下我们做到哪里了。

在编程时也是如此,写了大段程序后,它很可能开始脑子不清楚了。我会把刚才记录的公理设计规划文档再贴进对话里,告诉它我们现在已经进行到哪一步了。让它继续。


以上就是使用公理设计进行氛围编程的大致过程。供参考。