1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
/*++
Copyright (c) 2025 Microsoft Corporation
Module Name:
nla_transcendentals.h
Author:
Nikolaj Bjorner (nbjorner)
Description:
End-game consistency check for transcendental function applications
(sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, asinh, acosh,
atanh, exp, atan2, log) registered by theory_lra.
nlsat/nra_solver reason about polynomial arithmetic over algebraic
numbers and have no representation for transcendental functions, so
the nla_core search never gets any feedback connecting an "input"
variable to an "output" variable that is meant to track a
transcendental function applied to it. Currently these operators are
treated as underspecified/uninterpreted by the arith theory solver
(see arith_internalize.cpp) or purified away (purify_arith_tactic.cpp)
rather than consumed here; this module is the nla_core-side consumer
that such front-end wiring would eventually feed.
This module implements a cheap, best-effort delta-satisfiability
check in the sense of Gallego-Hernandez, Lipparini, Mansutti, "MCSAT
Modulo Transcendental Arithmetics" (arXiv:2606.00697): given the
current numeric assignment to the input/output variables of a
registered application, it evaluates the transcendental function in
floating point, inflates the result by a conservative error bound to
account for floating point round-off, and checks whether the output
value is within a delta-tolerance of the (inflated) function value.
- A value within floating-point round-off is treated as consistent.
A value outside that bound is recorded for nlsat refinement, even if
it is still within the configured delta tolerance; delta tolerance
alone does not certify a model.
This check is unsound-incomplete "on purpose": it never returns
l_false, and accepting an assignment as consistent within delta does
not certify that an exact model exists. It is meant to be a fast
filter that runs before the (nonexistent, for transcendentals) exact
decision procedure.
Note on the split of responsibilities with nlsat: this module used
to also derive its own reactive Taylor-sandwich/box-refinement
polynomial lemmas whenever the delta-check above failed (bracketing
arg in a small box, sampling op in floating point over that box, and
asserting the resulting enclosure as a conflict clause). That
machinery has been removed: it was floating point-driven and could
nudge the LP assignment indefinitely without ever producing a
certificate, and it duplicated exact, terminating refinement that
nlsat now performs itself (see nlsat/nlsat_transcendentals.h/.cpp,
in particular its Taylor/Maclaurin brackets for exp/log/atan/sin/cos).
Instead, a value outside the round-off bound is only recorded
(has_observed_failure); core::check_transcendentals_and_finish uses
that, together with should_run_bounded_nlsat()'s backoff scheduler,
to hand the problem to nra_solver/nlsat, whose own
nlsat::transcendentals engine refines the polynomial constraints
relating arg and val internally. This module therefore only ever
contributes *linear* facts to the LP relaxation (range axioms,
global linear-majorant/monotonicity/sign lemmas below), while every
polynomial constraint on a transcendental application is owned by
nlsat.
Two additional, stronger propagation mechanisms are layered on top of
the per-application delta-check above:
- Range axioms: mathematically exact (or, where pi is involved,
outward-rounded so as to remain exact) global bounds on the output
variable are asserted once, permanently, as soon as an application
is registered (e.g. -1 <= sin(t), cos(t) <= 1; -1 < tanh(t) < 1;
cosh(t) >= 1; 0 <= acos(t) <= pi; -pi/2 <= asin(t) <= pi/2; -pi/2 <
atan(t) < pi/2; acosh(t) >= 0). These are true unconditionally, so
unlike the lemmas below they are added directly as column bounds
(lar_solver::add_var_bound) rather than as conflict clauses, and
immediately prune the search space rather than waiting for an
inconsistent sample to be found first.
- Cross-application identity axioms (sin(t)^2+cos(t)^2=1,
cosh(t)^2-sinh(t)^2=1, cosh(t)^2*(1-tanh(t)^2)=1, for two
applications of complementary ops sharing the *same argument
variable*) are no longer tracked or asserted here: nra_solver
forwards every registered application straight to nlsat (see
register_transcendentals_with_nlsat), and
nlsat::transcendentals::add (nlsat/nlsat_transcendentals.cpp) already
discovers matching pairs and asserts the identical permanent
polynomial equality itself the moment the second application of a
pair reaches it - duplicating that bookkeeping on this side only
grew this module without adding any coverage nlsat did not already
provide.
- Global linear-majorant lemmas: sin, tanh, and atan all satisfy
op(0) = 0 and are 1-Lipschitz, so none of them can cross the line
y = x: op(t) <= t for every t >= 0, and op(t) >= t for every t <= 0.
Unlike range axioms, this relates val to arg (not a single column
to a constant) and unlike a permanent bound it need not always
hold trivially against the current box, so it is asserted as an
actual (exact, tolerance-free) two-literal lemma via lemma_builder
- e.g. (arg < 0 \/ val <= arg) - rather than as a column bound.
check_app tries this check (check_linear_majorant) *before* the
floating point delta-check/box-refinement lemma above, since it is
exact and, when it applies, strictly stronger evidence than a
locally-sampled enclosure.
- Sign-on-range lemmas: once pi is registered (see add_pi/pi_var), sin
and cos each satisfy an exact sign fact on a pi-relative range that
the floating point delta-check above cannot express as a *global*,
tolerance-free fact: sin(t) > 0 for 0 < t < pi and sin(t) < 0 for
-pi < t < 0; cos(t) > 0 for -pi/2 < t < pi/2 and cos(t) < 0 for pi/2
< t < 3pi/2 (the latter expressed via the doubled argument 2t vs
+/-pi, so no separate pi/2 variable is needed). Like the linear
majorant above this is asserted as an exact two-or-three-literal
lemma via lemma_builder - e.g. (arg <= 0 \/ arg >= pi \/ val > 0) -
referencing pi_var() symbolically rather than only its numeric
bound, and is tried with the same priority as the linear majorant,
before the floating point delta-check.
--*/
namespace nla