|
| 1 | +# Certification by abstract interpretation over polynomial degrees, in the style of tracer-type |
| 2 | +# sparsity detection (Gowda et al., "Sparsity Programming", NeurIPS 2019 program-transformations |
| 3 | +# workshop): the tracked arguments are seeded with degree-1 tracer numbers and the program is |
| 4 | +# executed on them, propagating a sound upper bound on the real-arithmetic polynomial degree. |
| 5 | +# Non-polynomial operations on non-constant values poison the result, and every value-inspecting |
| 6 | +# predicate on a tracer (comparisons, `isnan`, rounding, conversion) throws, so any computation |
| 7 | +# whose control flow or value flow would need the actual numbers aborts the trace instead of |
| 8 | +# producing an unsound certificate. `hasbranching` is additionally required to prove the traced |
| 9 | +# path is the only path. |
| 10 | + |
| 11 | +# Degrees saturate at `_NOTPOLY` ("not a polynomial of any bounded degree"). |
| 12 | +const _NOTPOLY = typemax(Int) ÷ 2 |
| 13 | + |
| 14 | +struct PolyDegree <: Real |
| 15 | + d::Int |
| 16 | +end |
| 17 | + |
| 18 | +_satadd(a::Int, b::Int) = a >= _NOTPOLY || b >= _NOTPOLY ? _NOTPOLY : a + b |
| 19 | +_satmul(a::Int, b::Int) = a >= _NOTPOLY || b >= _NOTPOLY ? _NOTPOLY : min(a * b, _NOTPOLY) |
| 20 | + |
| 21 | +Base.promote_rule(::Type{PolyDegree}, ::Type{<:Real}) = PolyDegree |
| 22 | +Base.convert(::Type{PolyDegree}, x::PolyDegree) = x |
| 23 | +Base.convert(::Type{PolyDegree}, x::Real) = PolyDegree(0) |
| 24 | +PolyDegree(x::PolyDegree) = x |
| 25 | +# Disambiguators against Base's cross-family `Number` constructors: any value converted into the |
| 26 | +# tracer domain is by definition not a seeded variable, hence a constant (degree 0). |
| 27 | +PolyDegree(::Base.TwicePrecision) = PolyDegree(0) |
| 28 | +PolyDegree(::Complex) = PolyDegree(0) |
| 29 | +PolyDegree(::AbstractChar) = PolyDegree(0) |
| 30 | +Base.zero(::Type{PolyDegree}) = PolyDegree(0) |
| 31 | +Base.zero(::PolyDegree) = PolyDegree(0) |
| 32 | +Base.one(::Type{PolyDegree}) = PolyDegree(0) |
| 33 | +Base.one(::PolyDegree) = PolyDegree(0) |
| 34 | +Base.oneunit(::Type{PolyDegree}) = PolyDegree(0) |
| 35 | +Base.float(x::PolyDegree) = x |
| 36 | +Base.widen(::Type{PolyDegree}) = PolyDegree |
| 37 | + |
| 38 | +Base.:+(a::PolyDegree, b::PolyDegree) = PolyDegree(max(a.d, b.d)) |
| 39 | +Base.:-(a::PolyDegree, b::PolyDegree) = PolyDegree(max(a.d, b.d)) |
| 40 | +Base.:-(a::PolyDegree) = a |
| 41 | +Base.:+(a::PolyDegree) = a |
| 42 | +Base.:*(a::PolyDegree, b::PolyDegree) = PolyDegree(_satadd(a.d, b.d)) |
| 43 | +Base.:/(a::PolyDegree, b::PolyDegree) = b.d == 0 ? a : PolyDegree(_NOTPOLY) |
| 44 | +Base.:\(a::PolyDegree, b::PolyDegree) = a.d == 0 ? b : PolyDegree(_NOTPOLY) |
| 45 | +Base.inv(a::PolyDegree) = a.d == 0 ? a : PolyDegree(_NOTPOLY) |
| 46 | +Base.muladd(a::PolyDegree, b::PolyDegree, c::PolyDegree) = a * b + c |
| 47 | +Base.fma(a::PolyDegree, b::PolyDegree, c::PolyDegree) = a * b + c |
| 48 | +Base.abs2(a::PolyDegree) = a * a |
| 49 | +Base.conj(a::PolyDegree) = a |
| 50 | +Base.real(a::PolyDegree) = a |
| 51 | + |
| 52 | +function Base.:^(a::PolyDegree, n::Integer) |
| 53 | + n == 0 && return PolyDegree(0) |
| 54 | + n > 0 && return PolyDegree(_satmul(a.d, Int(n))) |
| 55 | + return a.d == 0 ? a : PolyDegree(_NOTPOLY) |
| 56 | +end |
| 57 | +Base.:^(a::PolyDegree, b::PolyDegree) = |
| 58 | + a.d == 0 && b.d == 0 ? PolyDegree(0) : PolyDegree(_NOTPOLY) |
| 59 | + |
| 60 | +# Non-polynomial scalar functions: constants map to constants; anything else poisons. |
| 61 | +for fn in ( |
| 62 | + :sqrt, :cbrt, :exp, :exp2, :exp10, :expm1, :log, :log2, :log10, :log1p, |
| 63 | + :sin, :cos, :tan, :asin, :acos, :atan, :sinh, :cosh, :tanh, :asinh, :acosh, |
| 64 | + :atanh, :sinpi, :cospi, :sec, :csc, :cot, :abs, :sign, |
| 65 | + ) |
| 66 | + @eval Base.$fn(a::PolyDegree) = a.d == 0 ? a : PolyDegree(_NOTPOLY) |
| 67 | +end |
| 68 | +Base.atan(a::PolyDegree, b::PolyDegree) = |
| 69 | + a.d == 0 && b.d == 0 ? PolyDegree(0) : PolyDegree(_NOTPOLY) |
| 70 | +Base.hypot(a::PolyDegree, b::PolyDegree) = |
| 71 | + a.d == 0 && b.d == 0 ? PolyDegree(0) : PolyDegree(_NOTPOLY) |
| 72 | +Base.mod(a::PolyDegree, b::PolyDegree) = PolyDegree(_NOTPOLY) |
| 73 | +Base.rem(a::PolyDegree, b::PolyDegree) = PolyDegree(_NOTPOLY) |
| 74 | + |
| 75 | +struct DegreeTracerError <: Exception |
| 76 | + op::Symbol |
| 77 | +end |
| 78 | +function Base.showerror(io::IO, e::DegreeTracerError) |
| 79 | + return print( |
| 80 | + io, "DegreeTracerError: `", e.op, "` needs the value of a traced number, which a ", |
| 81 | + "degree tracer does not carry; the polynomial-degree certificate is abandoned." |
| 82 | + ) |
| 83 | +end |
| 84 | + |
| 85 | +# Every predicate or conversion that would need the traced VALUE aborts the trace: allowing any |
| 86 | +# of these to answer would let value-dependent control or value flow leak into the certificate. |
| 87 | +for fn in (:isless, :(==), :<, :(<=), :isequal) |
| 88 | + @eval Base.$fn(a::PolyDegree, b::PolyDegree) = throw(DegreeTracerError($(QuoteNode(fn)))) |
| 89 | +end |
| 90 | +for fn in (:isnan, :isinf, :isfinite, :iszero, :isone, :signbit, :isinteger) |
| 91 | + @eval Base.$fn(a::PolyDegree) = throw(DegreeTracerError($(QuoteNode(fn)))) |
| 92 | +end |
| 93 | +for fn in (:floor, :ceil, :trunc, :round) |
| 94 | + @eval Base.$fn(a::PolyDegree) = throw(DegreeTracerError($(QuoteNode(fn)))) |
| 95 | +end |
| 96 | + |
| 97 | +_seed(x::Real) = PolyDegree(1) |
| 98 | +_seed(x::AbstractArray{<:Real}) = map(_ -> PolyDegree(1), x) |
| 99 | +_seed(x) = throw(DegreeTracerError(:seed)) |
| 100 | + |
| 101 | +_max_degree(y::PolyDegree) = y.d |
| 102 | +_max_degree(y::Real) = 0 |
| 103 | +_max_degree(y::AbstractArray) = isempty(y) ? 0 : maximum(_max_degree, y) |
| 104 | +_max_degree(y::Tuple) = isempty(y) ? 0 : maximum(_max_degree, y) |
| 105 | +_max_degree(@nospecialize(y)) = _NOTPOLY |
| 106 | + |
| 107 | +_wrt_indices(wrt::Integer, n) = (Int(wrt),) |
| 108 | +_wrt_indices(wrt::Colon, n) = ntuple(identity, n) |
| 109 | +_wrt_indices(wrt, n) = Tuple(Int.(collect(wrt))) |
| 110 | + |
| 111 | +# Certified upper bound on the real-arithmetic polynomial degree of `f` in the arguments selected |
| 112 | +# by `wrt`, or `_NOTPOLY` when no certificate can be produced (non-polynomial operations reached |
| 113 | +# non-constant values, the trace aborted, or `f` branches on values). |
| 114 | +function _degree_bound(f, args, wrt) |
| 115 | + idx = _wrt_indices(wrt, length(args)) |
| 116 | + all(i -> 1 <= i <= length(args), idx) || throw(ArgumentError("wrt index out of range")) |
| 117 | + d = try |
| 118 | + targs = ntuple(i -> i in idx ? _seed(args[i]) : args[i], length(args)) |
| 119 | + _max_degree(f(targs...)) |
| 120 | + catch |
| 121 | + _NOTPOLY |
| 122 | + end |
| 123 | + d < _NOTPOLY || return _NOTPOLY |
| 124 | + # The trace certifies the executed path; `hasbranching` certifies it is the only path. |
| 125 | + return hasbranching(f, args...) ? _NOTPOLY : d |
| 126 | +end |
| 127 | + |
| 128 | +""" |
| 129 | + islinear(f, x...; wrt = 1) -> Bool |
| 130 | +
|
| 131 | +Attempt to *prove* that `f` is an affine (polynomial degree ≤ 1) function of the arguments |
| 132 | +selected by `wrt` (an index, collection of indices, or `:` for all; default the first argument), |
| 133 | +holding the remaining arguments fixed at the values given. Arrays are tracked elementwise. |
| 134 | +
|
| 135 | +`true` is a certificate under real arithmetic: the degree bound is established by abstract |
| 136 | +interpretation with degree-tracking tracer numbers, and [`hasbranching`](@ref) additionally |
| 137 | +proves the traced path is the only path. `false` means *not proven* -- `f` may still be linear |
| 138 | +(e.g. the bound does not model cancellation: `x^2 - x^2 + x` is not certified), so use `false` |
| 139 | +as "fall back to the general path", never as a proof of nonlinearity. |
| 140 | +
|
| 141 | +```jldoctest |
| 142 | +julia> using FunctionProperties |
| 143 | +
|
| 144 | +julia> islinear((u, p, t) -> p[1] * u[1] + p[2], [1.0], [2.0, 3.0], 0.0) |
| 145 | +true |
| 146 | +
|
| 147 | +julia> islinear((u, p, t) -> u[1] * u[2], [1.0, 2.0], nothing, 0.0) |
| 148 | +false |
| 149 | +``` |
| 150 | +""" |
| 151 | +function islinear(f, x...; wrt = 1) |
| 152 | + return _degree_bound(f, x, wrt) <= 1 |
| 153 | +end |
| 154 | + |
| 155 | +""" |
| 156 | + isquadratic(f, x...; wrt = 1) -> Bool |
| 157 | +
|
| 158 | +Attempt to *prove* that `f` is a polynomial of degree ≤ 2 in the arguments selected by `wrt`, |
| 159 | +holding the remaining arguments fixed. Same certification semantics and conservatism as |
| 160 | +[`islinear`](@ref): `true` is a proof under real arithmetic, `false` means not proven. |
| 161 | +
|
| 162 | +```jldoctest |
| 163 | +julia> using FunctionProperties |
| 164 | +
|
| 165 | +julia> isquadratic((u, p, t) -> u[1] * u[2] + p[1] * u[1], [1.0, 2.0], [3.0], 0.0) |
| 166 | +true |
| 167 | +
|
| 168 | +julia> isquadratic((u, p, t) -> exp(u[1]), [1.0], nothing, 0.0) |
| 169 | +false |
| 170 | +``` |
| 171 | +""" |
| 172 | +function isquadratic(f, x...; wrt = 1) |
| 173 | + return _degree_bound(f, x, wrt) <= 2 |
| 174 | +end |
0 commit comments