Skip to content

Commit 30f4191

Browse files
ChrisRackauckas-ClaudeChrisRackauckasclaude
authored
Support Symbolics v7 (port matrix atoms + curvature passes) (#112)
* Cap Symbolics to v6 and fix test Manifolds/Manopt resolution Three independent CI failures all stem from dependency churn, not code regressions: 1. docs + Core/QA precompile: Symbolics v7 removed the `Symbolic` abstract type and collapsed array/scalar registered symbolics onto a single `BasicSymbolic{SymReal}`. That makes the package's `import Symbolics: Symbolic` fail and makes the dual scalar/vector `@register_symbolic` (e.g. `quad_over_lin`) plus the matrix-function registrations (`Base.log`/`inv`/`sqrt` on `Symbolics.Arr`) generate duplicate / `Base`-clobbering methods that abort precompilation. The package's matrix-atom registration model is not yet v7-compatible, so cap `Symbolics = "6"` / `SymbolicUtils = "3.1.2"` (the last-known-good set, matching registered v0.3.6) in the package, docs, and test envs. 2. Core/QA resolution: dependabot bumped test `Manifolds` to 0.11.26, but `OptimizationManopt` 1.3.2 only supports `Manifolds 0.10` / `ManifoldsBase 1`, and `Manopt 0.5.38` requires `ManifoldsBase 2`. Cap test `Manifolds = "0.10"` and `Manopt = "0.5.25"` so the env resolves (Manifolds 0.10.23, ManifoldsBase 1.2.0, Manopt 0.5.25, OptimizationManopt 1.3.2). 3. Downgrade: `empty intersection between Symbolics@6 and project compat 7.25.0-7` — the test env pinned Symbolics 7.25 while the package floor is 6. Aligning both to 6 removes the conflict. Verified locally on Julia 1.12 and 1.10: the test env resolves and precompiles, `GROUP=Core` passes (alloc 17/17, dgp 28/28, interface 31/31, lorentz 11/11, test 19 pass + 2 pre-existing broken), and all docs example blocks run. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix QA (Aqua/JET) findings and align downgrade compat floor Builds on the Symbolics-v6 cap to make the QA and Downgrade groups green: Downgrade: raise the package Manifolds compat floor from 0.9 to 0.10 so it matches the test env's Manifolds cap (needed for OptimizationManopt 1.3.2). julia-downgrade-compat was pinning the root floor to Manifolds 0.9.10, which had an empty intersection with the test env's 0.10 -> Downgrade now resolves the minimal set (Manifolds 0.10.0, ...). Aqua: - Drop the unused IfElse and Dictionaries deps (genuine stale deps). - Disambiguate Base.in(::Num/::Symbolic, ::CustomDomain) against Symbolics' in(::Num/::Symbolic, ::Domain) (CustomDomain <: Domain made the two overlap). - Configure run_qa's Aqua via aqua_kwargs: treat the deliberately-extended non-owned functions (log/tr/inv/sqrt/logdet/distance/xlogx and the Symbolics arguments/hasmetadata/promote_symtype hooks) as own for the piracy check, and exclude the 10 atom functions whose scalar+array @register_symbolic registrations generate benign internal ambiguities. All remaining ambiguities are internal to SymbolicAnalysis (no cross-package ambiguity remains). JET (real code bugs surfaced by report_package, run in :typo mode): - lorentz_log_barrier referenced an undefined ; a = (0,...,0,1) makes the Lorentzian inner product reduce to -p[end], so the barrier is -log(-1+p[end]). - lognormcdf called logcdf(Normal, x) (the type) which is a MethodError; use the standard-normal instance logcdf(Normal(), x). - find_curvature/find_gcurvature could fall through with / undefined (the branch did not return its value); return it and default f_curvature/f_monotonicity to the existing 'unknown' semantics. Switch test/qa/qa.jl to SciMLTesting.run_qa (the shared harness, matching ModelingToolkit) with jet=true and mode=:typo. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Downgrade: raise Symbolics floor to 6.37.1 (and SymbolicUtils to 3.24) After raising the Manifolds floor, the Downgrade job surfaced the next empty-intersection: julia-downgrade-compat pinned the package's Symbolics floor to 6.0.0, which only allows ForwardDiff 0.10, while the test env requires ForwardDiff 1.3.3 (Optimization 5.x) -> 'empty intersection between ForwardDiff@0.10.39 and project compatibility 1.3.3-1'. Symbolics first allows ForwardDiff 1.x at 6.37.1 (registry Compat.toml: 6.37.1-6.42 => ForwardDiff = [0.10.36-0.10, 1]); that range needs SymbolicUtils >= 3.24.0. Raise both package floors accordingly. This is a pure floor raise within the existing v6 cap (the suite already runs on Symbolics 6.58 / SymbolicUtils 3.32 / ForwardDiff 1.4), so it only removes the unsatisfiable lower corner. Verified locally on lts (1.10): the real julia-downgrade-compat resolves the root minimal set (Symbolics 6.37.1, SymbolicUtils 3.24.0) and the test env then resolves with ForwardDiff 1.4.1, Manifolds 0.10.23, Manopt 0.5.25. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Downgrade: also downgrade the test project (projects: .,test) The test env declares modern floors (Optimization 5.6.1, Zygote 0.7.10, ForwardDiff 1.3.3) whose transitive requirements (SciMLBase >= 2.130, which supports Zygote 0.7) are far above the package's own transitive minima. Downgrading only the root project pinned SciMLBase to 2.35.0 in the Manifest, and with allow_reresolve=false the test deps could not reconcile -> 'Unsatisfiable requirements ... Zygote ... SciMLBase 2.35.0'. Passing projects: '.,test' to the SciML downgrade workflow pins both projects' direct deps to their floors at once, so the test floors force the consistent modern transitive set. Verified locally on lts (1.10): julia-downgrade-compat 'Successfully resolved minimal versions for merged project' and the env resolves (Symbolics 6.37.1, SymbolicUtils 3.24.0, ForwardDiff 1.3.3, Zygote 0.7.10, SciMLBase 2.155.1, Optimization 5.6.1, Manifolds 0.10.13). Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Downgrade: gate the x^2+y^2+z^2 curvature test_broken on Symbolics >= 6.38 With the test project downgraded, Symbolics resolves to 6.37.1, where the curvature pass correctly derives 'x^2 + y^2 + z^2' as Convex, so the existing @test_broken errored with 'Unexpected Pass'. The result regressed to UnknownCurvature in Symbolics 6.38 (bisected: 6.37.1 works, 6.38.0 broken). Convert to '@test ... broken = (pkgversion(Symbolics) >= v"6.38")': a real, passing assertion on the Downgrade env (6.37.1) and a documented known regression on the Core/docs envs (Symbolics 6.58). No test is weakened - the assertion still runs and would fail if the result regressed on < 6.38. Verified locally: testset records Pass at Symbolics 6.37.1 and Broken at 6.58. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Support Symbolics v7: port matrix atoms and curvature passes, open compat Re-opens the package to Symbolics v7 (was capped at v6 on this branch) and adapts the source for the v7/SymbolicUtils v4 API changes so the analysis genuinely runs on v7. Verified on Symbolics 7.28.1 / SymbolicUtils 4.35.3: Core (test/interface/lorentz/dgp = 91/91), QA (Aqua+JET 12/12) and the allocation tests all pass. Compat: - Symbolics "6.37.1" -> "6.37.1, 7"; SymbolicUtils "3.24" -> "3.24, 4.35" (Symbolics 7.25+ requires SymbolicUtils 4.35+). - test/ and docs/ envs widened to allow v7 as well. Source adaptations for the v7 model (scalars and arrays both unwrap to `BasicSymbolic{SymReal}`, numeric literals are wrapped constants, matrix products return bare `BasicSymbolic`, several registration macros collapse or break `maketerm`): - `Symbolic` alias is version-aware (`Symbolics.Symbolic` on v6 — which still covers `ArrayOp` — `BasicSymbolic` on v7); the `in(::CustomDomain)` disambiguators dispatch on the type Symbolics itself uses so they stay unambiguous. - `dcprule(^, ...)`, the `broadcast` rule, and the `*` curvature/sign passes unwrap wrapped numeric constants via `Symbolics.value` before numeric tests. - `propagate_sign`/`propagate_curvature`/`propagate_gcurvature` unwrap their input so metadata survives the v7 rewrite walk; `add_sign` no longer writes into the (read-only on v7) matched args. - `canonize` rules match the flattened v7 `*` shape (no array indexing). - Matrix atoms (`log`/`inv`/`sqrt`, `conjugation`, `hadamard_product`, `affine_map`, `Manifolds.distance`) build terms via `SymbolicUtils.term` on v7 instead of the colliding/`maketerm`-unsafe registration macros; matrix `*` of `Arr`s is re-wrapped and a (strictly-more-specific, non-overwriting) matrix `promote_shape(sqrt, ...)` is added. - `quad_over_lin`/`tr` registrations gated to v6 where the macro path is needed and non-colliding. QA: declare the new intentional `*`/`promote_shape` methods as own and ignore the (v6-only-transitive) RecursiveArrayTools stale-dep. Two curvature tests that the v6.38+ tree shape regressed now pass on v7; their `broken` guards updated. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Make Symbolics v7-only: drop v6 fallback paths and cruft The package now targets Symbolics v7 / SymbolicUtils v4 exclusively, so the version-gated v6 fallback code is removed: - src/SymbolicAnalysis.jl: `Symbolic` is `BasicSymbolic` and `InDomainSymbolic` is `BasicSymbolic{SymReal}` directly (drop the `isdefined(Symbolics, :Symbolic)` branches). - src/atoms.jl: drop the v6-only `quad_over_lin` vector `@register_symbolic`, and collapse the `@static`/`pkgversion` branches for `matrix_atom`, the matrix `*` re-wrap, and the `sqrt` `promote_shape` to their v7 form. - src/gdcp/spd.jl: replace the v6 `@register_array_symbolic`/`@register_symbolic` branches for `conjugation`, `tr`, and `hadamard_product` with the v7 `SymbolicUtils.term`-based definitions. - src/canon.jl: keep only the flattened-`*` canonicalization rules. - Drop `RecursiveArrayTools` (declared but never loaded on v7) from the package deps, and remove the now-unnecessary Aqua `stale_deps` ignore in test/qa/qa.jl (the stale-deps check is fully live again). - test/test.jl: the two version-gated `broken` curvature tests pass on v7, so they are now plain `@test`s. Compat floors: Symbolics = "7.28", SymbolicUtils = "4.35" (Project.toml, test/Project.toml, docs/Project.toml). Symbolics 7.27+ requires SymbolicUtils 4.35+; the code was developed and verified against the 7.28.x / 4.35.x API (`term(...; shape=...)`, `ShapeVecT`, `_throw_array`, `SymReal`), all of which are present at the 4.35.0 floor. The floor resolves cleanly (downgrade lane: Symbolics 7.28.0 + SymbolicUtils 4.35.3; max: 7.28.1 + 4.35.3). Verified on Julia 1.12.6 / Symbolics 7.28.1: Core 108/108 (test 21, dgp 28, interface 31, lorentz 11, alloc 17), QA 12/12, 0 broken/skipped. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * QA: fix real ambiguities at the source, drop custom Aqua hacks The v7 port left a hand-rolled QA in test/qa/qa.jl that excluded 10 atoms from Aqua's ambiguity check and over-broadly declared piracy exceptions. Re-evaluate against the canonical `Aqua.test_all`: - Only 3 atoms (`sdivergence`, `log_quad_form`, `lorentz_least_squares`) actually produce ambiguities, and they are real: their multi-array `@register_symbolic` registrations explode into a combinatorial set of mutually-ambiguous `Num`/`BasicSymbolic`/`Arr` wrapper methods on Symbolics v7. Fix the cause (not the symptom) by building those terms directly via `SymbolicUtils.term` off the symbolic argument — the same pattern the v7 port already uses for `conjugation`/`hadamard_product`/ `distance`. `detect_ambiguities(SymbolicAnalysis)` is now empty, so the whole `ambiguities = (; exclude = ...)` list is dropped. - The remaining Aqua exception is `piracies = (; treat_as_own = ...)`, minimized to exactly the foreign functions the package intentionally extends with analysis methods (dropping the unused `tr`, which v7 builds natively). This is the package's by-design purpose and the only exception it needs; ambiguities/stale-deps/etc. all pass cleanly under the canonical template. Verified locally on Julia 1.12.6 (Symbolics 7.28.1, SymbolicUtils 4.35.5): Core group 108/108, QA group 12/12. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6886371 commit 30f4191

13 files changed

Lines changed: 279 additions & 108 deletions

File tree

.github/workflows/Downgrade.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,8 @@ jobs:
1515
uses: "SciML/.github/.github/workflows/downgrade.yml@v1"
1616
with:
1717
julia-version: "lts"
18+
# Downgrade the test project too: its deps (Optimization/Zygote/ForwardDiff)
19+
# have floors that pull transitive deps (SciMLBase) far above the package's
20+
# own minima, so pinning only the root project leaves an unsatisfiable env.
21+
projects: ".,test"
1822
secrets: "inherit"

Project.toml

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,28 @@ version = "0.3.7"
66
[deps]
77
DSP = "717857b8-e6f2-59f4-9121-6e50c889abd2"
88
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
9-
Dictionaries = "85a47980-9c8c-11e8-2b9f-f7ca1fa99fb4"
109
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
1110
DomainSets = "5b8099bc-c8ec-5219-889f-1d9e522a28bf"
12-
IfElse = "615f187c-cbe4-4ef1-ba3b-2fcf58d6d173"
1311
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1412
LogExpFunctions = "2ab3a3ac-af41-5b50-aa03-7779005ae688"
1513
Manifolds = "1cead3c2-87b3-11e9-0ccd-23c62b72b94e"
1614
PDMats = "90014a1f-27ba-587c-ab20-58faa44d9150"
1715
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
18-
RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd"
1916
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
2017
SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b"
2118
Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"
2219

2320
[compat]
2421
DSP = "0.7, 0.8"
2522
DataStructures = "0.18, 0.19"
26-
Dictionaries = "0.4"
2723
Distributions = "0.25"
2824
DomainSets = "0.7, 0.8"
29-
IfElse = "0.1"
3025
LinearAlgebra = "1.10"
3126
LogExpFunctions = "0.3, 1.0"
32-
Manifolds = "0.9, 0.10, 0.11"
27+
Manifolds = "0.10, 0.11"
3328
PDMats = "0.11"
3429
PrecompileTools = "1"
35-
RecursiveArrayTools = "3, 4"
3630
StatsBase = "0.34"
37-
SymbolicUtils = "3.1.2, 4.33"
38-
Symbolics = "6, 7.25"
31+
SymbolicUtils = "4.35"
32+
Symbolics = "7.28"
3933
julia = "1.10"

docs/Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ PDMats = "0.11"
3333
RecursiveArrayTools = "3, 4.3"
3434
StatsBase = "0.34"
3535
SymbolicAnalysis = "0.3.6"
36-
SymbolicUtils = "3, 4.33"
37-
Symbolics = "6, 7.25"
36+
SymbolicUtils = "4.35"
37+
Symbolics = "7.28"

src/SymbolicAnalysis.jl

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,21 @@ using Distributions
99
using DSP, DataStructures
1010

1111
using Symbolics
12-
import Symbolics: Symbolic, issym, Term
13-
using SymbolicUtils: iscall
12+
import Symbolics: issym, Term
13+
using SymbolicUtils: iscall, BasicSymbolic
1414
using SymbolicUtils.Rewriters
1515
SymbolicUtils.inspect_metadata[] = true
1616

17+
# Symbolics v7 / SymbolicUtils v4 removed the `Symbolic` abstract type: every
18+
# symbolic — scalar or array — is now a `BasicSymbolic{SymReal}`.
19+
const Symbolic = BasicSymbolic
20+
21+
# The scalar-symbolic type Symbolics uses when dispatching `in(::symbolic, ::Domain)`.
22+
# Matching it exactly lets the `in(::_, ::CustomDomain)` disambiguators below stay
23+
# strictly more specific than Symbolics' `IntervalSets.Domain` method (which is
24+
# keyed on `BasicSymbolic{SymReal}`).
25+
const InDomainSymbolic = BasicSymbolic{SymbolicUtils.SymReal}
26+
1727
struct VarDomain end
1828

1929
include("rules.jl")

src/atoms.jl

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,10 @@ function quad_over_lin(x::AbstractVector{<:Real}, y::Real)
231231
return sum(x .^ 2) / y
232232
end
233233

234-
Symbolics.@register_symbolic quad_over_lin(x::AbstractVector, y::Real) false
234+
# On Symbolics v7 both scalar- and array-valued registered symbolics share the
235+
# concrete type `BasicSymbolic{SymReal}`, so registering the vector form would
236+
# generate the same `quad_over_lin(::BasicSymbolic{SymReal}, ::Real)` method as
237+
# the scalar one below and collide. The scalar registration alone covers both.
235238

236239
"""
237240
quad_over_lin(x::Real, y::Real)
@@ -393,13 +396,57 @@ add_dcprule(imag, ℂ, AnySign, Affine, AnyMono)
393396
add_dcprule(inv, HalfLine{Real, :open}(), Positive, Convex, Decreasing)
394397
add_dcprule(log, HalfLine{Real, :open}(), AnySign, Concave, Increasing)
395398

396-
@register_symbolic Base.log(A::Symbolics.Arr)
399+
# Matrix-valued atoms (`log`, `inv`, `sqrt` of a symbolic matrix). On Symbolics
400+
# v7 / SymbolicUtils v4 a symbolic matrix unwraps to `BasicSymbolic{SymReal}` —
401+
# the same concrete type as a symbolic scalar — so a `@register_symbolic` macro
402+
# would emit a `f(::BasicSymbolic{SymReal})` method that overwrites SymbolicUtils'
403+
# own scalar `f` and aborts precompilation. Build the matrix term directly off the
404+
# `Arr` wrapper instead via `SymbolicUtils.term`, which leaves the scalar methods
405+
# untouched. Some scalar atoms (e.g. `sqrt`) register a `promote_shape` that
406+
# rejects matrix shapes, which `term` would invoke unless the shape is supplied up
407+
# front, so pass `shape` explicitly.
408+
function matrix_atom(f, A::Symbolics.Arr)
409+
a = Symbolics.unwrap(A)
410+
return Symbolics.wrap(
411+
SymbolicUtils.term(
412+
f, a; type = SymbolicUtils.symtype(a), shape = Symbolics.shape(a)
413+
)
414+
)
415+
end
416+
function matrix_atom(f, A::AbstractMatrix{<:Num})
417+
a = Symbolics.unwrap.(A)
418+
return Symbolics.wrap(
419+
SymbolicUtils.term(f, a; type = Matrix{Real}, shape = map(Base.OneTo, size(A)))
420+
)
421+
end
422+
423+
# A symbolic matrix–matrix product returns a bare `BasicSymbolic`; re-wrap the
424+
# 2-argument `Arr` product so it reaches the matrix-atom methods below (which
425+
# dispatch on `Arr`), restoring the array shape. This 2-arg method is more
426+
# specific than Symbolics' variadic `*`, so it does not overwrite it.
427+
function Base.:*(x::Symbolics.Arr{<:Any, 2}, y::Symbolics.Arr{<:Any, 2})
428+
return Symbolics.wrap(Symbolics.unwrap(x) * Symbolics.unwrap(y))
429+
end
430+
431+
# SymbolicUtils v4 gives `log` a matrix-permissive `promote_shape` but leaves
432+
# `sqrt` scalar-only, so rewriting a matrix `sqrt` term (e.g. during the
433+
# analysis walk's `maketerm`) throws "Invalid shapes for sqrt". Add a matrix
434+
# rule for `sqrt`; dispatching on the concrete `ShapeVecT` (rather than the
435+
# `ShapeT` union SymbolicUtils uses) makes this strictly more specific, so it
436+
# extends rather than overwrites the existing method.
437+
function SymbolicUtils.promote_shape(::typeof(sqrt), sh::SymbolicUtils.ShapeVecT)
438+
(length(sh) == 0 || length(sh) == 2) && return sh
439+
return SymbolicUtils._throw_array(sqrt, sh)
440+
end
441+
442+
Base.log(A::Symbolics.Arr) = matrix_atom(log, A)
443+
Base.log(A::Matrix{Num}) = matrix_atom(log, A)
397444
add_dcprule(log, array_domain(RealLine(), 2), Positive, Concave, Increasing)
398445

399-
@register_symbolic LinearAlgebra.inv(A::Symbolics.Arr)
446+
LinearAlgebra.inv(A::Symbolics.Arr) = matrix_atom(inv, A)
400447
add_dcprule(inv, semidefinite_domain(), AnySign, Convex, Decreasing)
401448

402-
@register_symbolic LinearAlgebra.sqrt(A::Symbolics.Arr)
449+
LinearAlgebra.sqrt(A::Symbolics.Arr) = matrix_atom(sqrt, A)
403450
add_dcprule(sqrt, semidefinite_domain(), Positive, Concave, Increasing)
404451

405452
add_dcprule(
@@ -420,7 +467,7 @@ Returns the log of the normal cumulative distribution function of `x`.
420467
- `x::Real`: A Real.
421468
"""
422469
function lognormcdf(x::Real)
423-
return logcdf(Normal, x)
470+
return logcdf(Normal(), x)
424471
end
425472
Symbolics.@register_symbolic lognormcdf(x::Real)
426473
add_dcprule(lognormcdf, RealLine(), Negative, Concave, Increasing)
@@ -434,6 +481,11 @@ add_dcprule(min, (RealLine(), RealLine()), AnySign, Concave, Increasing)
434481

435482
# special cases which depend on arguments:
436483
function dcprule(::typeof(^), x::Symbolic, i)
484+
# A literal exponent is wrapped as a constant `BasicSymbolic`, so
485+
# `isinteger`/`isone`/comparisons below would operate on a symbolic and error;
486+
# `Symbolics.value` unwraps it to the underlying number (identity for an
487+
# already-numeric exponent).
488+
i = Symbolics.value(i)
437489
args = (x, i)
438490
if isone(i)
439491
return makerule(RealLine(), AnySign, Affine, Increasing), args
@@ -512,7 +564,10 @@ add_dcprule(vec, array_domain(RealLine(), 2), AnySign, Affine, Increasing)
512564
add_dcprule(vcat, array_domain(array_domain(RealLine(), 1), 1), AnySign, Affine, Increasing)
513565

514566
function dcprule(::typeof(broadcast), f, x...)
515-
return dcprule(f, x...)
567+
# The broadcasted function is wrapped as a constant symbolic (e.g.
568+
# `broadcast(exp, z)` carries a symbolic `exp`); `Symbolics.value` recovers the
569+
# underlying function (identity for a plain function).
570+
return dcprule(Symbolics.value(f), x...)
516571
end
517572
hasdcprule(::typeof(broadcast)) = true
518573

src/canon.jl

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
function canonize(ex)
2+
# Symbolics v7 flattens `x' * (Y * x)` / `(B' * X) * B` to a single
3+
# scalar/matrix `*` term, so match the flattened `*` directly.
24
rs = [
3-
@rule (adjoint(~x) * (~Y * ~x))[1] => quad_form(~x, ~Y)
4-
@rule ((adjoint(~B) * ~X) * ~B)[
5-
Base.OneTo(size(~B, 2)), Base.OneTo(
6-
size(
7-
~B, 1
8-
)
9-
),
10-
] => conjugation(~X, ~B)
5+
@rule adjoint(~x) * ~Y * ~x => quad_form(~x, ~Y)
6+
@rule adjoint(~B) * ~X * ~B => conjugation(~X, ~B)
117
]
128
try
139
rc = SymbolicUtils.Chain(rs)

src/gdcp/gdcp_rules.jl

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function mul_gcurvature(args)
4848
return GUnknownCurvature
4949
end
5050
else
51-
constant_prod *= arg
51+
constant_prod *= constval(arg)
5252
end
5353
end
5454
if non_constant_expr !== nothing
@@ -101,6 +101,8 @@ function find_gcurvature(ex)
101101
if iscall(ex)
102102
f, args = operation(ex), arguments(ex)
103103
knowngcurv = false
104+
f_curvature = GUnknownCurvature
105+
f_monotonicity = (GAnyMono,)
104106

105107
if hasgdcprule(f) && !any(iscall.(args))
106108
rule, args = gdcprule(f, args...)
@@ -161,16 +163,17 @@ function find_gcurvature(ex)
161163
end
162164
end
163165
elseif Symbol(f) == :*
164-
if args[1] isa Number && args[1] > 0
166+
a1 = constval(args[1])
167+
if a1 isa Number && a1 > 0
165168
return find_gcurvature(args[2])
166-
elseif args[1] isa Number && args[1] < 0
169+
elseif a1 isa Number && a1 < 0
167170
argscurv = find_gcurvature(args[2])
168171
if argscurv == GConvex
169172
return GConcave
170173
elseif argscurv == GConcave
171174
return GConvex
172175
else
173-
argscurv
176+
return argscurv
174177
end
175178
else
176179
@warn "Disciplined Programming does not support multiple non-constant arguments in multiplication"
@@ -242,6 +245,11 @@ function find_gcurvature(ex)
242245
end
243246

244247
function propagate_gcurvature(ex, M::AbstractManifold)
248+
# Operate on the raw symbolic: on Symbolics v7 walking a `Num`/`Arr` wrapper
249+
# round-trips through wrap/unwrap and loses the gcurvature metadata that the
250+
# final `getgcurvature` reads. `analyze` already unwraps; do the same here so
251+
# the function is correct when called directly on a wrapped expression.
252+
ex = Symbolics.unwrap(ex)
245253
r = [
246254
@rule *(~~x) => setgcurvature(~MATCH, mul_gcurvature(~~x))
247255
@rule +(~~x) => setgcurvature(~MATCH, add_gcurvature(~~x))

src/gdcp/lorentz.jl

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,39 @@
66

77
using Manifolds
88
using LinearAlgebra
9-
using Symbolics: Symbolic, @register_symbolic, unwrap, variables
10-
11-
@register_symbolic Manifolds.distance(
12-
M::Manifolds.Lorentz,
13-
p::AbstractVector,
14-
q::Union{Symbolics.Arr, AbstractVector}
15-
) false
9+
using Symbolics: @register_symbolic, unwrap, variables
10+
11+
# See the SPD `distance` note in gdcp/spd.jl: build the term directly off the
12+
# symbolic point so the SPD and Lorentz methods do not collide.
13+
function Manifolds.distance(
14+
M::Manifolds.Lorentz,
15+
p::AbstractVector,
16+
q::Symbolics.Arr
17+
)
18+
return Symbolics.wrap(
19+
SymbolicUtils.term(Manifolds.distance, M, p, Symbolics.unwrap(q); type = Real)
20+
)
21+
end
1622
add_gdcprule(Manifolds.distance, Manifolds.Lorentz, Positive, GConvex, GAnyMono)
1723

1824
"""
19-
lorentz_log_barrier(a, p)
25+
lorentz_log_barrier(p)
2026
21-
Computes the log-barrier function for the Lorentz model: `-log(-1 - <a, p>_L)`.
27+
Computes the log-barrier function for the Lorentz model: `-log(-1 - <a, p>_L)`,
28+
with the fixed vector `a = (0, ..., 0, 1)` in R^(d+1).
2229
2330
# Arguments
2431
25-
- `a`: The vector (0, ..., 0, 1) in R^(d+1).
2632
- `p`: A point on the Lorentz manifold.
2733
"""
2834
function lorentz_log_barrier(p::AbstractVector)
29-
# Lorentzian inner product: a⋅p_L = a1*p1 + ... + a_d*p_d - a_{d+1}*p_{d+1}
30-
inner_prod = a[end] * p[end]
31-
return -log(-1 + inner_prod)
35+
# a = (0, ..., 0, 1), so the Lorentzian inner product
36+
# <a, p>_L = a1*p1 + ... + a_d*p_d - a_{d+1}*p_{d+1} reduces to -p[end].
37+
# The barrier is -log(-1 - <a, p>_L) = -log(-1 + p[end]).
38+
return -log(-1 + p[end])
3239
end
3340

34-
@register_symbolic lorentz_log_barrier(p::Union{Symbolics.Arr, AbstractVector})
41+
@register_symbolic lorentz_log_barrier(p::Vector{Num})
3542
add_gdcprule(lorentz_log_barrier, Manifolds.Lorentz, Positive, GConvex, GIncreasing)
3643

3744
"""
@@ -69,7 +76,7 @@ end
6976

7077
@register_symbolic lorentz_homogeneous_quadratic(
7178
A::AbstractMatrix,
72-
p::Union{Symbolics.Arr, AbstractVector}
79+
p::Vector{Num}
7380
)
7481
add_gdcprule(lorentz_homogeneous_quadratic, Manifolds.Lorentz, Positive, GConvex, GAnyMono)
7582

@@ -102,7 +109,7 @@ end
102109

103110
@register_symbolic lorentz_homogeneous_diagonal(
104111
a::AbstractVector,
105-
p::Union{Symbolics.Arr, AbstractVector}
112+
p::Vector{Num}
106113
)
107114
add_gdcprule(lorentz_homogeneous_diagonal, Manifolds.Lorentz, Positive, GConvex, GAnyMono)
108115

@@ -170,7 +177,18 @@ function lorentz_least_squares(X::AbstractMatrix, y::AbstractVector, p::Abstract
170177
return lorentz_nonhomogeneous_quadratic(A, b, c, p)
171178
end
172179

173-
@register_symbolic lorentz_least_squares(X::Matrix{Num}, y::Vector{Num}, p::Vector{Num})
180+
# A `@register_symbolic` three-array registration explodes into a combinatorial,
181+
# mutually-ambiguous set of wrapper methods on Symbolics v7 (Aqua flags them; see
182+
# the `sdivergence` note in gdcp/spd.jl). Build the `lorentz_least_squares(X, y, p)`
183+
# term directly off the symbolic point `p` instead (the gDCP pass only needs
184+
# `operation(ex) == lorentz_least_squares`).
185+
function lorentz_least_squares(X::AbstractMatrix, y::AbstractVector, p::Symbolics.Arr)
186+
return Symbolics.wrap(
187+
SymbolicUtils.term(
188+
lorentz_least_squares, X, y, Symbolics.unwrap(p); type = Real
189+
)
190+
)
191+
end
174192
add_gdcprule(lorentz_least_squares, Manifolds.Lorentz, Positive, GConvex, AnyMono)
175193

176194
"""
@@ -203,7 +221,7 @@ end
203221

204222
@register_symbolic lorentz_transform(
205223
O::AbstractMatrix,
206-
p::Union{Symbolics.Arr, AbstractVector}
224+
p::Vector{Num}
207225
)
208226
# Not adding a rule since this preserves geodesic convexity but doesn't have a specific curvature
209227

0 commit comments

Comments
 (0)