SymPy 假设查询系统:深入理解 ask() 函数、Q 谓词与 SAT 推理管线 📅 发布时间:2026/9/14 18:17:40 👁 浏览次数: SymPy 假设查询系统:深入理解 ask() 函数、Q 谓词与 SAT 推理管线【免费下载链接】sympyA computer algebra system written in pure Python项目地址: https://gitcode.com/GitHub_Trending/sy/sympy在符号计算中,判断一个表达式是整数、正数还是素数这类属性问题,与计算导数、积分属于不同性质的任务。本文围绕 SymPy 官方文档 ask.rst 所描述的假设查询(assumption query)功能展开,结合 sympy/assumptions/ask.py 的源码实现,完整讲解ask()的三种调用形式、三值返回语义、Q对象支持的全部谓词键,以及其背后单位子句快速路径 → 直接解析 → SAT 求解 → 线性实算术的多级推理管线。读完本文,你将能够熟练使用ask()进行属性查询与假设推理,理解Q谓词注册机制并扩展自定义谓词,并且能从源码层面解释 SymPy 在哪些情况下返回None、何时抛出ValueError。一、ask() 解决什么问题ask() 是 SymPy 假设系统(sympy.assumptions)对外暴露的核心查询入口,其模块级文档为 Module for querying SymPy objects about assumptions。它的作用是把一个**命题(proposition)在给定假设(assumptions)**下求值:若真值可确定,返回 Python 内置布尔值True或False;若真值无法确定,返回None。文档中特别强调,ask()与refine()是两类不同的工具:对命题而言,refine()在无法确定真值时不会返回None,而是把命题化简为符号化的布尔表达式(symbolic Boolean)。这一区别决定了两者在管线中定位不同——ask()面向是非题,refine()面向表达式化简。官方文档(ask.rst 通过automodule直接呈现ask()的 docstring)给出的标准示例如下,这些示例可以原样复制到交互式环境中验证:from sympy import ask, Q, pi from sympy.abc import x, y ask(Q.rational(pi)) # False:pi 是超越数,必非有理数 ask(Q.even(x*y), Q.even(x) Q.integer(y)) # True:偶数乘整数必为偶数 ask(Q.prime(4*x), Q.integer(x)) # False:4x 恒为合数(在整数域) print(ask(Q.odd(3*x))) # None:不知道 x 的性质则无法判定最后一行体现了三值语义:在不知道x的情况下,3*x的奇偶性不可判定,ask()返回None而不是猜测。异常语义ask()的 Raises 部分声明了两类异常,源码中对应如下检查逻辑:异常触发条件源码位置TypeErrorproposition或assumptions不是合法的布尔逻辑表达式(例如传入裸Predicate或 kind 非BooleanKind的对象)ask.py#L505-L509ValueError假设之间相互矛盾(unsatisfiable)ask.py#L532-L534矛盾假设的官方示例:ask(Q.integer(x), Q.even(x) Q.odd(x)) # ValueError: inconsistent assumptions Q.even(x) Q.odd(x)从源码看,ask()在把局部假设转成 CNF 后,先与已知事实基(known facts base)合并编码,再用satisfiable()(sympy/logic/inference.py)做一次可满足性检查;若检查返回False则立即抛出ValueError,这保证了后续推理不会建立在不一致的前提之上。二、ask() 的三种调用形式docstring 的 Syntax 部分列出了三个重载形式,签名对应源码 ask.py#L406 的def ask(proposition, assumptionsTrue, contextglobal_assumptions):ask(proposition)—— 在全局假设上下文(global_assumptions)下评估命题;ask(proposition, assumptions)—— 在全局上下文之外,额外叠加局部假设assumptions;context参数—— 可显式传入自定义的AssumptionsContext替代全局默认,默认值就是sympy.assumptions.global_assumptions。参数细节:proposition(Boolean):待评估的命题。若它不是AppliedPredicate实例(例如直接传入关系式x 0),源码会自动用Q.is_true包裹它,即key, args Q.is_true, (proposition,)(ask.py#L515-L518);assumptions(Boolean,可选):局部假设,默认True(即无额外假设)。Q是谓词的访问入口。所有已知谓词都作为Q实例的属性存在,例如Q.even返回判断参数是否为偶数的谓词对象;把谓词作用于表达式得到AppliedPredicate(sympy/assumptions/assume.py#L78-L106),它只包裹参数而不立即求值,真正的求值交给ask()或谓词的eval()。三、Q 对象:ask() 支持的全部谓词键Q的定义是 ask.py#L312 的Q AssumptionKeys(),其中AssumptionKeys类(ask.py#L20-L311)集中声明了ask()支持的全部键。类的 docstring 明确指出:应通过sympy.Q这个实例访问它。类内注释有两条重要约束:不得添加谓词键之外的方法或属性;原因是 SAT 求解器会枚举Q的属性来构建事实系统(fact system),非谓词属性会破坏这一机制。每个键都带memoize_property装饰,源码注释解释了原因:必须保证每个Predicate对象全局唯一,因为假设处理器(handlers)注册在这些对象上。各键按所在 handler 模块可归为五组:分组谓词键集合/数系类(handlers.sets)real,extended_real,imaginary,complex,integer,noninteger,rational,irrational,algebraic,transcendental,hermitian,antihermitian量纲/无穷类(handlers.calculus)finite,infinite,positive_infinite,negative_infinite序类(handlers.order)positive,negative,zero,nonzero,nonpositive,nonnegative,extended_positive,extended_negative,extended_nonzero,extended_nonpositive,extended_nonnegative数论类(handlers.ntheory)even,odd,prime,composite通用类(handlers.common)commutative,is_true矩阵类(handlers.matrices与predicates.matrices)symmetric,invertible,orthogonal,unitary,positive_definite,upper_triangular,lower_triangular,diagonal,fullrank,square,integer_elements,real_elements,complex_elements,singular,normal,triangular,unit_triangular二元关系类(relation.equality)eq,ne,gt,ge,lt,le二元关系谓词(Q.eq(a, b)、Q.lt(a, b)等)接受两个参数,是ask()中唯一支持双参数比较的形式。已知事实基:ask_generated.py 里的 CNF 子句谓词键之间的隐含蕴含关系被固化在 sympy/assumptions/ask_generated.py 中。该文件头部注释写明:Do NOT manually edit this file. Instead, run ./bin/ask_update.py,即它是自动生成的事实库,ask()通过get_all_known_facts()、get_known_facts_dict()(ask.py#L664-L665)引入它们。文件提供三个带cacheit缓存的函数:get_all_known_facts()(全部一阶谓词事实)、get_all_known_matrix_facts()(L123-L153)与get_all_known_number_facts()(L155 起)。事实以CNF 子句的 frozenset 集合表示,Literal(pred, True)表示谓词成立、False表示被否定。举几条真实子句说明其逻辑含义:frozenset((Literal(Q.composite, True), Literal(Q.prime, True)))—— 复合数蕴含素数谓词参与推理(即合数一定不是素数,由子句极性组合表达);frozenset((Literal(Q.even, True), Literal(Q.odd, True)))—— 偶与奇不可同时成立,这是ask(Q.integer(x), Q.even(x) Q.odd(x))抛出ValueError的事实依据;frozenset((Literal(Q.rational, True), Literal(Q.real, False)))—— 有理数必为实数;frozenset((Literal(Q.even, False), Literal(Q.integer, True), Literal(Q.odd, False)))—— 非偶数蕴含整数且不奇(针对整数语境下的互补关系)。这些事实使ask()不需要对每个查询重推数学定义,只需做一次逻辑蕴含检查。四、假设上下文:global_assumptions 与 assumingask()的第三个参数context默认是全局假设上下文global_assumptions。它是一个AssumptionsContext实例(sympy/assumptions/assume.py#L12-L75),本质是对 Pythonset的薄封装,初始为空。模块文档给出基本用法:from sympy import ask, Q from sympy.assumptions import global_assumptions from sympy.abc import x global_assumptions.add(Q.real(x)) ask(Q.real(x)) # True global_assumptions.remove(Q.real(x)) ask(Q.real(x)) # None模块假设文档(doc/src/modules/assumptions/index.rst)进一步推荐用assuming上下文管理器把假设限定在代码块内,避免污染全局状态:from sympy import * x Symbol(x) y Symbol(y) facts Q.positive(x), Q.positive(y) with assuming(*facts): print(ask(Q.positive(2*x y))) # Trueassuming、Q、ask、refine、global_assumptions等符号均在 sympy/assumptions/init.py 中统一导出,因此from sympy import ask, Q即可使用。五、ask() 的源码级推理管线ask()的函数体(ask.py#L496-L556)是一条逐级升级成本的管线,前三级廉价,后两级昂贵。理解它就能回答为什么这个查询快/为什么返回 None。第 0 步:规范化(_normalize_expr)进入推理前,proposition和assumptions都经过sympify与_normalize_expr()(ask.py#L401-L404)。规范化做两件事:_normalize_relations():把旧式关系对象Gt/Ge/Lt/Le/Eq/Ne统一改写为二元谓词Q.lt(b, a)、Q.le(b, a)、Q.eq(a, b)、Q.ne(a, b);_normalize_applied_predicates():把Q.gt(a, b)改写为Q.lt(b, a)、Q.ge(a, b)改写为Q.le(b, a),即系统内部只保留lt/le/eq/ne四种规范形式。第 1 步:CNF 化与相关事实提取假设被CNF.from_prop()转成合取范式(sympy/assumptions/cnf.py),再由_extract_all_facts()(ask.py#L314-L363)裁剪:只保留与命题参数直接相关的一元谓词子句——某子句中若出现与查询无关的表达式,整条子句被丢弃。这一步控制后续 SAT 问题的规模。第 2 步:一致性检查把已知事实 CNF与局部事实 CNF合并进EncodedCNF后调用satisfiable(),若不满足则抛出ValueError(finconsistent assumptions {assumptions})(ask.py#L533-L534)。第 3 步:单位子句快速路径(_ask_single_fact)_ask_single_fact()(ask.py#L559-L639)只扫描长度为 1 的单位子句,配合get_known_facts_dict()中的 req(蕴含)/rej(互斥)字典做常数次查表:若假设中某单位事实否定了命题的前置条件,直接返回False;若某事实直接蕴含命题,返回True;若某事实蕴含命题的否定,返回False;否则返回None交由下一级。其 docstring 示例展示了三种情形,例如Q.zero遇假设~Q.zero得False,Q.even遇假设Q.zero得True,Q.even遇假设Q.odd得False。第 4 步:直接解析(_eval_ask)仍无结论时调用key(*args)._eval_ask(assumptions)(ask.py#L542),它委托给谓词的多重分派处理器(Predicate.eval(),见 assume.py#L338-L349)。这一级只做直接解析,不做逻辑推理,例如对具体数值(素性检测、整数判定)可以立即出结果;抛出NotImplementedError时安静地返回None。测试文件 sympy/assumptions/tests/test_query.py 大量使用不触发 SAT 的_ask_recursive()(ask.py#L642-L661,只含前两级)验证具体数值的判定结果,例如11是素数且奇数、12是偶数且合数,而浮点1.0的Q.integer、Q.even、Q.prime均返回None。第 5 步:satask(SAT 求解器)再不行则进入satask()(sympy/assumptions/satask.py#L18-L77),这是最重的通用路径:get_all_relevant_facts()(satask.py#L305-L401)从命题与假设中递归提取相关表达式,再通过class_fact_registry()查询各类(如Abs、sqrt)注册的经典事实,迭代到不动点。docstring 例子:查询Q.zero(Abs(x))时,因为Abs注册了非负事实,求解器会自动获得Q.zero(Abs(x)) | Q.positive(Abs(x))这类子句;把已知事实按表达式逐个实例化并编码为EncodedCNF(对矩阵表达式载入矩阵事实集,对数值表达式载入数值事实集);check_satisfiability()(satask.py#L80-L127)使用内部 SAT 求解器SATSolver(Ipasir 接口,sympy/logic/algorithms/dpll2.py)。其核心技巧是引入一个 selector 变量,分别守卫命题成立与命题不成立两侧的子句(_encode_with_selector,satask.py#L130-L147),然后通过两次求解判断命题可为真与可为假,两者皆可为真时返回None,只可为其一时返回True/False,皆不可时抛出ValueError。satask()还有两个调优参数:iterations(相关事实递归提取的轮数,默认无穷直到不动点)与early_return(仅依据传播事实作答、默认False)。第 6 步:lra_satask(线性实算术回退)管线最后一级是lra_satask()(sympy/assumptions/lra_satask.py#L13-L29),它在 SAT 之上结合线性实算术(LRA)理论求解器处理不等式:它维护白名单WHITE_LIST(lra_satask.py#L34-L37),只允许Q.positive、Q.negative、Q.zero、Q.nonzero、扩展正负谓词与 LRA 允许的比较谓词参与;注释明确警告像Q.prime这样的谓词不能交给 LRA 处理(否则(x0) (x1) Q.prime(x)这类不可满足的公式会被误判为可满足);遇到白名单之外的谓词、MatrixKind表达式或nan时抛出UnhandledInput(sympy/logic/algorithms/lra_theory.py),ask()捕获该异常并返回None(ask.py#L551-L554);_preprocess()(lra_satask.py#L90)会把不等式拆解为严格不等式的析取(如x ! 3→x 3 | x 3),再把两侧分别交给satisfiable(..., use_lra_theoryTrue)判定。这也解释了 docstring Notes 中的历史限制说明:早期版本中假设里放关系式(如x 0)得不到有意义结果,当前源码已用 LRA 分支补齐了一部分线性不等式推理,但适用范围仍受白名单约束。六、自定义谓词:Predicate 与处理器注册Q的键并非封闭集合。Predicate基类(assume.py#L222-L349)支持通过子类化 多重分派扩展谓词,其 docstring 给出了性爱素数(sexy prime)的完整示例,可直接复制:from sympy import Predicate, Integer, Q, ask class SexyPrimePredicate(Predicate): name sexyprime Q.sexyprime SexyPrimePredicate() Q.sexyprime.register(Integer, Integer) def _(int1, int2, assumptions): args sorted([int1, int2]) if not all(ask(Q.prime(a), assumptions) for a in args): return False return args[1] - args[0] 6 ask(Q.sexyprime(5, 11)) # True要点:谓词求值走多重分派:Predicate.register(*types)(assume.py#L314-L321)把处理器注册到按参数类型的分派表上,eval()捕获NotImplementedError后返回None,使未注册类型安静落空到后续推理级;直接用Predicate(P)构造得到UndefinedPredicate(assume.py#L279-L291 的示例):可以参与构造布尔表达式,但调用register会抛出TypeError,适合搭建暂不求值的命题。七、性能取向与实践建议模块级文档(doc/src/modules/assumptions/index.rst 的 Performance improvements 一节)指出:涉及符号系数的查询会走逻辑推理,改进satisfiable会带来显著提速;同时当前系统不跨查询复用推理结果,并提及真值维护系统(truth maintenance system)作为可探索方向。结合源码结构可以给出实践建议:先具体后符号:对具体数值,第 3、4 级(单位子句 直接解析)通常就能出结果,test_query.py中的_ask_recursive用例即模拟了这条快路径;控制局部假设的规模:_extract_all_facts会把与命题无关的子句剔除,但无关假设仍会增加 CNF 规模,保持ask(proposition, assumptions)中假设的紧凑性;区分 None 与异常:None表示在给定假设下不可判定,ValueError表示假设自相矛盾,两者在自动化流程中应分别处理;不等式查询走 LRA:涉及Q.lt/Q.le等比较谓词时确认表达式落在WHITE_LIST允许范围内,否则lra_satask会以UnhandledInput放弃并整体返回None。八、小结ask()看似一个简单的三值查询函数,实则是 SymPy 假设系统的查询门面:它把Q谓词键(sympy/assumptions/ask.py#L20-L312)、自动生成的事实库(sympy/assumptions/ask_generated.py)、CNF/SAT 基础设施(sympy/assumptions/cnf.py、sympy/assumptions/satask.py)和 LRA 理论求解(sympy/assumptions/lra_satask.py)串成一条可预测、可解释的推理管线。对使用者而言,掌握ask(proposition, assumptions)的三值语义与assuming上下文即可覆盖绝大多数属性查询;对深入者而言,_ask_single_fact→_eval_ask→satask→lra_satask的分级结构,以及Predicate.register的处理器机制,正是理解 SymPy 如何在符号域中做数学直觉判定的关键入口。【免费下载链接】sympyA computer algebra system written in pure Python项目地址: https://gitcode.com/GitHub_Trending/sy/sympy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考