news 2026/10/13 6:12:12

Dapp-Learning 合约导读:Uniswap V3 SwapRouter 交易路由源码深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Dapp-Learning 合约导读:Uniswap V3 SwapRouter 交易路由源码深度解析
  • 示例工程
  • 区块链

【免费下载链接】Dapp-Learning

Dapp learning project for developers at all stages. Becoming and cultivating sovereign individuals. Nonprofit organization.

项目地址:https://gitcode.com/gh_mirrors/da/Dapp-Learning
点击查看免费下载

导读

本文基于 Dapp-Learning 仓库 defi/Uniswap-V3/contractGuide/SwapRouter.md 展开,系统拆解 Uniswap V3 外围合约(periphery)中SwapRouter的核心实现:从ExactInputParams/SwapCallbackData两个关键结构体,到exactInput多跳交易主流程、exactInputInternal单池交易内部函数、uniswapV3SwapCallback回调机制,再到hasMultiplePools/decodeFirstPool/toAddress/toUint24等路径编码解析工具与checkDeadline修饰器。读完本文,你将掌握 SwapRouter 在 Uniswap V3 架构中的定位、多跳路径交易的完整调用链与回调结算原理,并能结合仓库内的接口定义、MethodID 表与部署地址直接开展合约级调试与集成。

一、SwapRouter 在 Uniswap V3 架构中的定位

Uniswap v3 在代码层面将合约拆分为两个仓库:uniswap-v3-core与uniswap-v3-periphery,其分工如下(参见 contractGuide/readme.md):

  • core 层:
    • UniswapV3Factory:提供创建 Pool 的接口,并追踪所有 Pool;
    • UniswapV3Pool:实现代币交易、流动性管理、交易手续费收取与 oracle 数据管理。其接口实现粒度很低,不适合普通用户直接调用,错误的调用可能造成经济损失。
  • periphery 层:
    • SwapRouter:提供代币交易接口,是对UniswapV3Pool中交易相关接口的进一步封装,前端界面主要与该合约对接;
    • NonfungiblePositionManager:负责增加/移除/修改 Pool 流动性,并通过 ERC721 NFT 将流动性代币化。

SwapRouter的核心职责可以概括为:接受用户给出的交易路径(path)与输入数量,自动完成一笔或多笔 Pool 间串接交易,并在回调中完成 token 结算。它解决了UniswapV3Pool.swap接口粒度过低、需要调用方自行处理多池串联的问题。

二、核心数据结构:ExactInputParams 与 SwapCallbackData

ExactInputParams:精确输入交易的参数载体

exactInput的全部入参被封装进ExactInputParams结构体:

struct ExactInputParams { bytes path; // 路径 address recipient; // 收款地址 uint256 deadline; // 交易有效期 uint256 amountIn; // 输入的 token 数(输入的 token 地址就是 path 中的第一个地址) uint256 amountOutMinimum; // 预期交易最少获得的 token 数(获得的 token 地址就是 path 中最后一个地址) }

各字段的实战含义:

字段类型说明
pathbytes编码后的交易路径,由「token 地址 + fee 费率 + token 地址」交替拼接而成,见下文路径编码章节
recipientaddress最终收款地址;中间跳的收款方为 Router 合约自身(由合约托管,避免代币在中间环节落入用户地址产生额外转账)
deadlineuint256交易有效期(区块时间戳),超时则整个交易回滚,防止交易被延迟打包造成滑点损失
amountInuint256输入 token 数量,输入 token 即path中第一个地址
amountOutMinimumuint256最少输出数量,即滑点保护下限,输出 token 是path中最后一个地址

对照仓库中的接口定义 basic/21-aave-uni-loan/contracts/interfaces/uniswap-v3/ISwapRouter.sol 可以看到,同一结构体还被用于exactInput的外部接口签名,且接口声明了exactInputSingle、exactOutputSingle、exactOutput三个姊妹方法,分别对应单池精确输入、单池精确输出与多跳精确输出场景。exactOutput的ExactOutputParams中相应字段为amountOut与amountInMaximum(最多输入上限),语义与精确输入对称。

SwapCallbackData:交易回调函数的入参结构体

struct SwapCallbackData { bytes path; address payer; }
  • path:当前这一跳(hop)对应的 Pool 路径片段,回调时用它解析出tokenIn / tokenOut / fee以完成验证与结算;
  • payer:负责为本次交易付款的地址。第一跳的 payer 是调用者msg.sender,后续跳的 payer 切换为 Router 合约自身。

该结构体通过abi.encode(data)编码后作为Pool.swap的data参数传入,Pool 执行完交易后会在回调里原样解码使用。

三、exactInput:多跳路径交易主流程

exactInput是 SwapRouter 最核心的对外交易方法,通过 while 循环逐跳执行交易,直至遍历完整个路径:

/// @inheritdoc ISwapRouter function exactInput(ExactInputParams memory params) external payable override checkDeadline(params.deadline) returns (uint256 amountOut) { address payer = msg.sender; // msg.sender pays for the first hop // 通过循环,遍历传入的路径,进行交易 while (true) { // 交易路径是否存在2个及2个以上的Pool地址 bool hasMultiplePools = params.path.hasMultiplePools(); // the outputs of prior swaps become the inputs to subsequent ones params.amountIn = exactInputInternal( params.amountIn, hasMultiplePools ? address(this) : params.recipient, // for intermediate swaps, this contract custodies 0, // 传入价格 0 代表以市价交易 SwapCallbackData({ path: params.path.getFirstPool(), // only the first pool in the path is necessary payer: payer }) ); // decide whether to continue or terminate // 判断path中是否还有未交易的token地址,即是否需要继续交易 if (hasMultiplePools) { // 继续循环,需要将交易者设置为本合约地址 payer = address(this); // at this point, the caller has paid params.path = params.path.skipToken(); } else { // 交易完成跳出循环 amountOut = params.amountIn; break; } } // 检查交易输出量,不能过小 require(amountOut >= params.amountOutMinimum, 'Too little received'); }

执行过程可分解为以下关键步骤(同时可参考 contractGuide/readme.md 的流程梳理):

  1. 初始化 payer:第一跳由调用者msg.sender付款;
  2. 进入 while 循环:对当前第一个交易对执行具体交易;
  3. 判断hasMultiplePools:即path.length >= MULTIPLE_POOLS_MIN_LENGTH(3 个 token 地址 + 2 个 fee 的字节长度),判断是否还有第二个 Pool;
  4. 调用exactInputInternal执行当前跳的实际兑换:
    • 若存在多池,中间跳的recipient设为address(this),由 Router 合约托管中间输出,避免代币在多池之间反复进出用户地址;
    • sqrtPriceLimitX96传 0,表示以市价交易(无价格上限限制);
    • 回调数据只携带当前跳的 Pool 片段getFirstPool()与 payer;
  5. 决定循环去留:
    • 若还有下一跳,将 payer 切换为address(this)(此时调用者已付完款,后续由合约垫付),并通过skipToken()剔除已消费的 token 与 fee,回到第 2 步继续;
    • 否则跳出循环,amountOut即为最终输出数量;
  6. 滑点保护:最终require(amountOut >= params.amountOutMinimum, 'Too little received')校验实际输出不小于用户设定的最小值,防止路径价格剧烈波动造成损失。

路径选择的实战背景

在链下计算交易路径时(例如ETH -> DAI):

  • 可以直接通过ETH/DAI交易池完成(单池:tokenAddress0 + fee0 + tokenAddress1);
  • 也可以选择ETH -> USDC -> DAI路径,即依次经过ETH/USDC、USDC/DAI两个交易池(多池:tokenAddress0 + fee0 + tokenAddress1 + fee1 + tokenAddress2,其中间代币即 Base token);
  • token 地址在 path 中没有排序限制,zeroForOne的方向由地址大小比较决定。

四、exactInputInternal:单池交易内部函数

exactInputInternal是exactInput的私有内部函数,负责「解析路径片段 → 计算 Pool 地址 → 调用 Pool.swap 执行单池兑换」:

/// @dev Performs a single exact input swap function exactInputInternal( uint256 amountIn, address recipient, uint160 sqrtPriceLimitX96, SwapCallbackData memory data ) private returns (uint256 amountOut) { // allow swapping to the router address with address 0 // 允许0地址作为接受者 自动转为本合约地址 if (recipient == address(0)) recipient = address(this); // 从path中解析出Pool的关键信息 // 这里从交易链路(path)中解析出的地址有可能 tokenIn > tokenOut (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool(); // 判断两个token地址大小 // 由于Pool中price始终以 y/x 表示 (x地址 < y地址) // zeroForOne 实际代表的是Pool.swap中的交易方向 // true token x -> token y // false token y -> token x bool zeroForOne = tokenIn < tokenOut; // 调用Pool.swap 返回实际交易的token数量 // amount0 输入的实际数量 // amount1 输出的实际数量 // 执行完成后 Pool 会调用 uniswapV3SwapCallback 回调函数 (int256 amount0, int256 amount1) = getPool(tokenIn, tokenOut, fee).swap( recipient, zeroForOne, amountIn.toInt256(), // 传入价格 0 代表以市价交易 sqrtPriceLimitX96 == 0 ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) : sqrtPriceLimitX96, abi.encode(data) ); return uint256(-(zeroForOne ? amount1 : amount0)); }

关键设计点:

  • 0 地址兜底:recipient == address(0)时自动替换为 Router 合约地址;
  • 方向判定:Pool 中价格始终以y/x表示(address(x) < address(y)),因此zeroForOne = tokenIn < tokenOut。zeroForOne == true表示 token x 换 token y,反之 token y 换 token x;
  • 市价交易语义:sqrtPriceLimitX96 == 0时,将价格限制分别设为TickMath.MIN_SQRT_RATIO + 1(x→y 方向)或TickMath.MAX_SQRT_RATIO - 1(y→x 方向),即不做价格上限约束;
  • 返回值符号处理:Pool.swap返回的amount0/amount1中,输出方向的值为负数,因此用uint256(-(...))转为正数作为amountOut;
  • getPool 依据:getPool(tokenIn, tokenOut, fee)使用与 UniswapV3Factory 相同的池地址索引逻辑,即mapping(address => mapping(address => mapping(uint24 => address))),Pool 地址可在链下用 CREATE2 预先计算。

Pool.swap 的执行细节

exactInputInternal最终落点是 UniswapV3Pool.md 中的swap函数:

function swap( address recipient, // 交易输出token的接收者 bool zeroForOne, // 交易方向 int256 amountSpecified, // 输入的token数量 uint160 sqrtPriceLimitX96, // 交易的价格限制(超出即停止交易) bytes calldata data // 回调函数的入参数据 ) external override noDelegateCall returns (int256 amount0, int256 amount1)

Pool.swap 内部的执行特征:

  • amountSpecified > 0表示 exactInput 模式,负数表示 exactOutput 模式;
  • 交易在 while 循环中分步执行,直到输入 token 耗尽或触及价格限制(见 UniswapV3Pool.md);
  • 每一步通过tickBitmap.nextInitializedTickWithinOneWord寻找下一个已初始化 tick,用SwapMath.computeSwapStep计算该步的输入、输出与手续费;
  • 交易完成后:先把输出 token 转给接收者,再调用回调函数收回输入 token(见 UniswapV3Pool.md),这正是 SwapRouter 回调结算的基础。

五、uniswapV3SwapCallback:回调结算机制

Pool.swap执行完成后会回调msg.sender(即 SwapRouter)的uniswapV3SwapCallback。该回调只负责执行当前跳第一个 token 的转账:

/// @inheritdoc IUniswapV3SwapCallback function uniswapV3SwapCallback( int256 amount0Delta, int256 amount1Delta, bytes calldata _data ) external override { require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData)); (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool(); CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee); (bool isExactInput, uint256 amountToPay) = amount0Delta > 0 ? (tokenIn < tokenOut, uint256(amount0Delta)) : (tokenOut < tokenIn, uint256(amount1Delta)); if (isExactInput) { pay(tokenIn, data.payer, msg.sender, amountToPay); } else { // either initiate the next swap or pay if (data.path.hasMultiplePools()) { data.path = data.path.skipToken(); exactOutputInternal(amountToPay, msg.sender, 0, data); } else { amountInCached = amountToPay; tokenIn = tokenOut; // swap in/out because exact output swaps are reversed pay(tokenIn, data.payer, msg.sender, amountToPay); } } }

回调处理逻辑解读:

  1. 非零校验:require(amount0Delta > 0 || amount1Delta > 0),拒绝「完全发生在 0 流动性区间内」的交换;
  2. 解码与验证:abi.decode还原SwapCallbackData,解析出当前 Pool 的tokenIn / tokenOut / fee,并调用CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee)校验调用者确实是 Factory 注册的合法 Pool(防止伪造合约钓鱼);
  3. 判定方向:amount0Delta > 0时isExactInput = (tokenIn < tokenOut),否则为(tokenOut < tokenIn);
  4. 精确输入分支:直接调用pay(tokenIn, data.payer, msg.sender, amountToPay),把输入 token 从 payer 转给 Pool——即执行exactInput时,token 直接从付款方(用户或 Router 合约)转入 Pool;
  5. 精确输出分支:若路径还有剩余 Pool,则调用exactOutputInternal发起下一跳的精确输出交易;否则记录amountInCached并反向支付。

回调与闪电贷的关系

在 Uniswap v3 中,Pool.swap「先转出输出 token、再通过回调收回输入 token」的机制天然实现了借出与归还不同币种的闪电贷:接收者可以先拿到tokenOut,在同一个交易(区块)内完成套利等操作后再归还tokenIn;而Pool.flash则是借出与归还是同一币种的经典闪电贷接口(详见 UniswapV3Pool.md 与 contractGuide/readme.md 的 flash swap 小节)。

六、路径解析工具函数

hasMultiplePools:判断路径是否包含多个 Pool

/// @notice Returns true iff the path contains two or more pools /// @param path The encoded swap path /// @return True if path contains two or more pools, otherwise false function hasMultiplePools(bytes memory path) internal pure returns (bool) { // MULTIPLE_POOLS_MIN_LENGTH: 2个Pool地址+1个fee费率的字节长度 return path.length >= MULTIPLE_POOLS_MIN_LENGTH; }

MULTIPLE_POOLS_MIN_LENGTH对应「2 个 token 地址 + 1 个 fee」的长度:2 * 20 + 3 = 43字节。即只有 1 个 Pool 时 path 长度为20 + 3 + 20 = 43字节,一旦长度达到 43 字节说明路径中存在至少 2 个 Pool。

decodeFirstPool:解析路径中的第一个 Pool

/// @notice Decodes the first pool in path /// @param path The bytes encoded swap path /// @return tokenA The first token of the given pool /// @return tokenB The second token of the given pool /// @return fee The fee level of the pool function decodeFirstPool(bytes memory path) internal pure returns ( address tokenA, address tokenB, uint24 fee ) { // 返回第一个地址 tokenA = path.toAddress(0); fee = path.toUint24(ADDR_SIZE); tokenB = path.toAddress(NEXT_OFFSET); }

其中ADDR_SIZE = 20、NEXT_OFFSET = ADDR_SIZE + 3(fee 占 3 字节)。也就是说:path 的第 0~19 字节是tokenA,第 20~22 字节是fee,第 23~42 字节是tokenB。

path.toAddress:从 bytes 中读取地址

function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) { // 检查 _start+20 是否会溢出(超过uint256的最大值) // 若溢出则为负数 require(_start + 20 >= _start, 'toAddress_overflow'); // 检查_start 起始位 + 20字节(地址类型的长度) 不能超过bytes的总长度 require(_bytes.length >= _start + 20, 'toAddress_outOfBounds'); address tempAddress; assembly { // add(_bytes, 0x20) // 从 _bytes 指针开始向后越过32字节(16进制0x20) // 即 跳过内存中表达 _bytes 长度的区域(一个uint256数值) // add(..., _start) // 加上入参设置的读取起始位 // div(mload(...), 0x1000000000000000000000000) 这里有24个0,即 2^96 // 将mload的结果右移96位(12个字节) // 因为mload取出的数据是uint256类型(32个字节)而address类型是(20个字节) // 结果会自动在后面补0,即后12个字节(96位)补0,所以这里需要用除法将结果右移96位 tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000) } return tempAddress; }

原理解析:

  • bytes在内存中的前 32 字节存储的是 bytes 的长度数值(uint256),所以读取数据前要先加上0x20(32 字节)越过该长度区域;
  • address 是 uint160(20 个字节),但mload取出的是一个 uint256(32 个字节),低 20 字节是有效数据、高 12 个字节自动补 0,因此需要用除法将结果右移 96 位(12 字节 × 8 位 = 96 位);
  • 右移 96 位等价于除以 2^96,即十六进制的0x1000000000000000000000000(1 后面跟 24 个 0)。

关于该内存编码格式的官方说明可参考 Solidity 文档《Formal Specification of the Encoding》(ABI 规范章节)。

toUint24:从 bytes 中读取 fee 费率

function toUint24(bytes memory _bytes, uint256 _start) internal pure returns (uint24) { require(_start + 3 >= _start, 'toUint24_overflow'); require(_bytes.length >= _start + 3, 'toUint24_outOfBounds'); uint24 tempUint; assembly { tempUint := mload(add(add(_bytes, 0x3), _start)) } return tempUint; }

与toAddress同理,uint24占 3 个字节,读取时同样需要越过 bytes 的 32 字节长度区域。toUint24返回的即 Pool 的 fee 费率值(以百万分之一为基数,如 0.3% 对应 300)。

路径编码的 gas 优化动机

为什么 SwapRouter 要把路径编码进bytes而非直接传递结构体数组?仓库中的 PathEncoding.md 给出了量化分析:EVM 交易 calldata 中零字节与非零字节的 intrinsic gas 分别为 4 wei 与 16 wei(EIP-2028 之后),而 Solidity 的 ABI 编码按 32 字节对齐会引入大量补零字节。以 4 个「pool + fee」对为例:

  • 原始 ABI 编码(两个数组)消耗约292 * 4 + 92 * 16 = 2640gas;
  • 结构体数组编码消耗约230 * 4 + 90 * 16 = 2360gas(省 280);
  • Uniswap V3 采用定长类型直接拼接、不做 padding 的 bytes 编码,仅消耗约66 * 4 + 90 * 16 = 1704gas,无效数据占比从约 40% 降至约 15%。

这正是path采用紧凑 bytes 编码、并在使用时才通过toAddress/toUint24解码的原因——用链上解码的少量 gas 换取 calldata 传输成本的显著下降。

七、checkDeadline:交易有效期保护

modifier checkDeadline(uint256 deadline) { require(_blockTimestamp() <= deadline, 'Transaction too old'); _; }
  • 当区块时间戳超过deadline时,被修饰的函数终止执行并回滚(报错Transaction too old);
  • 作用:防止交易因 mempool 拥堵被延迟打包,导致成交价格与用户下单时预期的价格严重偏离;
  • 在exactInput、exactOutput等对外交易方法上均通过checkDeadline(params.deadline)施加保护。

八、SwapRouter 的接口面与部署地址

SwapRouter 完整的外部接口(含 MethodID,数据来自 defi/Uniswap-V3/InterfaceGuide/InfoList.md):

方法MethodID
exactInput(tuple(bytes,address,uint256,uint256,uint256))0xc04b8d59
exactInputSingle(tuple(address,address,uint24,address,uint256,uint256,uint256,uint160))0x414bf389
exactOutput(tuple(bytes,address,uint256,uint256,uint256))0xf28c0498
exactOutputSingle(tuple(address,address,uint24,address,uint256,uint256,uint256,uint160))0xdb3e2198
uniswapV3SwapCallback(int256,int256,bytes)0xfa461e33
multicall(bytes[])/refundETH()/unwrapWETH9(uint256,address)等辅助方法0xac9650d8/0x12210e8a/0x49404b7c等

其中exactInputSingle与ExactOutputSingle的入参结构体(tokenIn / tokenOut / fee / recipient / deadline / amountIn / amountOutMinimum / sqrtPriceLimitX96)可在 ISwapRouter.sol 中查看,它们支持传入非零的sqrtPriceLimitX96来设置单池交易的价格上限,是精确输入/输出在多池场景外的简化入口。

部署信息(来源 deploys.md,@uniswap/v3-peripheryv1.0.0):

  • SwapRouter 主网/测试网统一地址:0xe79dBeD0F7eA2f3cb60835a81E7771d540E621AD;
  • 演示环境(rinkeby)使用的 V3SwapRouter:0xE592427A0AEce92De3Edee1F18E0157C05861564(见 InfoList.md)。

九、调用链总览与源码路径索引

一次完整的Router.exactInput多跳交易调用链:

用户 └─> SwapRouter.exactInput (checkDeadline 校验) └─> while 循环(逐跳) └─> SwapRouter.exactInputInternal └─> 解析 decodeFirstPool → 计算 zeroForOne └─> Pool.swap(分步执行交易) ├─> 输出 token 转给 recipient └─> SwapRouter.uniswapV3SwapCallback(回调) ├─> CallbackValidation.verifyCallback 校验 └─> pay(tokenIn, payer, msg.sender) 回收输入 └─> 校验 amountOut >= amountOutMinimum

建议配合 contractGuide/readme.md 中的 ExactInput 程序流程图(xmind/ExactInput.png)一并阅读。

本文涉及的关键源码/文档索引(均在当前仓库内):

  • SwapRouter.md(本文主体)
  • UniswapV3Pool.md 的 swap 与回调
  • UniswapV3Factory.md 的池地址索引与 CREATE2 部署
  • PathEncoding.md 路径编码 gas 优化分析
  • fee.md 手续费与费率档位说明
  • ISwapRouter.sol 完整接口定义
  • InfoList.md SwapRouter MethodID 对照表

结语

SwapRouter 是 Uniswap V3 交易体验的「门面」:它用紧凑的 bytes 路径编码节省 calldata gas,用 while 循环串联多池交易,用回调机制完成「先出后进」的 token 结算,并用deadline与amountOutMinimum双保险保护交易者。理解exactInput → exactInputInternal → Pool.swap → uniswapV3SwapCallback这条完整调用链,是深入掌握 Uniswap V3 外围合约、以及开发聚合交易与套利机器人的基础。

  • 示例工程
  • 区块链

【免费下载链接】Dapp-Learning

Dapp learning project for developers at all stages. Becoming and cultivating sovereign individuals. Nonprofit organization.

项目地址:https://gitcode.com/gh_mirrors/da/Dapp-Learning
点击查看免费下载

相关推荐

上一篇:Angular Loading Bar终极指南:10个常见问题解决方案与实用技巧
下一篇:如何使用 Kit 进行PR自动化审查:开发者必备工作流

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 6:11:46

LibRec命令行启动三件套:配置文件、参数与主类协同指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 6:10:35

质量图引导相位解包:从路径选择到区域增长实现

简介&#xff1a;面向图像处理与信号处理学习者&#xff0c;这份压缩包提供了质量图引导相位解包方法的MATLAB源码与配套数据&#xff0c;用于解决含噪声或异常值场景下传统相位解包易沿错误路径扩散的问题。包内共5个文件&#xff0c;包含4个m脚本和1个mat数据文件&#xff1b…

作者头像 李华
网站建设 2026/10/13 6:10:10

微环谐振腔仿真选型:波动光学与波束包络模块的适用边界

做微环谐振腔与环形波导耦合的仿真&#xff0c;有一个绕不开的选择题&#xff1a;到底用波动光学模块老老实实全波求解&#xff0c;还是上波束包络模块省计算资源&#xff1f;我在光子集成相关的项目里两条路都跑过&#xff0c;也看到不少同事在这两个模块之间反复横跳。一根直…

作者头像 李华