跳到正文
北京时间
原文
Hacker News 热门(buzzing.cc 中文翻译)· alok-g·· 2026-06-17精选AI 评分76

Wolfram 语言和 Mathematica 15 版发布:内置 AI 助手、符号音乐等新功能

Wolfram 语言和 Mathematica 15 版、AI 助手、符号音乐等

AI 导读

在 Mathematica 诞生近 38 年后,Wolfram 语言与 Mathematica 发布 Version 15。每个笔记本内置 AI 助手,支持从 AI 环境中直接调用 Wolfram 技术。新增符号音乐系统、大规模时间序列与事件序列处理、分类数据计算、模型拟合超函数 ModelFit。笔记本支持千兆字节级大小与实时查找,首次引入侧边栏、视觉主题及弃用功能样式。强化了表格连接、多点可视化、图形刻度绘制与轨道运行计算等功能。DSolve 拐角处获得 AI 方法辅助,支持偏微分方程曲线坐标求解。扩充了矩阵分解、多元 zeta 函数与调和数、流线型部分分式分解。强化了 WebSocket 实时连接、Python 交互改进,支持 CUDA 内核作为外部函数,Wolfram Compute Services 新增 GPU 支持。

推荐理由

Wolfram Language 15 把 AI 助手直接内嵌进笔记本,加上符号音乐和 ModelFit 超级函数,对用代码思考的人来说,这是今年最扎实的版本升级。

正文 · AI 翻译

推出 Wolfram Language 与 Mathematica 第 15 版:内置(实用的)AI 以及大量全新核心功能

一次令人印象深刻的现代发布

1988 年 6 月 23 日,我们发布了 Mathematica 1.0 版。如今——将近 38 年之后——我们正在发布第 15 版,而为了体现它早已远远超出“数学”的范畴,我们现在称它为Wolfram Language。这是一次令人印象深刻的发布,带来了大量全新的核心功能。38 年之后竟然还有更多东西可以添加,这或许显得有些令人意外。但这正是思想史的典型轨迹:一个人弄明白的越多,能看到的就越远,能做的也就越多。而对于我们所有参与其中的人来说,这是一个非常令人满足的过程:年复一年地搭建一座越来越高的思想与技术之塔,借此我们能够触及越来越远的地方——如今则触及第 15 版的全部功能。

在过去四十年里,我们始终怀有一个不变的使命:尽可能广泛而深入地应用计算范式——并通过构建我们独特的计算语言来表征和计算世界,从而实现这一目标。在这四十年间,计算和计算范式的应用已大幅扩展——我认为,这在很大程度上要归功于我们引入的工具和理念。但如今又出现了一个新的驱动力:现代 AI。看到 AI 领域发生如此多意想不到的进展,令人兴奋不已。

对我们而言,一个直接的后果就是我们的用户基础已经从单纯的人类,扩展到了人类与 AI。而事实证明,我们为 Wolfram Language 的一致性设计所投入的所有努力——旨在让人类使用起来既简单又高效——如今也让 AI 用起来同样简单高效。

多年来,我们一直高度重视面向人类用户的界面,从我们为notebooks 这一概念所发明的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 内部调用 LLM(以及 Wolfram Notebooks)的方式。第二年,我们构建了相关技术,使我们能够发布 Notebook Assistant 作为 Wolfram System 的附加组件。然后今年二月,我们发布了 Foundation Tool 技术套件,进一步与 LLM 集成。如今,在 Version 15 中,我们正在推出又一个层次的 AI 集成:我们内置的 AI Assistant。

在版本 15 中新建一个笔记本,你就会在笔记本底部看到一个我们称之为“chatbar”的新元素(除非你已将其关闭),它会立即将你连接到我们的 AI Assistant:

AI Assistant chatbar

在 chatbar 中输入你想要的内容(你也可以粘贴图片等)。然后只需按下 ENTER,你的输入就会被发送给 AI Assistant,它会尝试帮你处理:

AI Assistant

即使你提出的问题相当模糊,AI Assistant 也会尽力给出精确的解读,并附上可读的 Wolfram Language 代码。按下 ,代码就会被插入到你的笔记本中,并立即运行:

Insert code

你可以把 chatbar 看作一种便捷、随时可用的方式,用来在笔记本中创建聊天单元。自 版本 14.2起你就可以做到的是,你也可以通过输入 ‘ 来创建聊天单元,从而开始一个新单元。

与任何聊天单元一样,通过 chatbar 创建的聊天单元可以利用笔记本中位于其上方内容的上下文。(要断开上下文,你可以在单元之间输入 ~ 来插入一个聊天分隔符。)

但版本 15 中更重大的变化是,chatbar 和聊天单元背后的 AI Assistant 现在可以在所有 Wolfram Notebooks 中立即使用。无需任何配置。而且对于 AI Assistant 的 Basic 级别,也无需额外订阅。

AI Assistant 的基础级别立即可用,它提供了一种超越文档的方式来帮助你用 Wolfram Language 做事。我们今天还发布了两个更高级别的 AI Assistant,可通过订阅获得:Pro 和 Research。Pro 让你能够处理更大、更复杂的项目,而 Research 则提供对最新前沿 AI 能力的访问。(现有的 Notebook Assistant 用户将自动转移到 AI Assistant Pro。)

要进入 AI Assistant 的控制选项,只需点击聊天栏侧边的 :

AI Assistant controls

如果你不想默认看到完整的聊天栏,点击 ,它就会被最小化:

Minimize chatbar

(如果你打开新的笔记本,最小化状态会被记住。你可以在主偏好设置菜单中全局控制聊天栏是否出现——甚至控制 AI Assistant 是否可用。)

从你的 AI 环境中使用 Wolfram

AI Assistant 让你可以在 Wolfram Notebooks 中访问 AI。但假设你在电脑上使用的是某个 AI 环境——比如 Claude Code 或 Codex。在 Version 15 中,现在可以轻松地将你的 AI 环境连接起来,调用你的桌面 Wolfram 系统。

如果你的电脑上有标准的 AI 环境——比如 Claude Code 或 Codex——Wolfram 系统通常能够检测到,并会在欢迎屏幕中直接显示一条横幅:

Configure stripe

点击此处会带你进入“偏好设置”对话框中的“面向 AI 的服务”标签页:

Preferences dialog

按下 即可配置系统上检测到的所有 AI 环境。

完成此操作后,这些 AI 环境将被设置为在需要时自动访问 Wolfram 工具。(这些工具包括若干种,例如用于求值 Wolfram Language 代码、用于读写笔记本,以及用于分析 Wolfram Language 代码的工具。)

从技术层面来说,其工作方式是使用新的 Wolfram Agent Tools 框架——你也可以在 Wolfram Language 中以编程方式使用它。例如,要接入所有检测到的 AI 环境,只需使用 Wolfram Language 函数 DeployAgentTools:

(若要针对特定的 AI 环境,请使用 DeployAgentTools["ClaudeCode"] 等。)

所有这些解释对人类来说都很好。但如果你是 AI 智能体(比如正在阅读本文),你可以直接前往一个描述如何配置一切的 Markdown 文件。事实上,身处现代,我们的主站 wolfram.com 会自动向请求它的 AI 智能体提供 Markdown。而且——为了让我们人类仍能了解情况——我们网站顶部新增了一个“面向 AI”链接:

For AIs link

时间序列(以及事件序列)大展身手

拥有随时间变化的数据是极其常见的。而且自10.0 版本(2014 年)起,我们就有了一个TimeSeries构造来表示这类数据。但在 15.0 版本中,我们有了一个强大得多(尽管完全兼容)的 TimeSeries 版本,能够处理庞大且更加多样化的数据集。我们新的 TimeSeries 框架基于Tabular框架,该框架是我们在14.2 版本中引入的,并且与之互操作。这样做的一个直接结果是立即支持多分量时间序列,即在每个时间点都定义了许多分量——它们与底层 Tabular 对象中的列相关联。另一个重要结果是,TimeSeries 现在从 Tabular 继承了其在处理缺失数据方面的全部精巧能力。

粗略来说,可以把 TimeSeries 看作一张带有 Timestamp 列的 Tabular 表。但它远不止于此。尤其是因为 TimeSeries 会自动在给定的各个具体时间点之间插值。或者更准确地说,它会在应当插值时插值(比如数字、数量等),而在不应当插值时则不插值(比如字符串或实体)。此外,TimeSeries 现在还会考虑时间的粒度,因此,例如在按日的时间序列中查询一个按周的值时,它会进行相应的平均。

在 15.0 版本中,相关格式现在可以直接作为 TimeSeries 对象导入:

你可以通过摘要框中的“预览”按钮查看实际数据的预览:

TimeSeries summary box

你还可以显式获取底层的表格数据,其中包含定义时间序列的特殊 Timestamp 列:

你可以直接在 TimeSeries 中使用 Tabular 的功能。例如,这里提取出时间序列的 "Track" 分量,并绘制其地图:

下面是时间序列 "Elevation" 分量的图:

这里对海拔时间序列按 15 分钟周期计算移动平均:

我们还可以直接获取插值后的值。例如这里获取的是一个瞬时(插值)值:

而这里获取的是在一小时范围内取平均的值:

顺便说一下,借助 Version 15.0 的另一个新功能,你现在可以用 TimeSeriesSummary 快速获取 TimeSeries 的摘要:

我们目前看到的时间序列只有区区 12,931 条记录。但新的 TimeSeries 框架通常可以处理包含数百万条记录的时间序列。例如,这里我根据通过 Wolfram Data Drop 收集的 databin,创建了一个涵盖过去十年大部分时间我的心率的时间序列:

Stephen Wolfram heart rate

除了我们全新的 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 这样的函数无法解析:

名义和有序数据都可以出现在 Tabular、TimeSeries 和 EventSeries 中——并且在那里处理得非常高效。

隆重推出 ModelFit 超级函数

几十年来,我们在 Wolfram Language 中一直有多种方式对数据进行拟合。但在 15.0 版本中,我们引入了一种强大的全新统一数据拟合方法,其核心是函数 ModelFit。

其基本概念是从模型的“符号化轮廓”出发,然后使用 ModelFit 填充具体细节,从而对特定数据进行拟合。例如,下面是一个指数模型的符号化轮廓:

实际上,这代表了“任何可能的指数模型,尚未填入具体的参数值”。但现在我们可以将其连同具体数据一起传给 ModelFit,从而得到一个具体的指数模型:

我们可以将其视为对原始数据的基于模型的近似。而且,例如,我们可以在某个特定点上对这个近似求值——就像我们可以对 InterpolatingFunction 或 PredictorFunction 求值一样:

下面是展示拟合效果的图:

我们也可以让拟合在绘图函数内部自动完成:

拟合效果如何?我们可以请求一份报告:

我们可以深入查看以获取更多细节:

在给出原始模型的“符号化轮廓”时,有时为模型中的参数和变量提供名称是很有用的:

这里我们说的是常数项必须为 0,然后我们为其他参数赋予不同的名称:

而在 ModelFit 中,你不仅可以请求最佳拟合模型,还可以请求它的各种属性:

版本 15 支持多种模型——未来的版本中还会带来更多。除了 ExponentialModel,还有用于对数模型和幂律模型的 LogModel 和 PowerModel。

这里有一个例子,拟合关于所有已知系外行星的数据(并且令人惊讶地精确再现了开普勒第三定律):

ModelFit 会处理单位等——在这里它“推导”出了地球年的长度:

ModelFit 让你可以一次尝试拟合多个模型。例如,PolynomialModel[UpTo[5]] 表示任意次数不超过 5 的多项式模型。ModelFit 默认返回最佳拟合模型,在这个例子中是一个三次模型:

在这种情况下,报告包含有关模型选择的信息

你也可以要求更多细节:

同样,我们也可以在绘图函数“内部”进行拟合:

LinearModel 允许你指定一个模型,它是基项(basis terms)的任意线性组合:

FormulaModel 允许你指定任意要拟合的公式,不一定是各项的线性组合:

还有 PeriodicModel,用于拟合周期性数据。这里我们要求进行包含 2 个频率分量的拟合:

ModelFit 可以直接处理 TimeSeries 等:

特别是对于结构更复杂的模型,通常会有若干"超参数",可以在关联(association)中指定。例如这里,你可以指定要包含的频率数量,以及在尝试识别每个频率时使用的样本数量:

ModelFit 不仅能处理传统的"统计学风格"模型,也能处理机器学习风格的模型。一个非常简单的例子是 NearestModel,它执行最近邻拟合:

默认情况下,NearestModel[ ] 处理单个最近邻。这里我们要求它对每个点使用 3 个最近邻:

ModelFit 可以处理任意维度的数据:

这份特定的数据来自一个 Tabular,而 ModelFit——像许多其他函数一样——被设置为可以直接从 Tabular 中提取列:

下面是一个稍微复杂一些的模型示例:多层感知机神经网络:

下面是这种情况下底层的神经网络:

顺便说一下,如果我们改变神经网络的超参数,就会发生这样的情况:

像这样的神经网络模型在复现和预测数据方面很有用,但并不能直接“可解释”。ModelFit 还支持 DecisionTreeModel,以生成可能具有可解释性的决策树模型。下面是我们用经典的 Titanic 数据集拟合一个深度为 2 的决策树模型所得到的结果:

与我们给出的其他示例不同,这里拟合的不仅有数值数据,还有分类数据。下面是将该模型应用于一个特定“数据点”(表示为关联)时发生的情况:

下面是整棵决策树的可视化:

引入符号化音乐

Wolfram Language的一个总体使命是为我们所能描述的一切开发一种计算语言表示。我们在 1991 年引入了基础声音,并在 2016 年引入了完整音频。而现在,在 Version 15 中,我们引入了音乐——以及一种符号化的计算表示,涵盖从音符到整份乐谱的一切。

在最底层,是音高的符号化表示:

你可以直接对音高进行计算:

而且,没错,这里已经有一些微妙之处了:

一个音符实际上是音高加上时值,这里是一个二分音符:

这是它的音高:

这是它的时值:

你可以对音乐时值进行算术运算:

除了单个音符,还有和弦。这是 G 大调和弦:

以下是其中出现的音高

以及对应的音程:

这是一个“算法构建的”和弦(你可以点击音符图标来播放):

而这会将和弦移调 5 个半音:

除了音符、和弦和休止符(由 MusicRest 表示)之外,表示音乐还有三个层级:MusicMeasure、MusicVoice 和 MusicScore。一个 MusicMeasure 对应音乐中的一个小节,包含一系列音符、和弦和休止符:

默认情况下,一个小节被假定为 拍号。以下指定了一个具有不同拍号的小节:

一系列小节随后被组合成一个声部:

最后,多个声部可以并行组合成一个乐谱(此处每个声部以不同颜色渲染):

那么真实的音乐作品呢?在 Version 15 中,我们可以将 MIDI 文件 导入为乐谱。以下是一个已经存在于 Wolfram Data Repository 中的乐谱:

MusicPlot 生成一种便捷的可视化表示:

我们可以对乐谱进行哪些类型的计算?

一件直接的事情是,我们可以算出它有多少个全音符那么长:

我们还可以算出它的音高范围:

以下是每个声部的绝对音高直方图:

下面这张图展示了不同音级类别的相对出现频率:

从这张图中,我们可以推断出乐谱的“有效调性”——不过 MusicMeasurements 可以直接完成这一工作:

我们在这里看到的所有内容都可以视为基于音乐的符号化表示。但有了这种符号化表示,就始终可以将其渲染为实际的音频:

为表格数据带来更大、更好的连接性

这个表格框架,我们在14.2 版本中引入,使得在Wolfram Language中处理表格数据变得极其高效。但这些数据从何而来?(还有,它们又去了哪里?)在 14.2 版本中,我们引入了高效导入 CSV 等文件,以及 Parquet 和 ArrowIPC 等列式数据格式文件的方法。随后在14.3 版本中,我们新增了直接从关系数据库导入数据的能力。

在版本 15 中,我们正在增强这些能力,并加入更多功能。首先,现在可以高效地从多种文件中仅导入特定的列(版本 14.2 已经允许高效地导入特定的行)。因此,举例来说,下面这段代码从 CSV 文件中仅导入两列:

只导入特定列(和行)的能力在处理超大规模数据集时可能至关重要——因为它让你可以把大部分数据集留在磁盘上,同时只将所需的部分高效地导入内存。

原始数据集可能在哪里?它可能在你的电脑上的某个文件里。但 DataConnectionObject(在 Version 14.2 中引入)也提供了对 Amazon S3、Azure Blob Storage 和 Dropbox 等数据存储的无缝访问。而现在在 Version 15.0 中,我们还新增了对 Azure Files 和 Azure Tables 的无缝访问。

DataConnectionObject 还提供了对关系型数据库中数据的访问(这是我们在 Version 14.3 中加入的功能)。在 Version 15.0 中,我们让这种访问的效率提升了一个数量级以上,使其现在几乎达到了可以想象的最快速度(以及最高的内存效率)。

在 Version 15.0 中,我们还从关系型数据库扩展到了多维数据库,具体支持 Databricks(以及 Snowflake)。因此,举例来说,下面展示了如何设置一个 DataConnectionObject,通过某个特定的多维(OLAP)查询来定义与数据“湖仓”的连接:

Version 15.0 中的另一个新功能是将 Tabular 与 ExternalEvaluate 连接起来,从而支持同时包含 Wolfram Language 和外部语言的表格数据工作流。因此,举例来说,你可以在 Python 中获取一个 pandas DataFrame,并通过 ExternalEvaluate 立即在 Wolfram Language 中获取其数据:

(而且,没错,我们在 Python 等语言封装方面所做的全部工作,让这件事的实现方式格外简洁高效。)

Tabular 的更多内容

我们花了很长时间才为新的 Tabular 框架设计出最初的设计方案。我很高兴地说,这个设计似乎运行得非常好。但正如 Wolfram Language 中新框架一贯的情况一样,一旦框架部署并投入使用,人们就会开始发现各种各样扩展和完善它的方式。Tabular 框架也是如此。在 Version 15 中,我们为 Tabular 框架引入了相当多的增强功能。

第一个功能很简单,但非常实用。当你在笔记本中有一个相当小的 Tabular(比如有几十列、几千行)时,Tabular“背后”的所有数据都会自动直接存储在笔记本中,因此只要你使用该笔记本,这些数据就随时可用。较大的 Tabular 对象则像笔记本中的许多大型对象(如 SparseArray 等)一样处理,并配有一个按钮,让你选择是否要将数据直接存储在笔记本中:

Version 15.0 中另一个新功能是函数 TabularSummary,它可以高效地生成 Tabular 的概要大纲——这里是我们刚刚导入的那个稍大的 Tabular:

TabularSummary 提供了灵活的方式来选择要汇总 Tabular 的哪些部分,以及如何汇总它们。例如,这里只要求包含数字的列,并要求对这些列进行完整的统计汇总:

如果我们想对这种数据建模呢?那么,我们可以直接使用全新的 Version 15.0 函数 ModelFit,借助 语法挑选出特定的列,对其拟合模型:

当我们手头有数字可用时,这类模型拟合效果最好。但如果你的数据包含的是实体——比如国家或树种——又该怎么办?我们如何从这些实体中推导出可用于建模的数字?Wolfram Language 内置了海量经过整理的各类实体数据。而在 Version 15.0 中,新增了一个函数 EntityAugmentColumns,让你可以立即对 Tabular 进行扩充,为其中某一列中的实体添加与之关联的数据(数值或其他类型均可):

Tabular 的一个重要特点是,它专门针对许多常见类型的数据进行了优化,比如数字和日期。在 Version 15.0 中,新增了一种数据类型,即由 Around 表示的近似数值数据:

顺便提一下,这个示例还展示了 Version 15 中的另一项新功能。我们在这里没有显式命名各列,因此它们仅以数字索引标注——这些索引现在在显示中以灰色呈现。

Tabular 还有许多其他细节上的改进和增强。其中之一是,Tabular 中的 GeoPosition 列现在可以处理地理投影,在需要时对位置进行恰当的重新投影。

可视化调优

Wolfram Language 图形被用在非常非常多的地方,我们非常希望确保它们始终保持一种新鲜、生动的观感。实现这一点的一个重要方面,是确保我们使用的颜色“看起来不过时”。我们希望整体的颜色选择保持一致。但我们也希望定期“美化”颜色,让它们“跟上时代”。

在过去几个版本中,我们对颜色做了大量美化。现在到了 Version 15,我们转向了图表中区域的情况——比如 PieChart。所以,举例来说,下面是一个在 Version 14.3 中渲染的饼图:

下面是 Version 15 中美化后的版本:

直方图之类的东西也有了新的默认颜色。这稍微更微妙一些,但

在 Version 15 中被替换为:

Version 15 中的另一个新功能与 PlotStyle 选项以及相关选项的扩展有关。在之前的版本中,你会用 PlotStyle 来指定 Plot 等中曲线的样式,但你会用 ChartStyle 来指定 BarChart 等中柱条的样式。之所以做出这种区分,与处理柱条分组等的样式有关。但在 Version 15 中,我们有了一种更精简、更统一的方式来做这件事——其一个结果就是,我们能够对一切使用 PlotStyle,而不会再有有时不得不使用 ChartStyle 所带来的潜在困惑。

因此,举例来说,现在这样可以用来为 BarChart 中的柱条设置样式:

如果你有几组柱条呢?ChartStyle 默认会为每组中对应的柱条指定样式,但 PlotStyle——为了与它在 Plot 等函数中的用法保持一致——指定的是整组的样式:

但现在,在 Version 15 中,我们有了一种新的基于关联的指定颜色方式,它让我们可以分别定义“元素”(即单个柱条)的样式与柱条组的样式:

同样的基于关联的机制也适用于 PlotLabels 等——例如,允许分别对单个柱条与柱条组进行标注:

在 ListPlot 等函数中也可以进行这种精细控制。这里我们定义了一个特定的“基础样式”,然后说明不同的点列表应如何渲染:

我们新的基于关联的机制所能实现的一些效果,其实通过现有选项的各种组合已经可以做到。但基于关联的机制让这一切都变得更加清晰、更加直观。

DistributionChart 也有类似的问题;它拥有很棒的功能,但只能通过略显晦涩的选项组合来使用。在 Version 15 中,我们将 DistributionChart(顺便说一句,这是一个非常好用且实用的函数)最重要的能力主流化了。

以下是 DistributionChart 新的默认行为——为每个数据集明显生成平滑的直方图分布:

如果你不想要平滑效果,只需将 "Histogram" 作为显式的第二个参数传入:

如果需要,你也可以获得双侧的“小提琴风格”外观:

或者你也可以显式地展示密度(可选地加上分位数线等):

Wolfram Language 拥有极其灵活的可视化能力。但我们始终在寻找让更多可视化更加便捷的方法。在 Version 15 中,我们新增了几个全新的可视化函数来帮助实现这一点。

其中之一是 BubbleHistogram。假设你有一组 {x,y} 值。Histogram3D 和 DensityHistogram 是可视化这些值分布的两种方式。在 Version 15 中,现在还新增了 BubbleHistogram:

在一个完全不同的方向上,Version 15 还新增了 PeriodicTablePlot。如果你不另行指定,它就只是“绘制一张元素周期表”:

但你也可以在元素周期表上“叠加”绘制内容。例如,这里要求绘制每种元素的相态:

多面板可视化

假设你有一组图形:

你可以把它们显示在一个网格中:

但从某种意义上说,这非常浪费(而且“嘈杂”):这些图形基本上具有相同的刻度,但我们却为每个图形重复这些刻度。在 Version 15 中,我们现在有了一个新函数 PlotGrid,它接收一组图形,并尝试以最优方式将它们渲染在一个网格中,尽可能共享刻度信息:

这其中有许多微妙之处。在这个例子中可见的一个微妙之处,与刻度是在不同行和不同列之间共享,还是仅在每一行和每一列内部共享有关。

以下是如何要求共享所有刻度——在这种情况下,现在使两行上的 y 刻度相同:

PlotGrid 还会处理标签,同样默认在可以共享的地方共享它们:

实际上,PlotGrid 会从各个图形中“收集”选项,然后尝试将它们组合起来,以构成一个一致的整体网格。PlotGrid 本身也可以被赋予选项。例如,你可以在 PlotGrid 中指定整体的 AspectRatio 或 ImageSize。但你也可以向 PlotGrid 提供 ItemAspectRatio 和 ItemSize 选项,用于指定网格中每个单独条目的宽高比或大小。

GB 级笔记本与实时查找

我们最初是在 1988 年随 Mathematica 1.0 一起引入 notebook 的。我认为可以公允地说,它们取得了巨大的成功,无论是作为一种工作方式,还是作为一种呈现和记住自己所做工作的方式。当年我们最初引入 notebook 时,它们会超过几兆字节大小,这种概念似乎难以想象。但——四十年后——notebook 可以变得非常庞大。有时是因为它们包含大型图形。有时是因为它们含有大型图标化表达式。有时则是因为有人在 notebook 中使用了 Save in Notebook,把视频或大型表格数据集直接保存进 notebook 里。

在许多方面,我们在处理大型 notebook 上做得相当不错。但过去我们常常需要做出取舍,以应对典型计算机相对较小的内存和较慢的大容量存储。然而,当最大 notebook 的规模开始逼近 GB 级时,我们意识到需要新的 notebook 基础设施。于是,大约十年前,我们启动了一个大型项目,从头重建我们的 notebook 基础设施。嗯,我很兴奋地说,那个项目现在已经完成——Version 15 拥有全新的、高效的多核多线程 notebook 基础设施。

结果是,现在人们可以常规地处理数 GB 大小的 notebook(而且除了存储之外,最终没有什么会限制 notebook 的可能大小)。在这一切之中,我们保持了完全兼容——因此 Version 15 仍然可以打开 Version 1 创建的 notebook(是的,我们对此进行了非常广泛的测试)。notebook 文件的底层结构仍然和以往一样。但让我们能够在新的 notebook 基础设施中实现这种性能的,是对 notebook 文件解析方式的彻底重新思考,利用了现代多遍解析方法。

但随之而来的是,大型笔记本的日常使用也带来了新的问题。其中尤为突出的是查找功能。在版本 15 中,基于我们全新的笔记本基础设施,我们打造了一套全新且高效的查找系统。

在笔记本中按下 CMDF/CTRLF,就会弹出该笔记本的查找对话框:

Find dialog

速度快到只要你一开始输入,就能看到笔记本中匹配项数量的统计。(即便笔记本有数 GB 大小,这一点也照样有效。)

查找对话框大体上以非常标准的方式工作。笔记本中的每个匹配项都会被高亮显示。(或 ENTER)会跳到下一个匹配项——输入框中的 nnn/nnn 显示会告诉你当前位于哪个匹配项。 是区分大小写的开关, 则是全词匹配的开关。

按下 >,就会打开查找对话框的替换部分:

Find and replace dialog

按下 执行一次替换,并跳到下一个。按下 则全部替换。

查找功能在 Wolfram Notebooks 中的工作方式涉及许多细节。其中之一是处理排版表达式和特殊字符。事实上,你可以输入一个排版结构并找到它:

Find typeset equations dialog

特殊字符,比如 α,也可以使用。不过你不能用 ESC 来输入它们,因为在系统层面这会关闭“查找”对话框。你仍然可以使用长名称,比如 \[Alpha],以及 SHIFTESC。

还有另一个微妙之处。在进行替换时,你只想替换笔记本中的“输入”内容;你想跳过出现在生成输出中的内容。而这一点通过以下事实体现出来:你在生成输出中找到的内容,其高亮显示带有虚线边框:

Find and replace inputs

自从我们在 1988 年引入它们以来,笔记本从根本上一直是“单窗格”文档,主要用于维护有时是动态内容的线性序列。而当需要另一个窗格时,典型的做法是它应该是另一个笔记本。在 Wolfram Cloud 中——遵循网络上一切最终都必须容纳在一个浏览器窗口中的典型模式——我们多年来仍然拥有各种类型的侧边栏。那么,现在侧边栏也来到了桌面笔记本中。在未来的版本中,将会有相当通用的侧边栏机制。但在 Version 15 中,侧边栏被引入是为了两个特定的、高价值的目的:笔记本属性和 AI Assistant。

在任何笔记本中,点击工具栏中的齿轮图标(或从 Window > Sidebars 菜单中选择),笔记本就会展开一个 Notebook Properties 侧边栏,让你可以查看(并修改)各种常用的笔记本级设置:

Notebook Properties sidebar menu

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

Notebook chat sidebar

(你只需拖动窗口中的分隔条即可调整侧边栏的宽度。)

笔记本迎来视觉主题

不是每个人都希望自己的笔记本看起来千篇一律。而能够在浅色和深色模式之间切换,是不同的人即便看同一个笔记本也会看到截然不同效果的一大方式。但在版本 15 中,还有另一种让笔记本看起来不同的重要方式:更改其视觉主题。你可能想借此来增强或减弱语法着色。你可能出于无障碍需求这样做,或者仅仅是出于审美偏好。

你可以使用“笔记本属性”侧边栏为特定笔记本更改主题,也可以使用“偏好设置”面板为所有笔记本全局更改主题。(你还可以通过设置 NotebookTheme 选项以编程方式更改笔记本主题。)

以下是“偏好设置”面板中的“主题”部分(其中既包括 Monokai、Solarized 和 Dracula 等标准的现代主题,也包括我们设计的主题,如 Wolfram Saturated 和 Stargazer):

Theming section

选择任意主题,它都会立即应用到你的笔记本。请注意,每个主题都同时有浅色模式和深色模式版本。

如果你在全局偏好设置中设定主题,它只会用来决定你系统上笔记本的外观;如果你把笔记本从你的系统发送给别人,决定它在对方眼中外观的是他们的主题,而不是你的。但如果你通过 Notebook Properties 侧边栏为某个特定笔记本设定主题,那么该主题会随笔记本一起携带,你发送笔记本给的任何人都会看到它。

笔记本主题实际上基于 Version 14.2 中引入的一项功能:ThemeColor。笔记本主题的工作方式是,笔记本的不同元素被标记为以不同的命名颜色渲染。例如,默认样式表中的标题单元格被标记为以命名颜色 "Accent1" 渲染:

再举一个例子,实体以 "Accent4" 渲染:

假设你想在图形中匹配这些颜色。那么,你可以通过引用这些命名颜色来实现:

如果有人把他们的主题切换为,比如说,CRT,那么这对他们来说会立即看起来不同:

Switching themes

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

Color picker

太长时,就撕掉

假设——就像我经常做的那样——你把 notebook 当作一种阐述的媒介。对于大段的输出,你该怎么办?如果你把它们留在 notebook 中一个展开的单元格里,它们会打断你阐述的流畅性。但如果你把单元格折叠起来,就没人能看出里面是什么了。那么,在 Version 15 中还有另一种选择:直接把它撕掉。

选中该单元格,然后在主菜单中选择 Cell > Elide with Tear(或在右键菜单中选择):

视频 · 前往原文观看

一旦你有了这个撕痕,就可以直接上下拖动它:

视频 · 前往原文观看

这一切相当简单——但非常实用。而且,实际上,我在自己写的东西里已经用了这个机制好多年了(包括关于之前几个 Wolfram Language 版本的介绍!)Wolfram Function Repository 里已经有一个函数可以做它的独立(且参数化)版本,已经有好几年了。但现在在 Version 15 中,它已完全集成到我们的 notebook 系统中。

它实际上是怎么工作的?嗯,"撕痕"是由一个确定性随机过程生成的,该过程以分配给该单元格的 UUID 作为种子——所以对于那个特定单元格,撕痕看起来总是一样的,但如果你复制该单元格,撕痕就会不同。(撕痕的实际渲染是使用高效的像素着色器完成的。)

顺便说一句,你可以给任何单元格添加撕痕——无论它包含图像、文本、交互式内容等等:

视频 · 前往原文观看

在光明中变暗

假设你在笔记本中以浅色模式工作,但想要得到一张深色模式的图片(比如用于演示)。在版本 15 中,有一个简单的方法可以做到这一点:只需使用 DarkModePane:

这里的选项与 Pane 中的相同:

你可以指定换行宽度:

以及高度——滚动条可选:

而且,没错,如果你处于深色模式,你可以用 LightModePane 做完全相反的操作。哦,如果你想用某种特殊的深色作为输出的背景,DarkModePane 是将一切(比如坐标轴及其标签)"翻转"为深色模式的好方法:

顺便说一句,如果你要记录浅色和深色模式下发生的情况,你就需要 LightModePane 和 DarkModePane——这些在我们的文档中会经常出现。

那个计算中发生了什么?Monitor 的单参数形式

你如何知道正在进行的计算内部发生了什么?你可以插入一些 Echo。或者——自 Version 6 起——你可以使用 Monitor。但 Monitor 过去一直以来的工作方式,是你需要明确告诉它你想监控哪些变量的值。对于像 Table 这样有具名变量可处理的函数来说,这没什么问题。但像 Map 这样的呢?你该如何监控它?

在 Version 15 中,有一种新的单参数形式的 Monitor,让你可以监控像 Map 这样的函数:

视频 · 前往原文观看

弹出的蓝色框会显示 Map 已经进行到哪一步,以及预计还需要多长时间才能完成。(它还包括一个 按钮,用于中止计算。)

单参数形式的 Monitor 适用于所有常见的函数——比如与 Map 相关的、与 Nest 和 Fold 相关的、与 Table 相关的,等等。

对于像 Table 这样的,你随时可以使用双参数形式,指定你想监控的内容:

视频 · 前往原文观看

但单参数形式就“全包了”,直接给你整体进度的信息,无需你明确考虑各个迭代变量等:

视频 · 前往原文观看

双参数形式 Monitor[expr, mon] 将监视mon在expr求值过程中发生的所有值变化。而 Monitor[expr] 则只关注expr中顶层函数的求值。换句话说,在单参数形式下,Monitor 需要直接包裹在你想要监视的函数外面,无论是 Map、Fold、Array还是其他什么。

现在可以持有子值了!

这是一个近四十年来我们一直设想有朝一日会处理、但始终显得棘手的边缘情况。如今在 Version 15 中,我们终于做到了:现在可以持有子值(subvalue)了!

这是什么意思?首先,什么是子值?当你进行如下赋值时

你正在为所谓的 f 的 downvalue 进行赋值。但如果你这样赋值呢:

在这种情况下,我们说你在为 g 赋一个 subvalue。

Subvalue 在很多场景下都很有用,尤其是在构建如下形式的运算符时:

好,那么 holding 这个概念呢?通常,如果你输入 f[1+1],会发生的是首先 1 + 1 被求值为 2,然后 f[2] 被求值。但如果 f “holds its arguments”(持有其参数),那么 1 + 1 不会先被求值,而是以 1 + 1 的形式传给 f。

为什么这很有用?想象一下写 x = 1,它会被解释为 Set[x, 1]。这里的关键在于 x 被保持(held)了。你想设置的是“x 本身”的值,而不是 x 的值。所以你需要把 x 传给 Set,而不让它先被求值。

之所以会这样运作,是由 Set 的属性决定的:HoldFirst 意味着 Set 的第一个参数应当被保持(held):

假设你做了这样一个赋值:

现在 u 的第一个参数将被保留——不过其他参数不会:

与此同时,如果你进行如下赋值

所有参数都将被保持:

那么,参数的保持与子值之间的交互是怎样的呢?假设你有一个像 u[x][y] 这样的表达式。如果 u 具有属性 HoldAll,那么在 u[x][y] 这样的表达式中,x 会被保持——但 y 不会:

那么现在,在 Version 15 中有一个新属性 SubValuesHoldAll——它会保持所有子值参数。设置这个属性

现在在 v[x][y] 中,y 会被保持,即使 x 会被求值:

而且,顺便说一下,这种保持会“一直向下传递”:

为什么这有用?最重要的是,因为它允许拥有能保持其参数的运算符形式。在设计各种函数时,我们多年来一直想要这个。例如,考虑 AppendTo。AppendTo 具有 HoldFirst 属性,因此 AppendTo[x, expr](就像 Set[x, expr])不会对 x 求值。

但 AppendTo 的运算符形式呢?我们希望能够写出 AppendTo[expr][x],并将 expr 追加到 x。但要做到这一点,就要求 x 保持不求值。而这——感谢 Version 15 中的 SubValuesHoldAll——现在成为可能。

运算符形式让函数式编程变得格外优雅和便捷。尤其是在过去十年左右,我们越来越多地引入这类形式,覆盖了广泛的函数。但对于某些函数(如 AppendTo),我们一直无法做到这一点——因为我们还没有 SubValuesHoldAll。而且,是的,就内部实现而言,SubValuesHoldAll 很棘手——因为它涉及一种必须非常小心处理的“求值前瞻”。但现在在 Version 15 中它已经完成,我们可以为大量新的、有用的运算符函数,以及 subvalues 的其他用途,打开设计机会。

推出即用型增量数据结构

假设你想在十亿个对象中进行搜索,也许是挑出具有某种特殊属性的对象。最简洁的代码写法,可能就是直接生成这十亿个对象,然后挑出你想要的那些。但当然,这十亿个对象可能很难全部存进内存。你可能会想,处理这个问题的唯一办法,就是想办法按顺序生成这些对象,然后写代码显式地遍历它们。

那么,在 15.0 版本中,有一种更好、更简洁——也更高效——的方法来实现这一点,那就是使用我们全新的IncrementalObject构造。IncrementalObject 基于IncrementalFunction技术,该技术我们在14.3 版本中引入,但现在它已经打包好,可以立即使用,无需显式进行代码编译等操作。

IncrementalObject 的基本思想是为一个(可能非常庞大的)事物集合提供一种符号化表示,其构建方式使得这些事物可以被增量式访问。因此,举例来说,这个增量对象表示 20 个对象的 20! ≈ 2 × 1018 种排列:

每次你请求这个增量对象的 NextValue 时,你现在都会得到序列中的下一个排列:

那么现在假设你想找到第一个阶为 20 的排列。你可以使用 Select 的增量版本:

你在这里得到的 IncrementalObject 只是所选排列的一个符号化表示。如果你想真正找出这个选择中的第一个排列,可以用 NextValue 来实现:

再次运行 NextValue,即可得到所选集合中的下一个排列:

而且,没错,它的阶为 20:

如果你好奇的话,以下是到达这一个排列之前必须测试的排列数量:

下面是另一个例子,这次使用 Subsets 的增量版本,解决一个背包式问题:找出前 20 个素数中加起来等于 500 的一个子集:

在 Version 15.0 中,我们为多种函数提供了增量版本。除了 Permutations 和 Subsets 之外,还有 Tuples,以及 Map、FoldList、Take 和 Range。下面是一个使用 Range 搜索完全数的例子:

如果我们想更进一步呢?也许我们想在不同的计算机上进行计算。那么,我们只需拿起这里得到的 IncrementalObject,然后在另一台计算机上继续运行它。它是一个(可传输的)符号表达式,以(“惰性”)方式表示我们计算的当前状态,随时可以在任意时刻继续。

未来版本中还会有更多关于增量计算的内容。但 IncrementalObject 已经提供了一种便捷的新方式来组织计算,让人能够以“先枚举、后选择”的方式思考,而计算则会自动按顺序执行,且内存占用极小。

大型代码库中的异常与错误处理

当一个人编写程序时,他大概心里清楚程序应该做什么。但如果出了差错呢?实际上,需要有次级代码路径来合理地处理可能发生的各种错误。而在较大的代码库中,以合理且有组织的方式处理错误这一问题往往变得越来越重要。

在 Wolfram Language 中,自 Version 1 起,我们就已经有了多种处理错误的方式。它们通常在特定函数或模块的局部层面上运作良好。但在 Version 15 中,我们现在引入了一种强大的全新全局错误处理机制,使用了符号化异常的概念。

在深入探讨之前,让我们先回顾一下 Wolfram Language 现有的错误处理机制。

在最基本的层面上,有一种理念是:在某些情况下,某个特定函数就是不会被求值(比如模式不匹配,或者 /; 条件不满足),并且会“返回其符号化的未求值形式”。然后还有一种理念是显式使用 Return 在出错时退出函数。

但这两个机制都非常局部;它们只在单个函数内部处理错误。

其实,即使在版本 1 中就已经有一种机制——此后被广泛使用——用于非局部错误处理:Throw 和 Catch。在代码的任意位置调用 Throw,它就会停止当前正在做的事,并返回到最近的包围它的 Catch。但这里有个问题(可以这么说):如果你正在调用的某个函数中(也许甚至不是你写的)有一个 Throw 呢?如果代码碰到了那个 Throw,它就会把你代码正在做的一切都抛掉(可以这么说)。

Throw 和 Catch 的通用机制是处理错误的强大方式。但挑战在于正确地控制和限定它的作用域。在 版本 3(1996 年)中,我们为 Throw 和 Catch 引入了标签,这为限定 Throw 和 Catch 的作用域提供了一个良好的基础底层机制。但在实践中,尤其是对于较大的代码库,它们用起来很繁琐,也难以管理。

许多年过去了。但最终在 12.2 版本(2020 年)中,我们引入了另一种非常简洁的机制来处理相当局部的错误:Confirm 和 Enclose。其思路是在一段代码中散布 Confirm 系列函数(Confirm、ConfirmQuiet、ConfirmBy、ConfirmMatch……),只要它们正确地确认了被要求确认的内容,就不会影响代码的运行——但一旦出了问题,它们会停止代码,并返回到最近的外围 Enclose。在最常见的形式中,Confirm 和 Enclose 直接出现在单个函数内部,并按词法方式处理,无需任何显式的标记。这对于处理单个函数内部的错误极为方便,但如果想把错误传播到该函数之外,就需要在每一层使用额外的 Confirm 和 Enclose 实例来显式请求这种传播。

那么,如果一个人拥有一个庞大的代码库,其中错误可能发生在某个函数中,并且需要向外传播,可能穿过许多对该错误一无所知的其他函数,该怎么办呢?在 15 版本中,我们引入了一种使用符号异常来处理这一问题的机制。

基本思路相当简单:使用 ThrowException 抛出一个具名异常,该异常会向上传播到最近的、被设置为处理相关类型异常的 CatchExceptions。通常异常的名称是符号,可以通过标准的作用域机制在包中进行作用域限定,包括我们在 Version 15 中引入的新机制。重要的是,异常类型还可以存在层级关系,因此针对更通用异常类型的 CatchExceptions 可以捕获在其内部发生的任何子类型异常。

举个简单的例子,让我们定义一个可以抛出异常的函式 fac:

现在让我们定义一个使用 fac 的函式 g

然后再定义一个使用 g 的函式 f,但这次它会捕获溢出异常:

现在我们可以使用 f,如果没有产生异常,它就照常计算其值:

但如果在对 f 求值的过程中任何地方产生了异常,它就会向上传播,而 f 的值将(默认情况下)是一个 Failure 对象:

注意,由于 f 捕获了异常,任何错误都不会传播到 f 的求值之外:

但如果我们直接对 g 求值会怎样?那样就没有 CatchExceptions 来捕获所产生的任何异常,因此异常会“接管一切”:

在这种情况下返回的是底层的 Exception 对象:即所生成异常的一种符号化表示。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:

Version 15 中的异常框架非常强大,能够轻松地为大型代码库添加良好的错误处理。事实上,我们已经在 Wolfram Language 内部代码的开发中使用该框架的初步版本数年之久。Version 15 中所包含的内容代表了大规模异常处理所需的大部分功能。未来还有一些附加特性,尤其是错误转换,即一段代码中产生的错误可以被转换为适用于另一段代码的形式。(例如,一个特定的内部溢出错误可以被转换为更通用的“该函数无法计算”错误。)与此相关,我们还计划引入内置 Wolfram Language 函数中产生的错误本体,以便用 Wolfram Language 编写的代码中的错误处理可以利用它。

隆重推出结构化包格式

什么x是那个 x 吗?一段代码中出现的 x 是否指代与另一段代码中的 x 相同的符号?在单段代码内,可以通过以下方式将名称(如 x)局部化Module。在不同代码片段之间,自Version 1.0起,人们就能够使用上下文来区分名称(如 x)的不同实例。one`x 与 two`x 是不同的 x。当然,为每个 x 实例都显式指定上下文(如 one`)会很不方便。因此(同样自 Version 1.0 起),就有了当前上下文$Context的概念,它允许指定任何新符号(比如 x)将在什么上下文中创建,以及$ContextPath的概念,它给出一个上下文列表,用于搜索输入中给定的符号(比如 x)。有了 $Context 和 $ContextPath,有助于避免始终需要显式指定上下文。但它们还不够。而且(同样自 Version 1.0 起),还有这些函数BeginPackage, 开始, 结束和EndPackage它们管理 $Context 和 $ContextPath 的设置。

因此,一直以来(自 1.0 版以来),Wolfram Language 包都包含 BeginPackage 之类的咒语。但这始终有些混乱。是的,一个包内的符号可以被局部化。而且一个包可以有子包。但要让符号既被局部化、又能在包之间共享,这一直都很复杂。多年来,人们为此发明了各种不同的机制。但在我们公司内部,我们逐渐收敛到了一种特定的机制。而现在在 15.0 版中,我们已将此机制内建到 Wolfram Language 中,成为新的结构化包格式。

当处理大量 Wolfram Language 代码时——特别是分布在目录树中多个文件里的代码——结构化包格式尤为重要。在我们传统的包设置中,符号定义在哪个文件中并没有特别的意义。但在结构化包格式中,所做出的关键假设是:定义在不同文件中的符号默认是不同的,也就是说它们的名称被视为处于不同的上下文中。换句话说,在结构化包格式中,新符号默认在其文件内“生来就是私有的”(即局部化的)。

但如果想让某个特定符号 x 成为“公开的”,并在其文件之外可用呢?那么可以用 PackageExported 声明一个符号为导出。因此在结构化包格式中,一个文件通常包含类似这样的内容:

Structured Package Format file

函数 pub1 等被导出为公共函数,而 priv1 等则作为私有函数保留在文件内部。如果你希望某个符号能在包内的不同文件之间共享,但又不希望在包外被访问,你只需把它放在 PackageScoped 中,而不是 PackageExported。

那么,如何用结构化包格式(Structured Package Format)来搭建一个完整的包呢?你需要把它的文件放进一棵目录树中。而且——至少在最简单的情况下——在这棵目录树的顶层,你要有一个 init.wl 文件,其中包含 PackageInitialize["name"],其中(通常情况下)name 既是你的包的基础上下文的名称,也是该包顶层目录的名称。(当这个包是某个 paclet 的一部分时,该 paclet 的 PacletInfo.wl 文件可以指定更复杂的目录结构、不同的初始化文件名等。)

当你使用结构化包格式的包时,你像使用传统 Wolfram Language 包那样,用一个上下文名称调用 Needs——这就会加载对应目录中的 init.wl 文件。而正是在 PackageInitialize 运行时,新的结构化包格式的魔力才得以发生——目录树中的其他文件会被加载,其符号默认被本地化。

结构化包格式中还有一个最后要提到的函数:PackageImport。当 PackageInitialize 加载文件时,有时你会想从其他包中导入定义。PackageImport 允许你导入某个给定包中的所有公开符号,或者——这一点很重要——只导入你需要的该包中的特定公开符号。

在传统的(自 1.0 版本以来的)包设置方式中,你的代码里会到处散落着 BeginPackage、Begin 等等。新的结构化包格式让你可以完全避免这些,以一种非常简洁且极简的方式指定哪些符号应该在何处可访问。

为什么我们花了这么多年才想出这个?对用户来说,结构化包格式在操作上看起来相当简单。但其底层发生的事情却相当复杂。以下是其中一个问题。如果 PackageInitialize 在某个 Wolfram Language 代码文件中遇到一个 x,它必须知道该符号 x 应该属于哪个上下文。但这可能只由该文件中后面出现的内容来定义,或者由某个完全不同的文件来定义。那么 PackageInitialize 如何处理这个问题呢?它会先扫描整个目录树,收集所有 PackageExported 和 PackageScoped 的实例,只有在处理完这些并确定了符号的上下文之后,它才真正读取目录树中的完整代码。换句话说,在对所有文件进行“真正的”语义处理之前,先进行了一次本质上是词法的扫描。而且,是的,要让这在所有情况下都能正常工作是非常棘手的。但在新的结构化包格式中,它做到了——并且它让人们能够以前所未有的更好、更简洁的方式搭建大型 Wolfram Language 代码库。

在图上绘图

如何在图的节点上绘制数值?在 Version 15 中,你只需使用 GraphValuePlot:

你可以用不同的方式来表示数值;这里我们说的是只用顶点大小

而这里我们同时使用了顶点形状和顶点大小:

各种标准图属性都可以直接在 GraphValuePlot 中得到支持。例如,这里展示了一个图,并在其上绘制了它的接近中心性:

GraphValuePlot 不仅支持在节点上绘制数值,也支持在边上绘制数值:

这里有一个例子,我们取一个边带有边容量标注的图,然后把这些数值绘制在边上:

GraphValuePlot 接收一个已有的图,然后在其上绘制数值。在 Version 15 中,另一个新函数是 TaggedNestGraph——它会构建一个图,其边上带有标签,默认情况下会根据这些标签设置样式。这里有一个例子,其中“f”和“g”边带有不同的标签,并具有不同的样式:

这里有一个稍大一些的例子:

Version 15 中另一项新内容是针对图的一组新高亮样式。这里我们使用光晕来高亮一些节点:

GraphValuePlot 是一个用于在图上绘图的高级函数。但它所做的任何事情,也都可以通过在图里显式指定顶点和边的渲染,在更低的层级上完成。而在最底层,还可以选择像 VertexShapeFunction 这样的选项,例如,它可以让你应用一个函数来完全控制每个顶点的“形状”。当然,这会变得相当繁琐,尤其是必须按顺序把顶点坐标、顶点大小和顶点名称作为三个参数传入。好吧,在版本 15 中,我们让这件事稍微容易了一些,允许从关联中访问这些值,比如 #Coordinates 等:

如何在地球地图上放置刻度?

当我们绘制地球地图时,实际上始终是在把 3D 的、大致呈球形的地球投影到 2D 地图上。实现这一点有很多方式,例如由 GeoProjection 选项指定,一个例子是:

但假设我们想读出这张地图上某个点的坐标。如果我们要求包含普通坐标轴,就会得到:

这些坐标轴上的坐标是最终投影地图的坐标。但如果我们想知道这些点在地球表面上的位置,比如用纬度和经度表示,该怎么办呢?好吧,在版本 15 中有一个新选项 GeoAxes,可以给我们“地理”或“经纬度”坐标轴:

在赤道上有一条“地理轴”;另一条,至少默认情况下,位于经度 0°,即格林尼治子午线。除了地理轴之外,还有地理网格线——它们与当前存在的地理轴上的刻度线对齐:

在某些地理投影下,事情会变得相当奇特。比如这里赤道变成了一个正方形:

哦,当然,这一切在月球(或其他行星)上同样适用:

(在内部,这个特定的投影是双周期 Jacobi 椭圆函数 JacobiSN 等的一个有趣应用。)

地理轴有很多选项——比如让轴在哪里交叉(GeoAxesOrigin):

如果你想完全控制坐标轴,可以指定一个 AxisObject——而在 GeoGraphics 中,这样的 AxisObject 会被正确地转换为你所使用的任何地理投影。

你的城市何时能看到日食?

数千年来,天文计算一直是推动精确科学发展的动力。而在历史上,最富挑战性的问题之一便是日食和月食的预测。如今,我们能够以足够的精度预测日食,例如在 2017 年,我们得以拥有一个网站,能够将美国境内日食抵达任意特定地点的时间预测到一秒以内——这是科学进步的令人瞩目的标志。我们所做的工作基于我们在 2014 年引入的函数 SolarEclipse,它可以计算大约 30,000 年周期内任何特定日食的属性。

但反过来呢?给定地球上的一个地点,在那里将会看到哪些日食?这既是天文计算也是地理计算中的一个挑战性问题。而在 Version 15 中,我们引入了函数 FindSolarEclipse 来实现这一点。这里我们在问,下一次从巨石阵可见的(非偏食)日食会在什么时候:

我猜还得等上一阵子……那过去呢?

这是那次日食的路径:

这是日全食抵达巨石阵的时间:

这是它持续的时间:

这是过去 10,000 年间从巨石阵可见的所有(非偏食)日食的时间线:

以下是它们所有的路径:

顺便说一句,FindSolarEclipse 也支持扩展地理区域,比如国家:

而且,是的,对美国来说还要等上一阵子。但是——借助相当多的能力组合——以下是未来一年内将经历日全食的国家列表:

发射进入轨道

天体力学的故事,首先且最重要的,是一个关于轨道的故事。在 Version 15 中,我们开始支持轨道的计算。在这个版本中,我们专注于("开普勒式")轨道根数,它们实际上给出了对轨道的瞬时近似。所以,举例来说,对于今天的火星,以下是我们得到的基本轨道根数:

我们可以把这些轨道根数看作给出了最能代表火星当前轨道的椭圆的参数。以下是该轨道的时间序列:

以下是预测的 10,000 年后的轨道根数:

其中大多数与当前的轨道根数非常相似,这表明近似未来轨道的椭圆与当前轨道的椭圆非常接近。(不过,"平近点角"基本上是火星在其轨道内的角度,所以它变化很快。)

我们可以计算行星、卫星、小行星和彗星的轨道,也可以计算航天器的轨道。以下是计算出的木星内层卫星的瞬时轨道(是的,伽利略卫星就是其中一些位于内侧、轨道非常“像小太阳系”的卫星):

同样地,以下是 GPS 卫星当前的轨道,在这里是围绕地球的轨道:

这些轨道都是椭圆形的。但 OrbitalElements 也能处理双曲线轨道——比如旅行者 2 号的路径,它显示出大于 1 的典型离心率(注意相对论定义的 TDB 时间系统):

让我们稍微探讨一下。以下是旅行者 2 号自发射以来每月与太阳距离的变化:

起初有一些与行星引力助推相关的异常波动——随后是一段对应于双曲线轨道的“滑行”阶段。但那条轨道是什么?嗯,它就是由旅行者 2 号当前轨道要素所指定的轨道。取这些要素并计算它们所隐含的位置,我们看到当前的这条双曲线轨道确实与位置相符——一直回溯到上一次引力助推的时刻:

我们可以从轨道要素中计算出许多东西。例如,这显示了每个月的总能量——说明第一次引力助推(来自木星)给了旅行者 2 号逃离太阳系所需的能量:

格拉斯曼、克利福德、外尔及其同道

“计算机代数”指的是什么?传统上,人们想到的是对多项式之类的东西进行运算,其中的变量(比如 x)最终应当表示数。但对于其他类型的代数呢,比如其中乘法运算不满足交换律的那种?

在 Version 14.3 中,我们为自由代数引入了非交换计算机代数——符号矩阵就是一个显著的例子。如今在 Version 15.0 中,我们为带关系的代数引入了非交换代数,特别是 Grassmann、Clifford 和 Weyl 代数。

因此,举例来说,GrassmannAlgebra 表示一个 Grassmann 代数

其(非交换的)乘法运算为 ⋀(输入为 \[Wedge])。在 Grassmann 代数中,乘法被定义为反交换的,而 NonCommutativeExpand 会尝试按为该代数指定的变量顺序排列变量(这里是 x 后接 y),因此举例来说:

CliffordAlgebra 表示一个 Clifford 代数

在此例中,x 和 y 被定义为“平方”等于 1,而 u 被定义为“平方”等于 –1:

(非交换乘法 可输入为 **。)

然后是 WeylAlgebra,它便于表示微分算子的复合,在这里实际上是通过链式法则“展开”的:

你也可以通过关系式定义自己的非交换代数:

(关于这一切有很多可说的;事实上,现在有一整本 Wolfram 专著名为“Non-Commutative Algebras”专门讨论它。)

Zeta 函数、多重对数与调和数走向多变量

“那个有闭式解吗?”嗯,这取决于人们所说的“闭式解”是什么意思。但在操作层面上,它往往意味着“结果能否用我们已经定义的函数来表示?”当然,这个问题的答案取决于你定义了哪些函数。在 Wolfram Language 的每一个新版本中,我们都尝试添加新的“特殊函数”,以帮助我们为更大一类问题提供闭式解。

嗯,在版本 15 中,我们添加了一组特别强大的新特殊函数,使我们能够大幅扩展可获得的闭式解结果的范围,尤其是在量子场论和解析数论中的应用。这些新特殊函数的基本情况是,它们是 Riemann zeta 函数、多重对数和调和数的多变量推广。但事实证明,它们相当广泛地出现在线性微分方程组的级数解、多变量有理函数的积分以及多变量求和中。

从 Version 1 开始,我们就有了普通的单变量 zeta 函数:

当它们足够简单时,这类求和的多变量类似形式仍然可以用单变量 zeta 来表示:

但一般来说,它们需要我们用新的 MultipleZeta 函数:

而且,没错,事情很快就会变得复杂起来

不过在 TraditionalForm 中,结果至少还算相当紧凑:

下面是一个涉及三变量 zeta 的无穷级数:

下面是一个仍然涉及多重 zeta 的单变量求和:

可以把多重对数函数看作类似 zeta,但额外加了一个“幂级数分子”:

于是有几种方式将多重对数函数推广到多变量情形。最直接的就是我们称之为 MultiplePolyLog 的那个:

但为了覆盖经常出现的其他情形,我们还添加了 GeneralizedPolyLog 和 HarmonicPolyLog。普通的 PolyLog 可以从如下单变量积分得到

或者一个二元积分,例如:

当我们将其扩展到更多变量时,就开始得到新型的多重对数函数:

在 Version 15 中“走向多变量化”的第三类函数是 HarmonicNumber:

我们还新增了一些单变量类型的调和数,例如:

但归根结底,这些新特殊函数最重要的是它们出现的计算范围有多么广泛。比如这里是一个微分方程解的渐近展开结果(恰好来自一个费曼图),其中充满了调和多重对数:

部分分式化繁为简

从 Version 1.0 起我们就有了 Apart 函数。但现在在 Version 15 中,我们“拆解了 Apart”,使其在算法上更加精确和精巧——并且能够访问更多的部分。

Apart 背后的核心操作是计算部分分式——现在有了一个专门用于此的函数:

在这个特定情况下,结果与 Apart 的输出相同:

但在这种情况下

Apart 在无法再对有理数进行分母因式分解时就会停止,但 PartialFractions 默认会继续进行,这里使用复数根来完成完整的因式分解:

如果你不想使用复数根,可以告诉 PartialFractions 只使用 Reals:

部分分式被用于许多符号算法中(最早可追溯到 1703 年用于积分有理函数的原始方法)——而在不同情况下,所需要的正是结果中的不同部分。因此在 Version 15 中,我们引入了 PartialFractionElements,以便直接访问不同的部分:

大量新的矩阵分解

在某种程度上,矩阵不过是数值的数组,或者在 Wolfram Language 中,就是列表的列表。但根据矩阵将要用于什么目的,通常还有其他表示方式能更好地捕捉其“算法本质”。这正是矩阵分解的用武之地。自 1990 年代初以来,我们就已经拥有若干最常见矩阵分解的函数。但在 Version 15 中,它们正在被更新和精简,同时还新增了一些强大的新函数。

矩阵分解的一个典型例子(恰好是 Version 15 中新增的)是 LDLDecomposition——我们将一个矩阵分解为“L”和“D”两部分:

我们可以从这些部分重建出原始矩阵:

但关键在于,如果我们用矩阵来表示线性方程组,那么“L”和“D”这两部分正是我们能够立即高效求解所需要的。原则上,我们总是可以通过将 LinearSolve 直接作用于原始矩阵来得到解:

但使用“L”和“D”这两部分要高效得多

因为对于三角矩阵和对角矩阵,LinearSolve 所需做的工作要少得多。

像 LDLDecomposition 这样的函数默认会利用新的结构化矩阵对象,比如 LowerTriangularMatrix——它们能优化具有特定形式的矩阵的存储和计算。(选项 TargetStructure 让你可以控制将使用哪种矩阵结构。)

我们从 Version 3 起就拥有的一个矩阵分解是 LUDecomposition。但现在在 Version 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)。但并不是专门用于数学函数。尽管如此,我们已经探索这些可能性有一段时间了,而在 Version 15 中,我们首次开始在一个符号数学函数内部使用神经网络方法,具体来说就是 DSolve。

可以认为神经网络从根本上做的是近似计算。那么,如何把它用于像 DSolve 这样产生精确符号结果的函数呢?基本思路是用神经网络来“猜测”一个可能的解,然后用精确的符号计算来检验它,只有在验证通过时才将其作为结果返回。

值得一提的是,我们在 Wolfram Language 中实际上有许多非常强大的符号计算算法(其中不少是我们发明的),它们在内部使用近似(通常是数值近似),然后用精确的符号方法来过滤或验证结果。但 Version 15 中的 DSolve 是我们首次专门在一个符号数学计算函数内部使用神经网络。

DSolve 试图解决的根本问题是什么?给定一个微分方程,然后要求找到一个函数——最好是在结构上尽可能简单的函数——来求解该方程。那么,如何训练神经网络来做这件事呢?基本思路是生成大量函数,然后找出它们所满足的微分方程,再把这些微分方程以及我们知道能求解它们的函数作为训练数据提供给神经网络。

如何为神经网络编码一个数学表达式?我们本质上把数学当作自然语言来处理,将其转换成一串 token。而我们使用的神经网络是 Transformer,就像 LLM 中一样。

那么结果如何?这里有一个微分方程的例子,Version 14.3 无法求解,但——得益于我们新的神经网络方法——Version 15.0 可以:

而且,是的,这看起来有点像刻意安排的:一个非常复杂的方程,“恰好”有解。而且,是的,这是一个合理的批评。这也引出了一个问题:人们实际可能想要求解的微分方程的分布会是什么样的。从 Wolfram|Alpha 中,我们原则上其实拥有相当多的相关信息。而且我们长期以来一直在从积分表之类的资料中积累基准测试。那么神经网络在这些基准上表现如何?不太好。例如,在 1959 年经典的 Kamke 手册中的 638 个(一阶)微分方程中,我们的神经网络方法只能求解 6 个。而我们的“传统”算法方法则能求出 100% 的解。

但如果我们只是“合成地”生成要求解的方程呢?我们可以做与生成训练数据时相同的事情,按概率生成表达式树(本质上使用一个马尔可夫过程,其状态转移是具名函数的应用,比如 Sin 和 Log)——然后找出这些表达式所满足的方程。如果我们以这种方式生成一百万个方程,我们会发现,是的,神经网络能够为其中约 80% 找到正确的解。但是——关键就在这里——我们传统的算法方法也能找到几乎所有这些解。而最终,在我们的测试方程中,只有 0.003% 能够被神经网络成功求解,却无法被我们的传统方法求解。那么这是否意味着神经网络基本上没什么用?嗯,能够多解出几个方程总是好的(哪怕只多 0.003%)。但有两件事让神经网络更有用。第一,当它奏效时,它往往能比我们的传统算法方法快得多地给出答案。第二,在相当多的情况下,它给出的答案比传统算法方法所能找到的要简单得多。

下面是 DSolve 在 14.3 版本中对一个特定微分方程所做处理的一个例子:

如果我们应用 FullSimplify,那么几分钟后我们得到:

但现在在 15.0 版本中,得益于我们新的神经网络方法,我们得到的结果如下:

这是一个漂亮而优雅的结果。当然,它相当接近我们提供的训练数据中出现过的东西。但神经网络做了它最擅长的事:它实际上成功地为它所见过的那类微分方程的解建立了一个模型,并且能够利用这个模型进行一定程度的泛化。

这当然是一个不错的演示。而且很有可能,某个在实际场景中出现的微分方程,如今将能够以符号方式求解,而以前却做不到。这很难说。但对我们来说,已经很有意思的是,我们能够将一种神经网络方法植入 Wolfram Language 的一个核心数学函数中。而我们为此构建的流水线,未来将在任何有意义的地方加以应用。

(顺便说一句,你可能会想,新解和旧解实际上是否是同一个。两者都包含一个任意常数 。但如果我们取它们的差,FullSimplify 能够成功证明它恰好是 。换句话说,这个差是一个常数,而对于像这样的线性方程,它可以被吸收进 。所以,是的,这些解是相同的,尽管它们在代数上以不同的形式表述。)

偏微分方程走向曲线坐标

早在引入基础向量分析函数的时候,比如Div和Grad是在Version 9我们还引入了坐标图的概念——这样,举例来说,你就可以在极坐标下计算拉普拉斯算子:

在版本 15 中,我们现在将坐标图的支持扩展到了整个 PDE 建模系统。例如,下面是从 LaplacianPDETerm 计算得到的极坐标下的拉普拉斯算子:

我们可以把这个 PDE 项放入一个完整的 PDE 中,并在极坐标下求解它:

对于像标量场拉普拉斯算子这样的东西,这一切都相对简单,但事情很快就会变得更复杂。下面是极坐标下的一个流体流动 PDE 组件:

事实上,这对 CoordinateChartData 支持的任何曲线坐标系都适用。下面是球坐标下的情况:

下面是长球坐标下的情况:

在版本 15 中,现在还可以将曲线坐标用于数值 PDE。下面是一个在极坐标下建立并求解的特征值问题(注意,边界条件现在也采用极坐标):

PDE 解中的派生量

假设你正在求解一个固体力学问题,使用的是用 SolidMechanicsPDEComponent 之类的东西建立的 PDE。你直接计算得到的量是位移场——它给出固体中每个点在各个方向上的位移:

但通常你真正想要的是由此推导出的某个量。例如,这里计算的是与该位移场对应的应变场:

但这对应于每个点上复杂的二阶应变张量。而人们通常希望进一步简化求解结果,比如从中推导出每个点上的某个纯标量。在 Version 14.1 中,我们引入了 VonMisesStress,对 stress field 做了类似的事情。在 Version 15 中,我们现在引入了 EquivalentStrain,对 strain field 做同样的事情:

顺便说一下,这就是 EquivalentStrain 实际做的事情——这里以符号形式展示:

除了用于固体力学的 EquivalentStrain 之外,Version 15 还引入了用于流体力学的 FluidViscousStress 和 FluidViscosity,以及用于研究磁场的 MagneticFluxDensity(“B field”)和 MagneticFieldIntensity(“H field”)。

如何近似一个系统工程模型?

有幂级数。有插值函数。有基础神经网络。所有这些都可以被视为提供了比事物本身使用更快的近似。但假设你有一个系统工程模型,由 SystemModel 表示,也许来自 Wolfram System Modeler。你如何才能得到比该事物本身使用更快的近似?

版本 15 引入了函数 SystemModelSurrogateTrain,用于借助现代机器学习方法创建此类近似。其基本思路是选取系统模型行为空间的某一部分,然后进行一系列仿真,并对它们产生的“合成数据”进行拟合,最终得到对原始模型的高效连续时间神经网络近似。

举个简单的例子,考虑一个电动机的模型:

下图是该模型在某个特定参数值下计算出的某个变量的行为曲线:

SystemModelSurrogateTrain 让你能够对底层模型进行“代理”近似,从而在指定的参数范围内高效地捕捉模型中特定变量的行为:

如果我们进行与上面相同的计算,得到的结果本质上完全相同——但速度要快得多:

代理模型不再通过求解微分方程来得到结果,而只是对神经网络进行求值,我们可以从 SystemModelSurrogate 对象中提取该神经网络:

对于越来越复杂的工程系统,代理模型正变得越来越重要——它们对于让多种大规模系统优化变得切实可行至关重要,同时也使能够实时仿真的数字孪生成为可能。

面向控制系统的强化学习

近年来,强化学习在 AI 语境下被广泛讨论。但这一概念实际上起源于 1950 年代——当时名为“最优控制”——诞生于控制系统的研究与设计之中。在我们多年来于 Wolfram Language 中所支持的传统控制理论里,基本思路是为系统建立一个数学式的模型,然后对该模型的结构进行运算,从而推导出一个控制器,使其(尽可能)让系统达成既定目标。

而在强化学习中,人们并不依赖于拥有系统的数学式模型。相反,人们只是假设会通过反复“戳一戳”系统并观察其如何响应来了解它——然后迭代地得出一个能实现所需目标的控制器。实现这一点有多种策略;在 Version 15 中,我们引入了一种强大的策略,称为 Q learning。

Q learning 的基本思路是不断尝试学习“Q 函数”,该函数指定了当系统处于给定状态时,与某个给定(控制)动作相关联的“响应质量”——然后利用这个学到的 Q 函数从中推导出一个控制器。在 Version 15 中,我们引入了函数 LQRegulatorTrain 来进行基于线性二次调节器的 Q learning(是的,LQRegulatorTrain 中的“Q”与 Q learning 中的“Q”并不相同;它代表的是“quadratic”(二次),而非“quality”(质量))。

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 Language 中对天文学和天文数据的支持越来越深入。作为其中的一部分,在 Version 15 中我们引入了对 AVM 的导入——这是一种天文图像的元数据标准,用于指定给定图像来自天空中的哪个位置,以及它是如何被获取的。

近年来日益流行的一种格式是 Markdown(.md)。我们分别在 Version 14.2 和 Version 14.3 中首次引入了 Markdown 的导入和导出。在 Version 15 中,我们将 Markdown 支持扩展到包括链接、图像、表格等。在所有情况下,我们既能以关联、数据集等形式获取可计算数据,也能获得完整格式化的笔记本。

与近年才流行起来的 Markdown 相比,我们有 XML——自 2002 年以来我们就一直支持它。XML 往往是一种复杂但灵活的格式。在 Version 15 中,我们新增了将 XML 直接导入为可计算 Tree 对象的能力。

说到旧格式,还有 notebook,没错,我们最初是在 1987 年为 Mathematica 1.0 发明它的。此后四十年间,我们一直在持续开发和打磨 notebook 这一概念——甚至在 Version 15 中还在加入各种新功能。相当令人惊叹的是,我们一直保持了兼容性,因此 Version 15.0 仍然可以打开 Version 1.0 的 notebook。但在我们最初发明 notebook 近四分之一个世纪之后,人们终于开始模仿它们了(为什么花了这么久?)——或者更准确地说,模仿了它们的一些表面特性。但最终结果是,世界上出现了其他一些 notebook 格式——在 Version 15 中,我们加入了将其中两种——.vsnb 和 .ipynp——导入我们 notebook 的能力。(而且,没错,从我们的 notebook 导出没什么意义;“山寨”notebook 格式里缺失的东西太多了。)

通过 Web Socket 实现实时连接

如何获取从服务器流式传输过来的数据——或许还能对它作出响应?基本机制——在更广泛的世界中已经存在了几十年——就是使用 socket。十年前我们引入了对 TCP socket 的支持;后来我们又加入了对 ZMQ socket 的支持。而现在在 Version 15.0 中,我们正在加入对 web socket 的支持。

Web socket 被用于例如流式数据服务,以及获取流式结果,比如来自云端 LLM 的结果。Web socket 的一个重要特性是它们是双向的:即使在数据流式传输给你的同时,你也可以将数据发送回服务器。Web socket 的另一个重要特性是它们的初始连接是通过 http(或 https)建立的,因此它们可以继承与 http 头相关的各种能力(比如能够传入 API key 等)。

这里有一个非常简单的示例,基于标准的 echo.websocket.org 测试套接字。以下是我们连接该套接字的方式:

现在我们可以从套接字读取数据——获取该特定服务器发送的初始消息:

借助 SocketWriteMessage 和 SocketListen,我们就可以来回发送消息。我们的整个套接字系统以异步方式工作,适当地与 Dynamic 交互,以便在更新到来时立即获取。

作为一个更复杂的示例,以下是我们如何打开一个到当前版本 OpenAI LLM 系统的 Web 套接字连接:

而且,是的,在我们的 AI Assistant 中——以及 Chat Notebooks 等中——我们现在将使用 Web 套接字来获取实时流式结果。

在 Notebook 中使用 Python 及更多内容的更丰富用户体验

早在 2018 年(在 Version 11.3 中),我们引入了外部代码单元格,使得可以直接在 notebook 中包含——并运行——外部代码。在单元格开头输入 > 即可获得可能的代码类型菜单:

Menu of external code types

选择 Python,你就会得到一个 Python 单元格。但在 Version 15 中,这里有了新东西:

Python cell

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

External code cell session menu

这有什么意义?一个 notebook 可以连接到多个不同的、相互独立的 Python 会话,而会话菜单让你可以管理这些会话,并指定某个特定的单元格应该使用哪一个。

为什么你需要多个 Python 会话?嗯,如果 Python 是一个像 Wolfram Language 那样协调统一的系统,你大概就不需要了。但它并不是。相反,大部分功能都来自一个由各自独立开发的库组成的动物园,而不同的 Python 代码片段需要不同且互不兼容的库集合,这是非常常见的。而且,没错,如果你只用 Wolfram Language,就能避开所有这些混乱。但如果你已经有 Python 代码(而且,比如说,LLM 无法方便地把它直接翻译成 Wolfram Language),那么我们的外部求值机制就是为了让集成 Python 代码尽可能无痛而设计的。

其中一个重要部分,就是我们在 Version 14.0 中引入(并在 Version 14.3 中大幅增强)的封装机制,它让人能够拥有独立的、完全封装的 Python 会话,这些会话携带并管理自己的依赖项。现在在 Version 15.0 中——借助会话菜单——我们为这些被封装的会话引入了一个便捷的界面。

假设有一个带有某些依赖项、已经设置好的会话。现在,借助我们新的会话菜单系统,你可以选择那个会话作为你想用于某个特定外部代码单元格的会话。例如,这会设置一个带有特定依赖项的外部会话:

在外部代码单元格的会话菜单中,你现在可以要求使用一个正在运行的 Python 会话——然后选择这个会话:

Use a running Python session

会话可以被显式命名。但默认情况下,每个新创建的会话都会获得一个唯一的 UUID。这使得一件非常棒的事情成为可能:它让带有依赖项的外部代码单元格变得可移植。

下面是一个使用我们刚设置的会话的外部代码单元格:

Unique session UUID

但现在的关键在于,这个单元格实际上是完全自包含的。你可以把它带到另一台电脑上,发送给别人等等,它会随身携带自己的依赖项,因此其中的 Python 代码可以直接运行。

除了使用 StartExternalSession 以编程方式定义 Python 会话之外,你也可以交互式地完成,只需在会话菜单中直接选择使用一个新的 Python 会话。

Version 15 中 Python 的会话菜单还有另外几个实用的工具项。有一个格式化代码,它会应用自动代码格式化规则:

Automatic code formatting rules

还有一个移除未使用的导入,它会确定一段代码的实际依赖项,并移除任何不必要的导入。

优化与 GPU 化持续推进

Wolfram Language中充满了算法上复杂的函数,我们一直在让这些函数变得越来越高效。我们效率提升的一部分来自使用新算法(包括许多我们自己发明的算法);另一部分来自能够支持额外的硬件加速,尤其是 GPU 加速。

在 Version 15 中,整个系统有大量性能增强。而且——对于拥有 NVIDIA GPU 的用户——核心线性代数和核心图论功能都获得了额外的(实验性)加速。通过 Method→"HybridCPUGPU",诸如 LinearSolve 这样的函数会自动同时使用 CPU 和 GPU 硬件,在 10000×10000 矩阵等情况下实现显著的性能提升。

高效利用 GPU 的问题之一,是能够让 GPU 所操作的数据常驻于 GPU 内存中。在 Version 14.2 中,我们引入了 GPUArray,作为一种专门将数据存储在 GPU 内存中的方式。随着每一个后续版本的推出,我们让更多函数能够直接操作 GPUArray 对象。

在 Version 15 中,我们新增了一组基于 GPU 的数组重排函数。下面是一个 2000×2000 的 GPU 数组:

假设你的计算机拥有合适的 GPU,这就能非常高效地生成一个 GPU 数组,而说来复杂,它的形状与原始数组截然不同:

使用 GPU 仍然是一件相当繁琐的事情,不同的 GPU 和不同的计算机系统具有不同的能力。我们正在系统性地为越来越多的 GPU 配置实现越来越多的功能,每一种情况下都要搭建高度优化的 GPU kernel。

事实上,即便在 GPU 之外,不同硬件所对应的不同支持方式也存在复杂的问题。例如,在 Version 15 中,我们为稀疏数组的 LinearSolve 添加了 Method"MUMPS",大幅加速了诸如面向 ARM、Apple 等处理器的大规模数值 PDE 求解等任务。

将 CUDA Kernel 作为外部函数

如果你想编写自己的 GPU 代码,并将其集成到 Wolfram Language 中,该怎么办?在 Version 15.0 中,有了一种新机制,可以创建执行 CUDA kernel 的 ExternalFunction 对象。当然,这只在支持 CUDA 并安装了合适 GPU 的系统上有效。下面是一个示例,我们给出纯 CUDA C++ 代码,并得到一个执行该代码的 ExternalFunction:

现在假设你设置了一个 GPUArray——它将驻留在你的 GPU 上:

这会在该数组上运行这个 CUDA 函数:

下面是结果(没错,这个特定的 CUDA kernel 做的事情相当简单,就是给每个数组元素加上 1000):

CUDA 代码往往非常底层,很快就会变得相当复杂繁琐。但在 15.0 版本中,还有另一种进行 GPU 编程的方式:使用 Wolfram Language 配合 Wolfram Compiler,通过 LibraryFunction 调用 CUDA 内建函数。举例来说,下面是同一个 CUDA kernel 的另一个版本,但这次是用 Wolfram Language 配合 Wolfram Compiler 实现的:

现在——就像上面我们使用原始 CUDA 代码那样——我们可以运行它并得到结果:

Wolfram Compute Services 获得 GPU 支持

去年年底,我们推出了 Wolfram Compute Services——以实现无缝的大规模远程计算。在过去几个月中,我们为 Wolfram Compute Services 添加了多项功能,这些功能现已在 15 版本中完全集成。特别是,Wolfram Compute Services 现在不仅可以使用 CPU,还可以使用高端 GPU。

一旦你的 Wolfram Language 代码可以正常运行,你就可以直接使用 RemoteBatchSubmit 将其发送到 Wolfram Compute Services。这里我们有一些神经网络训练任务,正在提交到 NVIDIA L40S GPU 上运行:

大约 40 分钟后,我们收到一封邮件,说结果已经准备好了

Wolfram Compute Services batch job success

然后我们就可以取回它们:

Wolfram Compute Services 的另一个新功能是 RemoteGeoZone 选项,它指定远程计算应在世界上的哪个位置进行——目前可选值为 "UnitedStates" 和 "EuropeanUnion"。

Wolfram Compute Services 被设置为使用公有云基础设施。但我们正在开发的高性能计算套件(High-Performance Computing Kit),将允许私有集群及其他机构或组织的基础设施被设置为与 RemoteBatchSubmit 协同工作。

在 LLM Functions 中使用 Wolfram Foundation Tool

我们最近发布了面向 LLM 系统的 Wolfram Foundation Tool,它让 LLM 系统能够访问 Wolfram Language、Wolfram Knowledgebase、Wolfram|Alpha 等的能力。实现这一点的核心技术是我们所称的计算增强生成(computation-augmented generation,CAG),它是检索增强生成(RAG)的一种无限类比,通过实时计算来补充 LLM 的能力。我们的 Notebook Assistant(现为 AI Assistant)已经在使用某个版本的 CAG 和我们的 Foundation Tool 系统。但在 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 Assistant 的 CAG 系统的访问,该系统特别适合帮助人们编写 Wolfram Language 代码。例如,这里我们以编程方式生成关于如何在 Wolfram Language 中做某件事的信息:

还有更多……

除了我们已经讨论过的所有内容之外,版本 15 中还有各种各样的其他新功能。有些是对现有函数的扩展和优化;有些则是更多全新的函数。

例如,有 PopovDecomposition 和 OrderedSchurDecomposition:两种额外的矩阵分解。还有 PfaffianDet,用于计算反对称矩阵的 Pfaffian:

为完善我们庞大的积分变换集合,Version 15 引入了 DiscreteHilbertTransform(此处针对的实际上相当于一个 delta 函数):

在图形方面,我们确保 PlanarFaceList 会首先列出平面图的“外部面”

如果你不想要它,还有一个选项:

在图形布局方面,还有几项我们尚未提及的更新,包括新的内置边形状函数——其中这个函数能呈现出相当华丽的视觉效果:

如何表示平滑的 3D 曲面?近 20 年来,我们一直用 BSplineSurface 来实现这一点。在 Version 15 中——作为我们完善 CAD 风格几何工具的一部分——我们还引入了 BezierSurface:

在一个相当不同的领域,Version 15 延续了我们在化学能力方面的开发,特别是引入了 MoleculeSubstructure,用于表示分子中的子结构。它可以找出分子中符合某种特定分子模式的情况:

然后它会绘制出这些子结构:

Version 15 中同样新增的还有 MoleculeFingerprint 以及用于立体化学的 MoleculeValue 属性。

版本 15 中还有更多内容可以讨论。但在这个“永远瞄准月球”的时刻,我想以版本 15 中一个与月球相关的新功能作为结尾。十多年前,我们引入了 NightHemisphere(以及 DayHemisphere)来在地图上显示白天和黑夜:

而现在,我们已经将这一能力扩展到了“地球之外”:

当然,也扩展到了月球。要查看从地球上看月球是什么样子,我们需要使用正交投影:

但这是怎么回事?昼夜分界线在哪里?然后我意识到:今天是新月!

一切都在按预期运行。我们正在仔细地填补又一个细节。而且,没错,再过一周就是上弦月了:

来源:Hacker News 热门(buzzing.cc 中文翻译) · writings.stephenwolfram.com