Wolfram 语言和 Mathematica 15 版发布:内置 AI 助手、符号音乐等新功能
阅读原文· writings.stephenwolfram.comWolfram Language 15 把 AI 助手直接内嵌进笔记本,加上符号音乐和 ModelFit 超级函数,对用代码思考的人来说,这是今年最扎实的版本升级。
在 Mathematica 诞生近 38 年后,Wolfram 语言与 Mathematica 发布 Version 15。每个笔记本内置 AI 助手,支持从 AI 环境中直接调用 Wolfram 技术。新增符号音乐系统、大规模时间序列与事件序列处理、分类数据计算、模型拟合超函数 ModelFit。笔记本支持千兆字节级大小与实时查找,首次引入侧边栏、视觉主题及弃用功能样式。强化了表格连接、多点可视化、图形刻度绘制与轨道运行计算等功能。DSolve 拐角处获得 AI 方法辅助,支持偏微分方程曲线坐标求解。扩充了矩阵分解、多元 zeta 函数与调和数、流线型部分分式分解。强化了 WebSocket 实时连接、Python 交互改进,支持 CUDA 内核作为外部函数,Wolfram Compute Services 新增 GPU 支持。
- 置顶
- 面向现代时代的重磅发布
- 每个笔记本都配备 AI 助手
- 从你的 AI 环境中使用 Wolfram
- 时间序列(和事件序列)大规模升级
- 计算进入分类数据领域
- 引入 ModelFit 超级函数
- 引入符号化音乐
- 表格数据连接更大更强
- 更多表格功能
- 可视化调优
- 多面板可视化
- GB 级笔记本与实时查找
- 笔记本迎来首个侧边栏
- 笔记本引入视觉主题
- 内容过长时自动截断
- 在明亮模式下使用深色主题
- 计算中发生了什么?Monitor 的单参数形式
- 子值现在可以被固定!
- 引入即用型增量数据结构
- 大型代码库中的异常与错误处理
- 引入结构化包格式
- 在图上进行绘图
- 如何在地图上放置刻度标记?
- 你的城市何时能看到日食?
- 进入轨道
- 格拉斯曼、克利福德、外尔与朋友们
- 泽塔函数、多重对数函数与调和数进入多元领域
- 部分分式得到简化
- 大量新的矩阵分解
- DSolve 的边界问题获得 AI 方法辅助
- 偏微分方程进入曲线坐标系
- 偏微分方程解中的导出量
- 如何近似一个系统工程模型?
- 用于控制系统的强化学习
- 导入与导出最新格式
- 通过 WebSocket 实现实时连接
- 在笔记本中使用 Python 等获得更丰富的用户体验
- 优化与 GPU 化持续推进
- 将 CUDA 内核作为外部函数
- Wolfram Compute Services 获得 GPU 支持
- 在 LLM 函数中使用 Wolfram 基础工具
- 更多功能…
与 Stephen Wolfram 一起探索 Wolfram 语言 15 »(2026 年 6 月 16 日 @ 美国东部时间下午 4:30)
发布 Wolfram 语言与 Mathematica 15 版本:内置(实用)AI 与大量新核心功能
June 16, 2026面向现代时代的令人印象深刻的版本
1988 年 6 月 23 日,我们发布了 Mathematica 1.0 版。今天——将近 38 年后——我们发布了第 15 版,鉴于它已远远超出“数学”的范畴,我们现在称之为 Wolfram 语言。这是一个令人印象深刻的版本,包含大量全新的核心功能。38 年后竟然还有东西可加,这或许看起来令人惊讶。但这就像知识史的典型轨迹:一个人理解得越多,就能看得越远,也就能做得越多。对于我们所有参与其中的人来说,这是一个非常令人满意的过程:年复一年地建造一座越来越高耸的思想与技术之塔,让我们能够触及更远的地方——今天,它已涵盖第 15 版的所有功能。
过去四十年里,我们始终秉持同一个使命:尽可能广泛而深入地应用计算范式——并通过构建我们独特的计算语言来表征和计算世界。在这四十年间,计算与计算范式的应用已大幅扩展,我认为这在很大程度上得益于我们引入的工具与理念。但如今又出现了一个新驱动力:现代人工智能。看到 AI 领域取得如此多出乎意料的进展,令人振奋。
对我们而言,其中一个直接后果是用户基础从仅限人类拓展为人类与 AI 并用。结果证明,我们在 Wolfram Language 连贯设计上所投入的所有努力——旨在让人类使用起来简单高效——如今也使得 AI 使用起来同样简单高效。
多年来,我们一直高度重视面向人类用户的界面,这一理念始于我们在 1.0 版本中发明的笔记本概念。现在我们也在强调面向 AI 的界面,以便让 AI 和 AI 系统(以及使用它们的人类)能够尽可能顺畅地接触到我们的技术。
我们的技术无疑是 AI 的强大工具,同时也是人类借助 AI 的强大工具。因为它为人类提供了一种独特的方式来形式化事物,并精确知晓正在陈述或执行的内容。我一直将 Wolfram Language 的发展视为:为计算范式所做的工作,正如几个世纪前数学记号为数学范式所做的工作——为表征和交流思想提供了一种精简而精确的方式。
当你用自然语言告诉 AI 你想要什么时,固然方便,但——除了相当简单的情况外——并不精确。然而,如果 AI 生成了 Wolfram Language 代码,那么它就能精确地向你展示 AI 理解的内容,并让你判断这是否真的是你想要的。
Wolfram Language 在这里扮演着独特的角色。传统编程语言是人类书写、计算机读取的语言。但 Wolfram Language 超越了编程语言——它是一套全方位的计算语言。它不仅供人类书写,也供人类阅读,成为一种帮助人类将思维形式化、精确化的工具。而在如今这个 AI 时代,它又提供了一种独特的方式来精确表达人们所谈论的内容——充分利用计算范式以及计算化的世界表征方式。
是的,AI 并不总是正确的。但关键在于,将 Wolfram Language 作为精确性(及正确性)的载体——作为锚定所做之事、生成可靠输出、从而能以系统化方式放心使用的方式。
近年来(尤其是今年)有一个大趋势,就是“用 AI 写代码”。没错,如果你要生成像网站这样以“看起来对”为目标、且不在乎“内部代码在做什么”的东西,这确实是一个很好的、甚至极具变革性的解决方案。但在许多场景下,尤其是在更偏技术的领域,“看起来对”远远不够:你需要真正知道正在计算什么。这正是 Wolfram Language 至关重要的原因。因为它能提供最高层次、最便于人类理解的对所做之事的表征,并提供一种将精确的计算片段封装起来、以便在任何地方反复使用的方式。
现代 AI 在编程领域取得的成功令人瞩目且始料未及。但从某种意义上说,它对我们而言远没有对传统编程语言那么影响重大。因为几十年来,我们的使命一直是尽可能多地将计算规范和计算过程自动化。而成果就是 7000 多个覆盖计算世界的原语——使人们能够极其简洁地表征大量事物。
其实几十年来我一直在说,传统编程的许多工作都可以通过使用 Wolfram Language 的高级构造来自动化。确实有很多人(包括我自己)多年来一直用 Wolfram Language 大幅扩展自身的计算能力,同时避免了编写大量传统编程语言的代码。
但如今 AI 提供了另一条路径——它能自动写出那些大量的传统编程语言代码。没错,它不是百分之百可靠,通常需要相当精妙的调校才能让它保持在正确轨道上。但至少,如果不在意具体计算什么,它为自动化提供了一条有价值的路径。
对于刚开始使用 Wolfram Language 的人——或者在自己不熟悉的领域工作的人——AI 提供了一个便捷的初始自动化层。但如果你已经熟练掌握了 Wolfram Language,通常你并不会想要这么做。Wolfram Language 提供了一种可以进行思考的媒介。一旦你熟练掌握了它,你通常能更轻松地直接用这门语言表达自己的想法,而不是先用日常自然语言把想法说出来。(我知道,当我在研究某件事时,我可以更快地开始键入 Wolfram Language 代码,而不是用自然语言描述我想做什么——至少做不到任何精准的描述。)
值得一提的是,Wolfram|Alpha 早在 17 年前就已经开创了使用自然语言来指定计算的概念。它采用的技术与现代 AI 不同——更侧重于处理自然语言的小片段,并能可靠地将其转换为精确的计算。但它早在多年前就已经让我们能够在 Wolfram Language 中利用自然语言,比如用于指定实体。如今,它也在帮助我们构建与 AI 之间更好的沟通通道。
最近几个月,关于AI在软件开发未来中的角色有很多讨论。那么它对我们所做的工作以及像Version 15这样的开发有什么影响呢?当然,在某些地方它确实很有帮助,特别是在处理我们系统中使用传统编程语言的部分(通常与外部接口或直接与硬件交互相关)。但如今Wolfram Language的大部分代码都是用Wolfram Language本身编写的——我们已经充分利用了该语言内置的所有自动化功能。随着语言的每个新版本,自动化的内容越来越多,开发工作的杠杆效应也越来越强。事实上,这正是我们在过去四十年中建造的卓越技术大厦得以实现的原因。而今天,它为我们带来了Version 15。
每个笔记本中的AI助手
在ChatGPT原始版本发布后的几周内,我们就构建了从LLM内部调用Wolfram Language(和Wolfram|Alpha)的方法——以及从Wolfram Language(和Wolfram Notebooks)内部调用LLM的方法。接下来的一年,我们构建了技术,使我们能够将笔记本助手(Notebook Assistant)作为Wolfram系统的附加组件发布。然后在今年二月,我们发布了基础工具技术套件(Foundation Tool technology suite),进一步与LLM集成。现在,在Version 15中,我们推出了另一层AI集成:内置的AI助手(AI Assistant)。
在Version 15中创建一个新笔记本(除非你已关闭该功能),你会看到笔记本底部有一个新元素,我们称之为“chatbar”,它立即将你连接到我们的AI助手:

在chatbar中输入你想要的内容(你也可以粘贴图片等)。然后只需按回车键,你的输入将被发送给AI助手,它将尝试帮助你:

即使你问的是相当模糊的问题,AI助手也会给出最精确解释的猜测,并附带可读的Wolfram Language代码。按下按钮,代码将被插入到你的笔记本中,然后立即运行:

你可以把聊天栏视为一种便捷、随时可用的方式,在笔记本中创建会话单元格。自版本 14.2 起,你已可以通过直接输入 `'` 来新建一个会话单元格,从而开启新单元格。
与任何会话单元格一样,通过聊天栏创建的会话单元格也可以利用笔记本中其上方内容的上下文。(要断开上下文,你可以在单元格之间输入 `~` 来插入会话分隔符。)
但版本 15 更重要的变化在于,聊天栏和会话单元格背后的 AI 助手现在已立即可用于所有 Wolfram 笔记本。无需任何配置。而且对于 AI 助手的基础级别,也无需额外订阅。
AI 助手的基础级别立即可用,作为一种超越文档的帮助方式,让你在使用 Wolfram 语言时获得支持。我们今天还发布了 AI 助手的两个更高级别,可通过订阅获得:Pro 和 Research。Pro 级别让你能够处理更大、更复杂的项目,Research 级别则提供对最新前沿 AI 能力的访问。(现有的笔记本助手用户将自动迁移至 AI 助手 Pro。)
要进入 AI 助手的控制界面,只需点击聊天栏侧边的按钮:

如果你不希望默认显示完整的聊天栏,点击该按钮即可将其最小化:

(最小化状态会在你打开新笔记本时被记住。你可以在主偏好设置菜单中全局控制聊天栏是否显示——甚至 AI 助手是否可用。)
从你的 AI 环境使用 Wolfram
AI 助手让你能够在 Wolfram 笔记本内部访问 AI。但假设你在计算机上使用一个 AI 环境——比如 Claude Code 或 Codex。在版本 15 中,现在可以轻松地将你的 AI 环境连接到你的桌面 Wolfram 系统并调用它。
如果你的计算机上有标准的 AI 环境——比如 Claude Code 或 Codex——Wolfram 系统通常能够检测到它,并在欢迎屏幕中显示一个条幅:
![]()
点击该条幅将进入偏好设置对话框中的“面向 AI 的服务”选项卡:

按下按钮即可配置系统上检测到的所有 AI 环境。
完成设置后,这些 AI 环境将自动配置好,以便在需要时随时调用 Wolfram 工具。(包括多个工具,例如用于执行 Wolfram 语言代码、读写笔记本以及分析 Wolfram 语言代码的工具。)
在技术层面,其实现方式是基于新的 Wolfram 智能体工具框架——你也可以在 Wolfram 语言内通过编程方式使用该框架。例如,要连接所有检测到的 AI 环境,只需使用 Wolfram 语言函数 `DeployAgentTools` 即可:
(针对特定 AI 环境,使用 `DeployAgentTools["ClaudeCode"]` 等调用方式。)
以上所有解释对人类来说已经足够清楚。但如果你是一个 AI 智能体(例如正在阅读本文),可以直接前往描述如何配置所有内容的 Markdown 文件。事实上,作为现代平台,我们的主网站 wolfram.com 会自动向请求的 AI 智能体提供 Markdown 内容。同时——为了让我们人类仍然能了解情况——网站顶部新增了一个“面向 AI”的链接:

时间序列(与事件序列)迈入大型化
跨时间的数据极为常见。自 10.0 版本(2014 年)起,我们就有了用于表示此类数据的 `TimeSeries` 构造。但在 15.0 版本中,我们推出了更强大(且完全兼容)的 `TimeSeries` 版本,能够处理海量且更多样化的数据集。新的 `TimeSeries` 框架基于 14.2 版本引入的 `Tabular` 框架构建,并与后者互通互操作。这带来的一个直接结果是支持多分量时间序列,即每个时间点可定义多个分量——这些分量对应底层 `Tabular` 对象中的列。另一个重要结果是 `TimeSeries` 现在继承了 `Tabular` 在缺失数据处理方面的全部精细能力。
粗略来看,可以将 TimeSeries 视为一种带有时间戳列的 Tabular 数据。但它远不止如此。尤其因为 TimeSeries 会在给定的具体时间之间自动插值。或者更准确地说,它会在应该插值的时候(比如对于数字、数量等)进行插值,而在不该插值的时候(比如对于字符串或实体)则不插值。此外,TimeSeries 现在会考虑时间的粒度,因此例如在以日为单位的时间序列中按周查询某个值时,会进行适当的平均计算。
在 Version 15.0 中,相关格式现在可以直接导入为 TimeSeries 对象:
通过摘要框中的“预览”按钮可以查看实际数据的预览:

你也可以显式获取底层的 Tabular 数据,其中包含专门用于定义时间序列的时间戳列:
你可以立即在 TimeSeries 中使用 Tabular 的功能。例如,下面这段代码选取了时间序列中的“Track”分量,并绘制了它的地图:
下面是对时间序列中“Elevation”分量的绘图:
下面对高程时间序列在 15 分钟窗口内取移动平均:
我们也可以直接获取插值后的值。例如,下面获取的是某个瞬时(插值后的)值:
而下面获取的是在一小时内的平均值:
顺便提一下,利用 Version 15.0 的另一项新功能,你现在可以通过 TimeSeriesSummary 快速获取 TimeSeries 的摘要:
到目前为止我们看到的时间序列只有 12,931 条记录,规模并不大。但新的 TimeSeries 框架能够常规处理包含数百万条记录的时间序列。例如,下面我使用 Wolfram Data Drop 收集的过去近十年间我的心率数据创建了一个时间序列:

除了我们新的时间序列(TimeSeries)框架之外,15.0 版本还引入了一个新的事件序列(EventSeries)框架。时间序列处理的是至少在概念上随时间连续变化的事物——比如骑行时到达的海拔高度。而事件序列处理的则是离散事件,比如服务器访问、按键或地震。在时间序列中,任何给定的分量在特定时刻只有一个值。而在事件序列中,原则上可以有任意数量的事件同时发生——特别是当时间粒度是“天”这样的单位时。
15.0 版本的新增函数是 TimeSeriesEvents,它可以从时间序列中提取各种类型的离散事件。例如,下面这个函数从上面的海拔数据中给出了局部最大值(以 10 分钟为周期):
结果是一个 EventSeries 对象,本例中仅包含 5 个点:
这里我们绘制了“连续”的时间序列,以及来自事件序列的离散点:
与 TimeSeriesEvents 互补的函数是 EventSeriesAccumulate,默认情况下它会统计事件序列中的事件数量,并给出这些事件的累积数量所对应的时间序列。
此外,15.0 版本还有一个新函数 EventSeriesLookup,它可以查找位于某个时间规范内(或者,比如说,紧邻之前或之后)的事件:
计算走进分类数据
在处理数据时,人们往往非常强调将数据数值化。但有时这并不是我们想要的。有时候,数据只需要说明某个对象属于哪个类别,而无需赋予它数值。例如,人们可能希望使用“小”、“中”、“大”这样的类别,或者“男”、“女”这样的类别。关键在于,将事物分配给这种有限、离散的类别集合,能够实现各种计算。
在 15.0 版本中,我们引入了一种通用的、符号化的分类数据表示方式。之前我们已有多种函数——比如 RandomChoice 和 CategoricalDistribution——来处理分类数据的各个方面。但现在,15.0 版本对这些分类数据进行了完全统一的处理,现有函数以及许多新函数都可以接入其中。
关于分类数据,首先要说的是它有两种基本类型。有序数据——比如“小”、“中”、“大”——其类别是有序的。以及名义数据——“男”和“女”——这类数据没有顺序。
在 15.0 版本中,我们使用 Ordinal 来表示一组有序类别:
而某个特定类别则表示为,例如:
其中的显示图标表示我们正在谈论的是有序类别中的哪一个。
以下是我们如何从有序类别集合中随机抽取 10 个样本:
由于类别是有序的,像 Max 这样的函数可以立即对它们进行操作:
我们还可以根据这些数据绘制直方图
值得注意的是,当某个类别中没有数据项时,会明确显示一个零。
我们可以将有序类别的符号化表示直接转化为(均匀的)分类分布:
然后可以在计算中使用这个分布,例如这里用于计算一个概率:
有时需要从分类数据中提取各种类型的值。以下是与有序数据相关的“得分”:
名义数据的处理方式大致相同,只是没有定义顺序
因此像 Max 这样的函数无法求解:
Nominal 和 Ordinal 都可以出现在 Tabular、TimeSeries 和 EventSeries 中,并且在那里得到非常高效的处理。
引入 ModelFit 超函数
几十年来,Wolfram 语言中有多种方法可以对数据进行拟合。但在 15.0 版本中,我们引入了一种强大的统一数据拟合新方法,以 ModelFit 函数为核心。
基本概念是从模型的“符号化轮廓”开始,然后使用 ModelFit 来填充具体细节,从而得到针对特定数据的拟合结果。例如,以下是指数模型的符号化轮廓:
实际上,这代表了“任何可能的指数模型,但未填入特定的参数值”。但现在我们可以将它与具体数据一起输入 ModelFit,从而得到一个具体的指数模型:
我们可以将其视为对原始数据的一种基于模型的近似。例如,我们可以在某个特定点上评估这个近似——就像我们评估一个 InterpolatingFunction 或 PredictorFunction 一样:
下面是一个显示拟合结果图:
我们也可以在绘图函数内部自动完成拟合:
拟合效果如何?我们可以请求一份报告:
我们可以深入到报告中获取更多细节:
在给出原始模型的“符号化框架”时,有时为模型中的参数和变量提供名称会很有用:
这里我们指定常数项必须为 0,然后为其他参数赋予不同的名称:
在 ModelFit 中,你不仅可以请求最佳拟合模型,还可以请求它的属性:
版本 15 支持多种模型——未来版本还会增加更多。除了 ExponentialModel,还有用于对数模型和幂律模型的 LogModel 和 PowerModel。
下面是一个示例,拟合关于所有已知系外行星的数据(并且惊人地准确复现了开普勒第三定律):
ModelFit 会处理单位等内容——这里它“推导”出了一个地球年的长度:
ModelFit 允许你尝试同时拟合多个模型。例如,PolynomialModel[UpTo[5]] 代表任何最高次数为 5 的多项式模型。ModelFit 默认返回最佳拟合模型,在本例中是一个三次模型:
本例中的报告包含模型选择的相关信息:
你也可以请求更多细节:
同样,我们也可以在绘图函数内部进行拟合:
LinearModel 允许你指定一个由基项的线性组合构成的模型:
FormulaModel 允许你指定任意公式进行拟合,而不一定是基项的线性组合:
还有用于拟合周期数据的 PeriodicModel。这里我们要求一个具有 2 个频率分量的拟合:
ModelFit 可以立即处理 TimeSeries 等数据:
特别是对于结构更复杂的模型,通常会有几个“超参数”,可以在一个关联中指定。例如,这里可以指定要包含的频率数量,以及用于识别每个频率时使用的样本数量:
ModelFit 不仅可以处理传统的“统计类”模型,也可以处理机器学习类模型。一个非常简单的例子是 NearestModel,它执行最近邻拟合:
默认情况下,NearestModel[ ] 处理单个最近邻。这里我们要求它对每个点使用 3 个最近邻:
ModelFit 可以处理任意维度的数据:
这组特定数据来自一个表格(Tabular),而 ModelFit——和许多其他函数一样——被设置为可以直接从表格中抽取列:
下面是一个稍微复杂一点的模型示例:多层感知机神经网络:
这是本例中对应的底层神经网络:
顺便说一下,如果我们改变神经网络的超参数,结果会是这样:
像这样的神经网络模型可以用于再现和预测数据,但不能直接“被解释”。ModelFit 还支持 DecisionTreeModel,用于生成潜在可解释的决策树模型。下面是我们使用经典的泰坦尼克号数据集并拟合一个深度为 2 的决策树模型时得到的结果:
与我们给出的其他示例不同,这个模型不仅拟合数值数据,还拟合分类数据。下面是将模型应用于一个特定的“数据点”(以关联表示)时发生的情况:
这是整个决策树的可视化结果:
引入符号化音乐
Wolfram 语言的核心使命之一是为我们所能触及的一切事物开发一种计算语言表示。我们早在 1991 年就引入了基础声音,并在 2016 年引入了完整的音频。现在,在 Version 15 中,我们引入了音乐——以及一种符号化、计算化的表示,涵盖从音符到完整乐谱的一切内容。
在最底层是音乐音高的符号化表示:
你可以直接对音乐音高进行计算:
是的,已经存在一些细微之处:
一个音符实际上就是一个音高加上一个时值,这里是一个二分音符:
这是它的音高:
这是它的时值:
你可以对音乐时值进行算术运算:
除了单个音符,还有和弦。这里是 G 大调和弦:
这是它里面出现的音高:
以及对应的音程:
这是一个“通过算法构造”的和弦(你可以点击音符图标来播放它):
现在这个和弦被移高了 5 个半音:
除了音符、和弦和休止符(用 MusicRest 表示)之外,表示音乐还有三个层次:MusicMeasure、MusicVoice 和 MusicScore。一个 MusicMeasure 对应音乐的一小节,包含若干音符、和弦和休止符的序列:
默认情况下,一个小节会被假定有一个拍号。这个则指定了一个不同拍号的小节:
然后一系列小节被组合成一个声部:
最后,多个声部可以并行组合成一个乐谱(这里每个声部用不同颜色渲染):
那么一首真正的乐曲呢?在 Version 15 中,我们可以将 MIDI 文件作为乐谱导入。这是 Wolfram Data Repository 中已有的一个乐谱:

MusicPlot 生成一个方便的视觉表示:
我们能对一个乐谱做哪些计算呢?
一个简单的事情是:我们可以算出它有多少个全音符长:
我们还可以算出它的音高范围:
这里是每个声部绝对音高的直方图:
这里是一张显示不同音高类别相对出现频率的图表:
从这张图中我们可以推断出乐谱的“有效调性”——尽管 MusicMeasurements 可以直接做到这一点:
我们在这里看到的所有东西,都可以视为基于音乐的符号化表示。但有了这种符号化表示,我们总可以将其渲染成实际的音频:
表格数据的更大、更好连接性
我们在 14.2 版本中引入的 Tabular 框架,使得在 Wolfram 语言中处理表格数据变得极为高效。但这些数据从何而来?(而且,又要去向何处?)在 14.2 版本中,我们引入了导入 CSV 等文件以及 Parquet、ArrowIPC 等列式数据格式文件的高效方法。随后在 14.3 版本中,我们增加了直接从关系型数据库导入数据的能力。
在 15 版本中,我们增强了这些功能,并增加了更多新特性。首先,现在可以从多种文件中有选择地仅导入特定列(14.2 版本已支持高效导入特定行)。例如,以下代码仅从一个 CSV 文件中导入两列:
在处理超大数据集时,仅导入特定列(和行)的能力至关重要——因为这样可以让你将大部分数据留在磁盘上,同时仅将所需部分高效导入内存。
原始数据集可以存放在哪里?它可以存放在你电脑上的某个文件中。但 14.2 版本引入的 DataConnectionObject,也提供了对 Amazon S3、Azure Blob Storage 和 Dropbox 等数据存储的无缝访问。现在在 15.0 版本中,我们还增加了对 Azure Files 和 Azure Tables 的无缝访问。
DataConnectionObject 还提供了对关系型数据库中数据的访问(这是我们在 14.3 版本中增加的功能)。在 15.0 版本中,我们将这种访问的效率提升了一个数量级以上,使其现在几乎达到了理论上的最快速度(并且最高效地利用了内存)。
在 15.0 版本中,我们还将支持范围从关系型数据库扩展到多维数据库,特别支持了 Databricks(以及 Snowflake)。例如,下面展示了如何建立一个 DataConnectionObject,通过特定的多维(OLAP)查询来定义与一个数据“湖仓一体”的连接:
15.0 版本的另一个新特性是 Tabular 与 ExternalEvaluate 的连接,从而支持包含 Wolfram 语言和外部语言的表格数据工作流。例如,你可以在 Python 中获取一个 pandas DataFrame,并通过 ExternalEvaluate 立即在 Wolfram 语言中获取其数据:
(是的,我们所有关于封装 Python 等语言的工作,都能以特别简洁的方式实现这一目标。)
更多针对 Tabular 的内容
我们花了很长时间来设计全新的 Tabular 框架。我很高兴地说,这个设计运行得非常好。但与 Wolfram 语言中所有新框架一样,一旦框架部署并投入使用,人们就会开始发现各种扩展和完善它的方式。Tabular 框架也是如此。在 Version 15 中,我们为 Tabular 框架引入了相当多的增强功能。
第一个增强功能很简单,但非常实用。当笔记本中有较小的 Tabular(比如几十列、几千行)时,该 Tabular“背后”的所有数据都会自动存储在笔记本中,因此无论何时使用笔记本,这些数据都可用。较大的 Tabular 对象会像笔记本中的许多大型对象(例如 `SparseArray` 等)一样处理,并提供一个按钮,让您选择是否将数据直接存储在笔记本中:
Version 15.0 中另一项新功能是 `TabularSummary`,它能高效生成 Tabular 的概要总结——下面这个例子展示的是我们刚刚导入的一个较大的 Tabular:
`TabularSummary` 提供了灵活的方式,可以选择要总结 Tabular 的哪些部分,以及如何总结它们。例如,以下代码只要求包含数字的列,并请求对这些列进行完整的统计摘要:
如果我们想对这些数据建模呢?我们可以立即使用 Version 15.0 的新函数 `ModelFit`,通过语法选取特定的列来拟合模型:
这种模型拟合在有数字可操作时效果最佳。但如果数据包含实体——比如国家或树木种类呢?我们如何从这些实体中派生出可用于建模的数字?实际上,Wolfram 语言包含了大量关于各种实体的精选数据。在 Version 15.0 中,有一个新函数 `EntityAugmentColumns`,可以让您立即增强 Tabular,添加与 Tabular 某列中实体关联的数据(数值或其他类型):
Tabular 的一个重要特点是它针对多种常见数据类型(如数字和日期)进行了专门优化。在 15.0 版本中,新增了一种数据类型,即由 Around 表示的近似数值数据:
顺便一提,这个示例还展示了第 15 版的另一个新特性。我们在这里没有显式命名各列,因此它们仅通过数字索引来标记——在显示中这些索引现在以灰色呈现。
Tabular 还有许多其他细节改进和增强。其中之一是,Tabular 中的 GeoPosition 列现在可以处理地理投影,在需要时对位置进行适当重投影。
可视化调整
Wolfram 语言的图形被广泛使用,我们一直致力于确保它们始终具有新鲜、活泼的外观。实现这一点的一个重要环节是确保我们使用的颜色“看起来跟上时代”。我们希望整体配色保持一致,但也希望定期“提升”颜色以使其“与时俱进”。
在过去几个版本中,我们对颜色做了大量提升工作。现在在第 15 版中,我们将注意力转向了图表中的区域,例如饼图。举个例子,下面是在 14.3 版中渲染的饼图:
下面是第 15 版中提升后的版本:
直方图等图表也有新的默认颜色。这个变化更微妙一些,但
在第 15 版中被替换为:
第 15 版的另一个新特性与 PlotStyle 选项的扩展以及相关选项有关。在之前的版本中,你使用 PlotStyle 来指定 Plot 等图表中曲线的样式,但需要使用 ChartStyle 来指定 BarChart 等图表中柱形的样式。之所以做此区分,是为了处理多组柱形等样式的问题。但在第 15 版中,我们采用了一种更精简、更统一的方式来实现这一点——其结果是现在我们可以在所有情况下使用 PlotStyle,而无需再因有时不得不使用 ChartStyle 而产生混淆。
例如,以下用法现在可以用于 BarChart 中柱形的样式设置:
如果你有几组条形图,该怎么办?ChartStyle 默认会为每组中的对应条形指定样式,而 PlotStyle 则(为了与 Plot 等函数中的用法保持一致)为整组指定样式:
但在第 15 版中,我们引入了一种新的基于关联的指定颜色的方式,它可以让你分别定义“元素”(即单个条形)与条形组的样式:
同样的基于关联的机制也适用于 PlotLabels 等——例如,它可以让你单独标记单个条形与条形组:
在 ListPlot 这样的函数中你也可以拥有这种精细的控制。这里我们定义了一个“基础样式”,然后说明不同点列表应该如何渲染:
我们新的基于关联的机制所能实现的一些功能,以前也可以通过现有选项的各种组合来实现。但基于关联的机制使这一切变得更加清晰和直接。
DistributionChart 也有类似的问题;它拥有很强大的功能,但只能通过略微晦涩的选项组合来访问。在第 15 版中,我们将 DistributionChart(顺便说一句,这是一个非常好用且实用的函数)最重要的功能主流化了。
以下是 DistributionChart 新的默认行为——为每个数据集直观地生成平滑的直方图分布:
如果你不想要平滑效果,只需使用 "Histogram" 作为显式的第二个参数:
如果你愿意,你可以获得双面的“小提琴式”外观:
或者你也可以显式显示密度(可选择添加分位数线等):
Wolfram 语言拥有极其灵活的可视化能力。但我们一直在寻找让更多可视化操作变得更加便捷的方法。在第 15 版中,我们添加了几个全新的可视化函数来帮助实现这一点。
其中之一是 BubbleHistogram。假设你有一组 {x,y} 值。Histogram3D 和 DensityHistogram 是可视化这些值分布的两种方法。在第 15 版中,现在还有 BubbleHistogram:
在完全不同的方向上,第 15 版还新增了 PeriodTablePlot。如果你不另行指定,它就会直接“绘制一张元素周期表”:
但你也可以将数据“叠加”在元素周期表上进行绘制。例如,这个命令要求绘制每种元素的相态:
多面板可视化
假设你有一组图表:
你可以将它们排列在网格中显示:
但这样做在很大程度上很浪费(而且“杂乱”):这些图表的基本比例尺相同,但我们却在每个图表中重复了这些比例尺。在版本 15 中,我们新增了一个函数 PlotGrid,它接收一组图表,并尝试以最优方式将它们排列在网格中,尽可能共享比例尺信息:
这涉及许多细微之处。本例中可见的一个问题在于:比例尺是在不同行和不同列之间共享,还是仅在每行和每列内部共享。
以下是如何要求所有比例尺共享的方式——在本例中,它使两行的 y 轴比例尺保持一致:
PlotGrid 也能处理标签,默认情况下同样尽可能共享标签:
实际上,PlotGrid 会从各个图表中“提取”选项,然后尝试将它们组合起来,形成一个一致的总体网格。PlotGrid 本身也可以指定选项。例如,你可以在 PlotGrid 中指定整体的 AspectRatio 或 ImageSize。但也可以为 PlotGrid 指定 ItemAspectRatio 和 ItemSize 选项,用以设定网格中每个单独项目的宽高比或尺寸。
千兆字节级笔记本与实时查找
我们最早在 1988 年随 Mathematica 1.0 推出了笔记本功能。我认为可以公平地说,它们取得了巨大成功,无论是作为完成工作的方式,还是作为展示和记录工作成果的途径。当年我们刚推出笔记本时,很难想象它们的大小会超过几兆字节。但——四十年后——笔记本可以变得非常大。有时是因为它们包含大型图形,有时是因为它们包含大型图标化表达式,有时则是因为有人点击了“保存到笔记本”功能,直接将视频或大型表格数据集保存到了笔记本中。
我们在处理大型笔记本方面已经取得了相当不错的成绩。但过去,为了适应普通计算机相对较小的内存和缓慢的大容量存储,我们常常需要做出权衡。然而,随着最大的笔记本文件大小开始逼近千兆字节,我们意识到需要新的笔记本基础设施。因此,大约十年前,我们启动了一个大型项目,从头重建我们的笔记本基础设施。现在,我很激动地宣布,这个项目已经完成——Version 15 拥有全新的、高效的多核多线程笔记本基础设施。
其结果是,现在可以轻松处理数 GB 大小的笔记本(除了存储空间外,没有任何因素从根本上限制笔记本文件的最大可能尺寸)。在此过程中,我们保持了完全的兼容性——因此 Version 15 仍然可以打开 Version 1 创建的笔记本(是的,我们对此进行了非常广泛的测试)。笔记本文件的底层结构一如既往,与以前相同。但能够在新笔记本基础设施中实现如此性能的关键,在于我们彻底重新思考了笔记本文件的解析方式,采用了现代多遍解析方法。
然而,随着大型笔记本的常规使用,也带来了新的问题。其中值得注意的是“查找”功能。在 Version 15 中,基于我们的新笔记本基础设施,我们拥有一个全新的、高效的查找系统。
在笔记本中按下 CMD+F / CTRL+F,就会弹出该笔记本的查找对话框:
![]()
操作速度非常快,你一开始输入,就会看到笔记本中匹配项的总数。(即使笔记本文件大小达到数 GB,这个功能也能正常工作。)
查找对话框大部分以非常标准的方式工作。笔记本中的每个匹配项都会被高亮显示。(或 ENTER 键)会跳转到下一个匹配项,而输入框中的 nnn/nnn 显示则标明你当前位于第几个匹配项。 用于切换大小写敏感性, 用于匹配整个单词。
按下 > 键,就会展开查找对话框中的替换部分:

按下 按钮执行单次替换,并跳转到下一个匹配项。按下 按钮则执行全部替换。
在 Wolfram 笔记本中,“查找”功能如何工作有很多细节。其中之一是处理排版表达式和特殊字符。事实上,你可以输入一个排版结构并查找它:
![]()
特殊字符,比如 α,也可以正常工作。不过你不能用 ESC 键输入它们,因为在系统层面这会关闭查找对话框。你仍然可以使用长名称如 \[Alpha],以及 SHIFT+ESC 键。
还有另一个微妙之处。在执行替换时,你只想替换笔记本中的“输入”内容;而跳过出现在生成输出中的内容。这一点通过一个标记来指示:你在生成输出中找到的内容,其高亮边框会显示为虚线:

自从 1988 年我们引入笔记本以来,它们本质上一直是“单面板”文档,主要用于维护一个有时包含动态内容的线性序列。当需要另一个面板时,典型的模式是让它成为另一个笔记本。在 Wolfram Cloud 中——遵循 Web 上一切最终都必须适配单个浏览器窗口的典型模式——我们多年来已经拥有各种侧边栏。现在,侧边栏也来到了桌面笔记本中。在未来的版本中,将会出现相当通用的侧边栏机制。但在第 15 版中,侧边栏是针对两个特定的高价值用途引入的:笔记本属性和 AI 助手。
在任何笔记本中,点击工具栏中的齿轮图标(或从“窗口 > 侧边栏”菜单中选取),笔记本就会弹出一个“笔记本属性”侧边栏,允许你查看(并修改)各种常用的笔记本级别设置:

第 15 版中侧边栏的另一个应用是 AI 助手。聊天栏让你在主笔记本窗口中创建聊天单元格。但有时,与 AI 进行一个“侧边聊天”会更方便,这不会直接影响你的笔记本主要内容。笔记本工具栏中的按钮会打开一个侧边聊天——在一个侧边栏中:

(你只需拖动窗口中的分隔线就可以调整侧边栏的宽度。)
视觉主题登陆笔记本
不是所有人都希望自己的笔记本看起来千篇一律。能够在浅色和深色模式之间切换,是不同用户即便使用同一个笔记本也能获得截然不同观感的主要方式之一。但在第 15 版中,还有另一种让笔记本外观与众不同的重要途径:更换其视觉主题。你可能想通过它来强化语法高亮效果,或让其更柔和;可能出于无障碍需求,也可能纯粹是审美偏好。
你可以通过“笔记本属性”侧边栏为特定笔记本更换主题,也可以通过“偏好设置”面板为所有笔记本全局更换主题。(此外,你还可以通过设置 NotebookTheme 选项,以编程方式更换笔记本主题。)
以下是“偏好设置”面板中的“主题”部分(其中既包含 Monokai、Solarized 和 Dracula 等标准的现代主题,也包含我们设计的 Wolfram Saturated 和 Stargazer 等主题):

选择任意主题,它会立即应用到你的笔记本上。请注意,每个主题都同时包含浅色模式和深色模式两个版本。
如果你在全局偏好设置中设置了主题,该主题仅用于决定你系统上笔记本的外观;当你将笔记本发送给其他人时,对方看到的将是他们自己的主题,而非你的。但如果你通过“笔记本属性”侧边栏为某个特定笔记本设置了主题,那么该主题会随笔记本一起传递,收到你发送的笔记本的任何人都会看到这个主题。
笔记本主题实际上基于第 14.2 版引入的一项特性:ThemeColor。笔记本主题的工作原理是:笔记本中的不同元素会被标记为使用不同的命名颜色进行渲染。例如,默认样式表中的标题单元格被标记为使用命名颜色“Accent1”进行渲染:
再比如,实体元素使用“Accent4”进行渲染:
假设你想在图形中匹配这些颜色。你可以通过引用这些命名颜色来实现:
如果某人将主题切换为 CRT(阴极射线管风格),那么对他们而言,这些图形会立刻呈现出不同的外观:

随着笔记本视觉主题的引入,第15版的另一项新功能是对取色器进行了扩展,允许选择命名颜色:

当它太长时,就会被撕裂
假设你像我经常做的那样,把笔记本当作展示讲解的媒介。遇到长长的输出内容该怎么办?如果把它们留在打开的单元格里,会打断展示讲解的流畅性。但如果关闭单元格,别人又无法知道里面到底有什么。在第15版中,还有另一种选择:直接把它“撕”下来。
选中该单元格,在主菜单(或右键菜单)中选择“单元格 > 撕裂省略”:
一旦有了撕裂效果,你就可以直接上下拖动它:
这一切都非常简单——但非常有用。实际上,我多年来在自己的写作中(包括关于之前Wolfram语言版本的介绍!)一直在使用这个机制。Wolfram函数库中已经有一个函数可以实现独立的(且参数化的)版本,已有几年时间。但在第15版中,它已完全集成到我们的笔记本系统中。
它实际上是如何工作的?撕裂效果是由一个确定性随机过程生成的,该过程以分配给该单元格的UUID为种子——因此,该特定单元格中的撕裂效果看起来总是相同的,但如果复制该单元格,撕裂效果就会不同。(撕裂的实际渲染使用高效的像素着色器完成。)
顺便提一下,你可以为任何单元格添加撕裂效果——无论它包含图片、文本、交互内容等:
在亮色模式下变暗
假设你在笔记本中处于亮色模式工作,但希望某一张图片以暗色模式呈现(例如用于演示)。在第15版中,有一种简单的方法:只需使用DarkModePane:
这里与Pane中的选项相同:
你可以指定一个包裹宽度:
以及一个高度——可选择是否显示滚动条:
是的,如果你处于暗色模式,也可以完全反过来操作,使用LightModePane。哦,如果你想用某种特殊的深色作为输出的背景,DarkModePane是一种很好的方式,可以将所有内容(如坐标轴及其标签)“翻转”为暗色模式。
顺便提一下,如果你要记录明暗模式下的情况,就需要 `LightModePane` 和 `DarkModePane`——这两个组件会在我们的文档中频繁出现。
那次计算中发生了什么?`Monitor` 的单参数形式
如何了解正在进行的计算内部发生了什么?你可以插入一些 `Echo`。或者——自第6版开始——你可以使用 `Monitor`。但过去 `Monitor` 的工作方式一直需要你显式告知要监控的变量。这对于监控像 `Table` 这类有命名变量的函数来说没问题。但像 `Map` 呢?你怎么监控它?
嗯,在第15版中有了一个新的 `Monitor` 单参数形式,可以让你监控像 `Map` 这样的函数:
弹出的蓝色方框会显示 `Map` 执行到了哪里,以及完成剩余部分预计还需要多久。(它还有一个中止计算的按钮。)
`Monitor` 的单参数形式适用于所有常见的函数——比如与 `Map` 相关的函数、与 `Nest` 和 `Fold` 相关的函数、与 `Table` 相关的函数等等。
对于像 `Table` 这样的函数,你总是可以使用双参数形式,指定要监控的内容:
但单参数形式则“自动完成一切”,为你提供整体进度的信息,而无需显式考虑单独的迭代变量等:
双参数形式 `Monitor[expr, mon]` 会监控 `expr` 求值过程中 `mon` 值的所有变化。而 `Monitor[expr]` 则只关注 `expr` 中顶层函数的求值。换句话说,在单参数形式中,`Monitor` 需要直接包裹在你想要监控的函数外面,无论是 `Map`、`Fold`、`Array` 还是其他函数。
子值现在可以被保持(Held)了!
这是一个边缘情况,近四十年来我们一直想象着有一天能处理好,但它似乎总是很难。嗯,现在在第15版中,我们终于做到了:子值现在可以被保持了!
这是什么意思?首先,什么是子值?当你进行这样的赋值时:
你是在为我们称为 `f` 的下值(downvalue)进行赋值。但如果你进行这样的赋值呢:
在这种情况下,我们说你在为 g 赋予一个子值。
子值在多种用途中都很有用,特别是在设置运算符形式时,例如:
好了,那么保持(holding)这个概念是怎么回事?通常,如果你输入 f[1+1],会发生的过程是:首先 1+1 被计算为 2,然后 f[2] 被计算。但如果 f“保持其参数”,那么 1+1 就不会先被计算,而是作为 1+1 传递给 f。
这有什么用?假设你写了 x = 1,它被解释为 Set[x, 1]。这里的 x 保持住很重要。你想要设置的是“x 本身”的值,而不是 x 的值。所以你需要将 x 传递给 Set 而不先对它进行求值。
事情以这种方式工作是由 Set 的属性决定的:HoldFirst 表示 Set 的第一个参数应该被保持:
假设你做了这个赋值:
现在 u 的第一个参数会被保持——但其他参数不会:
同时,如果你做了这个赋值:
那么所有参数都会被保持:
好的,那么参数的保持和子值之间有什么相互作用呢?假设你有一个表达式 u[x][y]。如果 u 具有属性 HoldAll,那么在 u[x][y] 这样的表达式中,x 会被保持——但 y 不会:
那么,在版本 15 中,有一个新属性 SubValuesHoldAll——它保持所有子值参数。设置这个属性:
现在在 v[x][y] 中,y 被保持住了,即使 x 会被求值:
并且,顺便一提,这种保持会“一直向下传递”:
这有什么用?最重要的是,它允许拥有能够保持其参数的运算符形式。在设计各种函数时,我们已经期盼这个功能多年了。例如,考虑 AppendTo。AppendTo 具有属性 HoldFirst,因此 AppendTo[x, expr](就像 Set[x, expr] 一样)不会对 x 求值。
但是 AppendTo 的运算符形式呢?我们希望能够写出 AppendTo[expr][x],并且让这个表达式将 expr 附加到 x 上。但要做到这一点,需要 x 保持不被求值。而这——感谢版本 15 中的 SubValuesHoldAll——现在已经成为了可能。
算子形式使得函数式编程尤其优雅和便捷。尤其是在过去十年左右,我们越来越多地为各种函数引入了这类形式。但对于某些函数(如 AppendTo),我们一直无法做到这一点——因为我们之前没有 SubValuesHoldAll。是的,从内部实现的角度看,SubValuesHoldAll 的实现很棘手——因为它涉及一种“求值前瞻”,需要非常小心地处理。但现在在 Version 15 中,这一难题已经解决,我们可以为大量新的、有用的算子函数以及子值的其他用途打开设计空间。
引入即用型增量数据结构
假设你想在数十亿个对象中进行搜索,也许是要挑出具有某种特殊属性的对象。最简洁的代码可能是先生成这数十亿个对象,然后挑出你想要的那些。但当然,这数十亿个对象可能很难存储在内存中。你可能会认为处理这个问题的唯一方法是找出如何顺序生成这些对象,然后编写显式循环遍历这些对象的代码。
在 Version 15.0 中,有一种更好、更简洁——也更高效——的方法,即使用我们新的 IncrementalObject 结构。IncrementalObject 基于我们在 Version 14.3 中引入的 IncrementalFunction 技术,但现在它被打包为即用型,无需显式代码编译等。
IncrementalObject 的基本思想是为一个(可能非常巨大的)事物集合提供符号表示,并设置为可以增量访问这些事物。例如,这个增量对象表示 20 个对象的 20! ≈ 2×10¹⁸ 种排列:
每次你要求这个增量对象的 NextValue 时,你现在都会得到序列中的下一个排列:
现在假设你想找到第一个阶数为 20 的排列。你可以使用 Select 的增量版本:
这里得到的 IncrementalObject 只是所选排列的符号表示。如果你想要实际找到这个选择中的第一个排列,你可以使用 NextValue 来实现:
再次运行 NextValue 即可获取选定集合中的下一个排列:
而且,没错,它的阶为 20:
如果你好奇的话,下面显示的是要到达这个排列需要测试多少个排列:
再看一个例子,这次使用 Subsets 的增量版本,解决一个背包类问题:找出前 20 个质数中总和为 500 的一个子集:
在 15.0 版本中,我们为多种函数提供了增量版本。除了 Permutations 和 Subsets,还有 Tuples,以及 Map、FoldList、Take 和 Range。下面是一个使用 Range 搜索完全数的例子:
如果我们想更进一步呢?也许我们想在不同的计算机上执行计算。那么,我们只需拿起这里得到的 IncrementalObject,然后在另一台计算机上重新开始运行它。它是一个(可传输的)符号表达式,以“惰性”方式表示当前计算状态,随时可以在任意时刻继续执行。
未来版本中还会推出更多与增量计算相关的内容。但 IncrementalObject 已经提供了一种方便的新方式来组织计算,让人们能够以“先枚举、后筛选”的方式思考,同时计算自动按顺序执行,内存占用极低。
大型代码库中的异常与错误处理
编写程序时,程序员通常心里清楚程序应该做什么。但如果出了意外呢?实际上,需要有能合理处理各种可能错误的辅助代码路径。而在大型代码库中,以合理且有条理的方式处理错误这个问题变得越来越重要。
自第 1 版以来,Wolfram 语言就有多种处理错误的方法。这些方法通常在特定函数或模块的局部范围内表现良好。但在第 15 版中,我们引入了一种强大的全新全局错误处理机制,利用了符号化异常的概念。
在深入介绍之前,我们先回顾一下 Wolfram 语言现有的错误处理机制。
在最基本的层面,存在这样一种理念:在某些情况下,特定函数不会被求值(例如模式不匹配,或 `/;` 条件不满足),而是“返回其符号化的未求值形式”。此外还有另一种理念:在出现问题时,显式使用 `Return` 来退出函数。
但这两种机制都非常局部;它们仅处理单个函数内部的错误。
实际上,即使在 Version 1 中就已经存在一种机制——自那时起被广泛使用——用于非局部错误处理:`Throw` 和 `Catch`。在代码的任何地方调用 `Throw`,它都会停止当前操作,并返回到最近的封闭 `Catch`。但这里有个陷阱(可以这么说):如果你所调用的函数(也许甚至不是你自己编写的)中有一个 `Throw`,会发生什么?如果代码触发了那个 `Throw`,它就会(可以这么说)抛弃你的代码正在做的一切。
`Throw` 和 `Catch` 的通用机制是处理错误的一种强大方式。但挑战在于如何正确控制和限定其作用域。在 Version 3(1996)中,我们为 `Throw` 和 `Catch` 引入了标签,这为主体的底层作用域机制提供了良好的基础。但在实践中,特别是对于较大的代码库,它们使用起来很繁琐且难以管理。
许多年过去了。最终在 Version 12.2(2020)中,我们引入了另一种非常简洁的机制来处理相当局部的错误:`Confirm` 和 `Enclose`。其思路是在一段代码中散布 `Confirm` 系列函数(`Confirm`、`ConfirmQuiet`、`ConfirmBy`、`ConfirmMatch` 等),假设它们能正确确认所请求的内容,则不会影响代码的运行——但如果出现问题,它们会停止代码,并返回到最近的封闭 `Enclose`。在其最常见的形式中,`Confirm` 和 `Enclose` 直接出现在单个函数内部,并按词法处理,无需任何显式标签。这对于处理单个函数内的错误极为方便,但如果希望将错误传播到该函数之外,则需要在每一层显式请求该传播,使用额外的 `Confirm` 和 `Enclose` 实例。
那么,如果有一个大型代码库,错误可能在某个函数中发生,并且需要向外传播,可能经过许多对该错误一无所知的函数,那该怎么办呢?在第 15 版中,我们引入了一种处理此问题的机制,即使用符号化异常(symbolic exceptions)。
基本思路很简单:使用 `ThrowException` 抛出一个具名异常,该异常会向上传播到最近的可捕获异常(`CatchExceptions`),前提是该 `CatchExceptions` 被设置为处理相关类型的异常。通常,异常的名称是符号,可以使用标准的作用域机制(包括我们在第 15 版中引入的新机制)将其限定在包中。重要的是,异常类型还可以存在层次结构,因此针对更通用异常类型的 `CatchExceptions` 可以捕获其内部发生的任何子类型异常。
举一个简单的例子,让我们定义一个可以抛出异常的函数 `fac`:
现在让我们定义一个使用 `fac` 的函数 `g`:
然后再定义另一个使用 `g` 的函数 `f`,但这次捕获了 `overflow` 异常:
现在我们可以使用 `f`,如果没有产生异常,它会像往常一样计算结果:
但如果 `f` 求值过程中任何地方产生了异常,该异常就会向上传播,并且 `f` 的值(默认情况下)将是一个 `Failure` 对象:
注意,因为 `f` 捕获了异常,所以任何错误都不会传播到 `f` 的求值之外:
但如果我们直接求值 `g` 会发生什么?此时没有任何 `CatchExceptions` 来捕获产生的异常,因此异常会“接管一切”:
在这种情况下的返回值是底层的 `Exception` 对象:一个表示所生成异常的符号化表示。异常对象包含几条数据:
`CatchExceptions` 可以利用这些数据。这里我们说的是,如果正在处理的是 `OverflowException` 类型的异常,那么应该将一个指定函数应用于该 `Exception` 对象,并返回其结果:
在抛出异常时赋予一个明确的“异常载荷”通常很方便。这里我们重新定义 `fac`,使其在产生异常时将 `x` 作为载荷包含进去:
现在我们的 `CatchExceptions` 就可以利用这个载荷了:
那么,如果我们有多种类型的异常会怎样?例如,假设我们在 fac 中引入了一个 InvalidTypeException:
原则上,我们可以通过在 CatchExceptions 中指定一个类型列表来捕获这两种异常:
但特别是当你处理很多类型的异常时,定义一个异常层次结构要方便得多。你可以使用 RegisterExceptionType 函数来实现这一点。这里我们将 OverflowException 和 InvalidTypeException 都注册为 ComputationException 的子类型:
现在,我们只需要使用 ComputationException 即可捕获 OverflowException 或 InvalidTypeException:
我们还可以设置更细致的异常处理,即在不同异常发生时执行不同的操作:
在我们的定义中,我们使用了显式的 If 来决定是否抛出异常。但在编写易读代码时,使用 Confirm 系列函数通常比显式条件判断更好。而且我们新的异常框架与 Confirm 和 Enclose 中现有的标记机制无缝协作。因此,下面是使用 ConfirmBy 编写的 fac 函数:
f 中的 CatchExceptions 现在将捕获 ConfirmBy 产生的 OverflowException——我们会看到两条消息:一条来自 ConfirmBy,另一条来自 CatchExceptions:
版本 15 中的异常框架非常强大,可以方便地为大型代码库添加良好的错误处理。事实上,我们已经在 Wolfram 语言的内部代码开发中使用该框架的初步版本好几年了。版本 15 中的内容涵盖了大规模异常处理所需的主要部分。还有一些额外功能即将推出,特别是错误翻译——即一段代码中产生的错误可以被翻译成适合另一段代码的形式。(例如,某个特定的内部溢出错误可能会被翻译成一个更通用的“该函数无法计算”的错误。)与此相关,我们还计划引入一套有关 Wolfram 语言内置函数中产生的错误的本体体系,以便用 Wolfram 语言编写的代码中的错误处理可以利用它。
引入结构化包格式
那个 x 究竟指的是哪个 x?一段代码中出现的 x,与另一段代码中出现的 x 是否指向同一个符号?在单段代码内部,可以使用 Module 来局部化某个名称(比如 x)。在不同代码段之间,自 1.0 版本以来,就可以通过上下文来区分同一名称(比如 x)的不同实例。one`x 与 two`x 是不同的 x。当然,如果每个实例 x 都显式指定上下文(如 one`)会很不方便。因此(同样自 1.0 版本起)引入了当前上下文 $Context 的概念,允许指定任何新符号(比如 x)将在哪个上下文中创建;还引入了 $ContextPath,它给出了一个上下文列表,用于在输入中搜索某个符号(比如 x)。有了 $Context 和 $ContextPath,可以避免总是显式指定上下文。但这还不够。于是(同样自 1.0 版本起)又有了 BeginPackage、Begin、End 和 EndPackage 这几个函数,用于管理 $Context 和 $ContextPath 的设置。
自 1.0 版本以来,Wolfram 语言的包一直包含 BeginPackage 等咒语式调用。但这始终有些混乱。没错,一个包内部的符号可以局部化,一个包也可以拥有子包。但对于那些既被局部化又在包之间共享的符号,处理起来一直很复杂。多年来,人们发明了各种各样的机制来解决这个问题。但在我们公司内部,逐渐收敛到了某一种特定的机制上。如今在 15.0 版本中,我们将这个机制内建到了 Wolfram 语言中,称为新的结构化包格式。
结构化包格式在处理较大规模的 Wolfram Language 代码时尤其重要——特别是当代码分布在目录树中多个文件里时。在我们传统的包设置中,符号定义在哪个文件中并没有特殊含义。但在结构化包格式中,一个关键假设是:默认情况下,定义在不同文件中的符号被视为不同的,即它们的名称被认为属于不同的上下文。换句话说,在结构化包格式中,新符号默认是“私有生成的”(即局部化)在其所在文件内。
但如果我们希望某个特定符号 x 是“公共的”,并能在其文件外部使用呢?那么可以用 PackageExported 将该符号声明为导出符号。因此,在结构化包格式中,文件通常包含如下内容:

函数 pub1 等被导出为公共函数,而 priv1 等则保持为文件内的私有局部函数。如果你希望某个符号在包内不同文件之间共享,但不在包外部可访问,只需将其放在 PackageScoped 中,而非 PackageExported 中。
那么,如何用结构化包格式建立一个完整的包呢?你将它的文件放在一个目录树中。在最简单的情况下,在该目录树的顶层有一个文件 init.wl,其中包含 PackageInitialize["name"],通常 name 既是包的基础上下文名称,也是包顶层目录的名称。(当包是 paclet 的一部分时,该 paclet 的 PacletInfo.wl 文件可以指定更复杂的目录结构、不同的初始化文件名等。)
当你使用结构化包格式的包时,你会像传统 Wolfram Language 包一样,用上下文名称调用 Needs——这样就会加载对应目录下的 init.wl 文件。正是在运行 PackageInitialize 时,新结构化包格式的魔法发生了——目录树中的其他文件被加载,其中的符号默认被局部化。
在结构化包格式中还有一个需要提及的函数:PackageImport。当 PackageInitialize 加载文件时,有时你会希望从其他包中导入定义。PackageImport 允许你导入给定包中的所有公开符号,或者更关键的是,仅从该包中导入你需要的特定公开符号。
在传统(自 Version 1.0 起)的设置包的方式中,你的代码里会散布着 BeginPackage、Begin 等语句。新的结构化包格式让你无需再这样做,并且能够以非常简洁、最小的方式指定哪些符号应该在何处可访问。
为什么我们花了这么多年才想出这个方案?对用户而言,结构化包格式的操作看起来相当简单。但其底层发生的事情却相当复杂。这是其中一个问题:如果 PackageInitialize 在 Wolfram 语言代码文件中遇到一个名为 x 的符号,它必须知道该符号 x 应处于哪个上下文中。但这很可能由该文件后面出现的内容或某个完全不同的文件来定义。那么 PackageInitialize 是如何处理这个问题的呢?它首先扫描整个目录树,收集所有 PackageExported 和 PackageScoped 的实例,只有在处理完这些并确定了符号的上下文之后,它才会真正读取目录树中的完整代码。换句话说,在所有文件进行“真正的”语义传递之前,需要先进行一次本质上的词法传递。没错,要让这一切在所有情况下都正常工作是非常棘手的。但在新的结构化包格式中,它做到了——并且它允许人们以比以往更好、更清晰的方式设置大型 Wolfram 语言代码库。
在图上的绘图
如何在图的节点上绘制值?在 Version 15 中,你可以直接使用 GraphValuePlot:
你可以用不同方式表示值;这里我们只说要用顶点大小来表示
这里我们同时使用顶点形状和顶点大小:
GraphValuePlot 直接支持各种标准的图属性。例如,下图展示了在其上绘制了紧密度中心度的图:
GraphValuePlot 不仅支持在节点上绘图,还支持在边上绘图。
这是一个例子,我们取一个边被标注了边容量的图,然后将这些值绘制在边上:
GraphValuePlot 接收一个已有的图,然后在其上绘制数值。在版本 15 中,另一个新函数是 TaggedNestGraph——它能构建一个图,其边上带有标签,默认情况下这些边会根据标签设置样式。下面是一个例子,其中“f”和“g”边被标记了不同的标签,并采用了不同的样式:
这是一个稍大一些的例子:
版本 15 的另一个新功能是一组新的图高亮样式。这里我们使用光晕来高亮某些节点:
GraphValuePlot 是一个用于在图上进行绘制的高级函数。但它能做的任何事情也可以通过显式指定图中顶点和边的渲染方式在更低级别完成。而在最低级别,有像 VertexShapeFunction 这样的选项,例如,可以应用一个函数来完全控制每个顶点的“形状”。当然,这可能会变得相当繁琐,尤其是需要按顺序提供顶点坐标、顶点大小和顶点名称这三个参数。不过,在版本 15 中,我们通过允许从关联(association)中访问这些值,比如使用 #Coordinates 等,使这一过程稍微简便了一些:
如何在地球地图上添加刻度?
当我们制作地球地图时,实际上总是将大致呈球形的三维地球投影到二维地图上。有许多方法可以做到这一点,例如通过 GeoProjection 选项指定,一个示例如下:
但假设我们想读取这张地图上某一点的坐标。如果我们要求包含普通的坐标轴,会得到:
这些坐标轴上的坐标是最终投影地图的坐标。但如果想了解点在地球表面上的位置,比如以经纬度表示呢?在版本 15 中,有一个新的选项 GeoAxes,它提供了“地理”或“经纬度”坐标轴:
有一条“地理轴”在赤道处;另一条(至少默认情况下)在经度 0° 处,即格林威治子午线。除了地理轴,还有地理网格线——它们与地理轴上的现有刻度对齐:
在某些地理投影下,结果会变得相当奇特。比如这里赤道是一个正方形:
哦,当然,它也能在月球(或其他行星)上正常工作:
(在内部,这种特定的投影是JacobiSN等双周期Jacobi椭圆函数的一个有趣应用。)
地理坐标轴有很多选项——比如坐标轴交叉的位置(GeoAxesOrigin):
如果你想完全控制坐标轴,就需要指定一个AxisObject——在GeoGraphics中,这样的AxisObject会被正确转换为你所使用的任何地理投影。
你的城市何时能看到日食?
几千年来,天文计算一直是精密科学发展的驱动力。而历史上最富挑战性的问题之一就是日食预测。科学进步的显著标志在于,如今我们已能将日食预测精确到这样的程度:例如在2017年,我们能够通过一个网站预测美国境内任何特定地点日食发生的时间,误差不超过一秒。我们当时使用的是2014年引入的SolarEclipse函数——该函数可计算约3万年内任何一次特定日食的属性。
那么反过来呢?给定地球上的一个地点,那里能看到哪些日食?这是一个涉及天文计算和地理计算的挑战性问题。但在第15版中,我们引入了FindSolarEclipse函数来解决这个问题。下面我们询问的是,巨石阵下一次能看到(非偏食的)日食是什么时候:
我想那还要等很久……过去的情况呢?
这是那次日食的路径:
这是全食到达巨石阵的时间:
这是持续时长:
这是过去一万年间巨石阵能看到的所有(非偏食)日食的时间线:
这是它们所有的路径:
顺便说一句,FindSolarEclipse也适用于扩展的地理区域,比如国家:
是的,对美国来说还要等很久。不过——利用一系列功能——以下是未来一年内将经历日全食的国家列表:
发射进入轨道
天体力学的发展,首先是一部关于轨道的故事。而在版本15中,我们开始支持基于轨道的计算功能。在这个版本中,我们主要关注("开普勒")轨道根数,它们实际上给出了轨道的一种瞬时近似。例如,以下是当前火星的基本轨道根数:
我们可以将这些轨道根数理解为描述当前火星轨道的最佳拟合椭圆的参数。以下是该轨道的时间序列图:
以下是预测未来1万年的轨道根数:
其中大多数与当前的轨道根数非常相似,这表明近似未来轨道的椭圆与近似当前轨道的椭圆非常相似。(不过,"平近点角"本质上就是火星在其轨道内的角度,所以变化很快。)
我们可以计算行星、卫星、小行星、彗星以及航天器的轨道。以下是木星内层卫星的瞬时轨道(没错,伽利略卫星就在其中,它们的轨道形态非常"不似太阳系内小行星带"):
类似地,以下是GPS卫星当前绕地球运行的轨道:
这些轨道都是椭圆形的。但轨道根数同样可以处理双曲线轨道——例如旅行者2号的路径,其偏心率明显大于1(注意使用的是相对论时间系统TDB):
我们稍微深入探讨一下。以下是旅行者2号自发射以来每月与太阳距离的变化:
初期存在一些因行星引力助推引起的微小波动——随后进入一个与双曲线轨道对应的"滑行"阶段。但这条轨道具体是怎样的?嗯,它正是由旅行者2号当前轨道根数所确定的。利用这些根数计算出对应的位置后,我们发现当前的双曲线轨道确实与自上一次引力助推以来的位置相符:
我们可以从天体轨道要素计算出很多东西。例如,下图显示了每个月的总能量——这说明了第一次引力助推(来自木星)是如何让旅行者2号获得逃离太阳系的能量的:
格拉斯曼、克利福德、外尔及其友
“计算机代数”是什么意思?传统上,人们想到的是对多项式之类对象进行运算,其中变量(比如 x)最终应该表示数值。但其他类型的代数又该如何理解呢?比如,乘法运算不满足交换律的情况?
在 14.3 版本中,我们为自由代数引入了非交换计算机代数——符号矩阵就是一个显著例子。现在在 15.0 版本中,我们为带有关系的代数引入了非交换代数,特别是格拉斯曼代数、克利福德代数和外尔代数。
例如,GrassmannAlgebra 表示一个格拉斯曼代数
其(非交换的)乘法运算用 ⋀(输入为 \[Wedge])表示。在格拉斯曼代数中,乘法被定义为反交换,而 NonCommutativeExpand 将尝试按照代数指定的顺序排列变量(此处先 x 后 y),例如:
CliffordAlgebra 表示一个克利福德代数
在此例中,x 和 y 被定义为其“平方”为 1,而 u 被定义为其“平方”为 –1:
(非交换乘法可以用 ** 输入。)
还有 WeylAlgebra,它适合表示微分算子的复合,这里实际上是利用链式法则进行了“展开”:
您也可以自行定义带有关系的非交换代数:
(关于这些内容还有很多可说的;事实上,现在有一整本 Wolfram 专著《非交换代数》专门讨论此主题。)
泽塔函数、多对数函数与调和数步入多元时代
“那个有闭式解吗?”嗯,这取决于“闭式”具体指什么。但从操作层面讲,它往往意味着“结果能否用我们已定义的函数表示?”而答案当然取决于人们定义了哪些函数。在每一版Wolfram语言中,我们都会尝试添加新的“特殊函数”,以帮助我们对更大类别的问题提供闭式解。
在15版中,我们添加了一系列特别强大的新特殊函数,它们能显著扩展我们可获得闭式结果的范围,尤其是在量子场论和解析数论的应用中。这些新特殊函数的基本特点是:它们是黎曼ζ函数、多重对数和调和数的多元推广。但事实证明,它们在线性微分方程组的级数解、多元有理函数的积分以及多元求和中出现得相当广泛。
从1版开始,我们就有了普通的、单变量的ζ函数:
当此类求和足够简单时,其多元类比仍可用单变量ζ函数处理:
但一般情况下,需要用到我们新的MultipleZeta函数:
是的,事情很快就会变得复杂起来
不过用TraditionalForm表示时,结果至少还算紧凑:
这是一个涉及三元ζ函数的无穷求和:
而这是一个仍然涉及多重ζ函数的单变量求和:
可以将多重对数视为ζ函数,但额外加上了一个“幂级数分子”:
将多重对数推广到多元情况有若干种方式。最直接的是我们称为MultiplePolyLog的函数:
但为了覆盖其他常见的情况,我们还增加了称为GeneralizedPolyLog和HarmonicPolyLog的函数。普通的PolyLog可以通过单变量积分得到,例如
或双变量积分,例如:
当我们将其扩展到更多变量时,便开始得到新型的多重对数:
15版中“走向多元”的第三类函数是HarmonicNumber:
我们还在添加一些新类型的单变量调和数,例如:
但说到底,这些新特殊函数最重要的意义在于它们出现的计算范围有多广。比如下面是一个微分方程(恰好来自费曼图)解的渐近展开结果,其中充满了调和多重对数:
部分分式处理得到简化
自 1.0 版本以来,我们就有了 Apart 函数。但在 15 版本中,我们“拆解了 Apart”,使其算法更精确、更复杂——并允许访问更多部分。
Apart 背后的核心运算是计算部分分式——现在有了一个专门的函数:
在这个具体例子中,结果与 Apart 相同:
但在这个例子中
Apart 在无法再对分母进行有理数域上的因式分解时停止,而 PartialFractions 默认会继续,这里使用复数根来完成完全因式分解:
如果你不想要复数根,可以告诉 PartialFractions 只使用实数:
部分分式被用于许多符号算法中(最早可追溯到 1703 年有理函数积分的原始方法)——不同情况下需要结果的不同部分。因此,在 15 版本中,我们引入了 PartialFractionElements 来直接访问不同部分:
大量新矩阵分解
在某种程度上,矩阵只是值的数组,或者在 Wolfram 语言中,只是列表的列表。但根据矩阵的用途,通常有其他表示形式能更好地捕捉其“算法本质”。这就是矩阵分解的用武之地。自 1990 年代初以来,我们就有了多种最常见矩阵分解的函数。但在 15 版本中,它们得到了更新和简化,并添加了一些强大的新分解。
一个典型的矩阵分解例子(恰好是 15 版本中新增的)是 LDLDecomposition——我们将一个矩阵分解为“L”和“D”部分:
我们可以从这些部分重构原始矩阵:
但关键在于,如果我们用矩阵来表示线性方程组,那么“L”和“D”部分正是我们为了能高效求解而直接需要的东西。原则上,我们总是可以通过对原始矩阵直接应用 LinearSolve 来得到一个解:
但使用“L”和“D”部分要高效得多,
因为对于三角矩阵和对角矩阵,LinearSolve 需要做的工作量要少得多。
像 LDLDecomposition 这样的函数默认会使用新的结构化矩阵对象,例如 LowerTriangularMatrix——这类对象能优化特定形式矩阵的存储和计算。(TargetStructure 选项允许你控制使用哪种矩阵结构。)
我们自第 3 版起就拥有的一种矩阵分解是 LUDecomposition。而现在在第 15 版中,它能够使用结构化矩阵——这使其既更高效又更便捷:
另一种新增的矩阵分解是 RankDecomposition——它将一个秩为 k 的 m×n 矩阵分解成 m×k 矩阵和 k×n 矩阵,
通过 Dot 可以从中重建出原始矩阵:
其他新增的矩阵分解包括 BunchKaufmanDecomposition 和 PolarDecomposition。此外,还有像 JordanReduce 和 FrobeniusReduce 这样的新函数,它们给出了 JordanDecomposition 和 FrobeniusDecomposition 的“核心”。
DSolve 的角落获得 AI 方法的些许帮助
“AI 难道不能解决一切吗?”2022 年 ChatGPT 出人意料的成功让很多人好奇,AI 系统(特别是神经网络)到底能在多大程度上涉足所有领域——包括数学。在其他地方,我已经讨论过这方面的科学原理。但可以这么说,有些地方无法避免深度计算,而神经网络并不擅长做这类计算(除非它们调用像 Wolfram Language 这样的工具)。然而,仍然有一些地方——甚至可能在数学领域——我们有理由认为神经网络 AI 所擅长的那种广泛的“启发式”计算可能会派上用场。
当然,Wolfram Language 中有很多函数早已使用神经网络(比如 ImageIdentify、SpeechRecognize 或 FeatureSpacePlot)。但数学相关函数并非如此。不过,我们一直在探索这种可能性,而在第 15 版中,我们首次开始在符号数学函数内部使用神经网络方法,具体来说就是 DSolve。
可以将神经网络本质上理解为进行近似计算。那么,如何将其用于像 DSolve 这样产生精确符号结果的函数呢?基本思路是:用神经网络“猜测”一个可能的解,然后利用精确的符号计算来验证它,只有验证通过时才将其作为结果返回。
值得一提的是,我们在 Wolfram Language 中实际上拥有许多非常强大的符号计算算法(通常是我们自己发明的),它们在内部会使用近似方法(通常是数值近似),然后利用精确的符号方法对结果进行筛选或验证。但第 15 版的 DSolve 是我们首次在符号数学计算函数中专门使用神经网络。
DSolve 试图解决的根本问题是什么?它接收一个微分方程,然后要求找到一个函数——最好是结构尽可能简单的函数——来求解该方程。那么,如何训练神经网络来完成这项工作?基本思路是:生成大量函数,然后找出它们所满足的微分方程,再将那些微分方程以及我们已知能求解它们的函数作为训练数据提供给神经网络。
如何为神经网络编码一个数学表达式?我们基本上将数学视为自然语言,并将其转化为一串模型 token。而我们所使用的神经网络是 Transformer 架构,就像在大语言模型中一样。
那么结果如何?这里有一个微分方程的例子,第 14.3 版无法求解,但得益于我们新的神经网络方法,第 15.0 版可以求解:
而且,是的,这看起来有点像刻意为之:一个“刚好”有解的非常复杂的方程。并且,是的,这是一个合理的批评。这引出了一个问题:人们实际想求解的微分方程分布会是什么样的。从 Wolfram|Alpha 来看,我们原则上其实掌握了相当多这方面的信息。而且我们长期以来一直在从像表格手册这类资料中积累基准测试。那么神经网络在这些问题上的表现如何呢?并不特别好。例如,在经典的 1959 年 Kamke 手册中的 638 个(一阶)微分方程里,我们的神经网络方法只能解出 6 个。而我们的“传统”算法方法,则能解出 100% 的解。
但如果我们只是“合成”生成一些方程来求解呢?我们可以做与生成训练数据时相同的事情:概率性地生成表达式树(本质上使用一个马尔可夫过程,其转移步骤是应用命名函数,比如 Sin 和 Log)——然后找出这些表达式满足的方程。如果我们用这种方法生成一百万个方程,我们会发现,神经网络确实能正确解出其中大约 80% 的方程。但是——关键来了——我们的传统算法方法也能解出几乎所有这 80% 的解。最终,在我们的测试方程中,只有 0.003% 能被神经网络成功解出,而传统方法却解不出来。那么这是否意味着神经网络基本没什么用呢?嗯,能多解出几个方程(哪怕只是多 0.003%)总是好的。但有两个因素让神经网络变得更有用。第一,当它有效时,通常能比我们的传统算法方法快得多地给出答案。第二,在相当多的情况下,它给出的答案比传统算法方法能找到的答案要简洁得多。
以下是 DSolve 在版本 14.3 中对一个特定微分方程所给出的结果示例:
如果我们应用 FullSimplify,几分钟后就会得到:
但在版本 15.0 中,借助我们新的神经网络方法,得到的结果如下:
这是一个漂亮而优雅的成果。当然,它与我们提供的训练数据中出现的内容相当接近。但神经网络已经做了它最擅长的事情:它实际上成功地建立了一个模型,用于描述它所见过的那些微分方程的解是什么样的,并且能够利用这个模型进行一定程度的泛化。
这无疑是一个精彩的演示。很可能,某些实际场景中遇到的微分方程,现在将能够以符号方式求解,而以前却做不到。这很难说。但对我们而言,有趣的是我们已经能够将一种神经网络方法嵌入到 Wolfram 语言的一个核心数学函数中。而我们为此构建的这条流水线,未来将在一切有意义的场合加以应用。
(顺便提一下,你可能会想知道新的解和旧的解是否相同。两者都包含一个任意常数 。但如果取它们的差,FullSimplify 可以成功证明差恰好是 。换句话说,这个差是一个常数——对于像这样的线性方程——它可以被吸收到 中。所以,是的,这两个解是相同的,尽管它们在代数上以不同的形式给出。)
偏微分方程走向曲线坐标
早在我们引入像 Div 和 Grad 这样的基本向量分析函数(9.0 版本)时,我们就同时引入了坐标图表的概念——这样,例如,你可以计算极坐标下的拉普拉斯算子:
在 15.0 版本中,我们现在将坐标图表的支持扩展到整个偏微分方程建模系统中。因此,例如,这里是根据 LaplacianPDETerm 计算出的极坐标拉普拉斯算子:
我们可以将这个偏微分方程项放入一个完整的偏微分方程中,并在极坐标下对其求解:
对于像标量场的拉普拉斯算子这样相对简单的情况,这一切都比较直接,但事情很快就会变得复杂。下面是一个极坐标下的流体流动偏微分方程分量:
事实上,这对于 CoordinateChartData 支持的任何曲线坐标系统都适用。这里是以球坐标为例:
这里是以长球坐标为例:
在版本 15 中,现在也可以使用曲线坐标系来处理数值偏微分方程。下面是一个在极坐标下设置并求解的特征值问题(注意边界条件现在也是极坐标形式):
偏微分方程解中的导出量
假设你正在用诸如 SolidMechanicsPDEComponent 这样的偏微分方程组件来求解一个固体力学问题。你直接计算出的量是位移场——它给出了固体中每个点在每个方向上的位移:
但通常你实际想要的是从这个位移场导出的某个量。例如,下面这段代码计算了与该位移场对应的应变场:
但这在每个点对应的是一个复杂的二阶应变张量。通常我们希望对解做进一步简化,比如从每个点导出某个纯标量。在版本 14.1 中,我们引入了 VonMisesStress 来对应力场做类似处理。在版本 15 中,我们现在引入了 EquivalentStrain 来对应变场做同样处理:
顺便提一下,这就是 EquivalentStrain 实际做的事情——这里以符号形式展示:
除了用于固体力学的 EquivalentStrain,版本 15 还引入了用于流体力学中的 FluidViscousStress 和 FluidViscosity,以及用于研究磁场的 MagneticFluxDensity(“B 场”)和 MagneticFieldIntensity(“H 场”)。
如何近似一个系统工程模型?
有幂级数、插值函数、基础神经网络。所有这些都可以视为对原始对象提供比其自身更快的近似。但假设你有一个由 SystemModel 表示、可能来自 Wolfram System Modeler 的系统工程模型。如何获得一个比模型本身更快的近似呢?
版本 15 引入了函数 SystemModelSurrogateTrain,利用现代机器学习方法来创建此类近似。基本思路是:选择系统模型行为空间的某一部分,然后进行一系列仿真,并对生成的“合成数据”进行拟合,最终得到原始模型的一个高效的连续时间神经网络近似。
举一个简单的例子:考虑一个电动机模型:
下图展示了从该模型计算的某个特定变量在某一参数取值下的行为曲线:
SystemModelSurrogateTrain 允许你对底层模型进行“代理”近似,从而高效捕捉该模型在特定参数范围内某些变量的行为:
如果重复上述计算,会得到基本相同的结果——但速度要快得多:
代理模型不再通过求解微分方程来得到结果,而是仅需评估一个神经网络,我们可以从 SystemModelSurrogate 对象中提取该网络:
对于越来越复杂的工程系统,代理模型正变得越来越重要;它们对于实现多种大规模系统优化以及构建可实时仿真的数字孪生都至关重要。
控制系统的强化学习
近年来,强化学习在人工智能领域被广泛讨论。但这一概念实际上起源于 20 世纪 50 年代,当时以“最优控制”之名出现在控制系统的研究与设计中。在传统控制理论(Wolfram 语言已支持多年)中,基本思路是拥有系统的数学形式模型,然后对该模型的结构进行操作,以推导出一个控制器(尽可能)使系统达到指定目标。
而强化学习则不同,它不依赖系统的数学形式模型。相反,它假设我们通过反复“戳一戳”系统并观察其响应来了解系统,然后迭代地得出一个能实现预期目标的控制器。实现这一目的有多种策略;在 Version 15 中,我们引入了一种名为 Q 学习的强大策略。
Q学习的基本思想是不断尝试学习“Q函数”,该函数定义了当系统处于给定状态时,与某个(控制)动作相关的“响应质量”——然后利用这个学习到的Q函数推导出一个控制器。在版本15中,我们引入了函数LQRegulatorTrain,用于执行基于线性二次型调节器的Q学习(没错,LQRegulatorTrain中的“Q”与Q学习中的“Q”不是同一个概念;它代表“二次型”而非“质量”)。
LQRegulatorTrain接收系统的表示(在强化学习中通常称为“环境”),并尝试最小化一个二次型,该二次型是在强化学习过程中累积的,其中包含与系统达到特定状态的距离以及所使用的控制量相关的项。
通常,表示系统的方式是给出一个函数,该函数接收系统的当前状态(比如x,在给定步骤k)以及某个输入(比如u),然后返回系统的新状态。举一个极其简单的例子,我们可以使用由以下函数指定的系统:
现在我们可以训练一个LQ调节器作为该系统的控制器:
结果是一个控制器的符号表示。我们可以利用这个结果,例如观察控制器如何“将状态响应驱动到零”:
如果需要,我们实际上可以获得这种情况下的Q函数本身(由于来自LQ调节器,这是一个二次型):
作为一个稍微更实际的例子,考虑一个直流电机,我们试图控制它使指针转动到特定角度。我们可以将其符号化地表示为SystemModel:
我们可以有一个实际的物理版本,然后使用我们的设备框架(如DeviceRead和DeviceWrite等函数)连接到传感器和执行器(或者使用我们的Microcontroller Kit通过微控制器连接)。但作为示例,我们暂时假设已经完成了整个系统建模流程,并得到了一段可以模拟该系统的外部C代码:
现在我们可以为这个(模拟的)系统训练一个调节器:
而且,是的,这个控制器成功将我们的模拟位置误差归零:
导入与导出最新格式
我们在 25 多年前首次推出了通过 Import 和 Export 进行的数据流式导入与导出。最初我们只处理几十种格式。随着时间的推移,这一数量已增长到近 300 种。而随着岁月流逝,总会有新的格式被定义或流行起来。因此,例如在 Version 15 中,我们引入了 TOML 和 YAML 的导入导出——这两种简单格式已越来越广泛地用于各类配置文件中。
在图像格式领域,最初有 GIF,然后是 JPEG,接着是 PNG。而现在,随着图像建模与表示能力的提升,出现了 HEIF 和 AVIF。在 Version 15 中,我们现在在所有平台上全面支持 HEIF 和 AVIF 的导入与导出——在给定质量水平下,通常可将图像大小缩减约一半。
过去几年里,Wolfram 语言对天文学与天文数据的支持越来越深入。作为其中的一部分,我们在 Version 15 中引入了对 AVM 的导入——这是一种天文图像的元数据标准,用于指定某张图像在天空中的来源以及采集方式。
近年来日益流行的一种格式是 Markdown(.md)。我们分别在 Version 14.2 和 Version 14.3 中首次引入了 Markdown 的导入和导出功能。在 Version 15 中,我们扩展了对 Markdown 的支持,涵盖了链接、图像、表格等。并且在所有情况下,我们既能以关联、数据集等形式获取可计算数据,也能得到完整格式的笔记本。
与近期才流行起来的 Markdown 不同,XML 自 2002 年起就得到了我们的支持。XML 往往是一种复杂但灵活的格式。在 Version 15 中,我们新增了将 XML 直接作为可计算的 Tree 对象进行导入的能力。
说到旧格式,就不得不提笔记本——是的,我们最初在1987年为Mathematica 1.0发明了它。此后的四十年里,我们一直在稳步发展和完善笔记本概念,即便在15版本中也加入了各种新功能。相当令人印象深刻的是,我们一直保持了兼容性,因此15.0版本仍然可以打开1.0版本的笔记本。但就在我们发明笔记本近四分之一个世纪后,人们终于开始模仿它们(为什么花了这么久?)——或者更准确地说,模仿了它们的一些表面功能。最终结果是,世界上出现了一些其他笔记本格式——在15版本中,我们增加了将其中两种格式(.vsnb 和 .ipynp)导入我们笔记本的功能。(是的,从我们的笔记本导出意义不大;那些“仿冒”笔记本格式缺失了太多东西。)
通过 WebSocket 实现实时连接
如何获取从服务器流式传输的数据——并可能同时对它做出响应?基本机制——在业界已存在数十年——是使用 socket。十年前,我们引入了对 TCP socket 的支持;后来增加了对 ZMQ socket 的支持。现在在15.0版本中,我们正在增加对 WebSocket 的支持。
WebSocket 例如用于流式数据服务,以及从基于云的 LLM 获取流式结果。WebSocket 的一个重要特性是双向通信:即使数据正在向您流式传输,您也可以向服务器发送数据。WebSocket 的另一个重要特性是,它们的初始连接是通过 http(或 https)建立的,因此可以继承与 http 头部相关的各种能力(例如能够传递 API 密钥等)。
这是一个非常简单的示例,基于标准 echo.websocket.org 测试 socket。下面是我们连接到此 socket 的方式:
现在我们可以从 socket 读取数据——接收这个特定服务器发送的初始消息:
使用 SocketWriteMessage 和 SocketListen,我们可以来回发送消息。我们的整个 socket 系统异步工作,与 Dynamic 适当交互,以便在数据一到达时就获取更新。
作为一个更复杂的例子,下面展示了我们如何打开一个 Web Socket 连接来对接当前版本的 OpenAI 大语言模型系统:
而且,是的,在我们的 AI 助手——以及聊天笔记本等——中,我们现在将使用 Web Socket 来获取实时流式结果。
在笔记本中使用 Python 及更多功能的更丰富用户体验
早在 2018 年(在 11.3 版本中),我们引入了外部代码单元格,使得可以直接在笔记本中包含并运行外部代码。在单元格开头输入 > 即可调出可能代码类型的菜单:

选择 Python 就会得到一个 Python 单元格。但在 15 版本中,这里有了新内容:

外部代码单元格带有一个“会话菜单”注释:

这有什么意义?单个笔记本可以连接到多个不同且独立的 Python 会话,而会话菜单让您能够管理这些会话,并指定特定单元格应该使用哪个会话。
为什么需要多个 Python 会话?好吧,如果 Python 像 Wolfram Language 一样是一个清晰连贯的系统,你大概就不需要了。但它并非如此。相反,大部分功能来自一堆独立开发的库,且不同 Python 代码片段往往需要不同且不兼容的库集合。而且,是的,如果你只使用 Wolfram Language,你就能避免所有这些混乱。但如果你已经有 Python 代码(例如,大语言模型不方便直接将其翻译成 Wolfram Language),那么我们的外部求值机制就是为了尽可能无痛地集成 Python 代码而设计的。
其中重要的一部分是我们在 14.0 版本中引入(并在 14.3 版本中大幅增强)的封装机制,它允许你拥有独立、完全封装的 Python 会话,这些会话携带并管理自己的依赖项。现在在 15.0 版本中——结合会话菜单——我们为这些封装会话引入了一个便捷界面。
假设已经存在一个带有某些依赖项的会话。现在,通过我们新的会话菜单系统,你可以选择该会话作为特定外部代码单元格想要使用的会话。例如,下面设置了一个带有特定依赖项的外部会话:
在外部代码单元的会话菜单中,现在可以请求"使用一个正在运行的 Python 会话"——然后选择该会话:

会话可以显式命名。但默认情况下,每个新创建的会话都会获得一个唯一的 UUID。而这使得一个非常酷的功能成为可能:它让带有依赖关系的外部代码单元变得可移植。
下面是一个使用我们刚刚设置的会话的外部代码单元:

但关键在于,这个单元实际上是完全自包含的。你可以把它复制到另一台电脑上,发送给其他人等等,它会随身携带其依赖关系,从而使其中的 Python 代码能够直接运行。
除了通过编程方式使用 `StartExternalSession` 定义 Python 会话之外,你也可以通过交互方式来实现——直接在会话菜单中选择"使用一个新的 Python 会话"。
在 Version 15 中,Python 的会话菜单还有另外几个有用的实用工具项。其中有"格式化代码",它应用自动代码格式化规则:

还有"移除未使用的导入",它会确定一段代码的实际依赖关系,并删除任何不必要的导入。
优化与 GPU 化持续推进
Wolfram 语言中充满了算法复杂的函数,我们正在持续使其效率越来越高。部分效率提升来自使用新算法(包括许多我们自行发明的算法);部分则来自能够支持额外的硬件加速,尤其是利用 GPU。
在 Version 15 中,整个系统有多处性能增强。对于拥有 NVIDIA GPU 的用户,核心线性代数和核心图论功能均获得了额外的(实验性)加速。使用 `Method→"HybridCPUGPU"` 时,像 `LinearSolve` 这样的函数会自动同时利用 CPU 和 GPU 硬件,在处理 10000×10000 矩阵等情况下实现显著的性能提升。
高效利用 GPU 的其中一个问题在于,需要让 GPU 处理的数据常驻 GPU 内存。在 Version 14.2 中,我们引入了 `GPUArray`,作为一种专门在 GPU 内存中存储数据的方式。随着每个后续版本,我们都在让更多函数能够直接操作 `GPUArray` 对象。
在版本 15 中,我们新增了一系列基于 GPU 的数组重排函数。下面是一个 2000×2000 的 GPU 数组:
假设你的计算机装有合适的 GPU,这段代码便能非常高效地生成一个 GPU 数组——复杂的是,其形状与原始数组截然不同:
使用 GPU 依然是一件相当繁琐的事,不同 GPU 和不同计算机系统所具备的能力各不相同。我们正针对越来越多的 GPU 配置,系统地实现越来越多的函数,并为每种配置设定高度优化的 GPU 内核。
事实上,即便不局限于 GPU,不同硬件在各类支持方式上也存在复杂的问题。例如,在版本 15 中,我们为稀疏数组的 LinearSolve 增加了 Method「MUMPS」,这能显著加速 ARM、Apple 等处理器上大规模数值 PDE 求解等任务的运行。
外部函数的 CUDA 内核
如果你想编写自己的 GPU 代码,并将其集成到 Wolfram Language 中,该怎么办?在版本 15.0 中,有一种新机制可以创建执行 CUDA 内核的 ExternalFunction 对象。是的,这仅在支持 CUDA 且装有合适 GPU 的系统上才能运行。下面是一个示例:我们提供纯 CUDA C++ 代码,并获得一个执行该代码的 ExternalFunction:
现在假设你设置了一个 GPUArray——它将驻留在你的 GPU 上:
这段代码会对该数组执行 CUDA 函数:
结果如下(没错,这个特定的 CUDA 内核做的事情非常简单:给每个数组元素加上 1000):
CUDA 代码通常处于非常底层,很快会变得相当复杂和繁琐。但在版本 15.0 中,还有另一种进行 GPU 编程的方式:将 Wolfram Language 与 Wolfram Compiler 结合使用,通过 LibraryFunction 调用原生的 CUDA 函数。例如,下面是上述同一个 CUDA 内核的版本,但这次使用 Wolfram Compiler 在 Wolfram Language 中实现:
现在——就像我们之前的纯 CUDA 代码一样——我们可以运行这段代码并获取结果:
Wolfram Compute Services 获得了 GPU 支持
去年底我们推出了 Wolfram Compute Services,旨在实现无缝的大规模远程计算。过去几个月里,我们为 Wolfram Compute Services 新增了多项功能,这些功能现已完全集成在 Version 15 中。具体来说,Wolfram Compute Services 现在不仅可以使用 CPU,还能调用高端 GPU。
一旦你的 Wolfram Language 代码运行正常,只需使用 RemoteBatchSubmit 就能将其提交到 Wolfram Compute Services。下面这个例子中,我们提交了一些神经网络训练任务,让它在 NVIDIA L40S GPU 上运行:
大约 40 分钟后,我们收到一封邮件,告知结果已准备就绪。

然后我们可以取回结果:
Wolfram Compute Services 的另一个新特性是 RemoteGeoZone 选项,它指定远程计算应在世界哪个区域执行——目前可供选择的是“UnitedStates”和“EuropeanUnion”。
Wolfram Compute Services 被设计为使用公有云基础设施。但我们正在开发的是高性能计算套件(High-Performance Computing Kit),它将允许设置私有集群及其他机构或组织的基础设施,以便与 RemoteBatchSubmit 配合使用。
在 LLM 函数中使用 Wolfram Foundation Tool
我们最近发布了面向 LLM 系统的 Wolfram Foundation Tool,它能让 LLM 系统调用 Wolfram Language、Wolfram Knowledgebase、Wolfram|Alpha 等能力。实现这一点的核心技术是我们所谓的计算增强生成(CAG),它是检索增强生成(RAG)的一种无限类比对,通过实时计算来补充 LLM 的能力。我们的 Notebook Assistant(现为 AI Assistant)已经在使用 CAG 和 Foundation Tool 系统的某个版本。但在 Version 15.0 中,我们将这一能力扩展到了所有 LLM 函数。现在,你可以通过指定适当的 LLMEvaluator 设置来访问我们 Foundation Tool 系列中的任何能力。
例如,这里我们使用 LLMSynthesize,配合完整的 Wolfram Agent One LLM + Foundation Tool 系统,得到的结果部分来自 LLM,部分来自 Wolfram Language 计算:
(你还可以在 LLMFunction、ChatEvaluate、LLMGraph 等模块中使用 LLMEvaluator → "AgentOne"。)
顺便提一下,你始终可以通过设置 `$LLMEvaluator` 变量,或在偏好设置面板中指定,来设定默认的 LLMEvaluator。
LLMEvaluator 的另一个实用设置是 "WolframAIAssistant"。这可以访问用于交互式 AI 助手的 CAG 系统,该系统特别适合帮助用户编写 Wolfram 语言代码。例如,这里我们正在通过编程方式生成关于如何在 Wolfram 语言中执行某项操作的信息:
更多新功能……
除了我们已讨论的所有内容外,版本 15 还包含各种其他新特性。有些是现有功能的扩展和优化,有些则是全新加入的函数。
例如,新增了 PopovDecomposition 和 OrderedSchurDecomposition:两种额外的矩阵分解方法。还有 PfaffianDet,用于计算反对称矩阵的 Pfaffian:
为了完善我们庞大的积分变换库,版本 15 引入了 DiscreteHilbertTransform(此处处理的是类似于 delta 函数的情况):
在图形方面,我们确保了 PlanarFaceList 会优先列出平面图的"外表面":
如果你不希望这样,也可以选择不使用该选项:
在图形布局方面,有多项更新。首先,新增了用于图形高亮的内置样式,例如"晕轮高亮":
以及新的内置边形状函数——这里展示了一种相当华丽的视觉效果:
如何表示光滑的三维曲面?近二十年来,我们一直使用 BSplineSurface 来实现这一目的。在版本 15 中,作为完善 CAD 风格几何工具的一部分,我们也引入了 BezierSurface:
在另一个完全不同的领域,版本 15 持续推进了化学功能的开发,特别引入了用于表示分子中子结构的 MoleculeSubstructure。该功能可以检测分子中是否存在特定的分子模式:
然后可以绘制出这些子结构:
版本 15 中还新增了 MoleculeFingerprint 以及用于立体化学的 MoleculeValue 属性。
版本 15 中还有更多内容可以讨论。不过,在一个“总是瞄准月球”的时刻,我想以版本 15 中与月球相关的一项新功能作为结尾。十多年前,我们引入了 NightHemisphere(和 DayHemisphere)来在地图上显示昼夜:
那么,现在我们已将该能力扩展到“行星之外”:
当然,也扩展到了月球。要看到从地球上看到的月球模样,我们需要使用正射投影:
但这是什么?昼夜分界线在哪里?然后我意识到:今天是新月!
一切运行正常。我们正在仔细填补又一个细节。是的,再过一周就会是上弦月:
发布于:人工智能、Mathematica、新技术、Wolfram 语言