From b864cf7866e3399da3fcf861d752849a2c8dc5b5 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 24 Jan 2024 11:16:24 -0500 Subject: [PATCH 001/129] Start drafting engine prototype --- engine-proto/engine.jl | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 engine-proto/engine.jl diff --git a/engine-proto/engine.jl b/engine-proto/engine.jl new file mode 100644 index 0000000..8b82b47 --- /dev/null +++ b/engine-proto/engine.jl @@ -0,0 +1,34 @@ +module Engine + +export Construction, Sphere, mprod, point + +using LinearAlgebra +using Groebner + +mutable struct Construction + nextid::Int64 + + Construction(; nextid = 0) = new(nextid) +end + +struct Sphere{T<:Number} + vec::Vector{T} + id + + function Sphere(vec::Vector{T}, ctx::Construction) where T <: Number + id = ctx.nextid + ctx.nextid += 1 + new{T}(vec, id) + end +end + +function mprod(sv::Sphere, sw::Sphere) + v = sv.vec + w = sw.vec + v[1]*w[2] + v[2]*w[1] - dot(v[3:end], w[3:end]) +end + +point(pt::Vector{<:Number}, ctx::Construction) = + Sphere([one(eltype(pt)), dot(pt, pt), pt...], ctx) + +end \ No newline at end of file -- 2.34.1 From 4d5aa3b327f680cb7d1e2478bd499faefdab1004 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 26 Jan 2024 11:14:32 -0500 Subject: [PATCH 002/129] Realize geometric elements as symbolic vectors --- engine-proto/engine.jl | 107 ++++++++++++++++++++++++++++++++++------- 1 file changed, 89 insertions(+), 18 deletions(-) diff --git a/engine-proto/engine.jl b/engine-proto/engine.jl index 8b82b47..df75fbe 100644 --- a/engine-proto/engine.jl +++ b/engine-proto/engine.jl @@ -1,34 +1,105 @@ module Engine -export Construction, Sphere, mprod, point +export Construction, mprod +import Subscripts using LinearAlgebra +using AbstractAlgebra using Groebner -mutable struct Construction - nextid::Int64 +# --- primitve elements --- + +mutable struct Point{T} + coords::Union{Vector{MPolyRingElem{T}}, Nothing} + vec::Union{Vector{MPolyRingElem{T}}, Nothing} - Construction(; nextid = 0) = new(nextid) + ## [to do] constructor argument never needed? + Point{T}(vec::Union{Vector{MPolyRingElem{T}}, Nothing} = nothing) where T = new(vec) end -struct Sphere{T<:Number} - vec::Vector{T} - id +coordnames(_::Point) = [:xₚ, :yₚ, :zₚ] + +function buildvec(pt::Point, coordqueue) + pt.coords = splice!(coordqueue, 1:3) + coordring = parent(coordqueue[1]) + pt.vec = [one(coordring), dot(pt.coords, pt.coords), pt.coords...] +end + +mutable struct Sphere{T} + coords::Union{Vector{MPolyRingElem{T}}, Nothing} + vec::Union{Vector{MPolyRingElem{T}}, Nothing} - function Sphere(vec::Vector{T}, ctx::Construction) where T <: Number - id = ctx.nextid - ctx.nextid += 1 - new{T}(vec, id) + Sphere{T}(vec::Union{Vector{MPolyRingElem{T}}, Nothing} = nothing) where T = new(vec) +end + +coordnames(_::Sphere) = [:rₛ, :sₛ, :xₛ, :yₛ, :zₛ] + +function buildvec(sph::Sphere, coordqueue) + sph.coords = splice!(coordqueue, 1:5) + sph.vec = sph.coords +end + +# --- primitive relations --- + +abstract type Relation{T} end + +mprod(v, w) = v[1]*w[2] + w[1]*v[2] - dot(v[3:end], w[3:end]) + +struct LiesOn{T} <: Relation{T} + pt::Point{T} + sph::Sphere{T} +end + +struct AlignsWithBy{T} <: Relation{T} + sph_v::Sphere{T} + sph_w::Sphere{T} + cos_angle::T +end + +# --- constructions --- + +mutable struct Construction{T} + points::Vector{Point{T}} + spheres::Vector{Sphere{T}} + + Construction{T}(; points = Point{T}[], spheres = Sphere{T}[]) where T = new{T}(points, spheres) +end + +function Base.push!(ctx::Construction{T}, elem::Point{T}) where T + push!(ctx.points, elem) +end + +function Base.push!(ctx::Construction{T}, elem::Sphere{T}) where T + push!(ctx.spheres, elem) +end + +function realize(ctx::Construction{T}) where T + # collect variable names + allcoordnames = Symbol[] + elements = vcat(ctx.points, ctx.spheres) + for (index, elem) in enumerate(elements) + subscript = Subscripts.sub(string(index)) + append!(allcoordnames, + [Symbol(name, subscript) for name in coordnames(elem)] + ) + end + + # construct coordinate ring + coordring, coordqueue = polynomial_ring(parent_type(T)(), allcoordnames) + + # construct coordinate vectors + for elem in elements + buildvec(elem, coordqueue) end end -function mprod(sv::Sphere, sw::Sphere) - v = sv.vec - w = sw.vec - v[1]*w[2] + v[2]*w[1] - dot(v[3:end], w[3:end]) end -point(pt::Vector{<:Number}, ctx::Construction) = - Sphere([one(eltype(pt)), dot(pt, pt), pt...], ctx) +# ~~~ sandbox setup ~~~ -end \ No newline at end of file +a = Engine.Point{Rational{Int64}}() +b = Engine.Point{Rational{Int64}}() +s = Engine.Sphere{Rational{Int64}}() +ctx = Engine.Construction{Rational{Int64}}(points = [a]) +Engine.push!(ctx, b) +Engine.push!(ctx, s) \ No newline at end of file -- 2.34.1 From 463a3b21e1fa1c1a531645b4f9cfafbb2fbd2034 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 27 Jan 2024 12:28:29 -0500 Subject: [PATCH 003/129] Realize relations as equations --- engine-proto/engine.jl | 87 ++++++++++++++++++++++++++++++------------ 1 file changed, 62 insertions(+), 25 deletions(-) diff --git a/engine-proto/engine.jl b/engine-proto/engine.jl index df75fbe..f672745 100644 --- a/engine-proto/engine.jl +++ b/engine-proto/engine.jl @@ -9,34 +9,47 @@ using Groebner # --- primitve elements --- -mutable struct Point{T} +abstract type Element{T} end + +mutable struct Point{T} <: Element{T} coords::Union{Vector{MPolyRingElem{T}}, Nothing} vec::Union{Vector{MPolyRingElem{T}}, Nothing} + rel::Nothing ## [to do] constructor argument never needed? - Point{T}(vec::Union{Vector{MPolyRingElem{T}}, Nothing} = nothing) where T = new(vec) + Point{T}( + coords::Union{Vector{MPolyRingElem{T}}, Nothing} = nothing, + vec::Union{Vector{MPolyRingElem{T}}, Nothing} = nothing + ) where T = new(coords, vec, nothing) end coordnames(_::Point) = [:xₚ, :yₚ, :zₚ] function buildvec(pt::Point, coordqueue) - pt.coords = splice!(coordqueue, 1:3) coordring = parent(coordqueue[1]) + pt.coords = splice!(coordqueue, 1:3) pt.vec = [one(coordring), dot(pt.coords, pt.coords), pt.coords...] end -mutable struct Sphere{T} +mutable struct Sphere{T} <: Element{T} coords::Union{Vector{MPolyRingElem{T}}, Nothing} vec::Union{Vector{MPolyRingElem{T}}, Nothing} + rel::Union{MPolyRingElem{T}, Nothing} - Sphere{T}(vec::Union{Vector{MPolyRingElem{T}}, Nothing} = nothing) where T = new(vec) + Sphere{T}( + coords::Union{Vector{MPolyRingElem{T}}, Nothing} = nothing, + vec::Union{Vector{MPolyRingElem{T}}, Nothing} = nothing, + rel::Union{MPolyRingElem{T}, Nothing} = nothing + ) where T = new(coords, vec, rel) end coordnames(_::Sphere) = [:rₛ, :sₛ, :xₛ, :yₛ, :zₛ] function buildvec(sph::Sphere, coordqueue) + coordring = parent(coordqueue[1]) sph.coords = splice!(coordqueue, 1:5) sph.vec = sph.coords + sph.rel = mprod(sph.coords, sph.coords) + one(coordring) end # --- primitive relations --- @@ -45,52 +58,70 @@ abstract type Relation{T} end mprod(v, w) = v[1]*w[2] + w[1]*v[2] - dot(v[3:end], w[3:end]) +# elements: point, sphere struct LiesOn{T} <: Relation{T} - pt::Point{T} - sph::Sphere{T} + elements::Vector{Element{T}} + + LiesOn{T}(pt::Point{T}, sph::Sphere{T}) where T = new{T}([pt, sph]) end +equation(rel::LiesOn) = dot(rel.elements[1].vec, rel.elements[2].vec) + +# elements: sphere, sphere struct AlignsWithBy{T} <: Relation{T} - sph_v::Sphere{T} - sph_w::Sphere{T} + elements::Vector{Element{T}} cos_angle::T + + LiesOn{T}(sph1::Point{T}, sph2::Sphere{T}, cos_angle::T) where T = new{T}([sph1, sph2], cos_angle) end +equation(rel::AlignsWithBy) = dot(rel.elements[1].vec, rel.elements[2].vec) - rel.cos_angle + # --- constructions --- mutable struct Construction{T} - points::Vector{Point{T}} - spheres::Vector{Sphere{T}} + elements::Set{Element{T}} + relations::Set{Relation{T}} - Construction{T}(; points = Point{T}[], spheres = Sphere{T}[]) where T = new{T}(points, spheres) + function Construction{T}(; elements = Set{Element{T}}(), relations = Set{Relation{T}}()) where T + allelements = union(elements, (rel.elements for rel in relations)...) + new{T}(allelements, relations) + end end -function Base.push!(ctx::Construction{T}, elem::Point{T}) where T - push!(ctx.points, elem) +function Base.push!(ctx::Construction{T}, elem::Element{T}) where T + push!(ctx.elements, elem) end -function Base.push!(ctx::Construction{T}, elem::Sphere{T}) where T - push!(ctx.spheres, elem) +function Base.push!(ctx::Construction{T}, rel::Relation{T}) where T + push!(ctx.relations, rel) + union!(ctx.elements, rel.elements) end function realize(ctx::Construction{T}) where T # collect variable names - allcoordnames = Symbol[] - elements = vcat(ctx.points, ctx.spheres) - for (index, elem) in enumerate(elements) + coordnamelist = Symbol[] + elemenum = enumerate(ctx.elements) + for (index, elem) in elemenum subscript = Subscripts.sub(string(index)) - append!(allcoordnames, + append!(coordnamelist, [Symbol(name, subscript) for name in coordnames(elem)] ) end # construct coordinate ring - coordring, coordqueue = polynomial_ring(parent_type(T)(), allcoordnames) + coordring, coordqueue = polynomial_ring(parent_type(T)(), coordnamelist, ordering = :degrevlex) # construct coordinate vectors - for elem in elements + for (_, elem) in elemenum buildvec(elem, coordqueue) end + + # turn relations into equations + vcat( + equation.(ctx.relations), + [elem.rel for elem in ctx.elements if !isnothing(elem.rel)] + ) end end @@ -98,8 +129,14 @@ end # ~~~ sandbox setup ~~~ a = Engine.Point{Rational{Int64}}() -b = Engine.Point{Rational{Int64}}() s = Engine.Sphere{Rational{Int64}}() -ctx = Engine.Construction{Rational{Int64}}(points = [a]) +a_on_s = Engine.LiesOn{Rational{Int64}}(a, s) +ctx = Engine.Construction{Rational{Int64}}(elements = Set([a]), relations= Set([a_on_s])) +eqns_a_s = Engine.realize(ctx) + +b = Engine.Point{Rational{Int64}}() +b_on_s = Engine.LiesOn{Rational{Int64}}(b, s) Engine.push!(ctx, b) -Engine.push!(ctx, s) \ No newline at end of file +Engine.push!(ctx, s) +Engine.push!(ctx, b_on_s) +eqns_ab_s = Engine.realize(ctx) \ No newline at end of file -- 2.34.1 From 86dbd9ea45ce9908c6895355c936a29ffe00629c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 27 Jan 2024 14:21:03 -0500 Subject: [PATCH 004/129] Order variables by coordinate and then element MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In other words, order coordinates like (rₛ₁, rₛ₂, sₛ₁, sₛ₂, xₛ₁, xₛ₂, xₚ₃, yₛ₁, yₛ₂, yₚ₃, zₛ₁, zₛ₂, zₚ₃) instead of like (rₛ₁, sₛ₁, xₛ₁, yₛ₁, zₛ₁, rₛ₂, sₛ₂, xₛ₂, yₛ₂, zₛ₂, xₚ₃, yₚ₃, zₚ₃). In the test cases, this really cuts down the size of the Gröbner basis. --- engine-proto/engine.jl | 76 +++++++++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/engine-proto/engine.jl b/engine-proto/engine.jl index f672745..7e68fe4 100644 --- a/engine-proto/engine.jl +++ b/engine-proto/engine.jl @@ -12,46 +12,68 @@ using Groebner abstract type Element{T} end mutable struct Point{T} <: Element{T} - coords::Union{Vector{MPolyRingElem{T}}, Nothing} + coords::Vector{MPolyRingElem{T}} vec::Union{Vector{MPolyRingElem{T}}, Nothing} rel::Nothing ## [to do] constructor argument never needed? Point{T}( - coords::Union{Vector{MPolyRingElem{T}}, Nothing} = nothing, + coords::Vector{MPolyRingElem{T}} = MPolyRingElem{T}[], vec::Union{Vector{MPolyRingElem{T}}, Nothing} = nothing ) where T = new(coords, vec, nothing) end -coordnames(_::Point) = [:xₚ, :yₚ, :zₚ] +##coordnames(_::Point) = [:xₚ, :yₚ, :zₚ] -function buildvec(pt::Point, coordqueue) - coordring = parent(coordqueue[1]) - pt.coords = splice!(coordqueue, 1:3) +function buildvec!(pt::Point) + coordring = parent(pt.coords[1]) pt.vec = [one(coordring), dot(pt.coords, pt.coords), pt.coords...] end mutable struct Sphere{T} <: Element{T} - coords::Union{Vector{MPolyRingElem{T}}, Nothing} + coords::Vector{MPolyRingElem{T}} vec::Union{Vector{MPolyRingElem{T}}, Nothing} rel::Union{MPolyRingElem{T}, Nothing} + ## [to do] constructor argument never needed? Sphere{T}( - coords::Union{Vector{MPolyRingElem{T}}, Nothing} = nothing, + coords::Vector{MPolyRingElem{T}} = MPolyRingElem{T}[], vec::Union{Vector{MPolyRingElem{T}}, Nothing} = nothing, rel::Union{MPolyRingElem{T}, Nothing} = nothing ) where T = new(coords, vec, rel) end -coordnames(_::Sphere) = [:rₛ, :sₛ, :xₛ, :yₛ, :zₛ] +##coordnames(_::Sphere) = [:rₛ, :sₛ, :xₛ, :yₛ, :zₛ] -function buildvec(sph::Sphere, coordqueue) - coordring = parent(coordqueue[1]) - sph.coords = splice!(coordqueue, 1:5) +function buildvec!(sph::Sphere) + coordring = parent(sph.coords[1]) sph.vec = sph.coords sph.rel = mprod(sph.coords, sph.coords) + one(coordring) end +const coordnames = IdDict{Symbol, Vector{Union{Symbol, Nothing}}}( + nameof(Point) => [nothing, nothing, :xₚ, :yₚ, :zₚ], + nameof(Sphere) => [:rₛ, :sₛ, :xₛ, :yₛ, :zₛ] +) + +coordname(elem::Element, index) = coordnames[nameof(typeof(elem))][index] + +function pushcoordname!(coordnamelist, indexed_elem::Tuple{Any, Element}, coordindex) + elemindex, elem = indexed_elem + name = coordname(elem, coordindex) + if !isnothing(name) + subscript = Subscripts.sub(string(elemindex)) + push!(coordnamelist, Symbol(name, subscript)) + end +end + +function takecoord!(coordlist, indexed_elem::Tuple{Any, Element}, coordindex) + elem = indexed_elem[2] + if !isnothing(coordname(elem, coordindex)) + push!(elem.coords, popfirst!(coordlist)) + end +end + # --- primitive relations --- abstract type Relation{T} end @@ -99,22 +121,38 @@ function Base.push!(ctx::Construction{T}, rel::Relation{T}) where T end function realize(ctx::Construction{T}) where T - # collect variable names + # collect coordinate names coordnamelist = Symbol[] elemenum = enumerate(ctx.elements) - for (index, elem) in elemenum - subscript = Subscripts.sub(string(index)) - append!(coordnamelist, - [Symbol(name, subscript) for name in coordnames(elem)] - ) + for coordindex in 1:5 + for indexed_elem in elemenum + pushcoordname!(coordnamelist, indexed_elem, coordindex) + end end + display(collect(elemenum)) + display(coordnamelist) + println() + # construct coordinate ring coordring, coordqueue = polynomial_ring(parent_type(T)(), coordnamelist, ordering = :degrevlex) + # retrieve coordinates + for (_, elem) in elemenum + empty!(elem.coords) + end + for coordindex in 1:5 + for indexed_elem in elemenum + takecoord!(coordqueue, indexed_elem, coordindex) + end + end + # construct coordinate vectors for (_, elem) in elemenum - buildvec(elem, coordqueue) + buildvec!(elem) + display(elem.coords) + display(elem.vec) + println() end # turn relations into equations -- 2.34.1 From c29000d912a15896f98c33af5aefd691899721ad Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 28 Jan 2024 01:34:13 -0500 Subject: [PATCH 005/129] Write a simple solver for the hitting set problem I think we need this to find the dimension of the solution variety. --- engine-proto/hitting-set.jl | 110 ++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 engine-proto/hitting-set.jl diff --git a/engine-proto/hitting-set.jl b/engine-proto/hitting-set.jl new file mode 100644 index 0000000..e9aacf4 --- /dev/null +++ b/engine-proto/hitting-set.jl @@ -0,0 +1,110 @@ +module HittingSet + +HittingSetProblem{T} = Pair{Set{T}, Vector{Pair{T, Set{Set{T}}}}} + +# `subsets` should be a collection of Set objects +function HittingSetProblem(subsets, chosen = Set()) + wholeset = union(subsets...) + T = eltype(wholeset) + unsorted_moves = [ + elt => Set(filter(s -> elt ∉ s, subsets)) + for elt in wholeset + ] + moves = sort(unsorted_moves, by = pair -> length(pair.second)) + Set{T}(chosen) => moves +end + +function Base.display(problem::HittingSetProblem{T}) where T + println("HittingSetProblem{$T}") + + chosen = problem.first + println(" {", join(string.(chosen), ", "), "}") + + moves = problem.second + for (choice, missed) in moves + println(" | ", choice) + for s in missed + println(" | | {", join(string.(s), ", "), "}") + end + end + println() +end + +function solve(pblm::HittingSetProblem{T}, maxdepth = Inf) where T + problems = Dict(pblm) + println(typeof(problems)) + while length(first(problems).first) < maxdepth + subproblems = typeof(problems)() + for (chosen, moves) in problems + if isempty(moves) + return chosen + else + for (choice, missed) in moves + to_be_chosen = union(chosen, Set([choice])) + if isempty(missed) + return to_be_chosen + elseif !haskey(subproblems, to_be_chosen) + push!(subproblems, HittingSetProblem(missed, to_be_chosen)) + end + end + end + end + problems = subproblems + end + problems +end + +function test(n = 1) + T = [Int64, Int64, Symbol, Symbol][n] + subsets = Set{T}.([ + [ + [1, 3, 5], + [2, 3, 4], + [1, 4], + [2, 3, 4, 5], + [4, 5] + ], + # example from Amit Chakrabarti's graduate-level algorithms class (CS 105) + # notes by Valika K. Wan and Khanh Do Ba, Winter 2005 + # https://www.cs.dartmouth.edu/~ac/Teach/CS105-Winter05/ + [ + [1, 3], [1, 4], [1, 5], + [1, 3], [1, 2, 4], [1, 2, 5], + [4, 3], [ 2, 4], [ 2, 5], + [6, 3], [6, 4], [ 5] + ], + [ + [:w, :x, :y], + [:x, :y, :z], + [:w, :z], + [:x, :y] + ], + # Wikipedia showcases this as an example of a problem where the greedy + # algorithm performs especially poorly + [ + [:a, :x, :t1], + [:a, :y, :t2], + [:a, :y, :t3], + [:a, :z, :t4], + [:a, :z, :t5], + [:a, :z, :t6], + [:a, :z, :t7], + [:b, :x, :t8], + [:b, :y, :t9], + [:b, :y, :t10], + [:b, :z, :t11], + [:b, :z, :t12], + [:b, :z, :t13], + [:b, :z, :t14] + ] + ][n]) + problem = HittingSetProblem(subsets) + if isa(problem, HittingSetProblem{T}) + println("Correct type") + else + println("Wrong type: ", typeof(problem)) + end + problem +end + +end \ No newline at end of file -- 2.34.1 From 59a527af43b869da239a4c3789a52dea52a0da09 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 29 Jan 2024 12:28:45 -0500 Subject: [PATCH 006/129] Correct Minkowski product; build chain of three spheres --- engine-proto/engine.jl | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/engine-proto/engine.jl b/engine-proto/engine.jl index 7e68fe4..f2dc981 100644 --- a/engine-proto/engine.jl +++ b/engine-proto/engine.jl @@ -78,7 +78,7 @@ end abstract type Relation{T} end -mprod(v, w) = v[1]*w[2] + w[1]*v[2] - dot(v[3:end], w[3:end]) +mprod(v, w) = (v[1]*w[2] + w[1]*v[2]) / 2 - dot(v[3:end], w[3:end]) # elements: point, sphere struct LiesOn{T} <: Relation{T} @@ -94,7 +94,7 @@ struct AlignsWithBy{T} <: Relation{T} elements::Vector{Element{T}} cos_angle::T - LiesOn{T}(sph1::Point{T}, sph2::Sphere{T}, cos_angle::T) where T = new{T}([sph1, sph2], cos_angle) + AlignsWithBy{T}(sph1::Sphere{T}, sph2::Sphere{T}, cos_angle::T) where T = new{T}([sph1, sph2], cos_angle) end equation(rel::AlignsWithBy) = dot(rel.elements[1].vec, rel.elements[2].vec) - rel.cos_angle @@ -166,15 +166,28 @@ end # ~~~ sandbox setup ~~~ -a = Engine.Point{Rational{Int64}}() -s = Engine.Sphere{Rational{Int64}}() -a_on_s = Engine.LiesOn{Rational{Int64}}(a, s) -ctx = Engine.Construction{Rational{Int64}}(elements = Set([a]), relations= Set([a_on_s])) +CoeffType = Rational{Int64} + +a = Engine.Point{CoeffType}() +s = Engine.Sphere{CoeffType}() +a_on_s = Engine.LiesOn{CoeffType}(a, s) +ctx = Engine.Construction{CoeffType}(elements = Set([a]), relations= Set([a_on_s])) eqns_a_s = Engine.realize(ctx) -b = Engine.Point{Rational{Int64}}() -b_on_s = Engine.LiesOn{Rational{Int64}}(b, s) +b = Engine.Point{CoeffType}() +b_on_s = Engine.LiesOn{CoeffType}(b, s) Engine.push!(ctx, b) Engine.push!(ctx, s) Engine.push!(ctx, b_on_s) -eqns_ab_s = Engine.realize(ctx) \ No newline at end of file +eqns_ab_s = Engine.realize(ctx) + +spheres = [Engine.Sphere{CoeffType}() for _ in 1:3] +tangencies = [ + Engine.AlignsWithBy{CoeffType}( + spheres[n], + spheres[mod1(n+1, length(spheres))], + -1//1 + ) + for n in 1:3 +] +ctx_chain = Engine.Construction{CoeffType}(elements = Set(spheres), relations = Set(tangencies)) \ No newline at end of file -- 2.34.1 From 0731c7aac18fee58daa0675e84eb065c029984d7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 29 Jan 2024 12:41:07 -0500 Subject: [PATCH 007/129] Correct relation equations --- engine-proto/engine.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engine-proto/engine.jl b/engine-proto/engine.jl index f2dc981..2f7294a 100644 --- a/engine-proto/engine.jl +++ b/engine-proto/engine.jl @@ -87,7 +87,7 @@ struct LiesOn{T} <: Relation{T} LiesOn{T}(pt::Point{T}, sph::Sphere{T}) where T = new{T}([pt, sph]) end -equation(rel::LiesOn) = dot(rel.elements[1].vec, rel.elements[2].vec) +equation(rel::LiesOn) = mprod(rel.elements[1].vec, rel.elements[2].vec) # elements: sphere, sphere struct AlignsWithBy{T} <: Relation{T} @@ -97,7 +97,7 @@ struct AlignsWithBy{T} <: Relation{T} AlignsWithBy{T}(sph1::Sphere{T}, sph2::Sphere{T}, cos_angle::T) where T = new{T}([sph1, sph2], cos_angle) end -equation(rel::AlignsWithBy) = dot(rel.elements[1].vec, rel.elements[2].vec) - rel.cos_angle +equation(rel::AlignsWithBy) = mprod(rel.elements[1].vec, rel.elements[2].vec) - rel.cos_angle # --- constructions --- -- 2.34.1 From 6349f298ae723c21361f7450207b64f8e7f5e19c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 29 Jan 2024 19:11:21 -0500 Subject: [PATCH 008/129] Extend AbstractAlgebra ideals to rational coefficients The extension should also let us work over finite fields of prime order, although we don't need to do that. --- engine-proto/engine.jl | 43 ++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/engine-proto/engine.jl b/engine-proto/engine.jl index 2f7294a..b282fdd 100644 --- a/engine-proto/engine.jl +++ b/engine-proto/engine.jl @@ -7,6 +7,26 @@ using LinearAlgebra using AbstractAlgebra using Groebner +# --- commutative algebra --- + +# as of version 0.36.6, AbstractAlgebra only supports ideals in multivariate +# polynomial rings when coefficients are integers. in `reduce_gens`, the +# `lmnode` constructor requires < to be defined on the coefficients, and the +# `reducer_size` heuristic requires `ndigits` to be defined on the coefficients. +# this patch for `reducer_size` removes the `ndigits` dependency +##function Generic.reducer_size(f::T) where {U <: MPolyRingElem{<:FieldElement}, V, N, T <: Generic.lmnode{U, V, N}} +## if f.size != 0.0 +## return f.size +## end +## return 0.0 + sum(j^2 for j in 1:length(f.poly)) +##end + +# as of version 0.36.6, AbstractAlgebra only supports ideals in multivariate +# polynomial rings when the coefficients are integers. we use Groebner to extend +# support to rationals and to finite fields of prime order +Generic.reduce_gens(I::Generic.Ideal{U}) where {T <: FieldElement, U <: MPolyRingElem{T}} = + Generic.Ideal{U}(base_ring(I), groebner(gens(I))) + # --- primitve elements --- abstract type Element{T} end @@ -23,8 +43,6 @@ mutable struct Point{T} <: Element{T} ) where T = new(coords, vec, nothing) end -##coordnames(_::Point) = [:xₚ, :yₚ, :zₚ] - function buildvec!(pt::Point) coordring = parent(pt.coords[1]) pt.vec = [one(coordring), dot(pt.coords, pt.coords), pt.coords...] @@ -43,8 +61,6 @@ mutable struct Sphere{T} <: Element{T} ) where T = new(coords, vec, rel) end -##coordnames(_::Sphere) = [:rₛ, :sₛ, :xₛ, :yₛ, :zₛ] - function buildvec!(sph::Sphere) coordring = parent(sph.coords[1]) sph.vec = sph.coords @@ -130,10 +146,6 @@ function realize(ctx::Construction{T}) where T end end - display(collect(elemenum)) - display(coordnamelist) - println() - # construct coordinate ring coordring, coordqueue = polynomial_ring(parent_type(T)(), coordnamelist, ordering = :degrevlex) @@ -150,16 +162,14 @@ function realize(ctx::Construction{T}) where T # construct coordinate vectors for (_, elem) in elemenum buildvec!(elem) - display(elem.coords) - display(elem.vec) - println() end # turn relations into equations - vcat( + eqns = vcat( equation.(ctx.relations), [elem.rel for elem in ctx.elements if !isnothing(elem.rel)] ) + Generic.Ideal(coordring, eqns) end end @@ -172,22 +182,23 @@ a = Engine.Point{CoeffType}() s = Engine.Sphere{CoeffType}() a_on_s = Engine.LiesOn{CoeffType}(a, s) ctx = Engine.Construction{CoeffType}(elements = Set([a]), relations= Set([a_on_s])) -eqns_a_s = Engine.realize(ctx) +ideal_a_s = Engine.realize(ctx) b = Engine.Point{CoeffType}() b_on_s = Engine.LiesOn{CoeffType}(b, s) Engine.push!(ctx, b) Engine.push!(ctx, s) Engine.push!(ctx, b_on_s) -eqns_ab_s = Engine.realize(ctx) +ideal_ab_s = Engine.realize(ctx) spheres = [Engine.Sphere{CoeffType}() for _ in 1:3] tangencies = [ Engine.AlignsWithBy{CoeffType}( spheres[n], spheres[mod1(n+1, length(spheres))], - -1//1 + CoeffType(-1//1) ) for n in 1:3 ] -ctx_chain = Engine.Construction{CoeffType}(elements = Set(spheres), relations = Set(tangencies)) \ No newline at end of file +ctx_chain = Engine.Construction{CoeffType}(elements = Set(spheres), relations = Set(tangencies)) +ideal_chain = Engine.realize(ctx_chain) \ No newline at end of file -- 2.34.1 From 4e02ee16fc32519d0d08773c58e59c44afa79ec8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 30 Jan 2024 02:45:14 -0500 Subject: [PATCH 009/129] Find dimension of solution variety --- engine-proto/engine.jl | 31 +++++++++++++++++-------------- engine-proto/hitting-set.jl | 15 ++++++++------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/engine-proto/engine.jl b/engine-proto/engine.jl index b282fdd..6d3636d 100644 --- a/engine-proto/engine.jl +++ b/engine-proto/engine.jl @@ -1,3 +1,5 @@ +include("hitting-set.jl") + module Engine export Construction, mprod @@ -6,27 +8,25 @@ import Subscripts using LinearAlgebra using AbstractAlgebra using Groebner +using ..HittingSet # --- commutative algebra --- -# as of version 0.36.6, AbstractAlgebra only supports ideals in multivariate -# polynomial rings when coefficients are integers. in `reduce_gens`, the -# `lmnode` constructor requires < to be defined on the coefficients, and the -# `reducer_size` heuristic requires `ndigits` to be defined on the coefficients. -# this patch for `reducer_size` removes the `ndigits` dependency -##function Generic.reducer_size(f::T) where {U <: MPolyRingElem{<:FieldElement}, V, N, T <: Generic.lmnode{U, V, N}} -## if f.size != 0.0 -## return f.size -## end -## return 0.0 + sum(j^2 for j in 1:length(f.poly)) -##end - # as of version 0.36.6, AbstractAlgebra only supports ideals in multivariate # polynomial rings when the coefficients are integers. we use Groebner to extend # support to rationals and to finite fields of prime order Generic.reduce_gens(I::Generic.Ideal{U}) where {T <: FieldElement, U <: MPolyRingElem{T}} = Generic.Ideal{U}(base_ring(I), groebner(gens(I))) +function codimension(I::Generic.Ideal{U}, maxdepth = Inf) where {T <: RingElement, U <: MPolyRingElem{T}} + leading = [exponent_vector(f, 1) for f in gens(I)] + targets = [Set(findall(.!iszero.(exp_vec))) for exp_vec in leading] + length(HittingSet.solve(HittingSetProblem(targets), maxdepth)) +end + +dimension(I::Generic.Ideal{U}, maxdepth = Inf) where {T <: RingElement, U <: MPolyRingElem{T}} = + length(gens(base_ring(I))) - codimension(I, maxdepth) + # --- primitve elements --- abstract type Element{T} end @@ -183,6 +183,7 @@ s = Engine.Sphere{CoeffType}() a_on_s = Engine.LiesOn{CoeffType}(a, s) ctx = Engine.Construction{CoeffType}(elements = Set([a]), relations= Set([a_on_s])) ideal_a_s = Engine.realize(ctx) +println("A point on a sphere: ", Engine.dimension(ideal_a_s), " degrees of freeom") b = Engine.Point{CoeffType}() b_on_s = Engine.LiesOn{CoeffType}(b, s) @@ -190,6 +191,7 @@ Engine.push!(ctx, b) Engine.push!(ctx, s) Engine.push!(ctx, b_on_s) ideal_ab_s = Engine.realize(ctx) +println("Two points on a sphere: ", Engine.dimension(ideal_ab_s), " degrees of freeom") spheres = [Engine.Sphere{CoeffType}() for _ in 1:3] tangencies = [ @@ -200,5 +202,6 @@ tangencies = [ ) for n in 1:3 ] -ctx_chain = Engine.Construction{CoeffType}(elements = Set(spheres), relations = Set(tangencies)) -ideal_chain = Engine.realize(ctx_chain) \ No newline at end of file +ctx_tan_sph = Engine.Construction{CoeffType}(elements = Set(spheres), relations = Set(tangencies)) +ideal_tan_sph = Engine.realize(ctx_tan_sph) +println("Three mutually tangent spheres: ", Engine.dimension(ideal_tan_sph), " degrees of freeom") \ No newline at end of file diff --git a/engine-proto/hitting-set.jl b/engine-proto/hitting-set.jl index e9aacf4..347c4d2 100644 --- a/engine-proto/hitting-set.jl +++ b/engine-proto/hitting-set.jl @@ -1,13 +1,15 @@ module HittingSet +export HittingSetProblem, solve + HittingSetProblem{T} = Pair{Set{T}, Vector{Pair{T, Set{Set{T}}}}} -# `subsets` should be a collection of Set objects -function HittingSetProblem(subsets, chosen = Set()) - wholeset = union(subsets...) +# `targets` should be a collection of Set objects +function HittingSetProblem(targets, chosen = Set()) + wholeset = union(targets...) T = eltype(wholeset) unsorted_moves = [ - elt => Set(filter(s -> elt ∉ s, subsets)) + elt => Set(filter(s -> elt ∉ s, targets)) for elt in wholeset ] moves = sort(unsorted_moves, by = pair -> length(pair.second)) @@ -32,7 +34,6 @@ end function solve(pblm::HittingSetProblem{T}, maxdepth = Inf) where T problems = Dict(pblm) - println(typeof(problems)) while length(first(problems).first) < maxdepth subproblems = typeof(problems)() for (chosen, moves) in problems @@ -56,7 +57,7 @@ end function test(n = 1) T = [Int64, Int64, Symbol, Symbol][n] - subsets = Set{T}.([ + targets = Set{T}.([ [ [1, 3, 5], [2, 3, 4], @@ -98,7 +99,7 @@ function test(n = 1) [:b, :z, :t14] ] ][n]) - problem = HittingSetProblem(subsets) + problem = HittingSetProblem(targets) if isa(problem, HittingSetProblem{T}) println("Correct type") else -- 2.34.1 From 65d23fb6676c73aad5817941c94d75573b6e4d10 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 30 Jan 2024 02:49:33 -0500 Subject: [PATCH 010/129] Use module names as filenames You're right: this naming convention seems to be standard for Julia modules now. --- engine-proto/{engine.jl => Engine.jl} | 2 +- engine-proto/{hitting-set.jl => HittingSet.jl} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename engine-proto/{engine.jl => Engine.jl} (99%) rename engine-proto/{hitting-set.jl => HittingSet.jl} (100%) diff --git a/engine-proto/engine.jl b/engine-proto/Engine.jl similarity index 99% rename from engine-proto/engine.jl rename to engine-proto/Engine.jl index 6d3636d..a632581 100644 --- a/engine-proto/engine.jl +++ b/engine-proto/Engine.jl @@ -1,4 +1,4 @@ -include("hitting-set.jl") +include("HittingSet.jl") module Engine diff --git a/engine-proto/hitting-set.jl b/engine-proto/HittingSet.jl similarity index 100% rename from engine-proto/hitting-set.jl rename to engine-proto/HittingSet.jl -- 2.34.1 From a3f3f6a31bde0f28c01cb35c9ad18f7719232d90 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 1 Feb 2024 16:13:22 -0500 Subject: [PATCH 011/129] Order spheres before points within each coordinate block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the cases I've tried so far, this leads to substantially smaller Gröbner bases. --- engine-proto/Engine.jl | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index a632581..1977e99 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -118,28 +118,39 @@ equation(rel::AlignsWithBy) = mprod(rel.elements[1].vec, rel.elements[2].vec) - # --- constructions --- mutable struct Construction{T} - elements::Set{Element{T}} + points::Set{Point{T}} + spheres::Set{Sphere{T}} relations::Set{Relation{T}} function Construction{T}(; elements = Set{Element{T}}(), relations = Set{Relation{T}}()) where T allelements = union(elements, (rel.elements for rel in relations)...) - new{T}(allelements, relations) + new{T}( + filter(elt -> isa(elt, Point), allelements), + filter(elt -> isa(elt, Sphere), allelements), + relations + ) end end -function Base.push!(ctx::Construction{T}, elem::Element{T}) where T - push!(ctx.elements, elem) +function Base.push!(ctx::Construction{T}, elem::Point{T}) where T + push!(ctx.points, elem) +end + +function Base.push!(ctx::Construction{T}, elem::Sphere{T}) where T + push!(ctx.spheres, elem) end function Base.push!(ctx::Construction{T}, rel::Relation{T}) where T push!(ctx.relations, rel) - union!(ctx.elements, rel.elements) + for elt in rel.elements + push!(ctx, elt) + end end function realize(ctx::Construction{T}) where T # collect coordinate names coordnamelist = Symbol[] - elemenum = enumerate(ctx.elements) + elemenum = enumerate(Iterators.flatten((ctx.spheres, ctx.points))) for coordindex in 1:5 for indexed_elem in elemenum pushcoordname!(coordnamelist, indexed_elem, coordindex) @@ -167,7 +178,7 @@ function realize(ctx::Construction{T}) where T # turn relations into equations eqns = vcat( equation.(ctx.relations), - [elem.rel for elem in ctx.elements if !isnothing(elem.rel)] + [elem.rel for (_, elem) in elemenum if !isnothing(elem.rel)] ) Generic.Ideal(coordring, eqns) end -- 2.34.1 From 21f09c4a4dfbea36ed28088030480bd7b6be22cb Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 4 Feb 2024 16:08:13 -0500 Subject: [PATCH 012/129] Switch element abbreviation from "elem" to "elt" --- engine-proto/Engine.jl | 46 +++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index 1977e99..72b5923 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -72,21 +72,21 @@ const coordnames = IdDict{Symbol, Vector{Union{Symbol, Nothing}}}( nameof(Sphere) => [:rₛ, :sₛ, :xₛ, :yₛ, :zₛ] ) -coordname(elem::Element, index) = coordnames[nameof(typeof(elem))][index] +coordname(elt::Element, index) = coordnames[nameof(typeof(elt))][index] -function pushcoordname!(coordnamelist, indexed_elem::Tuple{Any, Element}, coordindex) - elemindex, elem = indexed_elem - name = coordname(elem, coordindex) +function pushcoordname!(coordnamelist, indexed_elt::Tuple{Any, Element}, coordindex) + eltindex, elt = indexed_elt + name = coordname(elt, coordindex) if !isnothing(name) - subscript = Subscripts.sub(string(elemindex)) + subscript = Subscripts.sub(string(eltindex)) push!(coordnamelist, Symbol(name, subscript)) end end -function takecoord!(coordlist, indexed_elem::Tuple{Any, Element}, coordindex) - elem = indexed_elem[2] - if !isnothing(coordname(elem, coordindex)) - push!(elem.coords, popfirst!(coordlist)) +function takecoord!(coordlist, indexed_elt::Tuple{Any, Element}, coordindex) + elt = indexed_elt[2] + if !isnothing(coordname(elt, coordindex)) + push!(elt.coords, popfirst!(coordlist)) end end @@ -132,12 +132,12 @@ mutable struct Construction{T} end end -function Base.push!(ctx::Construction{T}, elem::Point{T}) where T - push!(ctx.points, elem) +function Base.push!(ctx::Construction{T}, elt::Point{T}) where T + push!(ctx.points, elt) end -function Base.push!(ctx::Construction{T}, elem::Sphere{T}) where T - push!(ctx.spheres, elem) +function Base.push!(ctx::Construction{T}, elt::Sphere{T}) where T + push!(ctx.spheres, elt) end function Base.push!(ctx::Construction{T}, rel::Relation{T}) where T @@ -150,10 +150,10 @@ end function realize(ctx::Construction{T}) where T # collect coordinate names coordnamelist = Symbol[] - elemenum = enumerate(Iterators.flatten((ctx.spheres, ctx.points))) + eltenum = enumerate(Iterators.flatten((ctx.spheres, ctx.points))) for coordindex in 1:5 - for indexed_elem in elemenum - pushcoordname!(coordnamelist, indexed_elem, coordindex) + for indexed_elt in eltenum + pushcoordname!(coordnamelist, indexed_elt, coordindex) end end @@ -161,24 +161,24 @@ function realize(ctx::Construction{T}) where T coordring, coordqueue = polynomial_ring(parent_type(T)(), coordnamelist, ordering = :degrevlex) # retrieve coordinates - for (_, elem) in elemenum - empty!(elem.coords) + for (_, elt) in eltenum + empty!(elt.coords) end for coordindex in 1:5 - for indexed_elem in elemenum - takecoord!(coordqueue, indexed_elem, coordindex) + for indexed_elt in eltenum + takecoord!(coordqueue, indexed_elt, coordindex) end end # construct coordinate vectors - for (_, elem) in elemenum - buildvec!(elem) + for (_, elt) in eltenum + buildvec!(elt) end # turn relations into equations eqns = vcat( equation.(ctx.relations), - [elem.rel for (_, elem) in elemenum if !isnothing(elem.rel)] + [elt.rel for (_, elt) in eltenum if !isnothing(elt.rel)], ) Generic.Ideal(coordring, eqns) end -- 2.34.1 From 43cbf8a3a0c3e2152306876e8481fbcab797f7f8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 5 Feb 2024 00:10:13 -0500 Subject: [PATCH 013/129] Add relations to center and orient the construction --- engine-proto/Engine.jl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index 72b5923..ac9ed35 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -178,8 +178,17 @@ function realize(ctx::Construction{T}) where T # turn relations into equations eqns = vcat( equation.(ctx.relations), - [elt.rel for (_, elt) in eltenum if !isnothing(elt.rel)], + [elt.rel for (_, elt) in eltenum if !isnothing(elt.rel)] ) + + # add relations to center and orient the construction + if !isempty(ctx.points) + append!(eqns, [sum(pt.coords[k] for pt in ctx.points) for k in 1:3]) + end + if !isempty(ctx.spheres) + append!(eqns, [sum(sph.coords[k] for sph in ctx.spheres) for k in 3:4]) + end + Generic.Ideal(coordring, eqns) end -- 2.34.1 From 45aaaafc8f44a33cf6fb74c1cbc3a09771785015 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 8 Feb 2024 01:53:55 -0500 Subject: [PATCH 014/129] Seek sample solutions by cutting with a hyperplane The example hyperplane yields a single solution, with multiplicity six. You can find it analytically by hand, and homotopy continuation finds it numerically. --- engine-proto/Engine.jl | 99 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 17 deletions(-) diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index ac9ed35..b5eee96 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -2,12 +2,13 @@ include("HittingSet.jl") module Engine -export Construction, mprod +export Construction, mprod, codimension, dimension import Subscripts using LinearAlgebra using AbstractAlgebra using Groebner +using HomotopyContinuation: Variable, Expression, System using ..HittingSet # --- commutative algebra --- @@ -27,6 +28,34 @@ end dimension(I::Generic.Ideal{U}, maxdepth = Inf) where {T <: RingElement, U <: MPolyRingElem{T}} = length(gens(base_ring(I))) - codimension(I, maxdepth) +# hat tip Sascha Timme +# https://github.com/JuliaHomotopyContinuation/HomotopyContinuation.jl/issues/520#issuecomment-1317681521 +function Base.convert(::Type{Expression}, f::MPolyRingElem) + variables = Variable.(symbols(parent(f))) + f_data = zip(coefficients(f), exponent_vectors(f)) + sum(cf * prod(variables .^ exp_vec) for (cf, exp_vec) in f_data) +end + +# create a ModelKit.System from an ideal in a multivariate polynomial ring. the +# variable ordering is taken from the polynomial ring +function System(I::Generic.Ideal) + eqns = Expression.(gens(I)) + variables = Variable.(symbols(base_ring(I))) + System(eqns, variables = variables) +end + +## [to do] not needed right now +# create a ModelKit.System from a list of elements of a multivariate polynomial +# ring. the variable ordering is taken from the polynomial ring +##function System(eqns::AbstractVector{MPolyRingElem}) +## if isempty(eqns) +## return System([]) +## else +## variables = Variable.(symbols(parent(f))) +## return System(Expression.(eqns), variables = variables) +## end +##end + # --- primitve elements --- abstract type Element{T} end @@ -189,39 +218,75 @@ function realize(ctx::Construction{T}) where T append!(eqns, [sum(sph.coords[k] for sph in ctx.spheres) for k in 3:4]) end - Generic.Ideal(coordring, eqns) + (Generic.Ideal(coordring, eqns), eqns) end end # ~~~ sandbox setup ~~~ +using AbstractAlgebra +using HomotopyContinuation + CoeffType = Rational{Int64} a = Engine.Point{CoeffType}() s = Engine.Sphere{CoeffType}() a_on_s = Engine.LiesOn{CoeffType}(a, s) ctx = Engine.Construction{CoeffType}(elements = Set([a]), relations= Set([a_on_s])) -ideal_a_s = Engine.realize(ctx) -println("A point on a sphere: ", Engine.dimension(ideal_a_s), " degrees of freeom") +##ideal_a_s = Engine.realize(ctx) +##println("A point on a sphere: ", Engine.dimension(ideal_a_s), " degrees of freedom") b = Engine.Point{CoeffType}() b_on_s = Engine.LiesOn{CoeffType}(b, s) Engine.push!(ctx, b) Engine.push!(ctx, s) Engine.push!(ctx, b_on_s) -ideal_ab_s = Engine.realize(ctx) -println("Two points on a sphere: ", Engine.dimension(ideal_ab_s), " degrees of freeom") +ideal_ab_s, eqns_ab_s = Engine.realize(ctx) +println("Two points on a sphere: ", Engine.dimension(ideal_ab_s), " degrees of freedom") -spheres = [Engine.Sphere{CoeffType}() for _ in 1:3] -tangencies = [ - Engine.AlignsWithBy{CoeffType}( - spheres[n], - spheres[mod1(n+1, length(spheres))], - CoeffType(-1//1) - ) - for n in 1:3 +##spheres = [Engine.Sphere{CoeffType}() for _ in 1:3] +##tangencies = [ +## Engine.AlignsWithBy{CoeffType}( +## spheres[n], +## spheres[mod1(n+1, length(spheres))], +## CoeffType(-1//1) +## ) +## for n in 1:3 +##] +##ctx_tan_sph = Engine.Construction{CoeffType}(elements = Set(spheres), relations = Set(tangencies)) +##ideal_tan_sph = Engine.realize(ctx_tan_sph) +##println("Three mutually tangent spheres: ", Engine.dimension(ideal_tan_sph), " degrees of freedom") + +# --- test rational cut --- + +cut = [ + sum(vcat(a.coords, (s.coords - [0, 0, 0, 0, 1]))) + sum(vcat([2, 1, 1] .* a.coords, [1, 2, 1, 1, 1] .* s.coords - [0, 0, 0, 0, 1])) + sum(vcat([1, 2, 0] .* a.coords, [1, 1, 0, 1, 2] .* s.coords - [0, 0, 0, 0, 1])) ] -ctx_tan_sph = Engine.Construction{CoeffType}(elements = Set(spheres), relations = Set(tangencies)) -ideal_tan_sph = Engine.realize(ctx_tan_sph) -println("Three mutually tangent spheres: ", Engine.dimension(ideal_tan_sph), " degrees of freeom") \ No newline at end of file +cut_ideal_ab_s = Generic.Ideal(base_ring(ideal_ab_s), [gens(ideal_ab_s); cut]) +cut_dim = Engine.dimension(cut_ideal_ab_s) +println("Two points on a sphere, after cut: ", cut_dim, " degrees of freedom") +if cut_dim == 0 + vbls = Variable.(symbols(base_ring(ideal_ab_s))) + cut_system = System([eqns_ab_s; cut], variables = vbls) + cut_result = HomotopyContinuation.solve(cut_system) + println("non-singular solutions:") + for soln in solutions(cut_result) + display(soln) + end + println("singular solutions:") + for sing in singular(cut_result) + display(sing.solution) + end + + # test corresponding witness set + cut_matrix = [1 1 1 1 0 1 1 0 1 1 0; 1 2 1 2 0 1 1 0 1 1 0; 1 1 0 1 0 1 2 0 2 0 0] + cut_subspace = LinearSubspace(cut_matrix, [1, 1, 1]) + witness = witness_set(System(eqns_ab_s, variables = vbls), cut_subspace) + println("witness solutions:") + for wtns in solutions(witness) + display(wtns) + end +end \ No newline at end of file -- 2.34.1 From f97090c9974cb7d3b33b06396f5aeb84dcf620de Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 8 Feb 2024 01:58:12 -0500 Subject: [PATCH 015/129] Try a cut that goes through the trivial solution The previous cut was supposed to do this, but I was missing some parentheses. --- engine-proto/Engine.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index b5eee96..41d3ed7 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -262,8 +262,8 @@ println("Two points on a sphere: ", Engine.dimension(ideal_ab_s), " degrees of f cut = [ sum(vcat(a.coords, (s.coords - [0, 0, 0, 0, 1]))) - sum(vcat([2, 1, 1] .* a.coords, [1, 2, 1, 1, 1] .* s.coords - [0, 0, 0, 0, 1])) - sum(vcat([1, 2, 0] .* a.coords, [1, 1, 0, 1, 2] .* s.coords - [0, 0, 0, 0, 1])) + sum(vcat([2, 1, 1] .* a.coords, [1, 2, 1, 1, 1] .* (s.coords - [0, 0, 0, 0, 1]))) + sum(vcat([1, 2, 0] .* a.coords, [1, 1, 0, 1, 2] .* (s.coords - [0, 0, 0, 0, 1]))) ] cut_ideal_ab_s = Generic.Ideal(base_ring(ideal_ab_s), [gens(ideal_ab_s); cut]) cut_dim = Engine.dimension(cut_ideal_ab_s) @@ -283,7 +283,7 @@ if cut_dim == 0 # test corresponding witness set cut_matrix = [1 1 1 1 0 1 1 0 1 1 0; 1 2 1 2 0 1 1 0 1 1 0; 1 1 0 1 0 1 2 0 2 0 0] - cut_subspace = LinearSubspace(cut_matrix, [1, 1, 1]) + cut_subspace = LinearSubspace(cut_matrix, [1, 1, 2]) witness = witness_set(System(eqns_ab_s, variables = vbls), cut_subspace) println("witness solutions:") for wtns in solutions(witness) -- 2.34.1 From 95c0ff14b249f4033864859fde61336f2ced5962 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 9 Feb 2024 17:09:43 -0500 Subject: [PATCH 016/129] Show explicitly that all coefficients are 1 in first cut equation --- engine-proto/Engine.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index 41d3ed7..38ed672 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -261,7 +261,7 @@ println("Two points on a sphere: ", Engine.dimension(ideal_ab_s), " degrees of f # --- test rational cut --- cut = [ - sum(vcat(a.coords, (s.coords - [0, 0, 0, 0, 1]))) + sum(vcat([1, 1, 1] .* a.coords, [1, 1, 1, 1, 1] .* (s.coords - [0, 0, 0, 0, 1]))) sum(vcat([2, 1, 1] .* a.coords, [1, 2, 1, 1, 1] .* (s.coords - [0, 0, 0, 0, 1]))) sum(vcat([1, 2, 0] .* a.coords, [1, 1, 0, 1, 2] .* (s.coords - [0, 0, 0, 0, 1]))) ] -- 2.34.1 From 34358a872800810975e23f8499efd201982d4641 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 9 Feb 2024 23:44:10 -0500 Subject: [PATCH 017/129] Find witnesses on random rational hyperplanes Choose hyperplanes that go through the trivial solution. --- engine-proto/Engine.jl | 83 ++++++++++++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index 38ed672..546bf21 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -225,6 +225,8 @@ end # ~~~ sandbox setup ~~~ +using Random +using Distributions using AbstractAlgebra using HomotopyContinuation @@ -243,7 +245,8 @@ Engine.push!(ctx, b) Engine.push!(ctx, s) Engine.push!(ctx, b_on_s) ideal_ab_s, eqns_ab_s = Engine.realize(ctx) -println("Two points on a sphere: ", Engine.dimension(ideal_ab_s), " degrees of freedom") +freedom = Engine.dimension(ideal_ab_s) +println("Two points on a sphere: ", freedom, " degrees of freedom") ##spheres = [Engine.Sphere{CoeffType}() for _ in 1:3] ##tangencies = [ @@ -260,33 +263,67 @@ println("Two points on a sphere: ", Engine.dimension(ideal_ab_s), " degrees of f # --- test rational cut --- +cut_coeffs = [ + 1 1 1 0 0 0 1 1 1 1 1; + 2 1 1 0 0 0 1 2 1 1 1; + 1 2 0 0 0 0 1 1 0 1 2 +] cut = [ - sum(vcat([1, 1, 1] .* a.coords, [1, 1, 1, 1, 1] .* (s.coords - [0, 0, 0, 0, 1]))) - sum(vcat([2, 1, 1] .* a.coords, [1, 2, 1, 1, 1] .* (s.coords - [0, 0, 0, 0, 1]))) - sum(vcat([1, 2, 0] .* a.coords, [1, 1, 0, 1, 2] .* (s.coords - [0, 0, 0, 0, 1]))) + sum(vcat(cf[1:3] .* a.coords, cf[4:6] .* b.coords, cf[7:end] .* (s.coords - [0, 0, 0, 0, 1]))) + for cf in eachrow(cut_coeffs) ] cut_ideal_ab_s = Generic.Ideal(base_ring(ideal_ab_s), [gens(ideal_ab_s); cut]) -cut_dim = Engine.dimension(cut_ideal_ab_s) -println("Two points on a sphere, after cut: ", cut_dim, " degrees of freedom") -if cut_dim == 0 - vbls = Variable.(symbols(base_ring(ideal_ab_s))) +cut_freedom = Engine.dimension(cut_ideal_ab_s) +println("Two points on a sphere, after cut: ", cut_freedom, " degrees of freedom") +if cut_freedom == 0 + coordring = base_ring(ideal_ab_s) + vbls = Variable.(symbols(coordring)) cut_system = System([eqns_ab_s; cut], variables = vbls) - cut_result = HomotopyContinuation.solve(cut_system) - println("non-singular solutions:") - for soln in solutions(cut_result) - display(soln) - end - println("singular solutions:") - for sing in singular(cut_result) - display(sing.solution) - end + ##cut_result = HomotopyContinuation.solve(cut_system) + ##println("non-singular solutions:") + ##for soln in solutions(cut_result) + ## display(soln) + ##end + ##println("singular solutions:") + ##for sing in singular(cut_result) + ## display(sing.solution) + ##end - # test corresponding witness set - cut_matrix = [1 1 1 1 0 1 1 0 1 1 0; 1 2 1 2 0 1 1 0 1 1 0; 1 1 0 1 0 1 2 0 2 0 0] - cut_subspace = LinearSubspace(cut_matrix, [1, 1, 2]) - witness = witness_set(System(eqns_ab_s, variables = vbls), cut_subspace) + # test a random witness set + max_slope = 2 + binom = Binomial(2max_slope, 1/2) + Random.seed!(6071) + samples = [] + for _ in 1:3 + cut_matrix = rand(binom, freedom, length(gens(coordring))) .- max_slope + ##cut_matrix = [ + ## 1 1 1 1 0 1 1 0 1 1 0; + ## 1 2 1 2 0 1 1 0 1 1 0; + ## 1 1 0 1 0 1 2 0 2 0 0 + ##] + sph_z_ind = indexin([sph.coords[5] for sph in ctx.spheres], gens(coordring)) + cut_offset = [sum(cf[sph_z_ind]) for cf in eachrow(cut_matrix)] + println("sphere z variables: ", vbls[sph_z_ind]) + display(cut_matrix) + display(cut_offset) + cut_subspace = LinearSubspace(cut_matrix, cut_offset) + wtns = witness_set(System(eqns_ab_s, variables = vbls), cut_subspace) + append!(samples, solution.(filter(isreal, results(wtns)))) + end println("witness solutions:") - for wtns in solutions(witness) - display(wtns) + for soln in samples + display([vbls round.(soln, digits = 6)]) + k_sq = abs2(soln[1]) + if abs2(soln[end-2]) > 1e-12 + if k_sq < 1e-12 + println("center at infinity: z coordinates $(round(soln[end], digits = 6)) and $(round(soln[end-1], digits = 6))}") + else + sum_sq = soln[4]^2 + soln[7]^2 + soln[end-2]^2 / k_sq + println("center on z axis: r² = $(round(1/k_sq, digits = 6)), x² + y² + h² = $(round(sum_sq, digits = 6))") + end + else + sum_sq = sum(soln[[4, 7, 10]] .^ 2) + println("center at origin: r² = $(round(1/k_sq, digits = 6)); x² + y² + z² = $(round(sum_sq, digits = 6))") + end end end \ No newline at end of file -- 2.34.1 From becefe0c47253f0d7017f8e045a8dbadb761a52f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 10 Feb 2024 00:59:50 -0500 Subject: [PATCH 018/129] Try switching to compiled system --- engine-proto/Engine.jl | 107 ++++++++++++++++++----------------------- 1 file changed, 47 insertions(+), 60 deletions(-) diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index 546bf21..4bce0d7 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -263,67 +263,54 @@ println("Two points on a sphere: ", freedom, " degrees of freedom") # --- test rational cut --- -cut_coeffs = [ - 1 1 1 0 0 0 1 1 1 1 1; - 2 1 1 0 0 0 1 2 1 1 1; - 1 2 0 0 0 0 1 1 0 1 2 -] -cut = [ - sum(vcat(cf[1:3] .* a.coords, cf[4:6] .* b.coords, cf[7:end] .* (s.coords - [0, 0, 0, 0, 1]))) - for cf in eachrow(cut_coeffs) -] -cut_ideal_ab_s = Generic.Ideal(base_ring(ideal_ab_s), [gens(ideal_ab_s); cut]) -cut_freedom = Engine.dimension(cut_ideal_ab_s) -println("Two points on a sphere, after cut: ", cut_freedom, " degrees of freedom") -if cut_freedom == 0 - coordring = base_ring(ideal_ab_s) - vbls = Variable.(symbols(coordring)) - cut_system = System([eqns_ab_s; cut], variables = vbls) - ##cut_result = HomotopyContinuation.solve(cut_system) - ##println("non-singular solutions:") - ##for soln in solutions(cut_result) - ## display(soln) - ##end - ##println("singular solutions:") - ##for sing in singular(cut_result) - ## display(sing.solution) - ##end - - # test a random witness set - max_slope = 2 - binom = Binomial(2max_slope, 1/2) - Random.seed!(6071) - samples = [] - for _ in 1:3 - cut_matrix = rand(binom, freedom, length(gens(coordring))) .- max_slope - ##cut_matrix = [ - ## 1 1 1 1 0 1 1 0 1 1 0; - ## 1 2 1 2 0 1 1 0 1 1 0; - ## 1 1 0 1 0 1 2 0 2 0 0 - ##] - sph_z_ind = indexin([sph.coords[5] for sph in ctx.spheres], gens(coordring)) - cut_offset = [sum(cf[sph_z_ind]) for cf in eachrow(cut_matrix)] - println("sphere z variables: ", vbls[sph_z_ind]) - display(cut_matrix) - display(cut_offset) - cut_subspace = LinearSubspace(cut_matrix, cut_offset) - wtns = witness_set(System(eqns_ab_s, variables = vbls), cut_subspace) - append!(samples, solution.(filter(isreal, results(wtns)))) - end - println("witness solutions:") - for soln in samples - display([vbls round.(soln, digits = 6)]) - k_sq = abs2(soln[1]) - if abs2(soln[end-2]) > 1e-12 - if k_sq < 1e-12 - println("center at infinity: z coordinates $(round(soln[end], digits = 6)) and $(round(soln[end-1], digits = 6))}") - else - sum_sq = soln[4]^2 + soln[7]^2 + soln[end-2]^2 / k_sq - println("center on z axis: r² = $(round(1/k_sq, digits = 6)), x² + y² + h² = $(round(sum_sq, digits = 6))") - end +coordring = base_ring(ideal_ab_s) +vbls = Variable.(symbols(coordring)) +##cut_system = CompiledSystem(System([eqns_ab_s; cut], variables = vbls)) +##cut_result = HomotopyContinuation.solve(cut_system) +##println("non-singular solutions:") +##for soln in solutions(cut_result) +## display(soln) +##end +##println("singular solutions:") +##for sing in singular(cut_result) +## display(sing.solution) +##end + +# test a random witness set +system = CompiledSystem(System(eqns_ab_s, variables = vbls)) +max_slope = 2 +binom = Binomial(2max_slope, 1/2) +Random.seed!(6071) +samples = [] +for _ in 1:3 + cut_matrix = rand(binom, freedom, length(gens(coordring))) .- max_slope + ##cut_matrix = [ + ## 1 1 1 1 0 1 1 0 1 1 0; + ## 1 2 1 2 0 1 1 0 1 1 0; + ## 1 1 0 1 0 1 2 0 2 0 0 + ##] + sph_z_ind = indexin([sph.coords[5] for sph in ctx.spheres], gens(coordring)) + cut_offset = [sum(cf[sph_z_ind]) for cf in eachrow(cut_matrix)] + println("sphere z variables: ", vbls[sph_z_ind]) + display(cut_matrix) + display(cut_offset) + cut_subspace = LinearSubspace(cut_matrix, cut_offset) + wtns = witness_set(system, cut_subspace) + append!(samples, solution.(filter(isreal, results(wtns)))) +end +println("witness solutions:") +for soln in samples + display([vbls round.(soln, digits = 6)]) + k_sq = abs2(soln[1]) + if abs2(soln[end-2]) > 1e-12 + if k_sq < 1e-12 + println("center at infinity: z coordinates $(round(soln[end], digits = 6)) and $(round(soln[end-1], digits = 6))}") else - sum_sq = sum(soln[[4, 7, 10]] .^ 2) - println("center at origin: r² = $(round(1/k_sq, digits = 6)); x² + y² + z² = $(round(sum_sq, digits = 6))") + sum_sq = soln[4]^2 + soln[7]^2 + soln[end-2]^2 / k_sq + println("center on z axis: r² = $(round(1/k_sq, digits = 6)), x² + y² + h² = $(round(sum_sq, digits = 6))") end + else + sum_sq = sum(soln[[4, 7, 10]] .^ 2) + println("center at origin: r² = $(round(1/k_sq, digits = 6)); x² + y² + z² = $(round(sum_sq, digits = 6))") end end \ No newline at end of file -- 2.34.1 From 06872a04afb2b211550b471012d2ade5b92bd0d6 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 10 Feb 2024 01:06:06 -0500 Subject: [PATCH 019/129] Say how many sample solutions we found --- engine-proto/Engine.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index 4bce0d7..9841223 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -298,7 +298,7 @@ for _ in 1:3 wtns = witness_set(system, cut_subspace) append!(samples, solution.(filter(isreal, results(wtns)))) end -println("witness solutions:") +println("$(length(samples)) sample solutions:") for soln in samples display([vbls round.(soln, digits = 6)]) k_sq = abs2(soln[1]) -- 2.34.1 From 8e33987f596ae9572b53885f5b4642105d45165c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 10 Feb 2024 13:46:01 -0500 Subject: [PATCH 020/129] Systematically try out different cut planes --- engine-proto/Engine.jl | 80 +++++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index 9841223..ba4300c 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -227,6 +227,7 @@ end using Random using Distributions +using LinearAlgebra using AbstractAlgebra using HomotopyContinuation @@ -278,39 +279,60 @@ vbls = Variable.(symbols(coordring)) # test a random witness set system = CompiledSystem(System(eqns_ab_s, variables = vbls)) -max_slope = 2 +sph_z_ind = indexin([sph.coords[5] for sph in ctx.spheres], gens(coordring)) +println("sphere z variables: ", vbls[sph_z_ind]) +trivial_soln = fill(0, length(gens(coordring))) +trivial_soln[sph_z_ind] .= 1 +println("trivial solutions: $trivial_soln") +norm2 = vec -> real(dot(conj.(vec), vec)) +is_nontrivial = soln -> norm2(abs.(real.(soln)) - trivial_soln) > 1e-4*length(gens(coordring)) +max_slope = 5 binom = Binomial(2max_slope, 1/2) Random.seed!(6071) -samples = [] -for _ in 1:3 - cut_matrix = rand(binom, freedom, length(gens(coordring))) .- max_slope - ##cut_matrix = [ - ## 1 1 1 1 0 1 1 0 1 1 0; - ## 1 2 1 2 0 1 1 0 1 1 0; - ## 1 1 0 1 0 1 2 0 2 0 0 - ##] - sph_z_ind = indexin([sph.coords[5] for sph in ctx.spheres], gens(coordring)) - cut_offset = [sum(cf[sph_z_ind]) for cf in eachrow(cut_matrix)] - println("sphere z variables: ", vbls[sph_z_ind]) - display(cut_matrix) - display(cut_offset) - cut_subspace = LinearSubspace(cut_matrix, cut_offset) - wtns = witness_set(system, cut_subspace) - append!(samples, solution.(filter(isreal, results(wtns)))) -end -println("$(length(samples)) sample solutions:") -for soln in samples - display([vbls round.(soln, digits = 6)]) - k_sq = abs2(soln[1]) - if abs2(soln[end-2]) > 1e-12 - if k_sq < 1e-12 - println("center at infinity: z coordinates $(round(soln[end], digits = 6)) and $(round(soln[end-1], digits = 6))}") +n_planes = 36 +for through_trivial in [false, true] + samples = [] + for _ in 1:n_planes + cut_matrix = rand(binom, freedom, length(gens(coordring))) .- max_slope + ##cut_matrix = [ + ## 1 1 1 1 0 1 1 0 1 1 0; + ## 1 2 1 2 0 1 1 0 1 1 0; + ## 1 1 0 1 0 1 2 0 2 0 0 + ##] + ## [verbose] display(cut_matrix) + if through_trivial + cut_offset = [sum(cf[sph_z_ind]) for cf in eachrow(cut_matrix)] + ## [verbose] display(cut_offset) + cut_subspace = LinearSubspace(cut_matrix, cut_offset) else - sum_sq = soln[4]^2 + soln[7]^2 + soln[end-2]^2 / k_sq - println("center on z axis: r² = $(round(1/k_sq, digits = 6)), x² + y² + h² = $(round(sum_sq, digits = 6))") + cut_subspace = LinearSubspace(cut_matrix, fill(0, 3)) end + wtns = witness_set(system, cut_subspace) + for soln in filter(is_nontrivial, solution.(filter(isreal, results(wtns)))) + if all(norm2(soln - samp) > 1e-4*length(gens(coordring)) for samp in samples) + push!(samples, soln) + end + end + end + if through_trivial + println("--- planes through trivial solution ---") else - sum_sq = sum(soln[[4, 7, 10]] .^ 2) - println("center at origin: r² = $(round(1/k_sq, digits = 6)); x² + y² + z² = $(round(sum_sq, digits = 6))") + println("--- planes through origin ---") + end + println("$(length(samples)) sample solutions, not including the trivial one:") + for soln in samples + ## [verbose] display([vbls round.(soln, digits = 6)]) + k_sq = abs2(soln[1]) + if abs2(soln[end-2]) > 1e-12 + if k_sq < 1e-12 + println(" center at infinity: z coordinates $(round(soln[end], digits = 6)) and $(round(soln[end-1], digits = 6))") + else + sum_sq = soln[4]^2 + soln[7]^2 + soln[end-2]^2 / k_sq + println(" center on z axis: r² = $(round(1/k_sq, digits = 6)), x² + y² + h² = $(round(sum_sq, digits = 6))") + end + else + sum_sq = sum(soln[[4, 7, 10]] .^ 2) + println(" center at origin: r² = $(round(1/k_sq, digits = 6)); x² + y² + z² = $(round(sum_sq, digits = 6))") + end end end \ No newline at end of file -- 2.34.1 From af1d31f6e61552b0320c7127ef3c720f75e40dea Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 10 Feb 2024 14:21:52 -0500 Subject: [PATCH 021/129] Test a scale constraint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In all but a few cases (for example, a single point on a plane), we should be able to us the radius-coradius boost symmetry to make the average co-radius—representing the "overall scale"—roughly one. --- engine-proto/Engine.jl | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index ba4300c..0b6162e 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -210,13 +210,17 @@ function realize(ctx::Construction{T}) where T [elt.rel for (_, elt) in eltenum if !isnothing(elt.rel)] ) - # add relations to center and orient the construction + # add relations to center, orient, and scale the construction if !isempty(ctx.points) append!(eqns, [sum(pt.coords[k] for pt in ctx.points) for k in 1:3]) end if !isempty(ctx.spheres) append!(eqns, [sum(sph.coords[k] for sph in ctx.spheres) for k in 3:4]) end + n_elts = length(ctx.points) + length(ctx.spheres) + if n_elts > 0 + push!(eqns, sum(elt.vec[2] for elt in Iterators.flatten((ctx.points, ctx.spheres))) - n_elts) + end (Generic.Ideal(coordring, eqns), eqns) end @@ -305,10 +309,14 @@ for through_trivial in [false, true] ## [verbose] display(cut_offset) cut_subspace = LinearSubspace(cut_matrix, cut_offset) else - cut_subspace = LinearSubspace(cut_matrix, fill(0, 3)) + cut_subspace = LinearSubspace(cut_matrix, fill(0, freedom)) end wtns = witness_set(system, cut_subspace) - for soln in filter(is_nontrivial, solution.(filter(isreal, results(wtns)))) + real_solns = solution.(filter(isreal, results(wtns))) + nontrivial_solns = filter(is_nontrivial, real_solns) + println("$(length(real_solns) - length(nontrivial_solns)) trivial solutions found") + for soln in nontrivial_solns + ##[test] for soln in filter(is_nontrivial, solution.(filter(isreal, results(wtns)))) if all(norm2(soln - samp) > 1e-4*length(gens(coordring)) for samp in samples) push!(samples, soln) end -- 2.34.1 From b3b7c2026daa828bdc119cdb37e8ca7d0a948a61 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 10 Feb 2024 14:50:50 -0500 Subject: [PATCH 022/129] Separate the algebraic and numerical parts of the engine --- engine-proto/Engine.Algebraic.jl | 201 +++++++++++++++++++++++++++ engine-proto/Engine.Numerical.jl | 25 ++++ engine-proto/Engine.jl | 229 +------------------------------ 3 files changed, 233 insertions(+), 222 deletions(-) create mode 100644 engine-proto/Engine.Algebraic.jl create mode 100644 engine-proto/Engine.Numerical.jl diff --git a/engine-proto/Engine.Algebraic.jl b/engine-proto/Engine.Algebraic.jl new file mode 100644 index 0000000..b9b790a --- /dev/null +++ b/engine-proto/Engine.Algebraic.jl @@ -0,0 +1,201 @@ +module Algebraic + +export + codimension, dimension, + Construction, realize, + Element, Point, Sphere, + Relation, LiesOn, AlignsWithBy, mprod + +import Subscripts +using LinearAlgebra +using AbstractAlgebra +using Groebner +using ...HittingSet + +# --- commutative algebra --- + +# as of version 0.36.6, AbstractAlgebra only supports ideals in multivariate +# polynomial rings when the coefficients are integers. we use Groebner to extend +# support to rationals and to finite fields of prime order +Generic.reduce_gens(I::Generic.Ideal{U}) where {T <: FieldElement, U <: MPolyRingElem{T}} = + Generic.Ideal{U}(base_ring(I), groebner(gens(I))) + +function codimension(I::Generic.Ideal{U}, maxdepth = Inf) where {T <: RingElement, U <: MPolyRingElem{T}} + leading = [exponent_vector(f, 1) for f in gens(I)] + targets = [Set(findall(.!iszero.(exp_vec))) for exp_vec in leading] + length(HittingSet.solve(HittingSetProblem(targets), maxdepth)) +end + +dimension(I::Generic.Ideal{U}, maxdepth = Inf) where {T <: RingElement, U <: MPolyRingElem{T}} = + length(gens(base_ring(I))) - codimension(I, maxdepth) + +# --- primitve elements --- + +abstract type Element{T} end + +mutable struct Point{T} <: Element{T} + coords::Vector{MPolyRingElem{T}} + vec::Union{Vector{MPolyRingElem{T}}, Nothing} + rel::Nothing + + ## [to do] constructor argument never needed? + Point{T}( + coords::Vector{MPolyRingElem{T}} = MPolyRingElem{T}[], + vec::Union{Vector{MPolyRingElem{T}}, Nothing} = nothing + ) where T = new(coords, vec, nothing) +end + +function buildvec!(pt::Point) + coordring = parent(pt.coords[1]) + pt.vec = [one(coordring), dot(pt.coords, pt.coords), pt.coords...] +end + +mutable struct Sphere{T} <: Element{T} + coords::Vector{MPolyRingElem{T}} + vec::Union{Vector{MPolyRingElem{T}}, Nothing} + rel::Union{MPolyRingElem{T}, Nothing} + + ## [to do] constructor argument never needed? + Sphere{T}( + coords::Vector{MPolyRingElem{T}} = MPolyRingElem{T}[], + vec::Union{Vector{MPolyRingElem{T}}, Nothing} = nothing, + rel::Union{MPolyRingElem{T}, Nothing} = nothing + ) where T = new(coords, vec, rel) +end + +function buildvec!(sph::Sphere) + coordring = parent(sph.coords[1]) + sph.vec = sph.coords + sph.rel = mprod(sph.coords, sph.coords) + one(coordring) +end + +const coordnames = IdDict{Symbol, Vector{Union{Symbol, Nothing}}}( + nameof(Point) => [nothing, nothing, :xₚ, :yₚ, :zₚ], + nameof(Sphere) => [:rₛ, :sₛ, :xₛ, :yₛ, :zₛ] +) + +coordname(elt::Element, index) = coordnames[nameof(typeof(elt))][index] + +function pushcoordname!(coordnamelist, indexed_elt::Tuple{Any, Element}, coordindex) + eltindex, elt = indexed_elt + name = coordname(elt, coordindex) + if !isnothing(name) + subscript = Subscripts.sub(string(eltindex)) + push!(coordnamelist, Symbol(name, subscript)) + end +end + +function takecoord!(coordlist, indexed_elt::Tuple{Any, Element}, coordindex) + elt = indexed_elt[2] + if !isnothing(coordname(elt, coordindex)) + push!(elt.coords, popfirst!(coordlist)) + end +end + +# --- primitive relations --- + +abstract type Relation{T} end + +mprod(v, w) = (v[1]*w[2] + w[1]*v[2]) / 2 - dot(v[3:end], w[3:end]) + +# elements: point, sphere +struct LiesOn{T} <: Relation{T} + elements::Vector{Element{T}} + + LiesOn{T}(pt::Point{T}, sph::Sphere{T}) where T = new{T}([pt, sph]) +end + +equation(rel::LiesOn) = mprod(rel.elements[1].vec, rel.elements[2].vec) + +# elements: sphere, sphere +struct AlignsWithBy{T} <: Relation{T} + elements::Vector{Element{T}} + cos_angle::T + + AlignsWithBy{T}(sph1::Sphere{T}, sph2::Sphere{T}, cos_angle::T) where T = new{T}([sph1, sph2], cos_angle) +end + +equation(rel::AlignsWithBy) = mprod(rel.elements[1].vec, rel.elements[2].vec) - rel.cos_angle + +# --- constructions --- + +mutable struct Construction{T} + points::Set{Point{T}} + spheres::Set{Sphere{T}} + relations::Set{Relation{T}} + + function Construction{T}(; elements = Set{Element{T}}(), relations = Set{Relation{T}}()) where T + allelements = union(elements, (rel.elements for rel in relations)...) + new{T}( + filter(elt -> isa(elt, Point), allelements), + filter(elt -> isa(elt, Sphere), allelements), + relations + ) + end +end + +function Base.push!(ctx::Construction{T}, elt::Point{T}) where T + push!(ctx.points, elt) +end + +function Base.push!(ctx::Construction{T}, elt::Sphere{T}) where T + push!(ctx.spheres, elt) +end + +function Base.push!(ctx::Construction{T}, rel::Relation{T}) where T + push!(ctx.relations, rel) + for elt in rel.elements + push!(ctx, elt) + end +end + +function realize(ctx::Construction{T}) where T + # collect coordinate names + coordnamelist = Symbol[] + eltenum = enumerate(Iterators.flatten((ctx.spheres, ctx.points))) + for coordindex in 1:5 + for indexed_elt in eltenum + pushcoordname!(coordnamelist, indexed_elt, coordindex) + end + end + + # construct coordinate ring + coordring, coordqueue = polynomial_ring(parent_type(T)(), coordnamelist, ordering = :degrevlex) + + # retrieve coordinates + for (_, elt) in eltenum + empty!(elt.coords) + end + for coordindex in 1:5 + for indexed_elt in eltenum + takecoord!(coordqueue, indexed_elt, coordindex) + end + end + + # construct coordinate vectors + for (_, elt) in eltenum + buildvec!(elt) + end + + # turn relations into equations + eqns = vcat( + equation.(ctx.relations), + [elt.rel for (_, elt) in eltenum if !isnothing(elt.rel)] + ) + + # add relations to center, orient, and scale the construction + if !isempty(ctx.points) + append!(eqns, [sum(pt.coords[k] for pt in ctx.points) for k in 1:3]) + end + if !isempty(ctx.spheres) + append!(eqns, [sum(sph.coords[k] for sph in ctx.spheres) for k in 3:4]) + end + n_elts = length(ctx.points) + length(ctx.spheres) + if n_elts > 0 + push!(eqns, sum(elt.vec[2] for elt in Iterators.flatten((ctx.points, ctx.spheres))) - n_elts) + end + + (Generic.Ideal(coordring, eqns), eqns) +end + +end \ No newline at end of file diff --git a/engine-proto/Engine.Numerical.jl b/engine-proto/Engine.Numerical.jl new file mode 100644 index 0000000..669bbda --- /dev/null +++ b/engine-proto/Engine.Numerical.jl @@ -0,0 +1,25 @@ +module Numerical + +using AbstractAlgebra +using HomotopyContinuation: Variable, Expression, System +using ..Algebraic + +# --- polynomial conversion --- + +# hat tip Sascha Timme +# https://github.com/JuliaHomotopyContinuation/HomotopyContinuation.jl/issues/520#issuecomment-1317681521 +function Base.convert(::Type{Expression}, f::MPolyRingElem) + variables = Variable.(symbols(parent(f))) + f_data = zip(coefficients(f), exponent_vectors(f)) + sum(cf * prod(variables .^ exp_vec) for (cf, exp_vec) in f_data) +end + +# create a ModelKit.System from an ideal in a multivariate polynomial ring. the +# variable ordering is taken from the polynomial ring +function System(I::Generic.Ideal) + eqns = Expression.(gens(I)) + variables = Variable.(symbols(base_ring(I))) + System(eqns, variables = variables) +end + +end \ No newline at end of file diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index 0b6162e..b1b0b30 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -2,229 +2,14 @@ include("HittingSet.jl") module Engine +include("Engine.Algebraic.jl") +include("Engine.Numerical.jl") + +using .Algebraic +using .Numerical + export Construction, mprod, codimension, dimension -import Subscripts -using LinearAlgebra -using AbstractAlgebra -using Groebner -using HomotopyContinuation: Variable, Expression, System -using ..HittingSet - -# --- commutative algebra --- - -# as of version 0.36.6, AbstractAlgebra only supports ideals in multivariate -# polynomial rings when the coefficients are integers. we use Groebner to extend -# support to rationals and to finite fields of prime order -Generic.reduce_gens(I::Generic.Ideal{U}) where {T <: FieldElement, U <: MPolyRingElem{T}} = - Generic.Ideal{U}(base_ring(I), groebner(gens(I))) - -function codimension(I::Generic.Ideal{U}, maxdepth = Inf) where {T <: RingElement, U <: MPolyRingElem{T}} - leading = [exponent_vector(f, 1) for f in gens(I)] - targets = [Set(findall(.!iszero.(exp_vec))) for exp_vec in leading] - length(HittingSet.solve(HittingSetProblem(targets), maxdepth)) -end - -dimension(I::Generic.Ideal{U}, maxdepth = Inf) where {T <: RingElement, U <: MPolyRingElem{T}} = - length(gens(base_ring(I))) - codimension(I, maxdepth) - -# hat tip Sascha Timme -# https://github.com/JuliaHomotopyContinuation/HomotopyContinuation.jl/issues/520#issuecomment-1317681521 -function Base.convert(::Type{Expression}, f::MPolyRingElem) - variables = Variable.(symbols(parent(f))) - f_data = zip(coefficients(f), exponent_vectors(f)) - sum(cf * prod(variables .^ exp_vec) for (cf, exp_vec) in f_data) -end - -# create a ModelKit.System from an ideal in a multivariate polynomial ring. the -# variable ordering is taken from the polynomial ring -function System(I::Generic.Ideal) - eqns = Expression.(gens(I)) - variables = Variable.(symbols(base_ring(I))) - System(eqns, variables = variables) -end - -## [to do] not needed right now -# create a ModelKit.System from a list of elements of a multivariate polynomial -# ring. the variable ordering is taken from the polynomial ring -##function System(eqns::AbstractVector{MPolyRingElem}) -## if isempty(eqns) -## return System([]) -## else -## variables = Variable.(symbols(parent(f))) -## return System(Expression.(eqns), variables = variables) -## end -##end - -# --- primitve elements --- - -abstract type Element{T} end - -mutable struct Point{T} <: Element{T} - coords::Vector{MPolyRingElem{T}} - vec::Union{Vector{MPolyRingElem{T}}, Nothing} - rel::Nothing - - ## [to do] constructor argument never needed? - Point{T}( - coords::Vector{MPolyRingElem{T}} = MPolyRingElem{T}[], - vec::Union{Vector{MPolyRingElem{T}}, Nothing} = nothing - ) where T = new(coords, vec, nothing) -end - -function buildvec!(pt::Point) - coordring = parent(pt.coords[1]) - pt.vec = [one(coordring), dot(pt.coords, pt.coords), pt.coords...] -end - -mutable struct Sphere{T} <: Element{T} - coords::Vector{MPolyRingElem{T}} - vec::Union{Vector{MPolyRingElem{T}}, Nothing} - rel::Union{MPolyRingElem{T}, Nothing} - - ## [to do] constructor argument never needed? - Sphere{T}( - coords::Vector{MPolyRingElem{T}} = MPolyRingElem{T}[], - vec::Union{Vector{MPolyRingElem{T}}, Nothing} = nothing, - rel::Union{MPolyRingElem{T}, Nothing} = nothing - ) where T = new(coords, vec, rel) -end - -function buildvec!(sph::Sphere) - coordring = parent(sph.coords[1]) - sph.vec = sph.coords - sph.rel = mprod(sph.coords, sph.coords) + one(coordring) -end - -const coordnames = IdDict{Symbol, Vector{Union{Symbol, Nothing}}}( - nameof(Point) => [nothing, nothing, :xₚ, :yₚ, :zₚ], - nameof(Sphere) => [:rₛ, :sₛ, :xₛ, :yₛ, :zₛ] -) - -coordname(elt::Element, index) = coordnames[nameof(typeof(elt))][index] - -function pushcoordname!(coordnamelist, indexed_elt::Tuple{Any, Element}, coordindex) - eltindex, elt = indexed_elt - name = coordname(elt, coordindex) - if !isnothing(name) - subscript = Subscripts.sub(string(eltindex)) - push!(coordnamelist, Symbol(name, subscript)) - end -end - -function takecoord!(coordlist, indexed_elt::Tuple{Any, Element}, coordindex) - elt = indexed_elt[2] - if !isnothing(coordname(elt, coordindex)) - push!(elt.coords, popfirst!(coordlist)) - end -end - -# --- primitive relations --- - -abstract type Relation{T} end - -mprod(v, w) = (v[1]*w[2] + w[1]*v[2]) / 2 - dot(v[3:end], w[3:end]) - -# elements: point, sphere -struct LiesOn{T} <: Relation{T} - elements::Vector{Element{T}} - - LiesOn{T}(pt::Point{T}, sph::Sphere{T}) where T = new{T}([pt, sph]) -end - -equation(rel::LiesOn) = mprod(rel.elements[1].vec, rel.elements[2].vec) - -# elements: sphere, sphere -struct AlignsWithBy{T} <: Relation{T} - elements::Vector{Element{T}} - cos_angle::T - - AlignsWithBy{T}(sph1::Sphere{T}, sph2::Sphere{T}, cos_angle::T) where T = new{T}([sph1, sph2], cos_angle) -end - -equation(rel::AlignsWithBy) = mprod(rel.elements[1].vec, rel.elements[2].vec) - rel.cos_angle - -# --- constructions --- - -mutable struct Construction{T} - points::Set{Point{T}} - spheres::Set{Sphere{T}} - relations::Set{Relation{T}} - - function Construction{T}(; elements = Set{Element{T}}(), relations = Set{Relation{T}}()) where T - allelements = union(elements, (rel.elements for rel in relations)...) - new{T}( - filter(elt -> isa(elt, Point), allelements), - filter(elt -> isa(elt, Sphere), allelements), - relations - ) - end -end - -function Base.push!(ctx::Construction{T}, elt::Point{T}) where T - push!(ctx.points, elt) -end - -function Base.push!(ctx::Construction{T}, elt::Sphere{T}) where T - push!(ctx.spheres, elt) -end - -function Base.push!(ctx::Construction{T}, rel::Relation{T}) where T - push!(ctx.relations, rel) - for elt in rel.elements - push!(ctx, elt) - end -end - -function realize(ctx::Construction{T}) where T - # collect coordinate names - coordnamelist = Symbol[] - eltenum = enumerate(Iterators.flatten((ctx.spheres, ctx.points))) - for coordindex in 1:5 - for indexed_elt in eltenum - pushcoordname!(coordnamelist, indexed_elt, coordindex) - end - end - - # construct coordinate ring - coordring, coordqueue = polynomial_ring(parent_type(T)(), coordnamelist, ordering = :degrevlex) - - # retrieve coordinates - for (_, elt) in eltenum - empty!(elt.coords) - end - for coordindex in 1:5 - for indexed_elt in eltenum - takecoord!(coordqueue, indexed_elt, coordindex) - end - end - - # construct coordinate vectors - for (_, elt) in eltenum - buildvec!(elt) - end - - # turn relations into equations - eqns = vcat( - equation.(ctx.relations), - [elt.rel for (_, elt) in eltenum if !isnothing(elt.rel)] - ) - - # add relations to center, orient, and scale the construction - if !isempty(ctx.points) - append!(eqns, [sum(pt.coords[k] for pt in ctx.points) for k in 1:3]) - end - if !isempty(ctx.spheres) - append!(eqns, [sum(sph.coords[k] for sph in ctx.spheres) for k in 3:4]) - end - n_elts = length(ctx.points) + length(ctx.spheres) - if n_elts > 0 - push!(eqns, sum(elt.vec[2] for elt in Iterators.flatten((ctx.points, ctx.spheres))) - n_elts) - end - - (Generic.Ideal(coordring, eqns), eqns) -end - end # ~~~ sandbox setup ~~~ @@ -293,7 +78,7 @@ is_nontrivial = soln -> norm2(abs.(real.(soln)) - trivial_soln) > 1e-4*length(ge max_slope = 5 binom = Binomial(2max_slope, 1/2) Random.seed!(6071) -n_planes = 36 +n_planes = 3 for through_trivial in [false, true] samples = [] for _ in 1:n_planes -- 2.34.1 From 621c4c577630b3dc7e351bdd0e521c7e2a31d8ba Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 10 Feb 2024 15:02:26 -0500 Subject: [PATCH 023/129] Try uniformly distributed hyperplane orientations Unit normals are uniformly distributed over the sphere. --- engine-proto/Engine.jl | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index b1b0b30..e6b326d 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -75,26 +75,29 @@ trivial_soln[sph_z_ind] .= 1 println("trivial solutions: $trivial_soln") norm2 = vec -> real(dot(conj.(vec), vec)) is_nontrivial = soln -> norm2(abs.(real.(soln)) - trivial_soln) > 1e-4*length(gens(coordring)) -max_slope = 5 -binom = Binomial(2max_slope, 1/2) +##max_slope = 5 +##binom = Binomial(2max_slope, 1/2) Random.seed!(6071) n_planes = 3 for through_trivial in [false, true] samples = [] for _ in 1:n_planes - cut_matrix = rand(binom, freedom, length(gens(coordring))) .- max_slope + cut_matrix = transpose(hcat( + (normalize(randn(length(gens(coordring)))) for _ in 1:freedom)... + )) + ##cut_matrix = rand(binom, freedom, length(gens(coordring))) .- max_slope ##cut_matrix = [ ## 1 1 1 1 0 1 1 0 1 1 0; ## 1 2 1 2 0 1 1 0 1 1 0; ## 1 1 0 1 0 1 2 0 2 0 0 ##] - ## [verbose] display(cut_matrix) + display(cut_matrix) ## [verbose] if through_trivial cut_offset = [sum(cf[sph_z_ind]) for cf in eachrow(cut_matrix)] - ## [verbose] display(cut_offset) + display(cut_offset) ## [verbose] cut_subspace = LinearSubspace(cut_matrix, cut_offset) else - cut_subspace = LinearSubspace(cut_matrix, fill(0, freedom)) + cut_subspace = LinearSubspace(cut_matrix, fill(0., freedom)) end wtns = witness_set(system, cut_subspace) real_solns = solution.(filter(isreal, results(wtns))) -- 2.34.1 From 6f18d4efcc560290366e4083fb7c81a349fce5a8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 10 Feb 2024 15:10:48 -0500 Subject: [PATCH 024/129] Test lots of uniformly distributed hyperplanes --- engine-proto/Engine.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index e6b326d..d8d4b52 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -78,7 +78,7 @@ is_nontrivial = soln -> norm2(abs.(real.(soln)) - trivial_soln) > 1e-4*length(ge ##max_slope = 5 ##binom = Binomial(2max_slope, 1/2) Random.seed!(6071) -n_planes = 3 +n_planes = 36 for through_trivial in [false, true] samples = [] for _ in 1:n_planes @@ -91,10 +91,10 @@ for through_trivial in [false, true] ## 1 2 1 2 0 1 1 0 1 1 0; ## 1 1 0 1 0 1 2 0 2 0 0 ##] - display(cut_matrix) ## [verbose] + ## display(cut_matrix) ## [verbose] if through_trivial cut_offset = [sum(cf[sph_z_ind]) for cf in eachrow(cut_matrix)] - display(cut_offset) ## [verbose] + ## display(cut_offset) ## [verbose] cut_subspace = LinearSubspace(cut_matrix, cut_offset) else cut_subspace = LinearSubspace(cut_matrix, fill(0., freedom)) @@ -104,7 +104,7 @@ for through_trivial in [false, true] nontrivial_solns = filter(is_nontrivial, real_solns) println("$(length(real_solns) - length(nontrivial_solns)) trivial solutions found") for soln in nontrivial_solns - ##[test] for soln in filter(is_nontrivial, solution.(filter(isreal, results(wtns)))) + ## [test] for soln in filter(is_nontrivial, solution.(filter(isreal, results(wtns)))) if all(norm2(soln - samp) > 1e-4*length(gens(coordring)) for samp in samples) push!(samples, soln) end @@ -117,7 +117,7 @@ for through_trivial in [false, true] end println("$(length(samples)) sample solutions, not including the trivial one:") for soln in samples - ## [verbose] display([vbls round.(soln, digits = 6)]) + ## display([vbls round.(soln, digits = 6)]) ## [verbose] k_sq = abs2(soln[1]) if abs2(soln[end-2]) > 1e-12 if k_sq < 1e-12 -- 2.34.1 From 1f173708eb57fc2305b9a17ab88bdf1375cc26fd Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 10 Feb 2024 17:39:26 -0500 Subject: [PATCH 025/129] Move random cut routine into engine --- engine-proto/Engine.Numerical.jl | 19 +++++++++++++++++-- engine-proto/Engine.jl | 20 +------------------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/engine-proto/Engine.Numerical.jl b/engine-proto/Engine.Numerical.jl index 669bbda..1ae7b97 100644 --- a/engine-proto/Engine.Numerical.jl +++ b/engine-proto/Engine.Numerical.jl @@ -1,7 +1,8 @@ module Numerical +using LinearAlgebra using AbstractAlgebra -using HomotopyContinuation: Variable, Expression, System +using HomotopyContinuation using ..Algebraic # --- polynomial conversion --- @@ -10,7 +11,7 @@ using ..Algebraic # https://github.com/JuliaHomotopyContinuation/HomotopyContinuation.jl/issues/520#issuecomment-1317681521 function Base.convert(::Type{Expression}, f::MPolyRingElem) variables = Variable.(symbols(parent(f))) - f_data = zip(coefficients(f), exponent_vectors(f)) + f_data = zip(AbstractAlgebra.coefficients(f), exponent_vectors(f)) sum(cf * prod(variables .^ exp_vec) for (cf, exp_vec) in f_data) end @@ -22,4 +23,18 @@ function System(I::Generic.Ideal) System(eqns, variables = variables) end +# --- sampling --- + +function real_samples(F::AbstractSystem, dim) + # choose a random real hyperplane of codimension `dim` by intersecting + # hyperplanes whose normal vectors are uniformly distributed over the unit + # sphere + # [to do] guard against the unlikely event that one of the normals is zero + normals = transpose(hcat( + (normalize(randn(nvariables(F))) for _ in 1:dim)... + )) + cut = LinearSubspace(normals, fill(0., dim)) + filter(isreal, results(witness_set(F, cut))) +end + end \ No newline at end of file diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index d8d4b52..f058085 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -82,25 +82,7 @@ n_planes = 36 for through_trivial in [false, true] samples = [] for _ in 1:n_planes - cut_matrix = transpose(hcat( - (normalize(randn(length(gens(coordring)))) for _ in 1:freedom)... - )) - ##cut_matrix = rand(binom, freedom, length(gens(coordring))) .- max_slope - ##cut_matrix = [ - ## 1 1 1 1 0 1 1 0 1 1 0; - ## 1 2 1 2 0 1 1 0 1 1 0; - ## 1 1 0 1 0 1 2 0 2 0 0 - ##] - ## display(cut_matrix) ## [verbose] - if through_trivial - cut_offset = [sum(cf[sph_z_ind]) for cf in eachrow(cut_matrix)] - ## display(cut_offset) ## [verbose] - cut_subspace = LinearSubspace(cut_matrix, cut_offset) - else - cut_subspace = LinearSubspace(cut_matrix, fill(0., freedom)) - end - wtns = witness_set(system, cut_subspace) - real_solns = solution.(filter(isreal, results(wtns))) + real_solns = solution.(Engine.Numerical.real_samples(system, freedom)) nontrivial_solns = filter(is_nontrivial, real_solns) println("$(length(real_solns) - length(nontrivial_solns)) trivial solutions found") for soln in nontrivial_solns -- 2.34.1 From 6cf07dc6a10766026cf30365c3cd431d87f17967 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 12 Feb 2024 20:34:12 -0500 Subject: [PATCH 026/129] Evaluate and display elements --- engine-proto/Engine.Numerical.jl | 16 +++++- engine-proto/Engine.jl | 90 ++++++++++++++++---------------- 2 files changed, 58 insertions(+), 48 deletions(-) diff --git a/engine-proto/Engine.Numerical.jl b/engine-proto/Engine.Numerical.jl index 1ae7b97..48fb682 100644 --- a/engine-proto/Engine.Numerical.jl +++ b/engine-proto/Engine.Numerical.jl @@ -2,7 +2,10 @@ module Numerical using LinearAlgebra using AbstractAlgebra -using HomotopyContinuation +using HomotopyContinuation: + Variable, Expression, AbstractSystem, System, LinearSubspace, + nvariables, isreal, witness_set, results +import GLMakie using ..Algebraic # --- polynomial conversion --- @@ -11,7 +14,7 @@ using ..Algebraic # https://github.com/JuliaHomotopyContinuation/HomotopyContinuation.jl/issues/520#issuecomment-1317681521 function Base.convert(::Type{Expression}, f::MPolyRingElem) variables = Variable.(symbols(parent(f))) - f_data = zip(AbstractAlgebra.coefficients(f), exponent_vectors(f)) + f_data = zip(coefficients(f), exponent_vectors(f)) sum(cf * prod(variables .^ exp_vec) for (cf, exp_vec) in f_data) end @@ -37,4 +40,13 @@ function real_samples(F::AbstractSystem, dim) filter(isreal, results(witness_set(F, cut))) end +AbstractAlgebra.evaluate(pt::Point, vals::Vector{<:RingElement}) = + GLMakie.Point3f([evaluate(u, vals) for u in pt.coords]) + +function AbstractAlgebra.evaluate(sph::Sphere, vals::Vector{<:RingElement}) + radius = 1 / evaluate(sph.coords[1], vals) + center = radius * [evaluate(u, vals) for u in sph.coords[3:end]] + GLMakie.Sphere(GLMakie.Point3f(center), radius) +end + end \ No newline at end of file diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index f058085..099a5f0 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -19,6 +19,7 @@ using Distributions using LinearAlgebra using AbstractAlgebra using HomotopyContinuation +using GLMakie CoeffType = Rational{Int64} @@ -55,62 +56,59 @@ println("Two points on a sphere: ", freedom, " degrees of freedom") coordring = base_ring(ideal_ab_s) vbls = Variable.(symbols(coordring)) -##cut_system = CompiledSystem(System([eqns_ab_s; cut], variables = vbls)) -##cut_result = HomotopyContinuation.solve(cut_system) -##println("non-singular solutions:") -##for soln in solutions(cut_result) -## display(soln) -##end -##println("singular solutions:") -##for sing in singular(cut_result) -## display(sing.solution) -##end # test a random witness set system = CompiledSystem(System(eqns_ab_s, variables = vbls)) sph_z_ind = indexin([sph.coords[5] for sph in ctx.spheres], gens(coordring)) println("sphere z variables: ", vbls[sph_z_ind]) -trivial_soln = fill(0, length(gens(coordring))) -trivial_soln[sph_z_ind] .= 1 -println("trivial solutions: $trivial_soln") +## [old] trivial_soln = fill(0, length(gens(coordring))) +## [old] trivial_soln[sph_z_ind] .= 1 +## [old] println("trivial solutions: $trivial_soln") norm2 = vec -> real(dot(conj.(vec), vec)) -is_nontrivial = soln -> norm2(abs.(real.(soln)) - trivial_soln) > 1e-4*length(gens(coordring)) -##max_slope = 5 -##binom = Binomial(2max_slope, 1/2) +## [old] is_nontrivial = soln -> norm2(abs.(real.(soln)) - trivial_soln) > 1e-4*length(gens(coordring)) Random.seed!(6071) -n_planes = 36 -for through_trivial in [false, true] - samples = [] - for _ in 1:n_planes - real_solns = solution.(Engine.Numerical.real_samples(system, freedom)) - nontrivial_solns = filter(is_nontrivial, real_solns) - println("$(length(real_solns) - length(nontrivial_solns)) trivial solutions found") - for soln in nontrivial_solns - ## [test] for soln in filter(is_nontrivial, solution.(filter(isreal, results(wtns)))) - if all(norm2(soln - samp) > 1e-4*length(gens(coordring)) for samp in samples) - push!(samples, soln) - end +n_planes = 3 +samples = [] +for _ in 1:n_planes + real_solns = solution.(Engine.Numerical.real_samples(system, freedom)) + ## [old] nontrivial_solns = filter(is_nontrivial, real_solns) + ## [old] println("$(length(real_solns) - length(nontrivial_solns)) trivial solutions found") + for soln in real_solns + if all(norm2(soln - samp) > 1e-4*length(gens(coordring)) for samp in samples) + push!(samples, soln) end end - if through_trivial - println("--- planes through trivial solution ---") - else - println("--- planes through origin ---") - end - println("$(length(samples)) sample solutions, not including the trivial one:") - for soln in samples - ## display([vbls round.(soln, digits = 6)]) ## [verbose] - k_sq = abs2(soln[1]) - if abs2(soln[end-2]) > 1e-12 - if k_sq < 1e-12 - println(" center at infinity: z coordinates $(round(soln[end], digits = 6)) and $(round(soln[end-1], digits = 6))") - else - sum_sq = soln[4]^2 + soln[7]^2 + soln[end-2]^2 / k_sq - println(" center on z axis: r² = $(round(1/k_sq, digits = 6)), x² + y² + h² = $(round(sum_sq, digits = 6))") - end +end +println("$(length(samples)) sample solutions:") +for soln in samples + ## display([vbls round.(soln, digits = 6)]) ## [verbose] + k_sq = abs2(soln[1]) + if abs2(soln[end-2]) > 1e-12 + if k_sq < 1e-12 + println(" center at infinity: z coordinates $(round(soln[end], digits = 6)) and $(round(soln[end-1], digits = 6))") else - sum_sq = sum(soln[[4, 7, 10]] .^ 2) - println(" center at origin: r² = $(round(1/k_sq, digits = 6)); x² + y² + z² = $(round(sum_sq, digits = 6))") + sum_sq = soln[4]^2 + soln[7]^2 + soln[end-2]^2 / k_sq + println(" center on z axis: r² = $(round(1/k_sq, digits = 6)), x² + y² + h² = $(round(sum_sq, digits = 6))") end + else + sum_sq = sum(soln[[4, 7, 10]] .^ 2) + println(" center at origin: r² = $(round(1/k_sq, digits = 6)); x² + y² + z² = $(round(sum_sq, digits = 6))") end +end + +# show a sample solution +function show_solution(vals) + # evaluate elements + real_vals = real.(vals) + disp_points = [Engine.Numerical.evaluate(pt, real_vals) for pt in ctx.points] + disp_spheres = [Engine.Numerical.evaluate(sph, real_vals) for sph in ctx.spheres] + + # create scene + scene = Scene() + cam3d!(scene) + scatter!(scene, disp_points, color = :green) + for sph in disp_spheres + mesh!(scene, sph, color = :gray) + end + scene end \ No newline at end of file -- 2.34.1 From a450f701fbfa1818c7795d654e6f2a65d2b75b15 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 12 Feb 2024 21:14:07 -0500 Subject: [PATCH 027/129] Try displaying a chain of spheres For three mutually tangent spheres, I couldn't find real solutions. --- engine-proto/Engine.jl | 56 ++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index 099a5f0..113c6a3 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -23,23 +23,23 @@ using GLMakie CoeffType = Rational{Int64} -a = Engine.Point{CoeffType}() -s = Engine.Sphere{CoeffType}() -a_on_s = Engine.LiesOn{CoeffType}(a, s) -ctx = Engine.Construction{CoeffType}(elements = Set([a]), relations= Set([a_on_s])) +##a = Engine.Point{CoeffType}() +##s = Engine.Sphere{CoeffType}() +##a_on_s = Engine.LiesOn{CoeffType}(a, s) +##ctx = Engine.Construction{CoeffType}(elements = Set([a]), relations= Set([a_on_s])) ##ideal_a_s = Engine.realize(ctx) -##println("A point on a sphere: ", Engine.dimension(ideal_a_s), " degrees of freedom") +##println("A point on a sphere: $(Engine.dimension(ideal_a_s)) degrees of freedom") -b = Engine.Point{CoeffType}() -b_on_s = Engine.LiesOn{CoeffType}(b, s) -Engine.push!(ctx, b) -Engine.push!(ctx, s) -Engine.push!(ctx, b_on_s) -ideal_ab_s, eqns_ab_s = Engine.realize(ctx) -freedom = Engine.dimension(ideal_ab_s) -println("Two points on a sphere: ", freedom, " degrees of freedom") +##b = Engine.Point{CoeffType}() +##b_on_s = Engine.LiesOn{CoeffType}(b, s) +##Engine.push!(ctx, b) +##Engine.push!(ctx, s) +##Engine.push!(ctx, b_on_s) +##ideal_ab_s, eqns_ab_s = Engine.realize(ctx) +##freedom = Engine.dimension(ideal_ab_s) +##println("Two points on a sphere: $freedom degrees of freedom") -##spheres = [Engine.Sphere{CoeffType}() for _ in 1:3] +spheres = [Engine.Sphere{CoeffType}() for _ in 1:3] ##tangencies = [ ## Engine.AlignsWithBy{CoeffType}( ## spheres[n], @@ -48,31 +48,29 @@ println("Two points on a sphere: ", freedom, " degrees of freedom") ## ) ## for n in 1:3 ##] -##ctx_tan_sph = Engine.Construction{CoeffType}(elements = Set(spheres), relations = Set(tangencies)) -##ideal_tan_sph = Engine.realize(ctx_tan_sph) -##println("Three mutually tangent spheres: ", Engine.dimension(ideal_tan_sph), " degrees of freedom") +tangencies = [ + Engine.AlignsWithBy{CoeffType}(spheres[1], spheres[2], CoeffType(-1)), + Engine.AlignsWithBy{CoeffType}(spheres[2], spheres[3], CoeffType(-1//2)) +] +ctx_tan_sph = Engine.Construction{CoeffType}(elements = Set(spheres), relations = Set(tangencies)) +ideal_tan_sph, eqns_tan_sph = Engine.realize(ctx_tan_sph) +freedom = Engine.dimension(ideal_tan_sph) +##println("Three mutually tangent spheres: $freedom degrees of freedom") +println("Chain of three spheres: $freedom degrees of freedom") # --- test rational cut --- -coordring = base_ring(ideal_ab_s) +coordring = base_ring(ideal_tan_sph) vbls = Variable.(symbols(coordring)) # test a random witness set -system = CompiledSystem(System(eqns_ab_s, variables = vbls)) -sph_z_ind = indexin([sph.coords[5] for sph in ctx.spheres], gens(coordring)) -println("sphere z variables: ", vbls[sph_z_ind]) -## [old] trivial_soln = fill(0, length(gens(coordring))) -## [old] trivial_soln[sph_z_ind] .= 1 -## [old] println("trivial solutions: $trivial_soln") +system = CompiledSystem(System(eqns_tan_sph, variables = vbls)) norm2 = vec -> real(dot(conj.(vec), vec)) -## [old] is_nontrivial = soln -> norm2(abs.(real.(soln)) - trivial_soln) > 1e-4*length(gens(coordring)) Random.seed!(6071) -n_planes = 3 +n_planes = 16 samples = [] for _ in 1:n_planes real_solns = solution.(Engine.Numerical.real_samples(system, freedom)) - ## [old] nontrivial_solns = filter(is_nontrivial, real_solns) - ## [old] println("$(length(real_solns) - length(nontrivial_solns)) trivial solutions found") for soln in real_solns if all(norm2(soln - samp) > 1e-4*length(gens(coordring)) for samp in samples) push!(samples, soln) @@ -97,7 +95,7 @@ for soln in samples end # show a sample solution -function show_solution(vals) +function show_solution(ctx, vals) # evaluate elements real_vals = real.(vals) disp_points = [Engine.Numerical.evaluate(pt, real_vals) for pt in ctx.points] -- 2.34.1 From 31d5e7e864704895dbf0273c46185476e53e40f1 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 12 Feb 2024 22:48:16 -0500 Subject: [PATCH 028/129] Play with two points on two spheres Guess conditions that make the scaling constraint impossible to satisfy. --- engine-proto/Engine.Algebraic.jl | 2 ++ engine-proto/Engine.jl | 43 +++++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/engine-proto/Engine.Algebraic.jl b/engine-proto/Engine.Algebraic.jl index b9b790a..ca39967 100644 --- a/engine-proto/Engine.Algebraic.jl +++ b/engine-proto/Engine.Algebraic.jl @@ -184,6 +184,8 @@ function realize(ctx::Construction{T}) where T ) # add relations to center, orient, and scale the construction + # [to do] the scaling constraint, as written, can be impossible to satisfy + # when all of the spheres have to go through the origin if !isempty(ctx.points) append!(eqns, [sum(pt.coords[k] for pt in ctx.points) for k in 1:3]) end diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index 113c6a3..7ddf72d 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -39,7 +39,7 @@ CoeffType = Rational{Int64} ##freedom = Engine.dimension(ideal_ab_s) ##println("Two points on a sphere: $freedom degrees of freedom") -spheres = [Engine.Sphere{CoeffType}() for _ in 1:3] +##spheres = [Engine.Sphere{CoeffType}() for _ in 1:3] ##tangencies = [ ## Engine.AlignsWithBy{CoeffType}( ## spheres[n], @@ -48,26 +48,45 @@ spheres = [Engine.Sphere{CoeffType}() for _ in 1:3] ## ) ## for n in 1:3 ##] -tangencies = [ - Engine.AlignsWithBy{CoeffType}(spheres[1], spheres[2], CoeffType(-1)), - Engine.AlignsWithBy{CoeffType}(spheres[2], spheres[3], CoeffType(-1//2)) -] -ctx_tan_sph = Engine.Construction{CoeffType}(elements = Set(spheres), relations = Set(tangencies)) -ideal_tan_sph, eqns_tan_sph = Engine.realize(ctx_tan_sph) -freedom = Engine.dimension(ideal_tan_sph) +##tangencies = [ + ##Engine.LiesOn{CoeffType}(points[1], spheres[2]), + ##Engine.LiesOn{CoeffType}(points[1], spheres[3]), + ##Engine.LiesOn{CoeffType}(points[2], spheres[3]), + ##Engine.LiesOn{CoeffType}(points[2], spheres[1]), + ##Engine.LiesOn{CoeffType}(points[3], spheres[1]), + ##Engine.LiesOn{CoeffType}(points[3], spheres[2]) +##] +##ctx_tan_sph = Engine.Construction{CoeffType}(elements = Set(spheres), relations = Set(tangencies)) +##ideal_tan_sph, eqns_tan_sph = Engine.realize(ctx_tan_sph) +##freedom = Engine.dimension(ideal_tan_sph) ##println("Three mutually tangent spheres: $freedom degrees of freedom") -println("Chain of three spheres: $freedom degrees of freedom") + +p = Engine.Point{CoeffType}() +q = Engine.Point{CoeffType}() +a = Engine.Sphere{CoeffType}() +b = Engine.Sphere{CoeffType}() +p_on_a = Engine.LiesOn{CoeffType}(p, a) +p_on_b = Engine.LiesOn{CoeffType}(p, b) +q_on_a = Engine.LiesOn{CoeffType}(q, a) +q_on_b = Engine.LiesOn{CoeffType}(q, b) +ctx_joined = Engine.Construction{CoeffType}( + elements = Set([p, q, a, b]), + relations= Set([p_on_a, p_on_b, q_on_a, q_on_b]) +) +ideal_joined, eqns_joined = Engine.realize(ctx_joined) +freedom = Engine.dimension(ideal_joined) +println("Two points on two spheres: $freedom degrees of freedom") # --- test rational cut --- -coordring = base_ring(ideal_tan_sph) +coordring = base_ring(ideal_joined) vbls = Variable.(symbols(coordring)) # test a random witness set -system = CompiledSystem(System(eqns_tan_sph, variables = vbls)) +system = CompiledSystem(System(eqns_joined, variables = vbls)) norm2 = vec -> real(dot(conj.(vec), vec)) Random.seed!(6071) -n_planes = 16 +n_planes = 3 samples = [] for _ in 1:n_planes real_solns = solution.(Engine.Numerical.real_samples(system, freedom)) -- 2.34.1 From e41bcc7e13d29217d51ed5e7810b2c8f4e904fed Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 13 Feb 2024 04:02:14 -0500 Subject: [PATCH 029/129] Explore the performance wall Three points on two spheres is too much. --- engine-proto/Engine.Algebraic.jl | 3 ++- engine-proto/Engine.jl | 19 ++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/engine-proto/Engine.Algebraic.jl b/engine-proto/Engine.Algebraic.jl index ca39967..380cee1 100644 --- a/engine-proto/Engine.Algebraic.jl +++ b/engine-proto/Engine.Algebraic.jl @@ -197,7 +197,8 @@ function realize(ctx::Construction{T}) where T push!(eqns, sum(elt.vec[2] for elt in Iterators.flatten((ctx.points, ctx.spheres))) - n_elts) end - (Generic.Ideal(coordring, eqns), eqns) + ## [test] (Generic.Ideal(coordring, eqns), eqns) + (nothing, eqns) end end \ No newline at end of file diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index 7ddf72d..49011c6 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -61,21 +61,18 @@ CoeffType = Rational{Int64} ##freedom = Engine.dimension(ideal_tan_sph) ##println("Three mutually tangent spheres: $freedom degrees of freedom") -p = Engine.Point{CoeffType}() -q = Engine.Point{CoeffType}() -a = Engine.Sphere{CoeffType}() -b = Engine.Sphere{CoeffType}() -p_on_a = Engine.LiesOn{CoeffType}(p, a) -p_on_b = Engine.LiesOn{CoeffType}(p, b) -q_on_a = Engine.LiesOn{CoeffType}(q, a) -q_on_b = Engine.LiesOn{CoeffType}(q, b) +points = [Engine.Point{CoeffType}() for _ in 1:3] +spheres = [Engine.Sphere{CoeffType}() for _ in 1:2] ctx_joined = Engine.Construction{CoeffType}( - elements = Set([p, q, a, b]), - relations= Set([p_on_a, p_on_b, q_on_a, q_on_b]) + elements = Set([points; spheres]), + relations= Set([ + Engine.LiesOn{CoeffType}(pt, sph) + for pt in points for sph in spheres + ]) ) ideal_joined, eqns_joined = Engine.realize(ctx_joined) freedom = Engine.dimension(ideal_joined) -println("Two points on two spheres: $freedom degrees of freedom") +println("$(length(points)) points on $(length(spheres)) spheres: $freedom degrees of freedom") # --- test rational cut --- -- 2.34.1 From 291d5c8ff685b642fd3535b6c46179c9f134c469 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 15 Feb 2024 13:28:01 -0800 Subject: [PATCH 030/129] Study mutually tangent spheres with two fixed --- engine-proto/Engine.Algebraic.jl | 24 ++++----- engine-proto/Engine.jl | 93 +++++++++++++++++--------------- 2 files changed, 62 insertions(+), 55 deletions(-) diff --git a/engine-proto/Engine.Algebraic.jl b/engine-proto/Engine.Algebraic.jl index 380cee1..b6fc7a7 100644 --- a/engine-proto/Engine.Algebraic.jl +++ b/engine-proto/Engine.Algebraic.jl @@ -186,19 +186,19 @@ function realize(ctx::Construction{T}) where T # add relations to center, orient, and scale the construction # [to do] the scaling constraint, as written, can be impossible to satisfy # when all of the spheres have to go through the origin - if !isempty(ctx.points) - append!(eqns, [sum(pt.coords[k] for pt in ctx.points) for k in 1:3]) - end - if !isempty(ctx.spheres) - append!(eqns, [sum(sph.coords[k] for sph in ctx.spheres) for k in 3:4]) - end - n_elts = length(ctx.points) + length(ctx.spheres) - if n_elts > 0 - push!(eqns, sum(elt.vec[2] for elt in Iterators.flatten((ctx.points, ctx.spheres))) - n_elts) - end + ##if !isempty(ctx.points) + ## append!(eqns, [sum(pt.coords[k] for pt in ctx.points) for k in 1:3]) + ##end + ##if !isempty(ctx.spheres) + ## append!(eqns, [sum(sph.coords[k] for sph in ctx.spheres) for k in 3:4]) + ##end + ##n_elts = length(ctx.points) + length(ctx.spheres) + ##if n_elts > 0 + ## push!(eqns, sum(elt.vec[2] for elt in Iterators.flatten((ctx.points, ctx.spheres))) - n_elts) + ##end - ## [test] (Generic.Ideal(coordring, eqns), eqns) - (nothing, eqns) + (Generic.Ideal(coordring, eqns), eqns) + ## [test] (nothing, eqns) end end \ No newline at end of file diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index 49011c6..0410f6d 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -39,15 +39,15 @@ CoeffType = Rational{Int64} ##freedom = Engine.dimension(ideal_ab_s) ##println("Two points on a sphere: $freedom degrees of freedom") -##spheres = [Engine.Sphere{CoeffType}() for _ in 1:3] -##tangencies = [ -## Engine.AlignsWithBy{CoeffType}( -## spheres[n], -## spheres[mod1(n+1, length(spheres))], -## CoeffType(-1//1) -## ) -## for n in 1:3 -##] +spheres = [Engine.Sphere{CoeffType}() for _ in 1:3] +tangencies = [ + Engine.AlignsWithBy{CoeffType}( + spheres[n], + spheres[mod1(n+1, length(spheres))], + CoeffType(-1)^n + ) + for n in 1:3 +] ##tangencies = [ ##Engine.LiesOn{CoeffType}(points[1], spheres[2]), ##Engine.LiesOn{CoeffType}(points[1], spheres[3]), @@ -56,34 +56,41 @@ CoeffType = Rational{Int64} ##Engine.LiesOn{CoeffType}(points[3], spheres[1]), ##Engine.LiesOn{CoeffType}(points[3], spheres[2]) ##] -##ctx_tan_sph = Engine.Construction{CoeffType}(elements = Set(spheres), relations = Set(tangencies)) -##ideal_tan_sph, eqns_tan_sph = Engine.realize(ctx_tan_sph) -##freedom = Engine.dimension(ideal_tan_sph) -##println("Three mutually tangent spheres: $freedom degrees of freedom") +ctx_tan_sph = Engine.Construction{CoeffType}(elements = Set(spheres), relations = Set(tangencies)) +ideal_tan_sph, eqns_tan_sph = Engine.realize(ctx_tan_sph) +##small_eqns_tan_sph = eqns_tan_sph +small_eqns_tan_sph = [ + eqns_tan_sph; + spheres[2].coords - [1, 0, 0, 0, 1]; + spheres[3].coords - [1, 0, 0, 0, -1]; +] +small_ideal_tan_sph = Generic.Ideal(base_ring(ideal_tan_sph), small_eqns_tan_sph) +freedom = Engine.dimension(small_ideal_tan_sph) +println("Three mutually tangent spheres, with two fixed: $freedom degrees of freedom") -points = [Engine.Point{CoeffType}() for _ in 1:3] -spheres = [Engine.Sphere{CoeffType}() for _ in 1:2] -ctx_joined = Engine.Construction{CoeffType}( - elements = Set([points; spheres]), - relations= Set([ - Engine.LiesOn{CoeffType}(pt, sph) - for pt in points for sph in spheres - ]) -) -ideal_joined, eqns_joined = Engine.realize(ctx_joined) -freedom = Engine.dimension(ideal_joined) -println("$(length(points)) points on $(length(spheres)) spheres: $freedom degrees of freedom") +##points = [Engine.Point{CoeffType}() for _ in 1:3] +##spheres = [Engine.Sphere{CoeffType}() for _ in 1:2] +##ctx_joined = Engine.Construction{CoeffType}( +## elements = Set([points; spheres]), +## relations= Set([ +## Engine.LiesOn{CoeffType}(pt, sph) +## for pt in points for sph in spheres +## ]) +##) +##ideal_joined, eqns_joined = Engine.realize(ctx_joined) +##freedom = Engine.dimension(ideal_joined) +##println("$(length(points)) points on $(length(spheres)) spheres: $freedom degrees of freedom") # --- test rational cut --- -coordring = base_ring(ideal_joined) +coordring = base_ring(small_ideal_tan_sph) vbls = Variable.(symbols(coordring)) # test a random witness set -system = CompiledSystem(System(eqns_joined, variables = vbls)) +system = CompiledSystem(System(small_eqns_tan_sph, variables = vbls)) norm2 = vec -> real(dot(conj.(vec), vec)) Random.seed!(6071) -n_planes = 3 +n_planes = 36 samples = [] for _ in 1:n_planes real_solns = solution.(Engine.Numerical.real_samples(system, freedom)) @@ -94,21 +101,21 @@ for _ in 1:n_planes end end println("$(length(samples)) sample solutions:") -for soln in samples - ## display([vbls round.(soln, digits = 6)]) ## [verbose] - k_sq = abs2(soln[1]) - if abs2(soln[end-2]) > 1e-12 - if k_sq < 1e-12 - println(" center at infinity: z coordinates $(round(soln[end], digits = 6)) and $(round(soln[end-1], digits = 6))") - else - sum_sq = soln[4]^2 + soln[7]^2 + soln[end-2]^2 / k_sq - println(" center on z axis: r² = $(round(1/k_sq, digits = 6)), x² + y² + h² = $(round(sum_sq, digits = 6))") - end - else - sum_sq = sum(soln[[4, 7, 10]] .^ 2) - println(" center at origin: r² = $(round(1/k_sq, digits = 6)); x² + y² + z² = $(round(sum_sq, digits = 6))") - end -end +##for soln in samples +## ## display([vbls round.(soln, digits = 6)]) ## [verbose] +## k_sq = abs2(soln[1]) +## if abs2(soln[end-2]) > 1e-12 +## if k_sq < 1e-12 +## println(" center at infinity: z coordinates $(round(soln[end], digits = 6)) and $(round(soln[end-1], digits = 6))") +## else +## sum_sq = soln[4]^2 + soln[7]^2 + soln[end-2]^2 / k_sq +## println(" center on z axis: r² = $(round(1/k_sq, digits = 6)), x² + y² + h² = $(round(sum_sq, digits = 6))") +## end +## else +## sum_sq = sum(soln[[4, 7, 10]] .^ 2) +## println(" center at origin: r² = $(round(1/k_sq, digits = 6)); x² + y² + z² = $(round(sum_sq, digits = 6))") +## end +##end # show a sample solution function show_solution(ctx, vals) -- 2.34.1 From 8d8bc9162cd494798821b1d83c2f338a788e736d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 15 Feb 2024 14:27:41 -0800 Subject: [PATCH 031/129] Store elements in arrays to keep order stable This seems to restore reproducibility. --- engine-proto/Engine.Algebraic.jl | 8 ++++---- engine-proto/Engine.jl | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/engine-proto/Engine.Algebraic.jl b/engine-proto/Engine.Algebraic.jl index b6fc7a7..f14f58c 100644 --- a/engine-proto/Engine.Algebraic.jl +++ b/engine-proto/Engine.Algebraic.jl @@ -120,11 +120,11 @@ equation(rel::AlignsWithBy) = mprod(rel.elements[1].vec, rel.elements[2].vec) - # --- constructions --- mutable struct Construction{T} - points::Set{Point{T}} - spheres::Set{Sphere{T}} - relations::Set{Relation{T}} + points::Vector{Point{T}} + spheres::Vector{Sphere{T}} + relations::Vector{Relation{T}} - function Construction{T}(; elements = Set{Element{T}}(), relations = Set{Relation{T}}()) where T + function Construction{T}(; elements = Vector{Element{T}}(), relations = Vector{Relation{T}}()) where T allelements = union(elements, (rel.elements for rel in relations)...) new{T}( filter(elt -> isa(elt, Point), allelements), diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index 0410f6d..d283e72 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -56,7 +56,7 @@ tangencies = [ ##Engine.LiesOn{CoeffType}(points[3], spheres[1]), ##Engine.LiesOn{CoeffType}(points[3], spheres[2]) ##] -ctx_tan_sph = Engine.Construction{CoeffType}(elements = Set(spheres), relations = Set(tangencies)) +ctx_tan_sph = Engine.Construction{CoeffType}(elements = spheres, relations = tangencies) ideal_tan_sph, eqns_tan_sph = Engine.realize(ctx_tan_sph) ##small_eqns_tan_sph = eqns_tan_sph small_eqns_tan_sph = [ -- 2.34.1 From ae5db0f9eaa71a4f87b70e5d95efd43fa0fd9426 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 15 Feb 2024 16:00:46 -0800 Subject: [PATCH 032/129] Make results reproducible --- engine-proto/Engine.Numerical.jl | 7 ++++--- engine-proto/Engine.jl | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/engine-proto/Engine.Numerical.jl b/engine-proto/Engine.Numerical.jl index 48fb682..d1e14bd 100644 --- a/engine-proto/Engine.Numerical.jl +++ b/engine-proto/Engine.Numerical.jl @@ -1,5 +1,6 @@ module Numerical +using Random: default_rng using LinearAlgebra using AbstractAlgebra using HomotopyContinuation: @@ -28,16 +29,16 @@ end # --- sampling --- -function real_samples(F::AbstractSystem, dim) +function real_samples(F::AbstractSystem, dim; rng = default_rng()) # choose a random real hyperplane of codimension `dim` by intersecting # hyperplanes whose normal vectors are uniformly distributed over the unit # sphere # [to do] guard against the unlikely event that one of the normals is zero normals = transpose(hcat( - (normalize(randn(nvariables(F))) for _ in 1:dim)... + (normalize(randn(rng, nvariables(F))) for _ in 1:dim)... )) cut = LinearSubspace(normals, fill(0., dim)) - filter(isreal, results(witness_set(F, cut))) + filter(isreal, results(witness_set(F, cut, seed = 0x1974abba))) end AbstractAlgebra.evaluate(pt::Point, vals::Vector{<:RingElement}) = diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index d283e72..7146e80 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -89,11 +89,11 @@ vbls = Variable.(symbols(coordring)) # test a random witness set system = CompiledSystem(System(small_eqns_tan_sph, variables = vbls)) norm2 = vec -> real(dot(conj.(vec), vec)) -Random.seed!(6071) -n_planes = 36 +rng = MersenneTwister(6071) +n_planes = 3 samples = [] for _ in 1:n_planes - real_solns = solution.(Engine.Numerical.real_samples(system, freedom)) + real_solns = solution.(Engine.Numerical.real_samples(system, freedom, rng = rng)) for soln in real_solns if all(norm2(soln - samp) > 1e-4*length(gens(coordring)) for samp in samples) push!(samples, soln) -- 2.34.1 From ba365174d3a643c033a04178706fe04bfe2bc555 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 15 Feb 2024 16:16:06 -0800 Subject: [PATCH 033/129] Find real solutions for three mutually tangent spheres I'm not sure why the solver wasn't working before. It might've been just an unlucky random number draw. --- engine-proto/Engine.Algebraic.jl | 20 ++++++++++---------- engine-proto/Engine.jl | 18 +++++++++--------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/engine-proto/Engine.Algebraic.jl b/engine-proto/Engine.Algebraic.jl index f14f58c..2d28017 100644 --- a/engine-proto/Engine.Algebraic.jl +++ b/engine-proto/Engine.Algebraic.jl @@ -186,16 +186,16 @@ function realize(ctx::Construction{T}) where T # add relations to center, orient, and scale the construction # [to do] the scaling constraint, as written, can be impossible to satisfy # when all of the spheres have to go through the origin - ##if !isempty(ctx.points) - ## append!(eqns, [sum(pt.coords[k] for pt in ctx.points) for k in 1:3]) - ##end - ##if !isempty(ctx.spheres) - ## append!(eqns, [sum(sph.coords[k] for sph in ctx.spheres) for k in 3:4]) - ##end - ##n_elts = length(ctx.points) + length(ctx.spheres) - ##if n_elts > 0 - ## push!(eqns, sum(elt.vec[2] for elt in Iterators.flatten((ctx.points, ctx.spheres))) - n_elts) - ##end + if !isempty(ctx.points) + append!(eqns, [sum(pt.coords[k] for pt in ctx.points) for k in 1:3]) + end + if !isempty(ctx.spheres) + append!(eqns, [sum(sph.coords[k] for sph in ctx.spheres) for k in 3:4]) + end + n_elts = length(ctx.points) + length(ctx.spheres) + if n_elts > 0 + push!(eqns, sum(elt.vec[2] for elt in Iterators.flatten((ctx.points, ctx.spheres))) - n_elts) + end (Generic.Ideal(coordring, eqns), eqns) ## [test] (nothing, eqns) diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index 7146e80..e92eed4 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -59,13 +59,13 @@ tangencies = [ ctx_tan_sph = Engine.Construction{CoeffType}(elements = spheres, relations = tangencies) ideal_tan_sph, eqns_tan_sph = Engine.realize(ctx_tan_sph) ##small_eqns_tan_sph = eqns_tan_sph -small_eqns_tan_sph = [ - eqns_tan_sph; - spheres[2].coords - [1, 0, 0, 0, 1]; - spheres[3].coords - [1, 0, 0, 0, -1]; -] -small_ideal_tan_sph = Generic.Ideal(base_ring(ideal_tan_sph), small_eqns_tan_sph) -freedom = Engine.dimension(small_ideal_tan_sph) +##small_eqns_tan_sph = [ +## eqns_tan_sph; +## spheres[2].coords - [1, 0, 0, 0, 1]; +## spheres[3].coords - [1, 0, 0, 0, -1]; +##] +##small_ideal_tan_sph = Generic.Ideal(base_ring(ideal_tan_sph), small_eqns_tan_sph) +freedom = Engine.dimension(ideal_tan_sph) println("Three mutually tangent spheres, with two fixed: $freedom degrees of freedom") ##points = [Engine.Point{CoeffType}() for _ in 1:3] @@ -83,11 +83,11 @@ println("Three mutually tangent spheres, with two fixed: $freedom degrees of fre # --- test rational cut --- -coordring = base_ring(small_ideal_tan_sph) +coordring = base_ring(ideal_tan_sph) vbls = Variable.(symbols(coordring)) # test a random witness set -system = CompiledSystem(System(small_eqns_tan_sph, variables = vbls)) +system = CompiledSystem(System(eqns_tan_sph, variables = vbls)) norm2 = vec -> real(dot(conj.(vec), vec)) rng = MersenneTwister(6071) n_planes = 3 -- 2.34.1 From f2000e5731f52e2583d15619738acbd86c7c7974 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 15 Feb 2024 16:25:09 -0800 Subject: [PATCH 034/129] Test different sign patterns for cosines It seems like there are real solutions if and only if the product of the cosines is positive. --- engine-proto/Engine.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index e92eed4..a517117 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -44,7 +44,7 @@ tangencies = [ Engine.AlignsWithBy{CoeffType}( spheres[n], spheres[mod1(n+1, length(spheres))], - CoeffType(-1)^n + CoeffType([1, 1, 1][n]) ) for n in 1:3 ] @@ -90,7 +90,7 @@ vbls = Variable.(symbols(coordring)) system = CompiledSystem(System(eqns_tan_sph, variables = vbls)) norm2 = vec -> real(dot(conj.(vec), vec)) rng = MersenneTwister(6071) -n_planes = 3 +n_planes = 36 samples = [] for _ in 1:n_planes real_solns = solution.(Engine.Numerical.real_samples(system, freedom, rng = rng)) -- 2.34.1 From 3170a933e4594befb3395882dc453b3f96143ae7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 15 Feb 2024 17:16:37 -0800 Subject: [PATCH 035/129] Clean up example of three mutually tangent spheres --- engine-proto/Engine.Algebraic.jl | 1 - engine-proto/Engine.jl | 67 ++------------------------------ 2 files changed, 4 insertions(+), 64 deletions(-) diff --git a/engine-proto/Engine.Algebraic.jl b/engine-proto/Engine.Algebraic.jl index 2d28017..a9b6667 100644 --- a/engine-proto/Engine.Algebraic.jl +++ b/engine-proto/Engine.Algebraic.jl @@ -198,7 +198,6 @@ function realize(ctx::Construction{T}) where T end (Generic.Ideal(coordring, eqns), eqns) - ## [test] (nothing, eqns) end end \ No newline at end of file diff --git a/engine-proto/Engine.jl b/engine-proto/Engine.jl index a517117..f6f92c5 100644 --- a/engine-proto/Engine.jl +++ b/engine-proto/Engine.jl @@ -23,63 +23,19 @@ using GLMakie CoeffType = Rational{Int64} -##a = Engine.Point{CoeffType}() -##s = Engine.Sphere{CoeffType}() -##a_on_s = Engine.LiesOn{CoeffType}(a, s) -##ctx = Engine.Construction{CoeffType}(elements = Set([a]), relations= Set([a_on_s])) -##ideal_a_s = Engine.realize(ctx) -##println("A point on a sphere: $(Engine.dimension(ideal_a_s)) degrees of freedom") - -##b = Engine.Point{CoeffType}() -##b_on_s = Engine.LiesOn{CoeffType}(b, s) -##Engine.push!(ctx, b) -##Engine.push!(ctx, s) -##Engine.push!(ctx, b_on_s) -##ideal_ab_s, eqns_ab_s = Engine.realize(ctx) -##freedom = Engine.dimension(ideal_ab_s) -##println("Two points on a sphere: $freedom degrees of freedom") - spheres = [Engine.Sphere{CoeffType}() for _ in 1:3] tangencies = [ Engine.AlignsWithBy{CoeffType}( spheres[n], spheres[mod1(n+1, length(spheres))], - CoeffType([1, 1, 1][n]) + CoeffType(1) ) for n in 1:3 ] -##tangencies = [ - ##Engine.LiesOn{CoeffType}(points[1], spheres[2]), - ##Engine.LiesOn{CoeffType}(points[1], spheres[3]), - ##Engine.LiesOn{CoeffType}(points[2], spheres[3]), - ##Engine.LiesOn{CoeffType}(points[2], spheres[1]), - ##Engine.LiesOn{CoeffType}(points[3], spheres[1]), - ##Engine.LiesOn{CoeffType}(points[3], spheres[2]) -##] ctx_tan_sph = Engine.Construction{CoeffType}(elements = spheres, relations = tangencies) ideal_tan_sph, eqns_tan_sph = Engine.realize(ctx_tan_sph) -##small_eqns_tan_sph = eqns_tan_sph -##small_eqns_tan_sph = [ -## eqns_tan_sph; -## spheres[2].coords - [1, 0, 0, 0, 1]; -## spheres[3].coords - [1, 0, 0, 0, -1]; -##] -##small_ideal_tan_sph = Generic.Ideal(base_ring(ideal_tan_sph), small_eqns_tan_sph) freedom = Engine.dimension(ideal_tan_sph) -println("Three mutually tangent spheres, with two fixed: $freedom degrees of freedom") - -##points = [Engine.Point{CoeffType}() for _ in 1:3] -##spheres = [Engine.Sphere{CoeffType}() for _ in 1:2] -##ctx_joined = Engine.Construction{CoeffType}( -## elements = Set([points; spheres]), -## relations= Set([ -## Engine.LiesOn{CoeffType}(pt, sph) -## for pt in points for sph in spheres -## ]) -##) -##ideal_joined, eqns_joined = Engine.realize(ctx_joined) -##freedom = Engine.dimension(ideal_joined) -##println("$(length(points)) points on $(length(spheres)) spheres: $freedom degrees of freedom") +println("Three mutually tangent spheres: $freedom degrees of freedom") # --- test rational cut --- @@ -90,7 +46,7 @@ vbls = Variable.(symbols(coordring)) system = CompiledSystem(System(eqns_tan_sph, variables = vbls)) norm2 = vec -> real(dot(conj.(vec), vec)) rng = MersenneTwister(6071) -n_planes = 36 +n_planes = 6 samples = [] for _ in 1:n_planes real_solns = solution.(Engine.Numerical.real_samples(system, freedom, rng = rng)) @@ -100,22 +56,7 @@ for _ in 1:n_planes end end end -println("$(length(samples)) sample solutions:") -##for soln in samples -## ## display([vbls round.(soln, digits = 6)]) ## [verbose] -## k_sq = abs2(soln[1]) -## if abs2(soln[end-2]) > 1e-12 -## if k_sq < 1e-12 -## println(" center at infinity: z coordinates $(round(soln[end], digits = 6)) and $(round(soln[end-1], digits = 6))") -## else -## sum_sq = soln[4]^2 + soln[7]^2 + soln[end-2]^2 / k_sq -## println(" center on z axis: r² = $(round(1/k_sq, digits = 6)), x² + y² + h² = $(round(sum_sq, digits = 6))") -## end -## else -## sum_sq = sum(soln[[4, 7, 10]] .^ 2) -## println(" center at origin: r² = $(round(1/k_sq, digits = 6)); x² + y² + z² = $(round(sum_sq, digits = 6))") -## end -##end +println("Found $(length(samples)) sample solutions") # show a sample solution function show_solution(ctx, vals) -- 2.34.1 From 16826cf07c692390f965cd2fe9de5491810caa1d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 20 Feb 2024 22:35:24 -0500 Subject: [PATCH 036/129] Try out the Gram matrix approach --- engine-proto/gram-test/gram-test.jl | 30 +++++++++++++++++++++++++++ engine-proto/gram-test/gram-test.sage | 27 ++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 engine-proto/gram-test/gram-test.jl create mode 100644 engine-proto/gram-test/gram-test.sage diff --git a/engine-proto/gram-test/gram-test.jl b/engine-proto/gram-test/gram-test.jl new file mode 100644 index 0000000..b053097 --- /dev/null +++ b/engine-proto/gram-test/gram-test.jl @@ -0,0 +1,30 @@ +using LinearAlgebra +using AbstractAlgebra + +F, (a, b, c) = rational_function_field(Generic.Rationals{BigInt}(), ["a", "b", "c"]) +M = matrix_space(F, 5, 5) + +# three mutually tangent spheres which are all perpendicular to the x, y plane +gram = M(F.([ + -1 0 0 0 0; + 0 -1 a b c; + 0 a -1 1 1; + 0 b 1 -1 1; + 0 c 1 1 -1; +])) + +r, p, L, U = lu(gram) +solution = transpose(p * L) +mform = U * inv(transpose(L)) + +concrete = [evaluate(entry, [0, 1, -3//4]) for entry in solution] + +std_basis = [ + 0 0 0 1 1; + 0 0 0 1 -1; + 1 0 0 0 0; + 0 1 0 0 0; + 0 0 1 0 0 +] +std_solution = M(F.(std_basis)) * solution +std_concrete = std_basis * concrete \ No newline at end of file diff --git a/engine-proto/gram-test/gram-test.sage b/engine-proto/gram-test/gram-test.sage new file mode 100644 index 0000000..a95ce97 --- /dev/null +++ b/engine-proto/gram-test/gram-test.sage @@ -0,0 +1,27 @@ +F = QQ['a', 'b', 'c'].fraction_field() +a, b, c = F.gens() + +# three mutually tangent spheres which are all perpendicular to the x, y plane +gram = matrix([ + [-1, 0, 0, 0, 0], + [0, -1, a, b, c], + [0, a, -1, 1, 1], + [0, b, 1, -1, 1], + [0, c, 1, 1, -1] +]) + +P, L, U = gram.LU() +solution = (P * L).transpose() +mform = U * L.transpose().inverse() + +concrete = solution.subs({a: 0, b: 1, c: -3/4}) + +std_basis = matrix([ + [0, 0, 0, 1, 1], + [0, 0, 0, 1, -1], + [1, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 0, 0] +]) +std_solution = std_basis * solution +std_concrete = std_basis * concrete \ No newline at end of file -- 2.34.1 From 717e5a6200a61db02b3cc03ae6117cf394a4ad6f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 21 Feb 2024 02:50:22 -0500 Subject: [PATCH 037/129] Extend Gram matrix automatically The signature of the Minkowski form on the subspace spanned by the Gram matrix should tell us what the big Gram matrix has to look like --- engine-proto/gram-test/gram-test.jl | 73 ++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/engine-proto/gram-test/gram-test.jl b/engine-proto/gram-test/gram-test.jl index b053097..c962967 100644 --- a/engine-proto/gram-test/gram-test.jl +++ b/engine-proto/gram-test/gram-test.jl @@ -1,23 +1,56 @@ using LinearAlgebra using AbstractAlgebra -F, (a, b, c) = rational_function_field(Generic.Rationals{BigInt}(), ["a", "b", "c"]) -M = matrix_space(F, 5, 5) +function printgood(msg) + printstyled("✓", color = :green) + println(" ", msg) +end + +function printbad(msg) + printstyled("✗", color = :red) + println(" ", msg) +end + +F, gens = rational_function_field(Generic.Rationals{BigInt}(), ["a₁", "a₂", "b₁", "b₂", "c₁", "c₂"]) +a = gens[1:2] +b = gens[3:4] +c = gens[5:6] # three mutually tangent spheres which are all perpendicular to the x, y plane -gram = M(F.([ - -1 0 0 0 0; - 0 -1 a b c; - 0 a -1 1 1; - 0 b 1 -1 1; - 0 c 1 1 -1; +gram = [ + -1 1 1; + 1 -1 1; + 1 1 -1 +] + +eig = eigen(gram) +n_pos = count(eig.values .> 0.5) +n_neg = count(eig.values .< -0.5) +if n_pos + n_neg == size(gram, 1) + printgood("Non-degenerate subspace") +else + printbad("Degenerate subspace") +end +sig_rem = Int64[ones(2-n_pos); -ones(3-n_neg)] +unk = hcat(a, b, c) +M = matrix_space(F, 5, 5) +big_gram = M(F.([ + diagm(sig_rem) unk; + transpose(unk) gram ])) -r, p, L, U = lu(gram) -solution = transpose(p * L) -mform = U * inv(transpose(L)) +r, p, L, U = lu(big_gram) +if isone(p) + printgood("Found a solution") +else + printbad("Didn't find a solution") +end +solution = transpose(L) +mform = U * inv(solution) -concrete = [evaluate(entry, [0, 1, -3//4]) for entry in solution] +vals = [0, 0, 0, 1, 0, -3//4] +solution_ex = [evaluate(entry, vals) for entry in solution] +mform_ex = [evaluate(entry, vals) for entry in mform] std_basis = [ 0 0 0 1 1; @@ -27,4 +60,18 @@ std_basis = [ 0 0 1 0 0 ] std_solution = M(F.(std_basis)) * solution -std_concrete = std_basis * concrete \ No newline at end of file +std_solution_ex = std_basis * solution_ex + +println("Minkowski form:") +display(mform_ex) + +big_gram_recovered = transpose(solution_ex) * mform_ex * solution_ex +valid = all(iszero.( + [evaluate(entry, vals) for entry in big_gram] - big_gram_recovered +)) +if valid + printgood("Recovered Gram matrix:") +else + printbad("Didn't recover Gram matrix. Instead, got:") +end +display(big_gram_recovered) \ No newline at end of file -- 2.34.1 From ef33b8ee101b533894e7c04d8484e2531e96fd1a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 1 Mar 2024 13:26:20 -0500 Subject: [PATCH 038/129] Correct signature --- engine-proto/gram-test/gram-test.jl | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/engine-proto/gram-test/gram-test.jl b/engine-proto/gram-test/gram-test.jl index c962967..4b2f859 100644 --- a/engine-proto/gram-test/gram-test.jl +++ b/engine-proto/gram-test/gram-test.jl @@ -31,7 +31,7 @@ if n_pos + n_neg == size(gram, 1) else printbad("Degenerate subspace") end -sig_rem = Int64[ones(2-n_pos); -ones(3-n_neg)] +sig_rem = Int64[ones(1-n_pos); -ones(4-n_neg)] unk = hcat(a, b, c) M = matrix_space(F, 5, 5) big_gram = M(F.([ @@ -74,4 +74,12 @@ if valid else printbad("Didn't recover Gram matrix. Instead, got:") end -display(big_gram_recovered) \ No newline at end of file +display(big_gram_recovered) + +# this should be a solution +hand_solution = [0 0 1 0 0; 0 0 -1 2 2; 0 0 0 1 -1; 1 0 0 0 0; 0 1 0 0 0] +unmix = Rational{Int64}[[1//2 1//2; 1//2 -1//2] zeros(Int64, 2, 3); zeros(Int64, 3, 2) Matrix{Int64}(I, 3, 3)] +hand_solution_diag = unmix * hand_solution +big_gram_hand_recovered = transpose(hand_solution_diag) * diagm([1; -ones(Int64, 4)]) * hand_solution_diag +println("Gram matrix from hand-written solution:") +display(big_gram_hand_recovered) \ No newline at end of file -- 2.34.1 From 58a5c38e6229e6e30b9e468114c4f302140bb2eb Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 30 May 2024 00:36:03 -0700 Subject: [PATCH 039/129] Try numerical low-rank factorization The best technique I've found so far is the homemade gradient descent routine in `descent-test.jl`. --- engine-proto/gram-test/descent-test.jl | 137 ++++++++++++++++++++++++ engine-proto/gram-test/low-rank-test.jl | 49 +++++++++ engine-proto/gram-test/overlap-test.jl | 37 +++++++ 3 files changed, 223 insertions(+) create mode 100644 engine-proto/gram-test/descent-test.jl create mode 100644 engine-proto/gram-test/low-rank-test.jl create mode 100644 engine-proto/gram-test/overlap-test.jl diff --git a/engine-proto/gram-test/descent-test.jl b/engine-proto/gram-test/descent-test.jl new file mode 100644 index 0000000..9b6fb7a --- /dev/null +++ b/engine-proto/gram-test/descent-test.jl @@ -0,0 +1,137 @@ +using LinearAlgebra +using SparseArrays +using AbstractAlgebra +using PolynomialRoots + +# testing Gram matrix recovery using a homemade gradient descent routine + +# === gradient descent === + +# the difference between the matrices `target` and `attempt`, projected onto the +# subspace of matrices whose entries vanish at each empty index of `target` +function proj_diff(target, attempt) + I, J, values = findnz(target) + result = zeros(size(target)...) + for (i, j, val) in zip(I, J, values) + result[i, j] = val - attempt[i, j] + end + result +end + +# === example === + +# the Lorentz form +Q = diagm([-1, 1, 1, 1, 1]) + +# initialize the partial gram matrix for an arrangement of seven spheres in +# which spheres 1 through 5 are mutually tangent, and spheres 3 through 7 are +# also mutually tangent +I = Int64[] +J = Int64[] +values = BigFloat[] +for i in 1:7 + for j in 1:7 + if (i <= 5 && j <= 5) || (i >= 3 && j >= 3) + push!(I, i) + push!(J, j) + push!(values, i == j ? 1 : -1) + end + end +end +gram = sparse(I, J, values) + +# set the independent variable +# +# using gram[6, 2] or gram[7, 1] as the independent variable seems to stall +# convergence, even if its value comes from a known solution, like +# +# gram[6, 2] = 0.9936131705272925 +# +indep_val = -9//5 +gram[6, 1] = BigFloat(indep_val) +gram[1, 6] = gram[6, 1] + +# in this initial guess, the mutual tangency condition is satisfied for spheres +# 1 through 5 +guess = sqrt(0.5) * BigFloat[ + 1 1 1 1 2 0.2 0.1; + 0 0 0 0 -sqrt(6) 0.3 -0.2; + 1 1 -1 -1 0 -0.1 0.3; + 1 -1 1 -1 0 -0.5 0.4; + 1 -1 -1 1 0 0.1 -0.2 +] + +# search parameters +steps = 600 +line_search_max_steps = 100 +init_stepsize = BigFloat(1) +step_shrink_factor = BigFloat(0.5) +target_improvement_factor = BigFloat(0.5) + +# complete the gram matrix using gradient descent +loss_history = Array{BigFloat}(undef, steps + 1) +stepsize_history = Array{BigFloat}(undef, steps) +line_search_depth_history = fill(line_search_max_steps, steps) +stepsize = init_stepsize +L = copy(guess) +Δ_proj = proj_diff(gram, L'*Q*L) +loss = norm(Δ_proj) +for step in 1:steps + # find negative gradient of loss function + neg_grad = 4*Q*L*Δ_proj + slope = norm(neg_grad) + + # store current position and loss + L_last = L + loss_last = loss + loss_history[step] = loss + + # find a good step size using backtracking line search + for line_search_depth in 1:line_search_max_steps + stepsize_history[step] = stepsize + global L = L_last + stepsize * neg_grad + global Δ_proj = proj_diff(gram, L'*Q*L) + global loss = norm(Δ_proj) + improvement = loss_last - loss + if improvement >= target_improvement_factor * stepsize * slope + line_search_depth_history[step] = line_search_depth + break + end + global stepsize *= step_shrink_factor + end +end +completed_gram = L'*Q*L +loss_history[steps + 1] = loss +println("Completed Gram matrix:\n") +display(completed_gram) +println("\nLoss: ", loss, "\n") + +# === algebraic check === + +R, gens = polynomial_ring(Generic.Rationals{BigInt}(), ["x", "t₁", "t₂", "t₃"]) +x = gens[1] +t = gens[2:4] + +S, u = polynomial_ring(Generic.Rationals{BigInt}(), "u") + +M = matrix_space(R, 7, 7) +gram_symb = M(R[ + 1 -1 -1 -1 -1 t[1] t[2]; + -1 1 -1 -1 -1 x t[3] + -1 -1 1 -1 -1 -1 -1; + -1 -1 -1 1 -1 -1 -1; + -1 -1 -1 -1 1 -1 -1; + t[1] x -1 -1 -1 1 -1; + t[2] t[3] -1 -1 -1 -1 1 +]) +rank_constraints = det.([ + gram_symb[1:6, 1:6], + gram_symb[2:7, 2:7], + gram_symb[[1, 3, 4, 5, 6, 7], [1, 3, 4, 5, 6, 7]] +]) + +# solve for x and t +x_constraint = 25//16 * to_univariate(S, evaluate(rank_constraints[1], [2], [indep_val])) +t₂_constraint = 25//16 * to_univariate(S, evaluate(rank_constraints[3], [2], [indep_val])) +x_vals = PolynomialRoots.roots(x_constraint.coeffs) +t₂_vals = PolynomialRoots.roots(t₂_constraint.coeffs) diff --git a/engine-proto/gram-test/low-rank-test.jl b/engine-proto/gram-test/low-rank-test.jl new file mode 100644 index 0000000..d932a3d --- /dev/null +++ b/engine-proto/gram-test/low-rank-test.jl @@ -0,0 +1,49 @@ +using LowRankModels +using LinearAlgebra +using SparseArrays + +# testing Gram matrix recovery using the LowRankModels package + +# initialize the partial gram matrix for an arrangement of seven spheres in +# which spheres 1 through 5 are mutually tangent, and spheres 3 through 7 are +# also mutually tangent +I = Int64[] +J = Int64[] +values = Float64[] +for i in 1:7 + for j in 1:7 + if (i <= 5 && j <= 5) || (i >= 3 && j >= 3) + push!(I, i) + push!(J, j) + push!(values, i == j ? 1 : -1) + end + end +end +gram = sparse(I, J, values) + +# in this initial guess, the mutual tangency condition is satisfied for spheres +# 1 through 5 +X₀ = sqrt(0.5) * [ + 1 0 1 1 1; + 1 0 1 -1 -1; + 1 0 -1 1 -1; + 1 0 -1 -1 1; + 2 -sqrt(6) 0 0 0; + 0.2 0.3 -0.1 -0.2 0.1; + 0.1 -0.2 0.3 0.4 -0.1 +]' +Y₀ = diagm([-1, 1, 1, 1, 1]) * X₀ + +# search parameters +search_params = ProxGradParams( + 1.0; + max_iter = 100, + inner_iter = 1, + abs_tol = 1e-16, + rel_tol = 1e-9, + min_stepsize = 0.01 +) + +# complete gram matrix +model = GLRM(gram, QuadLoss(), ZeroReg(), ZeroReg(), 5, X = X₀, Y = Y₀) +X, Y, history = fit!(model, search_params) diff --git a/engine-proto/gram-test/overlap-test.jl b/engine-proto/gram-test/overlap-test.jl new file mode 100644 index 0000000..beeaeca --- /dev/null +++ b/engine-proto/gram-test/overlap-test.jl @@ -0,0 +1,37 @@ +using LinearAlgebra +using AbstractAlgebra + +function printgood(msg) + printstyled("✓", color = :green) + println(" ", msg) +end + +function printbad(msg) + printstyled("✗", color = :red) + println(" ", msg) +end + +F, gens = rational_function_field(Generic.Rationals{BigInt}(), ["x", "t₁", "t₂", "t₃"]) +x = gens[1] +t = gens[2:4] + +# three mutually tangent spheres which are all perpendicular to the x, y plane +M = matrix_space(F, 7, 7) +gram = M(F[ + 1 -1 -1 -1 -1 t[1] t[2]; + -1 1 -1 -1 -1 x t[3] + -1 -1 1 -1 -1 -1 -1; + -1 -1 -1 1 -1 -1 -1; + -1 -1 -1 -1 1 -1 -1; + t[1] x -1 -1 -1 1 -1; + t[2] t[3] -1 -1 -1 -1 1 +]) + +r, p, L, U = lu(gram) +if isone(p) + printgood("Found a solution") +else + printbad("Didn't find a solution") +end +solution = transpose(L) +mform = U * inv(solution) -- 2.34.1 From d1ce91d2aa6f02706a11dd533daeb86ab8523f21 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 24 Jun 2024 19:37:57 -0700 Subject: [PATCH 040/129] Get a Ganja.js visualization running in Blink --- engine-proto/ganja-test/ganja-test.html | 51 ++++++++++++++++++ engine-proto/ganja-test/ganja-test.jl | 71 +++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 engine-proto/ganja-test/ganja-test.html create mode 100644 engine-proto/ganja-test/ganja-test.jl diff --git a/engine-proto/ganja-test/ganja-test.html b/engine-proto/ganja-test/ganja-test.html new file mode 100644 index 0000000..f1ecf45 --- /dev/null +++ b/engine-proto/ganja-test/ganja-test.html @@ -0,0 +1,51 @@ + + + + + + + + + + diff --git a/engine-proto/ganja-test/ganja-test.jl b/engine-proto/ganja-test/ganja-test.jl new file mode 100644 index 0000000..ea8b334 --- /dev/null +++ b/engine-proto/ganja-test/ganja-test.jl @@ -0,0 +1,71 @@ +using Blink +import Blink: JSString + +# === styling utility === + +style!(w, stylesheet) = @js win begin + @var style = document.createElement("style"); + style.appendChild(document.createTextNode($stylesheet)); + document.head.appendChild(style); +end + +# === page source === + +stylesheet = """ +body { + background-color: #ffe0f0; +} + +/* needed to keep Ganja canvas from blowing up */ +canvas { + min-width: 600px; + max-width: 600px; + min-height: 600px; + max-height: 600px; +} +""" + +# the "points spheres plane" example from the Ganja coffee shop +# +# https://enkimute.github.io/ganja.js/examples/coffeeshop.html#cga3d_points_spheres_planes +# +sphere_example = """ +Algebra(4, 1, ()=>{ + // We start by defining a null basis, and upcasting for points + var ni = 1e4+1e5, no = .5e5-.5e4; + var up = (x)=> no + x + .5*x*x*ni; + + // Next we'll define 4 points + var p1 = up(1e1), p2 = up(1e2), p3 = up(-1e3), p4 = up(-1e2); + + // The outer product can be used to construct the sphere through + // any four points. + var s = ()=>p1^p2^p3^p4; + + // The outer product between any three points and infinity is a plane. + var p = ()=>p1^p2^p3^ni; + + // Graph the items. + document.body.appendChild(this.graph([ + 0x00FF0000, p1, "p1", p2, "p2", p3, "p3", p4, "p4", // points + 0xE0008800, p, "p", // plane + 0xE00000FF, s, "s" // sphere + ], {conformal: true, gl: true, grid: true})); +}); +""" + +# === page construction === + +# create window and open developer console +win = Window() +opentools(win) + +# set stylesheet +style!(win, stylesheet) + +# load Ganja.js +loadjs!(win, "https://unpkg.com/ganja.js") + +# launch Ganja visualization +body!(win, "", async=false) +js(win, JSString(sphere_example)) -- 2.34.1 From 3c344815196a63cec0e5887239db65facbf77426 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 25 Jun 2024 01:54:01 -0700 Subject: [PATCH 041/129] Get familiar with Ganja.js inline syntax --- engine-proto/ganja-test/ganja-test.html | 50 ++++++++++++------------- engine-proto/ganja-test/ganja-test.jl | 46 ++++++++++++----------- 2 files changed, 47 insertions(+), 49 deletions(-) diff --git a/engine-proto/ganja-test/ganja-test.html b/engine-proto/ganja-test/ganja-test.html index f1ecf45..26eba9d 100644 --- a/engine-proto/ganja-test/ganja-test.html +++ b/engine-proto/ganja-test/ganja-test.html @@ -18,34 +18,30 @@ diff --git a/engine-proto/ganja-test/ganja-test.jl b/engine-proto/ganja-test/ganja-test.jl index ea8b334..155caa9 100644 --- a/engine-proto/ganja-test/ganja-test.jl +++ b/engine-proto/ganja-test/ganja-test.jl @@ -30,28 +30,30 @@ canvas { # https://enkimute.github.io/ganja.js/examples/coffeeshop.html#cga3d_points_spheres_planes # sphere_example = """ -Algebra(4, 1, ()=>{ - // We start by defining a null basis, and upcasting for points - var ni = 1e4+1e5, no = .5e5-.5e4; - var up = (x)=> no + x + .5*x*x*ni; - - // Next we'll define 4 points - var p1 = up(1e1), p2 = up(1e2), p3 = up(-1e3), p4 = up(-1e2); - - // The outer product can be used to construct the sphere through - // any four points. - var s = ()=>p1^p2^p3^p4; - - // The outer product between any three points and infinity is a plane. - var p = ()=>p1^p2^p3^ni; - - // Graph the items. - document.body.appendChild(this.graph([ - 0x00FF0000, p1, "p1", p2, "p2", p3, "p3", p4, "p4", // points - 0xE0008800, p, "p", // plane - 0xE00000FF, s, "s" // sphere - ], {conformal: true, gl: true, grid: true})); -}); +// in the default view, e4 + e5 is the point at infinity +CGA3 = Algebra(4, 1); +v1 = CGA3.inline(() => 1e1 + 1e5)(); +v2 = CGA3.inline(() => 1e2 + 1e5)(); +v3 = CGA3.inline(() => 1e3 + 1e5)(); +w1 = CGA3.inline(() => 1e1 - Math.sqrt(0.2)*1e4 + Math.sqrt(1.2)*1e5)(); +w2 = CGA3.inline(() => 1e2 - Math.sqrt(0.2)*1e4 + Math.sqrt(1.2)*1e5)(); +w3 = CGA3.inline(() => 1e3 - Math.sqrt(0.2)*1e4 + Math.sqrt(1.2)*1e5)(); +s = CGA3.inline(() => -Math.sqrt(1.2)*1e4 + Math.sqrt(0.2)*1e5); + +document.body.appendChild(CGA3.graph( + [ + 0xff00b0, v1, + 0x00ffb0, v2, + 0x00b0ff, v3, + 0x800040, w1, + 0x008040, w2, + 0x004080, w3, + 0xd0e0f0, s + ], + { + conformal: true, gl: true, grid: true + } +)); """ # === page construction === -- 2.34.1 From 3b10c95d5ff92fb7f31475734c7a237f3d780780 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 25 Jun 2024 02:07:39 -0700 Subject: [PATCH 042/129] Clean up examples Declare JavaScript variables. Revise Julia comments to match new code. --- engine-proto/ganja-test/ganja-test.html | 16 ++++++++-------- engine-proto/ganja-test/ganja-test.jl | 24 ++++++++++-------------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/engine-proto/ganja-test/ganja-test.html b/engine-proto/ganja-test/ganja-test.html index 26eba9d..f1aeea7 100644 --- a/engine-proto/ganja-test/ganja-test.html +++ b/engine-proto/ganja-test/ganja-test.html @@ -19,14 +19,14 @@ +

diff --git a/engine-proto/ganja-test/ganja-test.jl b/engine-proto/ganja-test/ganja-test.jl index 9ea621b..0808089 100644 --- a/engine-proto/ganja-test/ganja-test.jl +++ b/engine-proto/ganja-test/ganja-test.jl @@ -25,31 +25,51 @@ canvas { } """ +controls = """ +

+""" + graph_script = """ // in the default view, e4 + e5 is the point at infinity let CGA3 = Algebra(4, 1); -let v1 = CGA3.inline(() => 1e1 + 1e5)(); -let v2 = CGA3.inline(() => 1e2 + 1e5)(); -let v3 = CGA3.inline(() => 1e3 + 1e5)(); -let w1 = CGA3.inline(() => 1e1 - Math.sqrt(0.2)*1e4 + Math.sqrt(1.2)*1e5)(); -let w2 = CGA3.inline(() => 1e2 - Math.sqrt(0.2)*1e4 + Math.sqrt(1.2)*1e5)(); -let w3 = CGA3.inline(() => 1e3 - Math.sqrt(0.2)*1e4 + Math.sqrt(1.2)*1e5)(); +let v = [ + CGA3.inline(() => 1e1 + 1e5)(), + CGA3.inline(() => 1e2 + 1e5)(), + CGA3.inline(() => 1e3 + 1e5)() +]; +let w = [ + CGA3.inline(() => 1e1 - Math.sqrt(0.2)*1e4 + Math.sqrt(1.2)*1e5)(), + CGA3.inline(() => 1e2 - Math.sqrt(0.2)*1e4 + Math.sqrt(1.2)*1e5)(), + CGA3.inline(() => 1e3 - Math.sqrt(0.2)*1e4 + Math.sqrt(1.2)*1e5)() +]; let s = CGA3.inline(() => -Math.sqrt(1.2)*1e4 + Math.sqrt(0.2)*1e5); -document.body.appendChild(CGA3.graph( - [ - 0xff00b0, v1, - 0x00ffb0, v2, - 0x00b0ff, v3, - 0x800040, w1, - 0x008040, w2, - 0x004080, w3, - 0xd0e0f0, s - ], +// create scene function +let scene = () => [ + 0xff00b0, v[0], + 0x00ffb0, v[1], + 0x00b0ff, v[2], + 0x800040, w[0], + 0x008040, w[1], + 0x004080, w[2], + 0xd0e0f0, s +]; + +// initialize graph +let gr = CGA3.graph( + scene, { conformal: true, gl: true, grid: true } -)); +) +document.body.appendChild(gr); + +// connect flip button +function flipPoint() { + v[0] = CGA3.Dual(CGA3.Mul(s, v[0])); + requestAnimationFrame(gr.update.bind(gr, scene)); +} +document.querySelector('#flip-button').addEventListener('click', flipPoint); """ # === page construction === @@ -65,5 +85,5 @@ style!(win, stylesheet) loadjs!(win, "https://unpkg.com/ganja.js") # launch Ganja visualization -body!(win, "", async=false) +body!(win, controls, async=false) js(win, JSString(graph_script)) -- 2.34.1 From 06a9dda5bb58079db955675a8ef7a4ab9fed4945 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 25 Jun 2024 13:40:40 -0700 Subject: [PATCH 044/129] Play with reflections Try configuration of five tangent spheres. --- engine-proto/ganja-test/ganja-test.html | 36 +++++++++++++------------ engine-proto/ganja-test/ganja-test.jl | 36 +++++++++++++------------ 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/engine-proto/ganja-test/ganja-test.html b/engine-proto/ganja-test/ganja-test.html index e1da682..e0d59bb 100644 --- a/engine-proto/ganja-test/ganja-test.html +++ b/engine-proto/ganja-test/ganja-test.html @@ -17,31 +17,25 @@ -

+

diff --git a/engine-proto/ganja-test/ganja-test.jl b/engine-proto/ganja-test/ganja-test.jl index 0808089..a5d7f6b 100644 --- a/engine-proto/ganja-test/ganja-test.jl +++ b/engine-proto/ganja-test/ganja-test.jl @@ -26,33 +26,27 @@ canvas { """ controls = """ -

+

""" graph_script = """ // in the default view, e4 + e5 is the point at infinity let CGA3 = Algebra(4, 1); let v = [ - CGA3.inline(() => 1e1 + 1e5)(), - CGA3.inline(() => 1e2 + 1e5)(), - CGA3.inline(() => 1e3 + 1e5)() + CGA3.inline(() => Math.sqrt(0.5)*( 1e1 + 1e2 + 1e3 + 1e5))(), + CGA3.inline(() => Math.sqrt(0.5)*( 1e1 - 1e2 - 1e3 + 1e5))(), + CGA3.inline(() => Math.sqrt(0.5)*(-1e1 + 1e2 - 1e3 + 1e5))(), + CGA3.inline(() => Math.sqrt(0.5)*(-1e1 - 1e2 + 1e3 + 1e5))(), + CGA3.inline(() => -Math.sqrt(3)*1e4 + Math.sqrt(2)*1e5)() ]; -let w = [ - CGA3.inline(() => 1e1 - Math.sqrt(0.2)*1e4 + Math.sqrt(1.2)*1e5)(), - CGA3.inline(() => 1e2 - Math.sqrt(0.2)*1e4 + Math.sqrt(1.2)*1e5)(), - CGA3.inline(() => 1e3 - Math.sqrt(0.2)*1e4 + Math.sqrt(1.2)*1e5)() -]; -let s = CGA3.inline(() => -Math.sqrt(1.2)*1e4 + Math.sqrt(0.2)*1e5); // create scene function let scene = () => [ 0xff00b0, v[0], 0x00ffb0, v[1], 0x00b0ff, v[2], - 0x800040, w[0], - 0x008040, w[1], - 0x004080, w[2], - 0xd0e0f0, s + 0x8040ff, v[3], + 0xc0c0c0, v[4] ]; // initialize graph @@ -65,11 +59,19 @@ let gr = CGA3.graph( document.body.appendChild(gr); // connect flip button -function flipPoint() { - v[0] = CGA3.Dual(CGA3.Mul(s, v[0])); +function flip() { + for (let n = 0; n < 4; ++n) { + // reflect + v[n] = CGA3.Mul(CGA3.Mul(v[4], v[n]), v[4]); + + // de-noise + for (let k = 6; k < v[n].length; ++k) { + v[n][k] = 0; + } + } requestAnimationFrame(gr.update.bind(gr, scene)); } -document.querySelector('#flip-button').addEventListener('click', flipPoint); +document.querySelector('#flip-button').addEventListener('click', flip); """ # === page construction === -- 2.34.1 From b7b5b9386b4cf9ca4f4f213bdc7f4066a868eadf Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 25 Jun 2024 16:30:19 -0700 Subject: [PATCH 045/129] Load elements from Julia into Ganja.js --- engine-proto/ganja-test/ganja-test.jl | 159 ++++++++++++++------------ 1 file changed, 89 insertions(+), 70 deletions(-) diff --git a/engine-proto/ganja-test/ganja-test.jl b/engine-proto/ganja-test/ganja-test.jl index a5d7f6b..69b768f 100644 --- a/engine-proto/ganja-test/ganja-test.jl +++ b/engine-proto/ganja-test/ganja-test.jl @@ -1,17 +1,30 @@ using Blink -import Blink: JSString -# === styling utility === +# === utilities === -style!(w, stylesheet) = @js win begin - @var style = document.createElement("style"); - style.appendChild(document.createTextNode($stylesheet)); - document.head.appendChild(style); +append_to_head!(w, type, content) = @js win begin + @var element = document.createElement($type) + element.appendChild(document.createTextNode($content)) + document.head.appendChild(element) end -# === page source === +style!(w, stylesheet) = append_to_head!(w, "style", stylesheet) -stylesheet = """ +script!(w, code) = append_to_head!(w, "script", code) + +function add_element!(vec) + full_vec = [0; vec; fill(0, 26)] + @js win elements.push(@new CGA3($full_vec)) +end + +# === build page === + +# create window and open developer console +win = Window() +opentools(win) + +# set stylesheet +style!(win, """ body { background-color: #ffe0f0; } @@ -23,69 +36,75 @@ canvas { min-height: 600px; max-height: 600px; } -""" - -controls = """ -

-""" - -graph_script = """ -// in the default view, e4 + e5 is the point at infinity -let CGA3 = Algebra(4, 1); -let v = [ - CGA3.inline(() => Math.sqrt(0.5)*( 1e1 + 1e2 + 1e3 + 1e5))(), - CGA3.inline(() => Math.sqrt(0.5)*( 1e1 - 1e2 - 1e3 + 1e5))(), - CGA3.inline(() => Math.sqrt(0.5)*(-1e1 + 1e2 - 1e3 + 1e5))(), - CGA3.inline(() => Math.sqrt(0.5)*(-1e1 - 1e2 + 1e3 + 1e5))(), - CGA3.inline(() => -Math.sqrt(3)*1e4 + Math.sqrt(2)*1e5)() -]; - -// create scene function -let scene = () => [ - 0xff00b0, v[0], - 0x00ffb0, v[1], - 0x00b0ff, v[2], - 0x8040ff, v[3], - 0xc0c0c0, v[4] -]; - -// initialize graph -let gr = CGA3.graph( - scene, - { - conformal: true, gl: true, grid: true - } -) -document.body.appendChild(gr); - -// connect flip button -function flip() { - for (let n = 0; n < 4; ++n) { - // reflect - v[n] = CGA3.Mul(CGA3.Mul(v[4], v[n]), v[4]); - - // de-noise - for (let k = 6; k < v[n].length; ++k) { - v[n][k] = 0; - } - } - requestAnimationFrame(gr.update.bind(gr, scene)); -} -document.querySelector('#flip-button').addEventListener('click', flip); -""" - -# === page construction === - -# create window and open developer console -win = Window() -opentools(win) - -# set stylesheet -style!(win, stylesheet) +""") # load Ganja.js loadjs!(win, "https://unpkg.com/ganja.js") -# launch Ganja visualization -body!(win, controls, async=false) -js(win, JSString(graph_script)) +# create global functions and variables +script!(win, """ + // create algebra + var CGA3 = Algebra(4, 1); + + // in the default view, e4 + e5 is the point at infinity + var elements = []; + + // create scene function + let scene = () => [ + 0xff00b0, elements[0], + 0x00ffb0, elements[1], + 0x00b0ff, elements[2], + 0x8040ff, elements[3], + 0xc0c0c0, elements[4] + ]; + + // declare visualization handle + var graph; + + function flip() { + for (let n = 0; n < 4; ++n) { + // reflect + elements[n] = CGA3.Mul(CGA3.Mul(elements[4], elements[n]), elements[4]); + + // de-noise + for (let k = 6; k < elements[n].length; ++k) { + elements[n][k] = 0; + } + } + requestAnimationFrame(graph.update.bind(graph, scene)); + } +""") + +# set up controls +body!(win, """ +

+""", async=false) + +# === set up visualization === + +# list elements. in the default view, e4 + e5 is the point at infinity +elements = sqrt(0.5) * BigFloat[ + 1 1 -1 -1 0; + 1 -1 1 -1 0; + 1 -1 -1 1 0; + 0 0 0 0 -sqrt(6); + 1 1 1 1 2 +] + +# load elements +for vec in eachcol(elements) + add_element!(vec) +end + +# initialize visualization +@js win begin + graph = CGA3.graph( + scene, + Dict( + "conformal" => true, + "gl" => true, + "grid" => true + ) + ) + document.body.appendChild(graph); +end \ No newline at end of file -- 2.34.1 From 182b5bb9f69f7e7bbbc524da85eb43fa62549510 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 25 Jun 2024 17:57:16 -0700 Subject: [PATCH 046/129] Generate palette automatically --- engine-proto/ganja-test/ganja-test.html | 44 ++++++++++++-------- engine-proto/ganja-test/ganja-test.jl | 53 ++++++++++++++++--------- 2 files changed, 63 insertions(+), 34 deletions(-) diff --git a/engine-proto/ganja-test/ganja-test.html b/engine-proto/ganja-test/ganja-test.html index e0d59bb..d6efc90 100644 --- a/engine-proto/ganja-test/ganja-test.html +++ b/engine-proto/ganja-test/ganja-test.html @@ -21,43 +21,55 @@ diff --git a/engine-proto/ganja-test/ganja-test.jl b/engine-proto/ganja-test/ganja-test.jl index 69b768f..b944eae 100644 --- a/engine-proto/ganja-test/ganja-test.jl +++ b/engine-proto/ganja-test/ganja-test.jl @@ -1,4 +1,5 @@ using Blink +using Colors # === utilities === @@ -13,8 +14,22 @@ style!(w, stylesheet) = append_to_head!(w, "style", stylesheet) script!(w, code) = append_to_head!(w, "script", code) function add_element!(vec) + # add element full_vec = [0; vec; fill(0, 26)] - @js win elements.push(@new CGA3($full_vec)) + n = @js win elements.push(@new CGA3($full_vec)) + + # generate palette. this is Gadfly's `default_discrete_colors` palette, + # available under the MIT license + palette = distinguishable_colors( + n, + [LCHab(70, 60, 240)], + transform = c -> deuteranopic(c, 0.5), + lchoices = Float64[65, 70, 75, 80], + cchoices = Float64[0, 50, 60, 70], + hchoices = range(0, stop=330, length=24) + ) + palette_packed = [RGB24(c).color for c in palette] + @js win palette = $palette_packed end # === build page === @@ -45,26 +60,28 @@ loadjs!(win, "https://unpkg.com/ganja.js") script!(win, """ // create algebra var CGA3 = Algebra(4, 1); - - // in the default view, e4 + e5 is the point at infinity + + // initialize element list and palette var elements = []; - - // create scene function - let scene = () => [ - 0xff00b0, elements[0], - 0x00ffb0, elements[1], - 0x00b0ff, elements[2], - 0x8040ff, elements[3], - 0xc0c0c0, elements[4] - ]; - + var palette = []; + // declare visualization handle var graph; - + + // create scene function + function scene() { + commands = []; + for (let n = 0; n < elements.length; ++n) { + commands.push(palette[n], elements[n]); + } + return commands; + } + function flip() { - for (let n = 0; n < 4; ++n) { + let last = elements.length - 1; + for (let n = 0; n < last; ++n) { // reflect - elements[n] = CGA3.Mul(CGA3.Mul(elements[4], elements[n]), elements[4]); + elements[n] = CGA3.Mul(CGA3.Mul(elements[last], elements[n]), elements[last]); // de-noise for (let k = 6; k < elements[n].length; ++k) { @@ -78,7 +95,7 @@ script!(win, """ # set up controls body!(win, """

-""", async=false) +""", async = false) # === set up visualization === @@ -106,5 +123,5 @@ end "grid" => true ) ) - document.body.appendChild(graph); + document.body.appendChild(graph) end \ No newline at end of file -- 2.34.1 From 665cb30ce0bf4bc442f5fcfbca24b51d73c1d116 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 25 Jun 2024 23:31:00 -0700 Subject: [PATCH 047/129] Correct indentation of CSS --- engine-proto/ganja-test/ganja-test.jl | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/engine-proto/ganja-test/ganja-test.jl b/engine-proto/ganja-test/ganja-test.jl index b944eae..19578bd 100644 --- a/engine-proto/ganja-test/ganja-test.jl +++ b/engine-proto/ganja-test/ganja-test.jl @@ -40,17 +40,17 @@ opentools(win) # set stylesheet style!(win, """ -body { - background-color: #ffe0f0; -} + body { + background-color: #ffe0f0; + } -/* needed to keep Ganja canvas from blowing up */ -canvas { - min-width: 600px; - max-width: 600px; - min-height: 600px; - max-height: 600px; -} + /* needed to keep Ganja canvas from blowing up */ + canvas { + min-width: 600px; + max-width: 600px; + min-height: 600px; + max-height: 600px; + } """) # load Ganja.js -- 2.34.1 From a3b1f4920c24c401c240308582009f28580bc9fe Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 26 Jun 2024 00:41:21 -0700 Subject: [PATCH 048/129] Build construction viewer module --- engine-proto/ConstructionViewer.jl | 123 ++++++++++++++++++++++++++ engine-proto/ganja-test/ganja-test.jl | 2 +- 2 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 engine-proto/ConstructionViewer.jl diff --git a/engine-proto/ConstructionViewer.jl b/engine-proto/ConstructionViewer.jl new file mode 100644 index 0000000..efa7293 --- /dev/null +++ b/engine-proto/ConstructionViewer.jl @@ -0,0 +1,123 @@ +module Viewer + +using Blink +using Colors + +export ConstructionViewer, display! + +# === Blink utilities === + +append_to_head!(w, type, content) = @js w begin + @var element = document.createElement($type) + element.appendChild(document.createTextNode($content)) + document.head.appendChild(element) +end + +style!(w, stylesheet) = append_to_head!(w, "style", stylesheet) + +script!(w, code) = append_to_head!(w, "script", code) + +# === construction viewer === + +mutable struct ConstructionViewer + win::Window + + function ConstructionViewer() + # create window and open developer console + win = Window() + opentools(win) + + # set stylesheet + style!(win, """ + /* needed to keep Ganja canvas from blowing up */ + canvas { + min-width: 600px; + max-width: 600px; + min-height: 600px; + max-height: 600px; + } + """) + + # load Ganja.js + loadjs!(win, "https://unpkg.com/ganja.js") + + # create global functions and variables + script!(win, """ + // create algebra + var CGA3 = Algebra(4, 1); + + // initialize element list and palette + var elements = []; + var palette = []; + + // declare visualization handle + var graph; + + // create scene function + function scene() { + commands = []; + for (let n = 0; n < elements.length; ++n) { + commands.push(palette[n], elements[n]); + } + return commands; + } + """) + + # create view + @js win begin + graph = CGA3.graph( + scene, + Dict( + "conformal" => true, + "gl" => true, + "grid" => true + ) + ) + document.body.replaceChildren(graph) + end + + new(win) + end +end + +function display!(viewer::ConstructionViewer, elements::Matrix) + # load elements + elements_full = [ + [0; elt; fill(0, 26)] + for elt in eachcol(elements) + ] + @js viewer.win elements = $elements_full.map((elt) -> @new CGA3(elt)) + + # generate palette. this is Gadfly's `default_discrete_colors` palette, + # available under the MIT license + palette = distinguishable_colors( + length(elements_full), + [LCHab(70, 60, 240)], + transform = c -> deuteranopic(c, 0.5), + lchoices = Float64[65, 70, 75, 80], + cchoices = Float64[0, 50, 60, 70], + hchoices = range(0, stop=330, length=24) + ) + palette_packed = [RGB24(c).color for c in palette] + @js viewer.win palette = $palette_packed + + # update view + @js viewer.win requestAnimationFrame(graph.update.bind(graph, scene)); +end + +end + +# ~~~ sandbox setup ~~~ + +# in the default view, e4 + e5 is the point at infinity +elements = sqrt(0.5) * BigFloat[ + 1 1 -1 -1 0; + 1 -1 1 -1 0; + 1 -1 -1 1 0; + 0 0 0 0 -sqrt(6); + 1 1 1 1 2 +] + +# show construction +viewer = Viewer.ConstructionViewer() +Viewer.display!(viewer, elements) \ No newline at end of file diff --git a/engine-proto/ganja-test/ganja-test.jl b/engine-proto/ganja-test/ganja-test.jl index 19578bd..6c55061 100644 --- a/engine-proto/ganja-test/ganja-test.jl +++ b/engine-proto/ganja-test/ganja-test.jl @@ -3,7 +3,7 @@ using Colors # === utilities === -append_to_head!(w, type, content) = @js win begin +append_to_head!(w, type, content) = @js w begin @var element = document.createElement($type) element.appendChild(document.createTextNode($content)) document.head.appendChild(element) -- 2.34.1 From 4a28a47520f1bd6a6291052a1db39e5f9a04e647 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 26 Jun 2024 01:06:27 -0700 Subject: [PATCH 049/129] Update namespace of AbstractAlgebra.Rationals --- engine-proto/gram-test/descent-test.jl | 4 ++-- engine-proto/gram-test/gram-test.jl | 2 +- engine-proto/gram-test/overlap-test.jl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/engine-proto/gram-test/descent-test.jl b/engine-proto/gram-test/descent-test.jl index 9b6fb7a..ecf0b3e 100644 --- a/engine-proto/gram-test/descent-test.jl +++ b/engine-proto/gram-test/descent-test.jl @@ -108,11 +108,11 @@ println("\nLoss: ", loss, "\n") # === algebraic check === -R, gens = polynomial_ring(Generic.Rationals{BigInt}(), ["x", "t₁", "t₂", "t₃"]) +R, gens = polynomial_ring(AbstractAlgebra.Rationals{BigInt}(), ["x", "t₁", "t₂", "t₃"]) x = gens[1] t = gens[2:4] -S, u = polynomial_ring(Generic.Rationals{BigInt}(), "u") +S, u = polynomial_ring(AbstractAlgebra.Rationals{BigInt}(), "u") M = matrix_space(R, 7, 7) gram_symb = M(R[ diff --git a/engine-proto/gram-test/gram-test.jl b/engine-proto/gram-test/gram-test.jl index 4b2f859..0e88ff4 100644 --- a/engine-proto/gram-test/gram-test.jl +++ b/engine-proto/gram-test/gram-test.jl @@ -11,7 +11,7 @@ function printbad(msg) println(" ", msg) end -F, gens = rational_function_field(Generic.Rationals{BigInt}(), ["a₁", "a₂", "b₁", "b₂", "c₁", "c₂"]) +F, gens = rational_function_field(AbstractAlgebra.Rationals{BigInt}(), ["a₁", "a₂", "b₁", "b₂", "c₁", "c₂"]) a = gens[1:2] b = gens[3:4] c = gens[5:6] diff --git a/engine-proto/gram-test/overlap-test.jl b/engine-proto/gram-test/overlap-test.jl index beeaeca..e75531a 100644 --- a/engine-proto/gram-test/overlap-test.jl +++ b/engine-proto/gram-test/overlap-test.jl @@ -11,7 +11,7 @@ function printbad(msg) println(" ", msg) end -F, gens = rational_function_field(Generic.Rationals{BigInt}(), ["x", "t₁", "t₂", "t₃"]) +F, gens = rational_function_field(AbstractAlgebra.Rationals{BigInt}(), ["x", "t₁", "t₂", "t₃"]) x = gens[1] t = gens[2:4] -- 2.34.1 From 2b6c4f4720212da7449e0b03f1b6566cebef6028 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 26 Jun 2024 11:28:47 -0700 Subject: [PATCH 050/129] Avoid naming conflict with identity transformation --- engine-proto/gram-test/descent-test.jl | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/engine-proto/gram-test/descent-test.jl b/engine-proto/gram-test/descent-test.jl index ecf0b3e..d895764 100644 --- a/engine-proto/gram-test/descent-test.jl +++ b/engine-proto/gram-test/descent-test.jl @@ -10,10 +10,10 @@ using PolynomialRoots # the difference between the matrices `target` and `attempt`, projected onto the # subspace of matrices whose entries vanish at each empty index of `target` function proj_diff(target, attempt) - I, J, values = findnz(target) + J, K, values = findnz(target) result = zeros(size(target)...) - for (i, j, val) in zip(I, J, values) - result[i, j] = val - attempt[i, j] + for (j, k, val) in zip(J, K, values) + result[j, k] = val - attempt[j, k] end result end @@ -26,19 +26,19 @@ Q = diagm([-1, 1, 1, 1, 1]) # initialize the partial gram matrix for an arrangement of seven spheres in # which spheres 1 through 5 are mutually tangent, and spheres 3 through 7 are # also mutually tangent -I = Int64[] J = Int64[] +K = Int64[] values = BigFloat[] -for i in 1:7 - for j in 1:7 - if (i <= 5 && j <= 5) || (i >= 3 && j >= 3) - push!(I, i) +for j in 1:7 + for k in 1:7 + if (j <= 5 && k <= 5) || (j >= 3 && k >= 3) push!(J, j) - push!(values, i == j ? 1 : -1) + push!(K, k) + push!(values, j == k ? 1 : -1) end end end -gram = sparse(I, J, values) +gram = sparse(J, K, values) # set the independent variable # -- 2.34.1 From c933e07312c0dc3b6c8523eeb811d14b92e7a6bc Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 26 Jun 2024 11:39:34 -0700 Subject: [PATCH 051/129] Switch to Ganja.js basis ordering --- engine-proto/gram-test/descent-test.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/engine-proto/gram-test/descent-test.jl b/engine-proto/gram-test/descent-test.jl index d895764..2f20143 100644 --- a/engine-proto/gram-test/descent-test.jl +++ b/engine-proto/gram-test/descent-test.jl @@ -21,7 +21,7 @@ end # === example === # the Lorentz form -Q = diagm([-1, 1, 1, 1, 1]) +Q = diagm([1, 1, 1, 1, -1]) # initialize the partial gram matrix for an arrangement of seven spheres in # which spheres 1 through 5 are mutually tangent, and spheres 3 through 7 are @@ -54,11 +54,11 @@ gram[1, 6] = gram[6, 1] # in this initial guess, the mutual tangency condition is satisfied for spheres # 1 through 5 guess = sqrt(0.5) * BigFloat[ - 1 1 1 1 2 0.2 0.1; - 0 0 0 0 -sqrt(6) 0.3 -0.2; 1 1 -1 -1 0 -0.1 0.3; 1 -1 1 -1 0 -0.5 0.4; - 1 -1 -1 1 0 0.1 -0.2 + 1 -1 -1 1 0 0.1 -0.2; + 0 0 0 0 -sqrt(6) 0.3 -0.2; + 1 1 1 1 2 0.2 0.1; ] # search parameters -- 2.34.1 From 7aaf134a3672ac712744ce475b7ea5810a4655c3 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 26 Jun 2024 13:15:54 -0700 Subject: [PATCH 052/129] Size the viewer window automatically --- engine-proto/ConstructionViewer.jl | 40 +++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/engine-proto/ConstructionViewer.jl b/engine-proto/ConstructionViewer.jl index efa7293..563857a 100644 --- a/engine-proto/ConstructionViewer.jl +++ b/engine-proto/ConstructionViewer.jl @@ -3,7 +3,7 @@ module Viewer using Blink using Colors -export ConstructionViewer, display! +export ConstructionViewer, display!, opentools!, closetools! # === Blink utilities === @@ -24,17 +24,23 @@ mutable struct ConstructionViewer function ConstructionViewer() # create window and open developer console - win = Window() - opentools(win) + win = Window(Blink.Dict(:width => 620, :height => 620)) # set stylesheet style!(win, """ - /* needed to keep Ganja canvas from blowing up */ + body { + background-color: #c8c0d0; + } + + /* maximum dimensions are needed to keep Ganja canvas from blowing up */ canvas { min-width: 600px; max-width: 600px; min-height: 600px; max-height: 600px; + margin-top: 10px; + margin-left: 10px; + border-radius: 10px; } """) @@ -50,8 +56,9 @@ mutable struct ConstructionViewer var elements = []; var palette = []; - // declare visualization handle + // declare handles for the visualization and its options var graph; + var graphOpt; // create scene function function scene() { @@ -65,14 +72,13 @@ mutable struct ConstructionViewer # create view @js win begin - graph = CGA3.graph( - scene, - Dict( - "conformal" => true, - "gl" => true, - "grid" => true - ) + graphOpt = Dict( + :conformal => true, + :gl => true, + :grid => true, + :devicePixelRatio => window.devicePixelRatio ) + graph = CGA3.graph(scene, graphOpt) document.body.replaceChildren(graph) end @@ -105,6 +111,16 @@ function display!(viewer::ConstructionViewer, elements::Matrix) @js viewer.win requestAnimationFrame(graph.update.bind(graph, scene)); end +function opentools!(viewer::ConstructionViewer) + size(viewer.win, 1240, 620) + opentools(viewer.win) +end + +function closetools!(viewer::ConstructionViewer) + closetools(viewer.win) + size(viewer.win, 620, 620) +end + end # ~~~ sandbox setup ~~~ -- 2.34.1 From 3eb4fc6c91a58bc7c6a43a311847d4e89b1d857a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 26 Jun 2024 15:24:31 -0700 Subject: [PATCH 053/129] Add element visibility controls --- engine-proto/ConstructionViewer.jl | 110 +++++++++++++++++++++++------ 1 file changed, 90 insertions(+), 20 deletions(-) diff --git a/engine-proto/ConstructionViewer.jl b/engine-proto/ConstructionViewer.jl index 563857a..2c51ce6 100644 --- a/engine-proto/ConstructionViewer.jl +++ b/engine-proto/ConstructionViewer.jl @@ -2,6 +2,7 @@ module Viewer using Blink using Colors +using Printf export ConstructionViewer, display!, opentools!, closetools! @@ -24,23 +25,41 @@ mutable struct ConstructionViewer function ConstructionViewer() # create window and open developer console - win = Window(Blink.Dict(:width => 620, :height => 620)) + win = Window(Blink.Dict(:width => 620, :height => 830)) # set stylesheet style!(win, """ body { - background-color: #c8c0d0; + background-color: #ccc; } - /* maximum dimensions are needed to keep Ganja canvas from blowing up */ - canvas { - min-width: 600px; - max-width: 600px; - min-height: 600px; - max-height: 600px; + /* the maximum dimensions keep Ganja from blowing up the canvas */ + #view { + display: block; + width: 600px; + height: 600px; margin-top: 10px; margin-left: 10px; border-radius: 10px; + background-color: #f0f0f0; + } + + #control-panel { + width: 600px; + height: 200px; + box-sizing: border-box; + padding: 5px 10px 5px 10px; + margin-top: 10px; + margin-left: 10px; + border-radius: 10px; + background-color: #f0f0f0; + } + + #control-panel > div { + margin-top: 5px; + padding: 2px; + border-radius: 5px; + font-family: monospace; } """) @@ -56,30 +75,46 @@ mutable struct ConstructionViewer var elements = []; var palette = []; - // declare handles for the visualization and its options - var graph; - var graphOpt; + // declare handles for the view and its options + var view; + var viewOpt; + + // declare handles for the controls + var controlPanel; + var visControls; // create scene function function scene() { commands = []; for (let n = 0; n < elements.length; ++n) { - commands.push(palette[n], elements[n]); + if (visControls[n].checked) { + commands.push(palette[n], elements[n]); + } } return commands; } + + function updateView() { + requestAnimationFrame(view.update.bind(view, scene)); + } """) - # create view @js win begin - graphOpt = Dict( + # create view + viewOpt = Dict( :conformal => true, :gl => true, - :grid => true, :devicePixelRatio => window.devicePixelRatio ) - graph = CGA3.graph(scene, graphOpt) - document.body.replaceChildren(graph) + view = CGA3.graph(scene, viewOpt) + view.setAttribute(:id, "view") + view.removeAttribute(:style) + document.body.replaceChildren(view) + + # create control panel + controlPanel = document.createElement(:div) + controlPanel.setAttribute(:id, "control-panel") + document.body.appendChild(controlPanel) end new(win) @@ -107,18 +142,53 @@ function display!(viewer::ConstructionViewer, elements::Matrix) palette_packed = [RGB24(c).color for c in palette] @js viewer.win palette = $palette_packed + # generate visibility controls + @js viewer.win begin + controlPanel.replaceChildren() + visControls = [] + end + for n in 1:size(elements, 2) + index_str = string(n) + vec_str = join(map(t -> @sprintf("%.3f", t), elements[:, n]), ", ") + style_str = "background-color: #$(hex(palette[n]));" + println(style_str) + @js viewer.win begin + # create container + @var container = document.createElement(:div) + container.setAttribute(:style, $style_str) + + # create checkbox + @var checkbox = document.createElement(:input) + checkbox.setAttribute(:type, "checkbox") + checkbox.setAttribute(:id, $index_str) + checkbox.setAttribute(:checked, "true") + checkbox.addEventListener(:input, updateView) + visControls.push(checkbox) + container.appendChild(checkbox) + + # create label + @var label = document.createElement(:label); + label.setAttribute(:for, $index_str) + label.appendChild(document.createTextNode($vec_str)) + container.appendChild(label) + + # add the control to the control panel + controlPanel.appendChild(container) + end + end + # update view - @js viewer.win requestAnimationFrame(graph.update.bind(graph, scene)); + @js viewer.win updateView() end function opentools!(viewer::ConstructionViewer) - size(viewer.win, 1240, 620) + size(viewer.win, 1240, 830) opentools(viewer.win) end function closetools!(viewer::ConstructionViewer) closetools(viewer.win) - size(viewer.win, 620, 620) + size(viewer.win, 620, 830) end end -- 2.34.1 From 5ea32ac53cfac68e1267d8863338c32695214c05 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 26 Jun 2024 15:51:57 -0700 Subject: [PATCH 054/129] Streamline visibility controls --- engine-proto/ConstructionViewer.jl | 55 +++++++++++++----------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/engine-proto/ConstructionViewer.jl b/engine-proto/ConstructionViewer.jl index 2c51ce6..2486cc9 100644 --- a/engine-proto/ConstructionViewer.jl +++ b/engine-proto/ConstructionViewer.jl @@ -57,8 +57,9 @@ mutable struct ConstructionViewer #control-panel > div { margin-top: 5px; - padding: 2px; + padding: 4px; border-radius: 5px; + border: solid; font-family: monospace; } """) @@ -81,13 +82,13 @@ mutable struct ConstructionViewer // declare handles for the controls var controlPanel; - var visControls; + var visToggles; // create scene function function scene() { commands = []; for (let n = 0; n < elements.length; ++n) { - if (visControls[n].checked) { + if (visToggles[n].checked) { commands.push(palette[n], elements[n]); } } @@ -142,38 +143,30 @@ function display!(viewer::ConstructionViewer, elements::Matrix) palette_packed = [RGB24(c).color for c in palette] @js viewer.win palette = $palette_packed - # generate visibility controls + # create visibility toggles @js viewer.win begin controlPanel.replaceChildren() - visControls = [] + visToggles = [] end - for n in 1:size(elements, 2) - index_str = string(n) - vec_str = join(map(t -> @sprintf("%.3f", t), elements[:, n]), ", ") - style_str = "background-color: #$(hex(palette[n]));" - println(style_str) + for (elt, c) in zip(eachcol(elements), palette) + vec_str = join(map(t -> @sprintf("%.3f", t), elt), ", ") + color_str = "#$(hex(c))" + style_str = "background-color: $color_str; border-color: $color_str;" @js viewer.win begin - # create container - @var container = document.createElement(:div) - container.setAttribute(:style, $style_str) - - # create checkbox - @var checkbox = document.createElement(:input) - checkbox.setAttribute(:type, "checkbox") - checkbox.setAttribute(:id, $index_str) - checkbox.setAttribute(:checked, "true") - checkbox.addEventListener(:input, updateView) - visControls.push(checkbox) - container.appendChild(checkbox) - - # create label - @var label = document.createElement(:label); - label.setAttribute(:for, $index_str) - label.appendChild(document.createTextNode($vec_str)) - container.appendChild(label) - - # add the control to the control panel - controlPanel.appendChild(container) + @var toggle = document.createElement(:div) + toggle.setAttribute(:style, $style_str) + toggle.checked = true + toggle.addEventListener( + "click", + () -> begin + toggle.checked = !toggle.checked + toggle.style.backgroundColor = toggle.checked ? $color_str : "inherit"; + updateView() + end + ) + toggle.appendChild(document.createTextNode($vec_str)) + visToggles.push(toggle); + controlPanel.appendChild(toggle); end end -- 2.34.1 From 05a824834d84a48018556678f412ef7a73318abe Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 26 Jun 2024 15:56:51 -0700 Subject: [PATCH 055/129] Let visibility controls scroll --- engine-proto/ConstructionViewer.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/engine-proto/ConstructionViewer.jl b/engine-proto/ConstructionViewer.jl index 2486cc9..7cb0450 100644 --- a/engine-proto/ConstructionViewer.jl +++ b/engine-proto/ConstructionViewer.jl @@ -51,6 +51,7 @@ mutable struct ConstructionViewer padding: 5px 10px 5px 10px; margin-top: 10px; margin-left: 10px; + overflow-y: scroll; border-radius: 10px; background-color: #f0f0f0; } -- 2.34.1 From 242d630cc6f43ebe032b09a20615251c4fa9bbe2 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 27 Jun 2024 21:45:41 -0700 Subject: [PATCH 056/129] Get Ganja.js to display planes --- engine-proto/ConstructionViewer.jl | 20 ++++++++++++++++---- engine-proto/ganja-test/ganja-test.html | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/engine-proto/ConstructionViewer.jl b/engine-proto/ConstructionViewer.jl index 7cb0450..bdd35fd 100644 --- a/engine-proto/ConstructionViewer.jl +++ b/engine-proto/ConstructionViewer.jl @@ -123,12 +123,24 @@ mutable struct ConstructionViewer end end +mprod(v, w) = + v[1]*w[1] + v[2]*w[2] + v[3]*w[3] + v[4]*w[4] - v[5]*w[5] + function display!(viewer::ConstructionViewer, elements::Matrix) # load elements - elements_full = [ - [0; elt; fill(0, 26)] - for elt in eachcol(elements) - ] + elements_full = [] + for elt in eachcol(elements) + if mprod(elt, elt) < 0.5 + elt_full = [0; elt; fill(0, 26)] + else + # `elt` is a spacelike vector, representing a generalized sphere, so we + # take its Hodge dual before passing it to Ganja.js. the dual represents + # the same generalized sphere, but Ganja.js only displays planes when + # they're represented by vectors in grade 4 rather than grade 1 + elt_full = [fill(0, 26); -elt[5]; -elt[4]; elt[3]; -elt[2]; elt[1]; 0] + end + push!(elements_full, elt_full) + end @js viewer.win elements = $elements_full.map((elt) -> @new CGA3(elt)) # generate palette. this is Gadfly's `default_discrete_colors` palette, diff --git a/engine-proto/ganja-test/ganja-test.html b/engine-proto/ganja-test/ganja-test.html index d6efc90..0207dcc 100644 --- a/engine-proto/ganja-test/ganja-test.html +++ b/engine-proto/ganja-test/ganja-test.html @@ -28,6 +28,25 @@ CGA3.inline(() => Math.sqrt(0.5)*(-1e1 - 1e2 + 1e3 + 1e5))(), CGA3.inline(() => -Math.sqrt(3)*1e4 + Math.sqrt(2)*1e5)() ]; + /* + these blocks of commented-out code can be used to confirm that a spacelike + vector and its Hodge dual represent the same generalized sphere + */ + /*let elements = [ + CGA3.inline(() => Math.sqrt(0.5)*!( 1e1 + 1e2 + 1e3 + 1e5))(), + CGA3.inline(() => Math.sqrt(0.5)*!( 1e1 - 1e2 - 1e3 + 1e5))(), + CGA3.inline(() => Math.sqrt(0.5)*!(-1e1 + 1e2 - 1e3 + 1e5))(), + CGA3.inline(() => Math.sqrt(0.5)*!(-1e1 - 1e2 + 1e3 + 1e5))(), + CGA3.inline(() => !(-Math.sqrt(3)*1e4 + Math.sqrt(2)*1e5))() + ];*/ + /*let elements = [ + CGA3.inline(() => 1e1 + 1e5)(), + CGA3.inline(() => 1e2 + 1e5)(), + CGA3.inline(() => 1e3 + 1e5)(), + CGA3.inline(() => -1e4 + 1e5)(), + CGA3.inline(() => Math.sqrt(0.5)*(1e1 + 1e2 + 1e3 + 1e5))(), + CGA3.inline(() => Math.sqrt(0.5)*!(1e1 + 1e2 + 1e3 - 0.01e4 + 1e5))() + ];*/ // set up palette var colorIndex; @@ -66,6 +85,7 @@ // de-noise for (let k = 6; k < elements[n].length; ++k) { + /*for (let k = 0; k < 26; ++k) {*/ elements[n][k] = 0; } } -- 2.34.1 From e7dde5800c79988aa0225d84822d55339b83978f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 2 Jul 2024 12:35:12 -0700 Subject: [PATCH 057/129] Do gradient descent entirely in BigFloat The previos version accidentally returned steps in Float64. --- engine-proto/gram-test/descent-test.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engine-proto/gram-test/descent-test.jl b/engine-proto/gram-test/descent-test.jl index 2f20143..168de5d 100644 --- a/engine-proto/gram-test/descent-test.jl +++ b/engine-proto/gram-test/descent-test.jl @@ -11,7 +11,7 @@ using PolynomialRoots # subspace of matrices whose entries vanish at each empty index of `target` function proj_diff(target, attempt) J, K, values = findnz(target) - result = zeros(size(target)...) + result = zeros(BigFloat, size(target)...) for (j, k, val) in zip(J, K, values) result[j, k] = val - attempt[j, k] end @@ -65,7 +65,7 @@ guess = sqrt(0.5) * BigFloat[ steps = 600 line_search_max_steps = 100 init_stepsize = BigFloat(1) -step_shrink_factor = BigFloat(0.5) +step_shrink_factor = BigFloat(0.9) target_improvement_factor = BigFloat(0.5) # complete the gram matrix using gradient descent -- 2.34.1 From 133519cacbc276f891fedd21663a7e64c6c8f58f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 2 Jul 2024 14:57:57 -0700 Subject: [PATCH 058/129] Encapsulate gradient descent code The completed gram matrix from this commit matches the one from commit e7dde58 to six decimal places. --- engine-proto/gram-test/Engine.jl | 100 +++++++++++++++++++++++++ engine-proto/gram-test/descent-test.jl | 68 ++--------------- 2 files changed, 106 insertions(+), 62 deletions(-) create mode 100644 engine-proto/gram-test/Engine.jl diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl new file mode 100644 index 0000000..1ab7272 --- /dev/null +++ b/engine-proto/gram-test/Engine.jl @@ -0,0 +1,100 @@ +module Engine + +using LinearAlgebra +using SparseArrays + +export Q, DescentHistory, realize_gram + +# the Lorentz form +Q = diagm([1, 1, 1, 1, -1]) + +# the difference between the matrices `target` and `attempt`, projected onto the +# subspace of matrices whose entries vanish at each empty index of `target` +function proj_diff(target::SparseMatrixCSC{T, <:Any}, attempt::Matrix{T}) where T + J, K, values = findnz(target) + result = zeros(size(target)...) + for (j, k, val) in zip(J, K, values) + result[j, k] = val - attempt[j, k] + end + result +end + +# a type for keeping track of gradient descent history +struct DescentHistory{T} + scaled_loss::Array{T} + stepsize::Array{T} + backoff_steps::Array{Int64} + + function DescentHistory{T}( + scaled_loss = Array{T}(undef, 0), + stepsize = Array{T}(undef, 0), + backoff_steps = Int64[] + ) where T + new(scaled_loss, stepsize, backoff_steps) + end +end + +# seek a matrix `L` for which `L'QL` matches the sparse matrix `gram` at every +# explicit entry of `gram`. use gradient descent starting from `guess` +function realize_gram( + gram::SparseMatrixCSC{T, <:Any}, + guess::Matrix{T}; + scaled_tol = 1e-30, + target_improvement = 0.5, + init_stepsize = 1.0, + backoff = 0.9, + max_descent_steps = 600, + max_backoff_steps = 110 +) where T <: Number + # start history + history = DescentHistory{T}() + + # scale tolerance + scale_adjustment = sqrt(T(nnz(gram))) + tol = scale_adjustment * scaled_tol + + # initialize variables + stepsize = init_stepsize + L = copy(guess) + + # do gradient descent + Δ_proj = proj_diff(gram, L'*Q*L) + loss = norm(Δ_proj) + for step in 1:max_descent_steps + # stop if the loss is tolerably low + if loss < tol + break + end + + # find negative gradient of loss function + neg_grad = 4*Q*L*Δ_proj + slope = norm(neg_grad) + + # store current position and loss + L_last = L + loss_last = loss + push!(history.scaled_loss, loss / scale_adjustment) + + # find a good step size using backtracking line search + push!(history.stepsize, 0) + push!(history.backoff_steps, max_backoff_steps) + for backoff_steps in 0:max_backoff_steps + history.stepsize[end] = stepsize + L = L_last + stepsize * neg_grad + Δ_proj = proj_diff(gram, L'*Q*L) + loss = norm(Δ_proj) + improvement = loss_last - loss + if improvement >= target_improvement * stepsize * slope + history.backoff_steps[end] = backoff_steps + break + end + stepsize *= backoff + end + end + + # return the factorization and its history + push!(history.scaled_loss, loss / scale_adjustment) + L, history +end + +end \ No newline at end of file diff --git a/engine-proto/gram-test/descent-test.jl b/engine-proto/gram-test/descent-test.jl index 168de5d..0c66311 100644 --- a/engine-proto/gram-test/descent-test.jl +++ b/engine-proto/gram-test/descent-test.jl @@ -1,28 +1,9 @@ -using LinearAlgebra +include("Engine.jl") + using SparseArrays using AbstractAlgebra using PolynomialRoots -# testing Gram matrix recovery using a homemade gradient descent routine - -# === gradient descent === - -# the difference between the matrices `target` and `attempt`, projected onto the -# subspace of matrices whose entries vanish at each empty index of `target` -function proj_diff(target, attempt) - J, K, values = findnz(target) - result = zeros(BigFloat, size(target)...) - for (j, k, val) in zip(J, K, values) - result[j, k] = val - attempt[j, k] - end - result -end - -# === example === - -# the Lorentz form -Q = diagm([1, 1, 1, 1, -1]) - # initialize the partial gram matrix for an arrangement of seven spheres in # which spheres 1 through 5 are mutually tangent, and spheres 3 through 7 are # also mutually tangent @@ -61,50 +42,13 @@ guess = sqrt(0.5) * BigFloat[ 1 1 1 1 2 0.2 0.1; ] -# search parameters -steps = 600 -line_search_max_steps = 100 -init_stepsize = BigFloat(1) -step_shrink_factor = BigFloat(0.9) -target_improvement_factor = BigFloat(0.5) - # complete the gram matrix using gradient descent -loss_history = Array{BigFloat}(undef, steps + 1) -stepsize_history = Array{BigFloat}(undef, steps) -line_search_depth_history = fill(line_search_max_steps, steps) -stepsize = init_stepsize -L = copy(guess) -Δ_proj = proj_diff(gram, L'*Q*L) -loss = norm(Δ_proj) -for step in 1:steps - # find negative gradient of loss function - neg_grad = 4*Q*L*Δ_proj - slope = norm(neg_grad) - - # store current position and loss - L_last = L - loss_last = loss - loss_history[step] = loss - - # find a good step size using backtracking line search - for line_search_depth in 1:line_search_max_steps - stepsize_history[step] = stepsize - global L = L_last + stepsize * neg_grad - global Δ_proj = proj_diff(gram, L'*Q*L) - global loss = norm(Δ_proj) - improvement = loss_last - loss - if improvement >= target_improvement_factor * stepsize * slope - line_search_depth_history[step] = line_search_depth - break - end - global stepsize *= step_shrink_factor - end -end -completed_gram = L'*Q*L -loss_history[steps + 1] = loss +L, history = Engine.realize_gram(gram, guess) +completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") display(completed_gram) -println("\nLoss: ", loss, "\n") +println("\nSteps: ", size(history.stepsize, 1)) +println("Loss: ", history.scaled_loss[end], "\n") # === algebraic check === -- 2.34.1 From 17fefff61ed74c03ad529a2ed7d4955a410f6037 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 2 Jul 2024 17:16:19 -0700 Subject: [PATCH 059/129] Name gradient descent test more specifically --- .../{descent-test.jl => overlapping-pyramids.jl} | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename engine-proto/gram-test/{descent-test.jl => overlapping-pyramids.jl} (89%) diff --git a/engine-proto/gram-test/descent-test.jl b/engine-proto/gram-test/overlapping-pyramids.jl similarity index 89% rename from engine-proto/gram-test/descent-test.jl rename to engine-proto/gram-test/overlapping-pyramids.jl index 0c66311..1be29e7 100644 --- a/engine-proto/gram-test/descent-test.jl +++ b/engine-proto/gram-test/overlapping-pyramids.jl @@ -34,12 +34,12 @@ gram[1, 6] = gram[6, 1] # in this initial guess, the mutual tangency condition is satisfied for spheres # 1 through 5 -guess = sqrt(0.5) * BigFloat[ - 1 1 -1 -1 0 -0.1 0.3; - 1 -1 1 -1 0 -0.5 0.4; - 1 -1 -1 1 0 0.1 -0.2; - 0 0 0 0 -sqrt(6) 0.3 -0.2; - 1 1 1 1 2 0.2 0.1; +guess = sqrt(1/BigFloat(2)) * BigFloat[ + 1 1 -1 -1 0 -0.1 0.3; + 1 -1 1 -1 0 -0.5 0.4; + 1 -1 -1 1 0 0.1 -0.2; + 0 0 0 0 -sqrt(BigFloat(6)) 0.3 -0.2; + 1 1 1 1 2 0.2 0.1; ] # complete the gram matrix using gradient descent -- 2.34.1 From abc53b4705610cd06ed352954cd185bd121d771b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 2 Jul 2024 17:16:31 -0700 Subject: [PATCH 060/129] Sketch random vector generator This needs to be rewritten: it can fail at generating spacelike vectors. --- engine-proto/gram-test/Engine.jl | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index 1ab7272..44c5285 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -2,8 +2,23 @@ module Engine using LinearAlgebra using SparseArrays +using Random -export Q, DescentHistory, realize_gram +export rand_on_shell, Q, DescentHistory, realize_gram + +# === guessing === + +##[TO DO] write a test to confirm that the outputs are on the correct shells +##[TO DO] this can fail at generating spacelike vectors +function rand_on_shell(rng::AbstractRNG, shells::Array{T}) where T <: Number + space_parts = randn(rng, T, 4, size(shells, 1)) + space_self_prods = [dot(x, x) for x in eachcol(space_parts)] + return [space_parts; sqrt.(space_self_prods .- shells)'] +end + +rand_on_shell(shells::Array{<:Number}) = rand_on_shell(Random.default_rng(), shells) + +# === Gram matrix realization === # the Lorentz form Q = diagm([1, 1, 1, 1, -1]) -- 2.34.1 From 7e94fef19e61d185ab405c32642a0598bc3179f2 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 6 Jul 2024 21:32:43 -0700 Subject: [PATCH 061/129] Improve random vector generator --- engine-proto/gram-test/Engine.jl | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index 44c5285..a0a59be 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -8,14 +8,29 @@ export rand_on_shell, Q, DescentHistory, realize_gram # === guessing === -##[TO DO] write a test to confirm that the outputs are on the correct shells -##[TO DO] this can fail at generating spacelike vectors -function rand_on_shell(rng::AbstractRNG, shells::Array{T}) where T <: Number - space_parts = randn(rng, T, 4, size(shells, 1)) - space_self_prods = [dot(x, x) for x in eachcol(space_parts)] - return [space_parts; sqrt.(space_self_prods .- shells)'] +sconh(t, u) = 0.5*(exp(t) + u*exp(-t)) + +function rand_on_sphere(rng::AbstractRNG, ::Type{T}, n) where T + out = randn(rng, T, n) + tries_left = 2 + while dot(out, out) < 1e-6 && tries_left > 0 + out = randn(rng, T, n) + tries_left -= 1 + end + normalize(out) end +##[TO DO] write a test to confirm that the outputs are on the correct shells +function rand_on_shell(rng::AbstractRNG, shell::T) where T <: Number + space_part = rand_on_sphere(rng, T, 4) + rapidity = randn(rng, T) + sig = sign(shell) + [sconh(rapidity, sig)*space_part; sconh(rapidity, -sig)] +end + +rand_on_shell(rng::AbstractRNG, shells::Array{T}) where T <: Number = + hcat([rand_on_shell(rng, sh) for sh in shells]...) + rand_on_shell(shells::Array{<:Number}) = rand_on_shell(Random.default_rng(), shells) # === Gram matrix realization === -- 2.34.1 From d39244d308513230ba2dd51e9c2b00bf4553a088 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 6 Jul 2024 21:35:09 -0700 Subject: [PATCH 062/129] Host Ganja.js locally --- engine-proto/ConstructionViewer.jl | 8 +- engine-proto/gram-test/ganja-1.0.204.js | 1913 +++++++++++++++++++++++ 2 files changed, 1919 insertions(+), 2 deletions(-) create mode 100644 engine-proto/gram-test/ganja-1.0.204.js diff --git a/engine-proto/ConstructionViewer.jl b/engine-proto/ConstructionViewer.jl index bdd35fd..29af212 100644 --- a/engine-proto/ConstructionViewer.jl +++ b/engine-proto/ConstructionViewer.jl @@ -65,8 +65,12 @@ mutable struct ConstructionViewer } """) - # load Ganja.js - loadjs!(win, "https://unpkg.com/ganja.js") + # load Ganja.js. for an automatically updated web-hosted version, load from + # + # https://unpkg.com/ganja.js + # + # instead + loadjs!(win, "http://localhost:8000/ganja-1.0.204.js") # create global functions and variables script!(win, """ diff --git a/engine-proto/gram-test/ganja-1.0.204.js b/engine-proto/gram-test/ganja-1.0.204.js new file mode 100644 index 0000000..1e95e42 --- /dev/null +++ b/engine-proto/gram-test/ganja-1.0.204.js @@ -0,0 +1,1913 @@ +/** Ganja.js - Geometric Algebra - Not Just Algebra. + * @author Enki + * @link https://github.com/enkimute/ganja.js + */ + +/*********************************************************************************************************************/ +// +// Ganja.js is an Algebra generator for javascript. It generates a wide variety of Algebra's and supports operator +// overloading, algebraic literals and a variety of graphing options. +// +// Ganja.js is designed with prototyping and educational purposes in mind. Clean mathematical syntax is the primary +// target. +// +// Ganja.js exports only one function called *Algebra*. This function is used to generate Algebra classes. (say complex +// numbers, minkowski or 3D CGA). The returned class can be used to create, add, multiply etc, but also to upgrade +// javascript functions with algebraic literals, operator overloading, vectors, matrices and much more. +// +// As a simple example, multiplying two complex numbers 3+2i and 1+4i could be done like this : +// +// var complex = Algebra(0,1); +// var a = new complex([3,2]); +// var b = new complex([1,3]); +// var result = a.Mul(b); +// +// But the same can be written using operator overloading and algebraic literals. (where scientific notation with +// lowercase e is overloaded to directly specify generators (e1, e2, e12, ...)) +// +// var result = Algebra(0,1,()=>(3+2e1)*(1+4e1)); +// +// Please see github for user documentation and examples. +// +/*********************************************************************************************************************/ + +// Documentation below is for implementors. I'll assume you know about Clifford Algebra's, grades, its products, etc .. +// I'll also assume you are familiar with ES6. My style may feel a bith mathematical, advise is to read slow. + +(function (name, context, definition) { + if (typeof module != 'undefined' && module.exports) module.exports = definition(); + else if (typeof define == 'function' && define.amd) define(name, definition); + else context[name] = definition(); +}('Algebra', this, function () { + +/** Some helpers for eigenvalues for bivector split in high-d spaces **/ + function QR(M) { + // helpers + const {abs,sqrt} = Math; + const hyp = (a,b)=>abs(a)>abs(b)?abs(a)*sqrt(1+(b/a)**2):b==0?0:abs(b)*sqrt(1+(a/b)**2); + const [m,n] = [M.length, M[0].length]; + var qr = M.map(r=>r.map(c=>c)), Q = M.map(r=>r.map(c=>0)), R = M.map(r=>r.map(c=>0)), d = [], k, i, j, nrm; + // helper matrix + for (k=0; k=0; --k) { + for (i=0; i{ + var res = A.map(r=>r.map(c=>0)); + for(let i=0;iA[i][i]); + } + +/** The Algebra class generator. Possible calling signatures : + * Algebra([func]) => algebra with no dimensions, i.e. R. Optional function for the translator. + * Algebra(p,[func]) => 'p' positive dimensions and an optional function to pass to the translator. + * Algebra(p,q,[func]) => 'p' positive and 'q' negative dimensions and optional function. + * Algebra(p,q,r,[func]) => 'p' positive, 'q' negative and 'r' zero dimensions and optional function. + * Algebra({ => for custom basis, cayley, mixing, etc pass in an object as first parameter. + * [p:p], => optional 'p' for # of positive dimensions + * [q:q], => optional 'q' for # of negative dimensions + * [r:r], => optional 'r' for # of zero dimensions + * [metric:array], => alternative for p,q,r. e.g. ([1,1,1,-1] for spacetime) + * [basis:array], => array of strings with basis names. (e.g. ['1','e1','e2','e12']) + * [Cayley:Cayley], => optional custom Cayley table (strings). (e.g. [['1','e1'],['e1','-1']]) + * [mix:boolean], => Allows mixing of various algebras. (for space efficiency). + * [graded:boolean], => Use a graded algebra implementation. (automatic for +6D) + * [baseType:Float32Array] => optional basetype to use. (only for flat generator) + * },[func]) => optional function for the translator. + **/ + return function Algebra(p,q,r) { + // Resolve possible calling signatures so we know the numbers for p,q,r. Last argument can always be a function. + var fu=arguments[arguments.length-1],options=p; if (options instanceof Object) { + q = (p.q || (p.metric && p.metric.filter(x=>x==-1).length))| 0; + r = (p.r || (p.metric && p.metric.filter(x=>x==0).length)) | 0; + p = p.p === undefined ? (p.metric && p.metric.filter(x=>x==1).length) || 0 : p.p || 0; + } else { options={}; p=p|0; r=r|0; q=q|0; }; + + // Support for multi-dual-algebras + if (options.dual || (p==0 && q==0 && r<0)) { r=options.dual=options.dual||-r; // Create a dual number algebra if r<0 (old) or options.dual set(new) + options.basis = [...Array(r+1)].map((a,i)=>i?'e0'+i:'1'); options.metric = [1,...Array(r)]; options.tot=r+1; + options.Cayley = [...Array(r+1)].map((a,i)=>[...Array(r+1)].map((y,j)=>i*j==0?((i+j)?'e0'+(i+j):'1'):'0')); + } + if (options.over) options.baseType = Array; + + // Calculate the total number of dimensions. + var tot = options.tot = (options.tot||(p||0)+(q||0)+(r||0)||(options.basis&&options.basis.length))|0; + + // Unless specified, generate a full set of Clifford basis names. We generate them as an array of strings by starting + // from numbers in binary representation and changing the set bits into their relative position. + // Basis names are ordered first per grade, then lexically (not cyclic!). + // For 10 or more dimensions all names will be double digits ! 1e01 instead of 1e1 .. + var basis=(options.basis&&(options.basis.length==2**tot||r<0||options.Cayley)&&options.basis)||[...Array(2**tot)] // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined] + .map((x,xi)=>(((1<<30)+xi).toString(2)).slice(-tot||-1) // => ["000", "001", "010", "011", "100", "101", "110", "111"] (index of array in base 2) + .replace(/./g,(a,ai)=>a=='0'?'':String.fromCharCode(66+ai-(r!=0)))) // => ["", "3", "2", "23", "1", "13", "12", "123"] (1 bits replaced with their positions, 0's removed) + .sort((a,b)=>(a.toString().length==b.toString().length)?(a>b?1:b>a?-1:0):a.toString().length-b.toString().length) // => ["", "1", "2", "3", "12", "13", "23", "123"] (sorted numerically) + .map(x=>x&&'e'+(x.replace(/./g,x=>('0'+(x.charCodeAt(0)-65)).slice(tot>9?-2:-1) ))||'1') // => ["1", "e1", "e2", "e3", "e12", "e13", "e23", "e123"] (converted to commonly used basis names) + + // See if the basis names start from 0 or 1, store grade per component and lowest component per grade. + var low=basis.length==1?1:basis[1].match(/\d+/g)[0]*1, + grades=options.grades||(options.dual&&basis.map((x,i)=>i?1:0))||basis.map(x=>tot>9?(x.length-1)/2:x.length-1), + grade_start=grades.map((a,b,c)=>c[b-1]!=a?b:-1).filter(x=>x+1).concat([basis.length]); + + // String-simplify a concatenation of two basis blades. (and supports custom basis names e.g. e21 instead of e12) + // This is the function that implements e1e1 = +1/-1/0 and e1e2=-e2e1. The brm function creates the remap dictionary. + var simplify = (s,p,q,r)=>{ + var sign=1,c,l,t=[],f=true,ss=s.match(tot>9?/(\d\d)/g:/(\d)/g);if (!ss) return s; s=ss; l=s.length; + while (f) { f=false; + // implement Ex*Ex = metric. + for (var i=0; i=(p+r)) sign*=-1; else if ((s[i]-low)t[i+1]) { c=t[i];t[i]=t[i+1];t[i+1]=c;sign*=-1;f=true; break;} if (f) { s=t;t=[];l=s.length; } + } + var ret=(sign==0)?'0':((sign==1)?'':'-')+(t.length?'e'+t.join(''):'1'); return (brm&&brm[ret])||(brm&&brm['-'+ret]&&'-'+brm['-'+ret])||ret; + }, + brm=(x=>{ var ret={}; for (var i in basis) ret[basis[i]=='1'?'1':simplify(basis[i],p,q,r)] = basis[i]; return ret; })(basis); + + // As an alternative to the string fiddling, one can also bit-fiddle. In this case the basisvectors are represented by integers with 1 bit per generator set. + var simplify_bits = (A,B,p2)=>{ var n=p2||(p+q+r),t=0,ab=A&B,res=A^B; if (ab&((1<>1); t&=B; t^=ab>>(p+r); t^=t>>16; t^=t>>8; t^=t>>4; return [1-2*(27030>>(t&15)&1),res]; }, + bc = (v)=>{ v=v-((v>>1)& 0x55555555); v=(v&0x33333333)+((v>>2)&0x33333333); var c=((v+(v>>4)&0xF0F0F0F)*0x1010101)>>24; return c }; + + if (!options.graded && tot <= 6 || options.graded===false || options.Cayley) { + // Faster and degenerate-metric-resistant dualization. (a remapping table that maps items into their duals). + var drm=basis.map((a,i)=>{ return {a:a,i:i} }) + .sort((a,b)=>a.a.length>b.a.length?1:a.a.lengthx.i).reverse(), + drms=drm.map((x,i)=>(x==0||i==0)?1:simplify(basis[x]+basis[i])[0]=='-'?-1:1); + + /// Store the full metric (also for bivectors etc ..) + var metric = options.Cayley&&options.Cayley.map((x,i)=>x[i]) || basis.map((x,xi)=>simplify(x+x,p,q,r)|0); metric[0]=1; + + /// Generate multiplication tables for the outer and geometric products. + var mulTable = options.Cayley||basis.map(x=>basis.map(y=>(x==1)?y:(y==1)?x:simplify(x+y,p,q,r))); + + // subalgebra support. (must be bit-order basis blades, does no error checking.) + if (options.even) options.basis = basis.filter(x=>x.length%2==1); + if (options.basis && !options.Cayley && r>=0 && options.basis.length != 2**tot) { + metric = metric.filter((x,i)=>options.basis.indexOf(basis[i])!=-1); + mulTable = mulTable.filter((x,i)=>options.basis.indexOf(basis[i])!=-1).map(x=>x.filter((x,i)=>options.basis.indexOf(basis[i])!=-1)); + basis = options.basis; + } + + /// Convert Cayley table to product matrices. The outer product selects the strict sum of the GP (but without metric), the inner product + /// is the left contraction. + var gp=basis.map(x=>basis.map(x=>'0')), cp=gp.map(x=>gp.map(x=>'0')), cps=gp.map(x=>gp.map(x=>'0')), op=gp.map(x=>gp.map(x=>'0')), gpo={}; // Storage for our product tables. + basis.forEach((x,xi)=>basis.forEach((y,yi)=>{ var n = mulTable[xi][yi].replace(/^-/,''); if (!gpo[n]) gpo[n]=[]; gpo[n].push([xi,yi]); })); + basis.forEach((o,oi)=>{ + gpo[o].forEach(([xi,yi])=>op[oi][xi]=(grades[oi]==grades[xi]+grades[yi])?((mulTable[xi][yi]=='0')?'0':((mulTable[xi][yi][0]!='-')?'':'-')+'b['+yi+']*this['+xi+']'):'0'); + gpo[o].forEach(([xi,yi])=>{ + gp[oi][xi] =((gp[oi][xi]=='0')?'':gp[oi][xi]+'+') + ((mulTable[xi][yi]=='0')?'0':((mulTable[xi][yi][0]!='-')?'':'-')+'b['+yi+']*this['+xi+']'); + cp[oi][xi] =((cp[oi][xi]=='0')?'':cp[oi][xi]+'+') + ((grades[oi]==grades[yi]-grades[xi])?gp[oi][xi]:'0'); + cps[oi][xi]=((cps[oi][xi]=='0')?'':cps[oi][xi]+'+') + ((grades[oi]==Math.abs(grades[yi]-grades[xi]))?gp[oi][xi]:'0'); + }); + }); + + /// Flat Algebra Multivector Base Class. + var generator = class MultiVector extends (options.baseType||Float32Array) { + /// constructor - create a floating point array with the correct number of coefficients. + constructor(a) { super(a||basis.length); return this; } + + /// grade selection - return a only the part of the input with the specified grade. + Grade(grade,res) { res=res||new this.constructor(); for (var i=0,l=res.length; i1e-10) res.push(((this[i]==1)&&i?'':((this[i]==-1)&&i)?'-':(this[i].toFixed(10)*1))+(i==0?'':tot==1&&q==1?'i':basis[i].replace('e','e_'))); return res.join('+').replace(/\+-/g,'-')||'0'; } + + /// Reversion, Involutions, Conjugation for any number of grades, component acces shortcuts. + get Negative (){ var res = new this.constructor(); for (var i=0; ia[drm[i]]*drms[i]); var res = new this.constructor(); res[res.length-1]=1; return this.Mul(res); }; + get UnDual (){ if (r) return this.map((x,i,a)=>a[drm[i]]*drms[a.length-i-1]); var res = new this.constructor(); res[res.length-1]=1; return this.Div(res); }; + get Length (){ return options.over?Math.sqrt(Math.abs(this.Mul(this.Conjugate).s.s)):Math.sqrt(Math.abs(this.Mul(this.Conjugate).s)); }; + get VLength (){ var res = 0; for (var i=0; i'res['+xi+']=b['+xi+']+this['+xi+']').join(';\n').replace(/(b|this)\[(.*?)\]/g,(a,b,c)=>options.mix?'('+b+'.'+(c|0?basis[c]:'s')+'||0)':a)+';\nreturn res') + generator.prototype.Scale = new Function('b,res','res=res||new this.constructor();\n'+basis.map((x,xi)=>'res['+xi+']=b*this['+xi+']').join(';\n').replace(/(b|this)\[(.*?)\]/g,(a,b,c)=>options.mix?'('+b+'.'+(c|0?basis[c]:'s')+'||0)':a)+';\nreturn res') + generator.prototype.Sub = new Function('b,res','res=res||new this.constructor();\n'+basis.map((x,xi)=>'res['+xi+']=this['+xi+']-b['+xi+']').join(';\n').replace(/(b|this)\[(.*?)\]/g,(a,b,c)=>options.mix?'('+b+'.'+(c|0?basis[c]:'s')+'||0)':a)+';\nreturn res') + generator.prototype.Mul = new Function('b,res','res=res||new this.constructor();\n'+gp.map((r,ri)=>'res['+ri+']='+r.join('+').replace(/\+\-/g,'-').replace(/(\w*?)\[(.*?)\]/g,(a,b,c)=>options.mix?'('+b+'.'+(c|0?basis[c]:'s')+'||0)':a).replace(/\+0/g,'')+';').join('\n')+'\nreturn res;'); + generator.prototype.LDot = new Function('b,res','res=res||new this.constructor();\n'+cp.map((r,ri)=>'res['+ri+']='+r.join('+').replace(/\+\-/g,'-').replace(/\+0/g,'').replace(/(\w*?)\[(.*?)\]/g,(a,b,c)=>options.mix?'('+b+'.'+(c|0?basis[c]:'s')+'||0)':a)+';').join('\n')+'\nreturn res;'); + generator.prototype.Dot = new Function('b,res','res=res||new this.constructor();\n'+cps.map((r,ri)=>'res['+ri+']='+r.join('+').replace(/\+\-/g,'-').replace(/\+0/g,'').replace(/(\w*?)\[(.*?)\]/g,(a,b,c)=>options.mix?'('+b+'.'+(c|0?basis[c]:'s')+'||0)':a)+';').join('\n')+'\nreturn res;'); + generator.prototype.Wedge = new Function('b,res','res=res||new this.constructor();\n'+op.map((r,ri)=>'res['+ri+']='+r.join('+').replace(/\+\-/g,'-').replace(/\+0/g,'').replace(/(\w*?)\[(.*?)\]/g,(a,b,c)=>options.mix?'('+b+'.'+(c|0?basis[c]:'s')+'||0)':a)+';').join('\n')+'\nreturn res;'); +// generator.prototype.Vee = new Function('b,res','res=res||new this.constructor();\n'+op.map((r,ri)=>'res['+drm[ri]+']='+r.map(x=>x.replace(/\[(.*?)\]/g,function(a,b){return '['+(drm[b|0])+']'})).join('+').replace(/\+\-/g,'-').replace(/\+0/g,'').replace(/(\w*?)\[(.*?)\]/g,(a,b,c)=>options.mix?'('+b+'.'+(c|0?basis[c]:'s')+'||0)':a)+';').join('\n')+'\nreturn res;'); + /// Conforms to the new Chapter 11 now. + generator.prototype.Vee = new Function('b,res',('res=res||new this.constructor();\n'+op.map((r,ri)=>'res['+drm[ri]+']='+drms[ri]+'*('+r.map(x=>x.replace(/\[(.*?)\]/g,function(a,b){return '['+(drm[b|0])+']'+(drms[b|0]>0?"":"*-1")})).join('+').replace(/\+\-/g,'-').replace(/\+0/g,'').replace(/(\w*?)\[(.*?)\]/g,(a,b,c)=>options.mix?'('+b+'.'+(c|0?basis[c]:'s')+'||0)':a)+');').join('\n')+'\nreturn res;').replace(/(b\[)|(this\[)/g,a=>a=='b['?'this[':'b[')); + generator.prototype.eigenValues = eigenValues; + + /// Add getter and setters for the basis vectors/bivectors etc .. + basis.forEach((b,i)=>Object.defineProperty(generator.prototype, i?b:'s', { + configurable: true, get(){ return this[i] }, set(x){ this[i]=x; } + })); + + /// Graded generator for high-dimensional algebras. + } else { + + /// extra graded lookups. + var basisg = grade_start.slice(0,grade_start.length-1).map((x,i)=>basis.slice(x,grade_start[i+1])); + var counts = grade_start.map((x,i,a)=>i==a.length-1?0:a[i+1]-x).slice(0,tot+1); + var basis_bits = basis.map(x=>x=='1'?0:x.slice(1).match(tot>9?/\d\d/g:/\d/g).reduce((a,b)=>a+(1<<(b-low)),0)), + bits_basis = []; basis_bits.forEach((b,i)=>bits_basis[b]=i); + var metric = basisg.map((x,xi)=>x.map((y,yi)=>simplify_bits(basis_bits[grade_start[xi]+yi],basis_bits[grade_start[xi]+yi])[0])); + var drms = basisg.map((x,xi)=>x.map((y,yi)=>simplify_bits(basis_bits[grade_start[xi]+yi],(~basis_bits[grade_start[xi]+yi])&((1<(typeof x=="string")?"-"+x:-x):undefined):this[i]; + else { if (r[i]==undefined) r[i]=[]; for(var j=0,m=Math.max(this[i].length,b[i].length);jx&&x.map(y=>typeof y=="string"?y+"*"+s:y*s)); } + + // geometric product. + Mul(b,r) { + r=r||new this.constructor(); var gotstring=false; + for (var i=0,x,gsx; gsx=grade_start[i],x=this[i],ig.map(e=>e&&(!(e+'').match(/-{0,1}\w+/))?'('+e+')':e)) + return r; + } + // outer product. + Wedge(b,r) { + r=r||new this.constructor(); + for (var i=0,x,gsx; gsx=grade_start[i],x=this[i],ix).sort((a,b)=>((a.match(/\d+/)[0]|0)-(b.match(/\d+/)[0]|0))||((a.match(/\d+$/)[0]|0)-(b.match(/\d+$/)[0]|0))).map(x=>x.replace(/\/\/\d+$/,'')); + var r2 = 'float sum=0.0; float res=0.0;\n', g=0; + r.forEach(x=>{ + var cg = x.match(/\d+/)[0]|0; + if (cg != g) r2 += "sum += res*res;\nres = 0.0;\n"; + r2 += x.replace(/\[\d+\]/,'') + '\n'; + g=cg; + }); + r2+= "sum += res*res;\n"; + return r2; + } + // Inner product glsl output. + IPNS_GLSL(b,point_source) { + var r='',count=0,curg; + for (var i=0,x,gsx; gsx=grade_start[i],x=this[i],ix).sort((a,b)=>((a.match(/\d+/)[0]|0)-(b.match(/\d+/)[0]|0))||((a.match(/\d+$/)[0]|0)-(b.match(/\d+$/)[0]|0))).map(x=>x.replace(/\/\/\d+$/,'')); + var r2 = 'float sum=0.0; float res=0.0;\n', g=0; + r.forEach(x=>{ + var cg = x.match(/\d+/)[0]|0; + if (cg != g) r2 += "sum += res*res;\nres = 0.0;\n"; + r2 += x.replace(/\[\d+\]/,'') + '\n'; + g=cg; + }); + r2+= "sum += res*res;\n"; + return r2; + } + // Left contraction. + LDot(b,r) { + r=r||new this.constructor(); + for (var i=0,x,gsx; gsx=grade_start[i],x=this[i],ig&&g.map((c,ci)=>!c?undefined:((c+'').match(/[\+\-\*]/)?'('+c+')':c)+(gi==0?"":basisg[gi][ci])).filter(x=>x).join('+')).filter(x=>x).join('+').replace(/\+\-/g,'-')||"0"; } + get s () { if (this[0]) return this[0][0]||0; return 0; } + get Length () { var res=0; this.forEach((g,gi)=>g&&g.forEach((e,ei)=>res+=(e||0)**2*metric[gi][ei])); return Math.abs(res)**.5; } + get VLength () { var res=0; this.forEach((g,gi)=>g&&g.forEach((e,ei)=>res+=(e||0)**2)); return Math.abs(res)**.5; } + get Reverse () { var r=new this.constructor(); this.forEach((x,gi)=>x&&x.forEach((e,ei)=>{if(!r[gi])r[gi]=[]; r[gi][ei] = this[gi][ei]*[1,1,-1,-1][gi%4]; })); return r; } + get Involute () { var r=new this.constructor(); this.forEach((x,gi)=>x&&x.forEach((e,ei)=>{if(!r[gi])r[gi]=[]; r[gi][ei] = this[gi][ei]*[1,-1,1,-1][gi%4]; })); return r; } + get Conjugate () { var r=new this.constructor(); this.forEach((x,gi)=>x&&x.forEach((e,ei)=>{if(!r[gi])r[gi]=[]; r[gi][ei] = this[gi][ei]*[1,-1,-1,1][gi%4]; })); return r; } + get Dual() { var r=new this.constructor(); this.forEach((g,gi)=>{ if (!g) return; r[tot-gi]=[]; g.forEach((e,ei)=>r[tot-gi][counts[gi]-1-ei]=drms[gi][ei]*e); }); return r; } + get Normalized () { return this.Scale(1/this.Length); } + } + + + // This generator is UNDER DEVELOPMENT - I'm publishing it so I can test on observable. + } + + // Generate a new class for our algebra. It extends the javascript typed arrays (default float32 but can be specified in options). + var res = class Element extends generator { + + // constructor - create a floating point array with the correct number of coefficients. + constructor(a) { super(a); if (this.upgrade) this.upgrade(); return this; } + + // Grade selection. (implemented by parent class). + Grade(grade,res) { res=res||new Element(); return super.Grade(grade,res); } + + // Right and Left divide - Defined on the elements, shortcuts to multiplying with the inverse. + Div (b,res) { return this.Mul(b.Inverse,res); } + LDiv (b,res) { return b.Inverse.Mul(this,res); } + + + // Bivector split - we handle all real cases, still have to add the complex cases for those exception scenarios. + Split (iter=50) { + var k = Math.floor((p+q+r)/2), OB = this.map(x=>x), B = this.map(x=>x), m = 1; + var Wi = [...Array(k)].map((r,i)=>{ m = m*(i+1); var Wi = B.Scale(1/m); B = B.Wedge(OB); return Wi; }); + if (k<3) { // The quadratic case is easy to solve. (for spaces <6D) + var TDT = this.Dot(this).s, TWT = this.Wedge(this); + if (TWT.VLength < 1E-5) return [this]; // bivector was simple. + var D = 0.5*Math.sqrt( TDT**2 - TWT.Mul(TWT).s ); + var eigen = [0.5*TDT + D, 0.5*TDT - D].sort((a,b)=>Math.abs(a)6D, closed form solutions of the characteristic polyn. are impossible, use eigenvalues of companion matrix. + var Wis = Wi.map((W,i)=>W.Mul(W).s*(-1)**(k-i+(k%2)) ); + var matrix = [...Array(k)].map((r,i)=>[...Array(k)].map((c,j)=>(j == k-1)?Wis[k-i-1]:(i-1==j)?1:0)); + var eigen = eigenValues(matrix,iter).sort((a,b)=>Math.abs(a){ + var r = Math.floor(k/2), N = Element.Scalar(0), DN = Element.Scalar(0); + for (var i=0; i<=r; ++i) { N.Add( Wi[2*i+1].Scale(v**(r-i)), N); DN.Add( Wi[2*i].Scale(v**(r-i)), DN); } + if (DN.VLength == 0) return Element.Scalar(0); + var ret = N.Div(DN); sum.Add(ret, sum); return ret; + }); + return [this.Sub(sum),...res]; // Smallest eigvalue becomes B-rest + } + + // Factorize a motor + Factorize (iter=50) { + var S = this.Grade(2).Split(iter); + var P = this.Scale(1); + // if (P.s) { + var R = S.slice(0,S.length-1).map((Si,i)=>{ + var Mi = Element.Scalar(P.s).Add(Si); + var scale = Math.sqrt(Mi.Reverse.Mul(Mi).s); + return Mi.Scale(1/scale); + }); + R.push( R.reduce((tot,fact)=>tot.Mul(fact.Reverse), Element.Scalar(1)).Mul(P) ); + // } + return R; + } + + // exp - closed form exp. + Exp (taylor = false) { + if (r==1 && tot<=4 && Math.abs(this[0])<1E-9 && !options.over && !taylor) { + // https://www.researchgate.net/publication/360528787_Normalization_Square_Roots_and_the_Exponential_and_Logarithmic_Maps_in_Geometric_Algebras_of_Less_than_6D + // 0 1 2 3 4 5 + // 5 6 7 8 9 10 + var l = (this[8]*this[8] + this[9]*this[9] + this[10]*this[10]); + if (l==0) return new Element([1, 0,0,0,0, this[5], this[6], this[7], 0, 0, 0, 0,0,0,0, 0]); + var m = (this[5]*this[10] + this[6]*this[9] + this[7]*this[8]), a = Math.sqrt(l), c = Math.cos(a), s = Math.sin(a)/a, t = m/l*(c-s); + var test = Element.Element(c, 0,0,0,0, s*this[5] + t*this[10], s*this[6] + t*this[9], s*this[7] + t*this[8], s*this[8], s*this[9], s*this[10], 0,0,0,0, m*s); + //return test; // tbc .. investigate pss coeff?? + + var u = Math.sqrt(Math.abs(this.Dot(this).s)); if (Math.abs(u)<1E-5) return this.Add(Element.Scalar(1)); + var v = this.Wedge(this).Scale(-1/(2*u)); + var res2 = Element.Add(Element.Sub(Math.cos(u),v.Scale(Math.sin(u))),Element.Div(Element.Mul((Element.Add(Math.sin(u),v.Scale(Math.cos(u)))),this),(Element.Add(u,v)))); + //if ([...test].map(x=>x.toFixed(1))+'' != [...res2].map(x=>x.toFixed(1))+'') { console.log(test, res2); debugger } + + return res2; + } + if (!taylor && Math.abs(this[0])<1E-9 && !options.over) { + return this.Grade(2).Split().reduce((total,simple)=>{ + var square = simple.Mul(simple).s, len = Math.sqrt(Math.abs(square)); + if (len <= 1E-5) return total.Mul(Element.Scalar(1).Add(simple)); + if (square < 0) return total.Mul(Element.Scalar(Math.cos(len)).Add(simple.Scale(Math.sin(len)/len)) ); + return total.Mul(Element.Scalar(Math.cosh(len)).Add(simple.Scale(Math.sinh(len)/len)) ); + },Element.Scalar(1)); + } + if (options.dual) { var f=Math.exp(this.s); return this.map((x,i)=>i?x*f:f); } + var res = Element.Scalar(1), y=1, M= this.Scale(1), N=this.Scale(1); for (var x=1; x<15; x++) { res=res.Add(M.Scale(1/y)); M=M.Mul(N); y=y*(x+1); }; + return res; + } + + // Log - only for up to 3D PGA for now + Log (compat = false) { + if (options.over) return; + if (!compat) { + if (tot==4 && q==0 && r==1 && !options.over) { // https://www.researchgate.net/publication/360528787_Normalization_Square_Roots_and_the_Exponential_and_Logarithmic_Maps_in_Geometric_Algebras_of_Less_than_6D + if (Math.abs(this.s)>=.99999) return Element.Bivector(this[5],this[6],this[7],0,0,0).Scale(Math.sign(this.s)); + var a = 1/(1 - this[0]*this[0]), b = Math.acos(this[0])*Math.sqrt(a), c = a*this[15]*(1 - this[0]*b); + return Element.Bivector( c*this[10] + b*this[5], -c*this[9] + b*this[6], c*this[8] + b*this[7], b*this[8], b*this[9], b*this[10] ); + } + return this.Factorize().reduce((sum,bi)=>{ + var [ci,si] = [bi.s, bi.Grade(2)]; + var square = si.Mul(si).s; + var len = Math.sqrt(Math.abs(square)); + if (Math.abs(square) < 1E-5) return sum.Add(si); + if (square < 0) return sum.Add(si.Scale(Math.acos(ci)/len)); + return sum.Add(si.Scale(Math.acosh(ci)/len)); + },Element.Scalar(0)); + } + var b = this.Grade(2), bdb = Element.Dot(b,b).s; + if (Math.abs(bdb)<=1E-5) return this.s<0?b.Scale(-1):b; + var s = Math.sqrt(-bdb), bwb = Element.Wedge(b,b); + if (Math.abs(bwb[bwb.length-1])<=1E-5 || Math.abs(this.s)<=1E-5) return b.Scale(Math.atan2(s,this.s)/s); + var p = bwb.Scale(-1/(2*s)); + return Element.Mul(Element.Mul((Element.Add(Math.atan2(s,this.s),p.Scale(1/this.s))),b),Element.Sub(s,p)).Scale(1/(s*s)); + } + + // Helper for efficient inverses. (custom involutions - negates grades in arguments). + Map () { var res=new Element(); return super.Map(res,...arguments); } + + // Factories - Make it easy to generate vectors, bivectors, etc when using the functional API. None of the examples use this but + // users that have used other GA libraries will expect these calls. The Coeff() is used internally when translating algebraic literals. + static Element() { return new Element([...arguments]); }; + static Coeff() { return (new Element()).Coeff(...arguments); } + static Scalar(x) { return (new Element()).Coeff(0,x); } + static Vector() { return (new Element()).nVector(1,...arguments); } + static Bivector() { return (new Element()).nVector(2,...arguments); } + static Trivector() { return (new Element()).nVector(3,...arguments); } + static nVector(n) { return (new Element()).nVector(...arguments); } + + // Static operators. The parser will always translate operators to these static calls so that scalars, vectors, matrices and other non-multivectors can also be handled. + // The static operators typically handle functions and matrices, calling through to element methods for multivectors. They are intended to be flexible and allow as many + // types of arguments as possible. If performance is a consideration, one should use the generated element methods instead. (which only accept multivector arguments) + static toEl(x) { if (x instanceof Function) x=x(); if (!(x instanceof Element)) x=Element.Scalar(x); return x; } + + // Addition and subtraction. Subtraction with only one parameter is negation. + static Add(a,b,res) { + // Resolve expressions passed in. + while(a.call)a=a(); while(b.call)b=b(); if (a.Add && b.Add) return a.Add(b,res); + // If either is a string, the result is a string. + if ((typeof a=='string')||(typeof b=='string')) return a.toString()+b.toString(); + // If only one is an array, add the other element to each of the elements. + if ((a instanceof Array && !a.Add)^(b instanceof Array && !b.Add)) return (a instanceof Array)?a.map(x=>Element.Add(x,b)):b.map(x=>Element.Add(a,x)); + // If both are equal length arrays, add elements one-by-one + if ((a instanceof Array)&&(b instanceof Array)&&a.length==b.length) return a.map((x,xi)=>Element.Add(x,b[xi])); + // If they're both not elements let javascript resolve it. + if (!(a instanceof Element || b instanceof Element)) return a+b; + // Here we're left with scalars and multivectors, call through to generated code. + a=Element.toEl(a); b=Element.toEl(b); return a.Add(b,res); + } + + static Sub(a,b,res) { + // Resolve expressions passed in. + while(a.call)a=a(); while(b&&b.call) b=b(); if (a.Sub && b && b.Sub) return a.Sub(b,res); + // If only one is an array, add the other element to each of the elements. + if (b&&((a instanceof Array)^(b instanceof Array))) return (a instanceof Array)?a.map(x=>Element.Sub(x,b)):b.map(x=>Element.Sub(a,x)); + // If both are equal length arrays, add elements one-by-one + if (b&&(a instanceof Array)&&(b instanceof Array)&&a.length==b.length) return a.map((x,xi)=>Element.Sub(x,b[xi])); + // Negation + if (arguments.length==1) return Element.Mul(a,-1); + // If none are elements here, let js do it. + if (!(a instanceof Element || b instanceof Element)) return a-b; + // Here we're left with scalars and multivectors, call through to generated code. + a=Element.toEl(a); b=Element.toEl(b); return a.Sub(b,res); + } + + // The geometric product. (or matrix*matrix, matrix*vector, vector*vector product if called with 1D and 2D arrays) + static Mul(a,b,res) { + // Resolve expressions + while(a.call&&!a.length)a=a(); while(b.call&&!b.length)b=b(); if (a.Mul && b.Mul) return a.Mul(b,res); + // still functions -> experimental curry style (dont use this.) + if (a.call && b.call) return (ai,bi)=>Element.Mul(a(ai),b(bi)); + // scalar mul. + if (Number.isFinite(a) && b.Scale) return b.Scale(a); else if (Number.isFinite(b) && a.Scale) return a.Scale(b); + // Handle matrices and vectors. + if ((a instanceof Array)&&(b instanceof Array)) { + // vector times vector performs a dot product. (which internally uses the GP on each component) + if((!(a[0] instanceof Array) || (a[0] instanceof Element)) &&(!(b[0] instanceof Array) || (b[0] instanceof Element))) { var r=tot?Element.Scalar(0):0; a.forEach((x,i)=>r=Element.Add(r,Element.Mul(x,b[i]),r)); return r; } + // Array times vector + if(!(b[0] instanceof Array)) return a.map((x,i)=>Element.Mul(a[i],b)); + // Array times Array + var r=a.map((x,i)=>b[0].map((y,j)=>{ var r=tot?Element.Scalar(0):0; x.forEach((xa,k)=>r=Element.Add(r,Element.Mul(xa,b[k][j]))); return r; })); + // Return resulting array or scalar if 1 by 1. + if (r.length==1 && r[0].length==1) return r[0][0]; else return r; + } + // Only one is an array multiply each of its elements with the other. + if ((a instanceof Array)^(b instanceof Array)) return (a instanceof Array)?a.map(x=>Element.Mul(x,b)):b.map(x=>Element.Mul(a,x)); + // Try js multiplication, else call through to geometric product. + var r=a*b; if (!isNaN(r)) return r; + a=Element.toEl(a); b=Element.toEl(b); return a.Mul(b,res); + } + + // The inner product. (default is left contraction). + static LDot(a,b,res) { + // Expressions + while(a.call)a=a(); while(b.call)b=b(); //if (a.LDot) return a.LDot(b,res); + // Map elements in array + if (b instanceof Array && !(a instanceof Array)) return b.map(x=>Element.LDot(a,x)); + if (a instanceof Array && !(b instanceof Array)) return a.map(x=>Element.LDot(x,b)); + // js if numbers, else contraction product. + if (!(a instanceof Element || b instanceof Element)) return a*b; + a=Element.toEl(a);b=Element.toEl(b); return a.LDot(b,res); + } + + // The symmetric inner product. (default is left contraction). + static Dot(a,b,res) { + // Expressions + while(a.call)a=a(); while(b.call)b=b(); //if (a.LDot) return a.LDot(b,res); + // js if numbers, else contraction product. + if (!(a instanceof Element || b instanceof Element)) return a|b; + a=Element.toEl(a);b=Element.toEl(b); return a.Dot(b,res); + } + + // The outer product. (Grassman product - no use of metric) + static Wedge(a,b,res) { + // normal behavior for booleans/numbers + if (typeof a in {boolean:1,number:1} && typeof b in {boolean:1,number:1}) return a^b; + // Expressions + while(a.call)a=a(); while(b.call)b=b(); if (a.Wedge) return a.Wedge(Element.toEl(b),res); + // The outer product of two vectors is a matrix .. internally Mul not Wedge ! + if (a instanceof Array && b instanceof Array) return a.map(xa=>b.map(xb=>Element.Mul(xa,xb))); + // js, else generated wedge product. + if (!(a instanceof Element || b instanceof Element)) return a*b; + a=Element.toEl(a);b=Element.toEl(b); return a.Wedge(b,res); + } + + // The regressive product. (Dual of the outer product of the duals). + static Vee(a,b,res) { + // normal behavior for booleans/numbers + if (typeof a in {boolean:1,number:1} && typeof b in {boolean:1,number:1}) return a&b; + // Expressions + while(a.call)a=a(); while(b.call)b=b(); if (a.Vee) return a.Vee(Element.toEl(b),res); + // js, else generated vee product. (shortcut for dual of wedge of duals) + if (!(a instanceof Element || b instanceof Element)) return 0; + a=Element.toEl(a);b=Element.toEl(b); return a.Vee(b,res); + } + + // The sandwich product. Provided for convenience (>>> operator) + static sw(a,b) { + // Skip strings/colors + if (typeof b == "string" || typeof b =="number") return b; + // Expressions + while(a.call)a=a(); while(b.call)b=b(); if (a.sw) return a.sw(b); + // Map elements in array + if (b instanceof Array && !b.Add) return b.map(x=>Element.sw(a,x)); + // Call through. no specific generated code for it so just perform the muls. + a=Element.toEl(a); b=Element.toEl(b); return a.Mul(b).Mul(a.Reverse); + } + + // Division - scalars or cal through to element method. + static Div(a,b,res) { + // Expressions + while(a.call)a=a(); while(b.call)b=b(); + // For DDG experiments, I'll include a quick cholesky on matrices here. (vector/matrix) + if ((a instanceof Array) && (b instanceof Array) && (b[0] instanceof Array)) { + // factor + var R = b.flat(), i, j, k, sum, i_n, j_n, n=b[0].length, s=new Array(n), x=new Array(n), yi; + for (i=0;i=0; i--) for (x[i] /= R[i*n+i], j=i+1; ja.map((r,ri)=>Element.Conjugate(a[ri][ci]))); return Element.toEl(a).Conjugate; } + static Normalize(a) { return Element.toEl(a).Normalized; }; + static Length(a) { return Element.toEl(a).Length }; + + // Comparison operators always use length. Handle expressions, then js or length comparison + static eq(a,b) { if (!(a instanceof Element)||!(b instanceof Element)) return a==b; while(a.call)a=a(); while(b.call)b=b(); for (var i=0; i(b instanceof Element?b.Length:b); } + static lte(a,b) { while(a.call)a=a(); while(b.call)b=b(); return (a instanceof Element?a.Length:a)<=(b instanceof Element?b.Length:b); } + static gte(a,b) { while(a.call)a=a(); while(b.call)b=b(); return (a instanceof Element?a.Length:a)>=(b instanceof Element?b.Length:b); } + + // Debug output and printing multivectors. + static describe(x) { if (x===true) console.log(`Basis\n${basis}\nMetric\n${metric.slice(1,1+tot)}\nCayley\n${mulTable.map(x=>(x.map(x=>(' '+x).slice(-2-tot)))).join('\n')}\nMatrix Form:\n`+gp.map(x=>x.map(x=>x.match(/(-*b\[\d+\])/)).map(x=>x&&((x[1].match(/-/)||' ')+String.fromCharCode(65+1*x[1].match(/\d+/)))||' 0')).join('\n')); return {basis:basisg||basis,metric,mulTable,matrix:gp.map(x=>x.map(x=>x.replace(/\*this\[.+\]/,'').replace(/b\[(\d+)\]/,(a,x)=>(metric[x]==-1||metric[x]==0&&grades[x]>1&&(-1)**grades[x]==(metric[basis.indexOf(basis[x].replace('0',''))]||(-1)**grades[x])?'-':'')+basis[x]).replace('--','')))} } + + // Direct sum of algebras - experimental + static sum(B){ + var A = Element; + // Get the multiplication tabe and basis. + var T1 = A.describe().mulTable, T2 = B.describe().mulTable; + var B1 = A.describe().basis, B2 = B.describe().basis; + // Get the maximum index of T1, minimum of T2 and rename T2 if needed. + var max_T1 = B1.filter(x=>x.match(/e/)).map(x=>x.match(/\d/g)).flat().map(x=>x|0).sort((a,b)=>b-a)[0]; + var max_T2 = B2.filter(x=>x.match(/e/)).map(x=>x.match(/\d/g)).flat().map(x=>x|0).sort((a,b)=>b-a)[0]; + var min_T2 = B2.filter(x=>x.match(/e/)).map(x=>x.match(/\d/g)).flat().map(x=>x|0).sort((a,b)=>a-b)[0]; + // remapping .. + T2 = T2.map(x=>x.map(y=>y.match(/e/)?y.replace(/(\d)/g,(x)=>(x|0)+max_T1):y.replace("1","e"+(1+max_T2+max_T1)))); + B2 = B2.map((y,i)=>i==0?y.replace("1","e"+(1+max_T2+max_T1)):y.replace(/(\d)/g,(x)=>(x|0)+max_T1)); + // Build the new basis and multable.. + var basis = [...B1,...B2]; + var Cayley = T1.map((x,i)=>[...x,...T2[0].map(x=>"0")]).concat(T2.map((x,i)=>[...T1[0].map(x=>"0"),...x])) + // Build the new algebra. + var grades = [...B1.map(x=>x=="1"?0:x.length-1),...B2.map((x,i)=>i?x.length-1:0)]; + var a = Algebra({basis,Cayley,grades,tot:Math.log2(B1.length)+Math.log2(B2.length)}) + // And patch up .. + a.Scalar = function(x) { + var res = new a(); + for (var i=0; i function of 1 parameter will be called with that parameter from -1 to 1 and graphed on a canvas. Returned values should also be in the [-1 1] range + // graph(function(x,y)) => functions of 2 parameters will be called from -1 to 1 on both arguments. Returned values can be 0-1 for greyscale or an array of three RGB values. + // graph(array) => array of algebraic elements (points, lines, circles, segments, texts, colors, ..) is graphed. + // graph(function=>array) => same as above, for animation scenario's this function is called each frame. + // An optional second parameter is an options object { width, height, animate, camera, scale, grid, canvas } + static graph(f,options) { + // Store the original input + if (!f) return; var origf=f; + // generate default options. + options=options||{}; options.scale=options.scale||1; options.camera=options.camera||(tot!=4?Element.Scalar(1): ( Element.Bivector(0,0,0,0,0,options.p||0).Exp() ).Mul( Element.Bivector(0,0,0,0,options.h||0,0).Exp()) ); + if (options.conformal && tot==4) var ni = options.ni||this.Coeff(4,1,3,1), no = options.no||this.Coeff(4,0.5,3,-0.5), minus_no = no.Scale(-1); + var ww=options.width, hh=options.height, cvs=options.canvas, tpcam=new Element([0,0,0,0,0,0,0,0,0,0,0,-5,0,0,1,0]),tpy=this.Coeff(4,1),tp=new Element(), + // project 3D to 2D. This allows to render 3D and 2D PGA with the same code. + project=(o)=>{ if (!o) return o; while (o.call) o=o(); +// if (o instanceof Element && o.length == 32) o = new Element([o[0],o[1],o[2],o[3],o[4],o[6],o[7],o[8],o[10],o[11],o[13],o[16],o[17],o[19],o[22],o[26]]); + // Clip 3D lines so they don't go past infinity. + if (o instanceof Element && o.length == 16 && o[8]**2+o[9]**2+o[10]**2>0.0001) { + o = [[options.clip||2,1,0,0],[-(options.clip||2),1,0,0],[options.clip||2,0,1,0],[-(options.clip||2),0,1,0],[options.clip||2,0,0,1],[-(options.clip||2),0,0,1]].map(v=>{ + var r = Element.Vector(...v).Wedge(o); return r[14]?r.Scale(1/r[14], r):undefined; + }).filter(x=>x && Math.abs(x[13])<= (options.clip||2)+0.001 && Math.abs(x[12]) <= (options.clip||2)+0.001 && Math.abs(x[11]) <= (options.clip||2) + 0.001).slice(0,2); + return o.map(o=>(tpcam).Vee(options.camera.Mul(o).Mul(options.camera.Conjugate)).Wedge(tpy)); + } + // Convert 3D planes to polies. + if (o instanceof Element && o.length == 16 && o.Grade(1).Length>0.01) { + var m = Element.Add(1, Element.Mul(o.Normalized, Element.Coeff(3,1))).Normalized, e0 = 0; + o=Element.sw(m,[[-1,-1],[-1,1],[1,1],[-1,-1],[1,1],[1,-1]].map(([x,z])=>Element.Trivector(x*o.Length,e0,z*o.Length,1))); + return o.map(o=>(tpcam).Vee(options.camera.Mul(o).Mul(options.camera.Conjugate)).Wedge(tpy)); + } + return (tot==4 && o instanceof Element && o.length==16)?(tpcam).Vee(options.camera.Mul(o).Mul(options.camera.Conjugate)).Wedge(tpy):(o.length==2**tot)?Element.sw(options.camera,o):o; + }; + // gl escape. + if (options.gl && !(tot==4 && options.conformal)) return Element.graphGL(f,options); if (options.up) return Element.graphGL2(f,options); + // if we get an array or function without parameters, we render c2d or p2d SVG points/lines/circles/etc + if (!(f instanceof Function) || f.length===0) { + // Our current cursor, color, animation state and 2D mapping. + var lx,ly,lr,color,res,anim=false,to2d=(tot==5)?[0, 8, 11, 13, 19, 17, 22, 26]:(tot==3)?[0,1,2,3,4,5,6,7]:[0,7,9,10,13,12,14,15]; + // Make sure we have an array of elements. (if its an object, convert to array with elements and names.) + if (f instanceof Function) f=f(); if (!(f instanceof Array)) f=[].concat.apply([],Object.keys(f).map((k)=>typeof f[k]=='number'?[f[k]]:[f[k],k])); + // The build function generates the actual SVG. It will be called everytime the user interacts or the anim flag is set. + function build(f,or) { + // Make sure we have an aray. + if (or && f && f instanceof Function) f=f(); + // Reset position and color for cursor. + lx=-2;ly=options.conformal?-1.85:1.85;lr=0;color='#444'; + // Create the svg element. (master template string till end of function) + var svg=new DOMParser().parseFromString(` + ${// Add a grid (option) + options.grid?(()=>{ + if (tot==4 && !options.conformal) { + const lines3d = (n,from,to,j,l=0, ox=0, oy=0, alpha=1)=>[``,...[...Array(n+1)].map((x,i)=>{ + var f=from.map((x,i)=>x*(i==3?1:(options.gridSize||1))), t=to.map((x,i)=>x*(i==3?1:(options.gridSize||1))); f[j] = t[j] = (i-(n/2))/(n/2) * (options.gridSize||1); + var D3a = Element.Trivector(...f), D2a = project(D3a), D3b = Element.Trivector(...t), D2b = project(D3b); + var lx=options.scale*D2a[drm[2]]/D2a[drm[1]]; if (drm[1]==6||drm[1]==14) lx*=-1; var ly=-options.scale*D2a[drm[3]]/D2a[drm[1]]; + var lx2=options.scale*D2b[drm[2]]/D2b[drm[1]]; if (drm[1]==6||drm[1]==14) lx2*=-1; var ly2=-options.scale*D2b[drm[3]]/D2b[drm[1]]; + var r = ``; + if (l && i && i!= n) r += `${((from[j]<0?-1:1)*(i-(n/2))/(n/2)*(options.gridSize||1)).toFixed(1)}` + return r; + }),'']; + var front = Element.sw(options.camera,Element.Trivector(1,0,0,0)).Dual.Dot(Element.Vector(0,0,0,1)).s, ff = front>0?1:-1; + var left = Element.sw(options.camera,Element.Trivector(0,0,1,0)).Dual.Dot(Element.Vector(0,0,0,1)).s, ll = left>0?1:-1; + var fa = Math.max(0,Math.min(1,5*Math.abs(left))), la = Math.max(0,Math.min(1,5*Math.abs(front))); + return [ + ...lines3d(20,[-1,-1,-1,1],[1,-1,1,1],2,options.labels?ff:0, 0, 0.05), + ...lines3d(20,[-1,-1,-1,1],[1,-1,1,1],0,options.labels?ll:0, 0, 0.05), + ...lines3d(20,[-1,-1,ll,1],[1,1,ll,1],0,0,0,0,fa), + ...lines3d(20,[-1,1,ll,1],[1,-1,ll,1],1,!options.labels?0:(ff!=-1)?1:2, ll*ff*-0.05, 0, fa), + ...lines3d(20,[ff,1,-1,1],[ff,-1,1,1],1,!options.labels?0:(ll!=-1)?1:2, ll*ff*0.05, 0, la), + ...lines3d(20,[ff,-1,-1,1],[ff,1,1,1],2,0,0,0,la), + ].join(''); + } + const s = options.scale, n = (10/s)|0, cx = options.camera.e02, cy = options.camera.e01, alpha = Math.min(1,(s-0.2)*10); if (options.scale<0.1) return; + const lines = (n,dir,space,width,color)=>[...Array(2*n+1)].map((x,xi)=>``) + return [``,...lines(n*2,0,0.2,0.005,'#DDD'),...lines(n*2,1,0.2,0.005,'#DDD'),...lines(n,0,1,0.005,'#AAA'),...lines(n,1,1,0.005,'#AAA'),...lines(n,0,5,0.005,'#444'),...lines(n,1,5,0.005,'#444')] + .concat(options.labels?[...Array(4*n+1)].map((x,xi)=>(xi-n*2==0)?``:`${((xi-n*2)*0.2).toFixed(1)}`):[]) + .concat(options.labels?[...Array(4*n+1)].map((x,xi)=>`${((xi-n*2)*-0.2).toFixed(1)}`):[]).join('')+''; + })():''} + // Handle conformal 2D elements. + ${options.conformal?f.map&&f.map((o,oidx)=>{ + // Optional animation handling. + if((o==Element.graph && or!==false)||(oidx==0&&options.animate&&or!==false)) { anim=true; requestAnimationFrame(()=>{var r=build(origf,(!res)||(document.body.contains(res))).innerHTML; if (res) res.innerHTML=r; }); if (!options.animate) return; } + // Resolve expressions passed in. + while (o.call) o=o(); + if (options.ipns && o instanceof Element) o = o.Dual; + var sc = options.scale; + var lineWidth = options.lineWidth || 1; + var pointRadius = options.pointRadius || 1; + var dash_for_r2 = (r2, render_r, target_width) => { + // imaginary circles are dotted + if (r2 >= 0) return 'none'; + var half_circum = render_r*Math.PI; + var width = half_circum / Math.max(Math.round(half_circum / target_width), 1); + return `${width} ${width}`; + }; + // Arrays are rendered as segments or polygons. (2 or more elements) + if (o instanceof Array) { lx=ly=lr=0; o=o.map(o=>{ while(o.call)o=o(); return o.Scale(-1/o.Dot(ni).s); }); o.forEach((o)=>{lx+=sc*(o.e1);ly+=sc*(-o.e2)});lx/=o.length;ly/=o.length; return o.length>2?``:``; } + // Allow insertion of literal svg strings. + if (typeof o =='string' && o[0]=='<') { return o; } + // Strings are rendered at the current cursor position. + if (typeof o =='string') { var res2=(o[0]=='_')?'':` ${o} `; ly+=0.14; return res2; } + // Numbers change the current color. + if (typeof o =='number') { color='#'+(o+(1<<25)).toString(16).slice(-6); return ''; }; + // All other elements are rendered .. + var ni_part = o.Dot(no.Scale(-1)); // O_i + n_o O_oi + var no_part = ni.Scale(-1).Dot(o); // O_o + O_oi n_i + if (ni_part.VLength * 1e-6 > no_part.VLength) { + // direction or dual - nothing to render + return ""; + } + var no_ni_part = no_part.Dot(no.Scale(-1)); // O_oi + var no_only_part = ni.Wedge(no_part).Dot(no.Scale(-1)); // O_o + + /* Note: making 1e-6 smaller increases the maximum circle radius before they are drawn as lines */ + if (no_ni_part.VLength * 1e-6 > no_only_part.VLength) { + var is_flat = true; + var direction = no_ni_part; + } + else { + var is_flat = false; + var direction = no_only_part; + } + // normalize to make the direction unitary + var dl = direction.Length; + o = o.Scale(1/dl); + direction = direction.Scale(1/dl) + + var b0=direction.Grade(0).VLength>0.001,b1=direction.Grade(1).VLength>0.001,b2=direction.Grade(2).VLength>0.001; + if (!is_flat && b0 && !b1 && !b2) { + // Points + if (direction.s < 0) { o = Element.Sub(o); } + lx=sc*(o.e1); ly=sc*(-o.e2); lr=0; return res2=``; + } else if (is_flat && !b0 && b1 && !b2) { + // Lines. + var loc=minus_no.LDot(o).Div(o), att=ni.Dot(o); + lx=sc*(-loc.e1); ly=sc*(loc.e2); lr=Math.atan2(-o[14],o[13])/Math.PI*180; return ``; + } else if (!is_flat && !b0 && !b1 && b2) { + // Circles + var loc=o.Div(ni.LDot(o)); lx=sc*(-loc.e1); ly=sc*(loc.e2); + var r2=o.Mul(o.Conjugate).s; + var r = Math.sqrt(Math.abs(r2))*sc; + return ``; + } else if (!is_flat && !b0 && b1 && !b2) { + // Point Pairs. + lr=0; var ei=ni,eo=no, nix=o.Wedge(ei), sqr=o.LDot(o).s/nix.LDot(nix).s, r=Math.sqrt(Math.abs(sqr)), attitude=((ei.Wedge(eo)).LDot(nix)).Normalized.Mul(Element.Scalar(r)), pos=o.Div(nix); pos=pos.Div( pos.LDot(Element.Sub(ei))); + if (nix==0) { pos = o.Dot(Element.Coeff(4,-1)); sqr=-1; } + lx=sc*(pos.e1); ly=sc*(-pos.e2); + if (sqr==0) return ``; + // Draw imaginary pairs hollow + if (sqr > 0) var fill = color||'green', stroke = 'none', dash_array = 'none'; + else var fill = 'none', stroke = color||'green'; + lx=sc*(pos.e1+attitude.e1); ly=sc*(-pos.e2-attitude.e2); + var res2=``; + lx=sc*(pos.e1-attitude.e1); ly=sc*(-pos.e2+attitude.e2); + return res2+``; + } else { + /* Unrecognized */ + return ""; + } + // Handle projective 2D and 3D elements. + }):f.map&&f.map((o,oidx)=>{ if((o==Element.graph && or!==false)||(oidx==0&&options.animate&&or!==false)) { anim=true; requestAnimationFrame(()=>{var r=build(origf,(!res)||(document.body.contains(res))).innerHTML; if (res) res.innerHTML=r; }); if (!options.animate) return; } while (o instanceof Function) o=o(); o=(o instanceof Array)?o.map(project):project(o); if (o===undefined) return; + // dual option dualizes before render + if (options.dual && o instanceof Element) o = o.Dual; + // line segments and polygons + if (o instanceof Array && o.length>1) { lx=ly=lr=0; o.forEach((o)=>{while (o.call) o=o(); lx+=options.scale*((drm[1]==6||drm[1]==14)?-1:1)*o[drm[2]]/o[drm[1]];ly+=options.scale*o[drm[3]]/o[drm[1]]});lx/=o.length;ly/=o.length; return o.length>2?``:``; } + // svg + if (typeof o =='string' && o[0]=='<') { return o; } + // Labels + if (typeof o =='string') { var res2=(o[0]=='_')?'':` ${o} `; ly-=0.14; return res2; } + // Colors + if (typeof o =='number') { color='#'+(o+(1<<25)).toString(16).slice(-6); return ''; }; + // Points + if (o[to2d[6]]**2 >0.0001) { lx=options.scale*o[drm[2]]/o[drm[1]]; if (drm[1]==6||drm[1]==14) lx*=-1; ly=options.scale*o[drm[3]]/o[drm[1]]; lr=0; var res2=``; ly+=0.05; lx-=0.1; return res2; } + // Lines + if (o[to2d[2]]**2+o[to2d[3]]**2>0.0001) { var l=Math.sqrt(o[to2d[2]]**2+o[to2d[3]]**2); o[to2d[2]]/=l; o[to2d[3]]/=l; o[to2d[1]]/=l; lx=0.5; ly=options.scale*((drm[1]==6)?-1:-1)*o[to2d[1]]; lr=-Math.atan2(o[to2d[2]],o[to2d[3]])/Math.PI*180; var res2=``; ly+=0.05; return res2; } + // Vectors + if (o[to2d[4]]**2+o[to2d[5]]**2>0.0001) { lr=0; ly+=0.05; lx+=0.1; var res2=``; ly=ly+o.e01/4*3-0.05; lx=lx-o.e02/4*3; return res2; } + }).join()}`,'text/html').body; + // return the inside of the created svg element. + return svg.removeChild(svg.firstChild); + }; + // Create the initial svg and install the mousehandlers. + res=build(f); res.value=f; res.options=options; res.setAttribute("stroke-width",options.lineWidth*0.005||0.005); + res.remake = (animate)=>{ options.animate = animate; if (animate) { var r=build(origf,(!res)||(document.body.contains(res))).innerHTML; if (res) res.innerHTML=r; }; return res;}; + //onmousedown="if(evt.target==this)this.sel=undefined" + var mousex,mousey,cammove=false; + res.onwheel=(e)=>{ e.preventDefault(); options.scale = Math.min(5,Math.max(0.1,(options.scale||1)-e.deltaY*0.0001)); if (!anim) {var r=build(origf,(!res)||(document.body.contains(res))).innerHTML; if (res) res.innerHTML=r; } } + res.onmousedown=(e)=>{ if (e.target == res) res.sel=undefined; mousex = e.clientX; mousey = e.clientY; cammove = true; } + res.onmousemove=(e)=>{ + if (cammove && tot==4 && !options.conformal) { + if (!e.buttons) { cammove=false; return; }; + var [dx,dy] = [(options.scale || 1)*(e.clientX - mousex)*3, 3*(options.scale || 1)*(e.clientY - mousey)]; + [mousex,mousey] = [e.clientX,e.clientY]; + if (res.sel !== undefined && f[res.sel].set) { + var [cw,ch] = [res.clientWidth, res.clientHeight]; + var ox = (1/(options.scale || 1)) * ((e.offsetX / cw) - 0.5) * (cw>ch?(cw/ch):1); + var oy = (1/(options.scale || 1)) * ((e.offsetY / ch) - 0.5) * (ch>cw?(ch/cw):1); + var tb = Element.sw(options.camera,f[res.sel]); + var z = -(tb.e012/tb.e123+5)/5*4; tb.e023 = ox*z*tb.e123; tb.e013 = oy*z*tb.e123; + f[res.sel].set(Element.sw(options.camera.Reverse, tb)); + //f[res.sel].set( Element.sw(Element.sw(options.camera.Reverse,Element.Bivector(-dx/res.clientWidth,dy/res.clientHeight,0,0,0,0).Exp()),f[res.sel]) ); + } else { + options.h = (options.h||0) + dx/300; + options.p = (options.p||0) - dy/600; + if (options.camera) options.camera.set( ( Element.Bivector(0,0,0,0,0,options.p).Exp() ).Mul( Element.Bivector(0,0,0,0,options.h,0).Exp() )/*.Mul(options.camera)*/ ) + } + if (!anim) {var r=build(origf,(!res)||(document.body.contains(res))).innerHTML; if (res) res.innerHTML=r; } + return; + } + if (res.sel===undefined || f[res.sel] == undefined || f[res.sel].set == undefined || !e.buttons) return; + var resx=res.getBoundingClientRect().width,resy=res.getBoundingClientRect().height, + x=((e.clientX-res.getBoundingClientRect().left)/(resx/4||128)-2)*(resx>resy?resx/resy:1),y=((e.clientY-res.getBoundingClientRect().top)/(resy/4||128)-2)*(resy>resx?resy/resx:1); + x/=options.scale;y/=options.scale; + if (options.conformal) { f[res.sel].set(this.Coeff(1,x,2,-y).Add(no).Add(ni.Scale(0.5*(x*x+y*y))) ) } + else { f[res.sel][drm[2]]=((drm[1]==6)?-x:x)-((tot<4)?2*options.camera.e01:0); f[res.sel][drm[3]]=-y+((tot<4)?2*options.camera.e02:0); f[res.sel][drm[1]]=1; f[res.sel].set(f[res.sel].Normalized)} + if (!anim) {var r=build(origf,(!res)||(document.body.contains(res))).innerHTML; if (res) res.innerHTML=r; } + res.dispatchEvent(new CustomEvent('input')) }; + return res; + } + // 1d and 2d functions are rendered on a canvas. + cvs=cvs||document.createElement('canvas'); if(ww)cvs.width=ww; if(hh)cvs.height=hh; var w=cvs.width,h=cvs.height,context=cvs.getContext('2d'), data=context.getImageData(0,0,w,h); + // Grid support for the canvas. + const [x_from,x_to,y_from,y_to]=options.range||[-1,1,1,-1]; + function drawGrid() { + const [X,Y]=[x=>(x-x_from)*w/(x_to-x_from),y=>(y-y_from)*h/(y_to-y_from)] + context.strokeStyle = "#008800"; context.lineWidth = 1; + // X and Y axis + context.beginPath(); + context.moveTo(X(x_from), Y(0)); context.lineTo(X(x_to ), Y(0)); context.stroke(); + context.moveTo(X(0), Y(y_from)); context.lineTo(X(0), Y(y_to )); context.stroke(); + // Draw ticks + context.strokeStyle = "#00FF00"; context.lineWidth = 2; context.font = "10px Arial"; context.fillStyle = "#448844"; + for (var i=x_from,j=y_from,ii=0; ii<=10; ++ii) { + context.beginPath(); j+= (y_to-y_from)/10; i+=(x_to-x_from)/10; + context.moveTo(X(i), Y(-(y_to - y_from)/200)); context.lineTo(X(i), Y((y_to - y_from)/200)); context.stroke(); + if(i.toFixed(1)!=0) context.fillText(i.toFixed(1), X(i-(x_to-x_from)/100), Y(-(y_to-y_from)/40)); + context.moveTo(X((x_to-x_from)/200), Y(j)); context.lineTo(X(-(x_to-x_from)/200), Y(j)); context.stroke(); + if(j.toFixed(1)!=0) context.fillText(j.toFixed(1), X((x_to-x_from)/100), Y(j)); + } + } + // two parameter functions .. evaluate for both and set resulting color. + if (f.length==2) for (var px=0; pxx*255).concat([255]),py*w*4+px*4); } + // one parameter function.. go over x range, use result as y. + else if (f.length==1) for (var px=0; px 0 && res < h-1) data.data.set([0,0,0,255],res*w*4+px*4); } + context.putImageData(data,0,0); + if (f.length == 1 || f.length == 2) if (options.grid) drawGrid(); + return cvs; + } + + // webGL2 Graphing function. (for OPNS/IPNS implicit 2D and 1D surfaces in 3D space). + static graphGL2(f,options) { + // Create canvas, get webGL2 context. + var canvas=document.createElement('canvas'); canvas.style.width=options.width||''; canvas.style.height=options.height||''; canvas.style.backgroundColor='#EEE'; + if (options.width && options.width.match && options.width.match(/px/i)) canvas.width = parseFloat(options.width)*(options.devicePixelRatio||devicePixelRatio||1); if (options.height && options.height.match && options.height.match(/px/i)) canvas.height = parseFloat(options.height)*(options.devicePixelRatio||devicePixelRatio||1); + var gl=canvas.getContext('webgl2',{alpha:options.alpha||false,preserveDrawingBuffer:true,antialias:true,powerPreference:'high-performance'}); + var gl2=!!gl; if (!gl) gl=canvas.getContext('webgl',{alpha:options.alpha||false,preserveDrawingBuffer:true,antialias:true,powerPreference:'high-performance'}); + gl.clearColor(240/255,240/255,240/255,1.0); gl.enable(gl.DEPTH_TEST); if (!gl2) { gl.getExtension("EXT_frag_depth"); gl.va = gl.getExtension('OES_vertex_array_object'); } + else gl.va = { createVertexArrayOES : gl.createVertexArray.bind(gl), bindVertexArrayOES : gl.bindVertexArray.bind(gl), deleteVertexArrayOES : gl.deleteVertexArray.bind(gl) } + // Compile vertex and fragment shader, return program. + var compile=(vs,fs)=>{ + var s=[gl.VERTEX_SHADER,gl.FRAGMENT_SHADER].map((t,i)=>{ + var r=gl.createShader(t); gl.shaderSource(r,[vs,fs][i]); gl.compileShader(r); + return gl.getShaderParameter(r, gl.COMPILE_STATUS)&&r||console.error(gl.getShaderInfoLog(r)); + }); + var p = gl.createProgram(); gl.attachShader(p, s[0]); gl.attachShader(p, s[1]); gl.linkProgram(p); + gl.getProgramParameter(p, gl.LINK_STATUS)||console.error(gl.getProgramInfoLog(p)); + return p; + }; + // Create vertex array and buffers, upload vertices and optionally texture coordinates. + var createVA=function(vtx) { + var r = gl.va.createVertexArrayOES(); gl.va.bindVertexArrayOES(r); + var b = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, b); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vtx), gl.STATIC_DRAW); + gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(0); + return {r,b} + }, + // Destroy Vertex array and delete buffers. + destroyVA=function(va) { + if (va.b) gl.deleteBuffer(va.b); if (va.r) gl.va.deleteVertexArrayOES(va.r); + } + // Drawing function + var M=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,5,1]; + var draw=function(p, tp, vtx, color, color2, ratio, texc, va, b,color3,r,g){ + gl.useProgram(p); gl.uniformMatrix4fv(gl.getUniformLocation(p, "mv"),false,M); + gl.uniformMatrix4fv(gl.getUniformLocation(p, "p"),false, [5,0,0,0,0,5*(ratio||1),0,0,0,0,1,2,0,0,-1,0]) + gl.uniform3fv(gl.getUniformLocation(p, "color"),new Float32Array(color)); + gl.uniform3fv(gl.getUniformLocation(p, "color2"),new Float32Array(color2)); + if (color3) gl.uniform3fv(gl.getUniformLocation(p, "color3"),new Float32Array(color3)); + if (b) gl.uniform1fv(gl.getUniformLocation(p, "b"),(new Float32Array(counts[g])).map((x,i)=>b[g][i]||0)); + if (texc) gl.uniform1i(gl.getUniformLocation(p, "texc"),0); + if (r) gl.uniform1f(gl.getUniformLocation(p,"ratio"),r); + var v; if (!va) v = createVA(vtx); else gl.va.bindVertexArrayOESOES(va.r); + gl.drawArrays(tp, 0, (va&&va.tcount)||vtx.length/3); + if (v) destroyVA(v); + } + // Compile the OPNS renderer. (sphere tracing) + var programs = [], genprog = grade=>compile(`${gl2?"#version 300 es":""} + ${gl2?"in":"attribute"} vec4 position; ${gl2?"out":"varying"} vec4 Pos; uniform mat4 mv; uniform mat4 p; + void main() { Pos=mv*position; gl_Position = p*Pos; }`, + `${!gl2?"#extension GL_EXT_frag_depth : enable":"#version 300 es"} + precision highp float; + uniform vec3 color; uniform vec3 color2; + uniform vec3 color3; uniform float b[${counts[grade]}]; + uniform float ratio; ${gl2?"out vec4 col;":""} + ${gl2?"in":"varying"} vec4 Pos; + float product_len (in float z, in float y, in float x, in float[${counts[grade]}] b) { + ${this.nVector(options.up.length>tot?2:1,[])[options.IPNS?"IPNS_GLSL":"OPNS_GLSL"](this.nVector(grade,[]), options.up)} + return sqrt(abs(sum)); + } + vec3 find_root (in vec3 start, vec3 dir, in float thresh) { + vec3 orig=start; + float lastd = 1000.0; + const int count=${(options.maxSteps||80)}; + for (int i=0; i0.0) { + vec3 n = normalize(vec3( + product_len(d2[0]+h,d2[1],d2[2],b)-product_len(d2[0]-h,d2[1],d2[2],b), + product_len(d2[0],d2[1]+h,d2[2],b)-product_len(d2[0],d2[1]-h,d2[2],b), + product_len(d2[0],d2[1],d2[2]+h,b)-product_len(d2[0],d2[1],d2[2]-h,b) + )); + ${gl2?"gl_FragDepth":"gl_FragDepthEXT"} = dl2/50.0; + ${gl2?"col":"gl_FragColor"} = vec4(max(0.2,abs(dot(n,normalize(L-d2))))*color3 + pow(abs(dot(n,normalize(normalize(L-d2)+dir))),100.0),1.0); + } else discard; + }`),genprog2D = grade=>compile(`${gl2?"#version 300 es":""} + ${gl2?"in":"attribute"} vec4 position; ${gl2?"out":"varying"} vec4 Pos; uniform mat4 mv; uniform mat4 p; + void main() { Pos=mv*position; gl_Position = p*Pos; }`, + `${!gl2?"#extension GL_EXT_frag_depth : enable":"#version 300 es"} + precision highp float; + uniform vec3 color; uniform vec3 color2; + uniform vec3 color3; uniform float b[${counts[grade]}]; + uniform float ratio; ${gl2?"out vec4 col;":""} + ${gl2?"in":"varying"} vec4 Pos; + float product_len (in float z, in float y, in float x, in float[${counts[grade]}] b) { + ${this.nVector(1,[])[options.IPNS?"IPNS_GLSL":"OPNS_GLSL"](this.nVector(grade,[]), options.up)} + return sqrt(abs(sum)); + } + void main() { + vec3 p = -5.0*normalize(color2) -Pos[0]/5.0*color + color2 + vec3(0.0,Pos[1]/5.0*ratio,0.0); + float d2 = 1.0 - 150.0*pow(product_len( p[0]*5.0, p[1]*5.0, p[2]*5.0, b),2.0); + if (d2>0.0) { + ${gl2?"col":"gl_FragColor"} = vec4(color3,d2); + } else discard; + }`) + // canvas update will (re)render the content. + var armed=0; + canvas.update = (x)=>{ + // Start by updating canvas size if needed and viewport. + var s = getComputedStyle(canvas); if (s.width) { canvas.width = parseFloat(s.width)*(options.devicePixelRatio||devicePixelRatio||1); canvas.height = parseFloat(s.height)*(options.devicePixelRatio||devicePixelRatio||1); } + gl.viewport(0,0, canvas.width|0,canvas.height|0); var r=canvas.width/canvas.height; + // Defaults, resolve function input + var a,p=[],l=[],t=[],c=[.5,.5,.5],alpha=0,lastpos=[-2,2,0.2]; gl.clear(gl.COLOR_BUFFER_BIT+gl.DEPTH_BUFFER_BIT); while (x.call) x=x(); + // Loop over all items to render. + for (var i=0,ll=x.length;i>>24)&0xff)/255; c[0]=((e>>>16)&0xff)/255; c[1]=((e>>>8)&0xff)/255; c[2]=(e&0xff)/255; } + if (e instanceof Element){ + var tt = options.spin?-performance.now()*options.spin/1000:-options.h||0; tt+=Math.PI/2; var r = canvas.height/canvas.width; + var g=tot-1; while(!e[g]&&g>1) g--; + if (!programs[tot-1-g]) programs[tot-1-g] = (options.up.find(x=>x.match&&x.match("z")))?genprog(g):genprog2D(g); + gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + draw(programs[tot-1-g],gl.TRIANGLES,[-2,-2,0,-2,2,0,2,-2,0,-2,2,0,2,-2,0,2,2,0],[Math.cos(tt),0,-Math.sin(tt)],[Math.sin(tt),0,Math.cos(tt)],undefined,undefined,undefined,e,c,r,g); + gl.disable(gl.BLEND); + } + } + // if we're no longer in the page .. stop doing the work. + armed++; if (document.body.contains(canvas)) armed=0; if (armed==2) return; + canvas.value=x; if (options&&!options.animate) canvas.dispatchEvent(new CustomEvent('input')); + if (options&&options.animate) { requestAnimationFrame(canvas.update.bind(canvas,f,options)); } + if (options&&options.still) { canvas.value=x; canvas.dispatchEvent(new CustomEvent('input')); canvas.im.width=canvas.width; canvas.im.height=canvas.height; canvas.im.src = canvas.toDataURL(); } + } + // Basic mouse interactivity. needs more love. + var sel=-1; canvas.oncontextmenu = canvas.onmousedown = (e)=>{ e.preventDefault(); e.stopPropagation(); sel=-2; + var rc = canvas.getBoundingClientRect(), mx=(e.x-rc.left)/(rc.right-rc.left)*2-1, my=((e.y-rc.top)/(rc.bottom-rc.top)*-4+2)*canvas.height/canvas.width; + canvas.onwheel=e=>{e.preventDefault(); e.stopPropagation(); options.z = (options.z||5)+e.deltaY/100; if (!options.animate) requestAnimationFrame(canvas.update.bind(canvas,f,options));} + canvas.onmouseup=e=>sel=-1; canvas.onmouseleave=e=>sel=-1; + canvas.onmousemove=(e)=>{ + var rc = canvas.getBoundingClientRect(); + var mx =(e.movementX)/(rc.right-rc.left)*2, my=((e.movementY)/(rc.bottom-rc.top)*-2)*canvas.height/canvas.width; + if (sel==-2) { options.h = (options.h||0)+mx; if (!options.animate) requestAnimationFrame(canvas.update.bind(canvas,f,options)); return; }; if (sel < 0) return; + } + } + canvas.value = f.call?f():f; canvas.options = options; + if (options&&options.still) { + var i=new Image(); canvas.im = i; return requestAnimationFrame(canvas.update.bind(canvas,f,options)),i; + } else return requestAnimationFrame(canvas.update.bind(canvas,f,options)),canvas; + + } + + + // webGL Graphing function. (for parametric defined objects) + static graphGL(f,options) { + // Create a canvas, webgl2 context and set some default GL options. + var canvas=document.createElement('canvas'); canvas.style.width=options.width||''; canvas.style.height=options.height||''; canvas.style.backgroundColor='#EEE'; + if (options.width && options.width.match && options.width.match(/px/i)) canvas.width = parseFloat(options.width); if (options.height && options.height.match && options.height.match(/px/i)) canvas.height = parseFloat(options.height); + var gl=canvas.getContext('webgl',{alpha:options.alpha||false,antialias:true,preserveDrawingBuffer:options.still||true,powerPreference:'high-performance'}); + gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); if (!options.alpha) gl.clearColor(240/255,240/255,240/255,1.0); gl.getExtension("OES_standard_derivatives"); gl.va=gl.getExtension("OES_vertex_array_object"); + // Compile vertex and fragment shader, return program. + var compile=(vs,fs)=>{ + var s=[gl.VERTEX_SHADER,gl.FRAGMENT_SHADER].map((t,i)=>{ + var r=gl.createShader(t); gl.shaderSource(r,[vs,fs][i]); gl.compileShader(r); + return gl.getShaderParameter(r, gl.COMPILE_STATUS)&&r||console.error(gl.getShaderInfoLog(r)); + }); + var p = gl.createProgram(); gl.attachShader(p, s[0]); gl.attachShader(p, s[1]); gl.linkProgram(p); + gl.getProgramParameter(p, gl.LINK_STATUS)||console.error(gl.getProgramInfoLog(p)); + return p; + }; + // Create vertex array and buffers, upload vertices and optionally texture coordinates. + var createVA=function(vtx, texc, idx, clr) { + var r = gl.va.createVertexArrayOES(); gl.va.bindVertexArrayOES(r); + var b = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, b); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vtx), gl.STATIC_DRAW); + gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(0); + if (texc){ + var b2=gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, b2); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texc), gl.STATIC_DRAW); + gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(1); + } + if (clr){ + var b3=gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, b3); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(clr), gl.STATIC_DRAW); + gl.vertexAttribPointer(texc?2:1, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(texc?2:1); + } + if (idx) { + var b4=gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, b4); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(idx), gl.STATIC_DRAW); + } + return {r,b,b2,b4,b3} + }, + // Destroy Vertex array and delete buffers. + destroyVA=function(va) { + [va.b,va.b2,va.b4,va.b3].forEach(x=>{if(x) gl.deleteBuffer(x)}); if (va.r) gl.va.deleteVertexArrayOES(va.r); + } + // Default modelview matrix, convert camera to matrix (biquaternion->matrix) + var M=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,5,1], mtx = (x,iscam=true)=>{ var t=options.spin?performance.now()*options.spin/1000:-options.h||0, t2=options.p||0; + var ct = Math.cos(t), st= Math.sin(t), ct2 = Math.cos(t2), st2 = Math.sin(t2), xx=options.posx||0, y=options.posy||0, z=options.posz||0, zoom=options.z||5; + if (tot==5) return [ct,st*-st2,st*ct2,0,0,ct2,st2,0,-st,ct*-st2,ct*ct2,0,xx*ct+z*-st,y*ct2+(xx*st+z*ct)*-st2,y*st2+xx*st+z*ct*ct2+zoom,1]; + x=x.Normalized; var y=x.Mul(x.Dual),X=x.e23,Y=-x.e13,Z=-x.e12,W=x.s; + var xx = X*X, xy = X*Y, xz = X*Z, xw = X*W, yy = Y*Y, yz = Y*Z, yw = Y*W, zz = Z*Z, zw = Z*W; + var mtx = [ 1-2*(yy+zz), 2*(xy+zw), 2*(xz-yw), 0, 2*(xy-zw), 1-2*(xx+zz), 2*(yz+xw), 0, 2*(xz+yw), 2*(yz-xw), 1-2*(xx+yy), 0, -2*y.e23, -2*y.e13, 2*y.e12+(iscam?5:0), 1]; + return mtx; + } + // Render the given vertices. (autocreates/destroys vertex array if not supplied). + var draw=function(p, tp, vtx, color, color2, ratio, texc, va, cbuf, allowcull=true){ + gl.useProgram(p); gl.uniformMatrix4fv(gl.getUniformLocation(p, "mv"),false,M); + gl.uniformMatrix4fv(gl.getUniformLocation(p, "p"),false, [5,0,0,0,0,5*(ratio||2),0,0,0,0,1,2,0,0,-1,0]) + gl.uniform3fv(gl.getUniformLocation(p, "color"),new Float32Array(color)); + gl.uniform3fv(gl.getUniformLocation(p, "color2"),new Float32Array(color2)); + //if (texc) gl.uniform1i(gl.getAttribLocation(p, "texc"),0); + var v; if (!va) v = createVA(vtx, texc, undefined, cbuf, p); else gl.va.bindVertexArrayOES(va.r); + if (options.cull && allowcull) gl.enable(gl.CULL_FACE); + if (va && va.b4) { + gl.drawElements(tp, va.tcount, gl.UNSIGNED_SHORT, 0); + } else { + gl.drawArrays(tp, 0, (va&&va.tcount)||vtx.length/3); + } + if (v) destroyVA(v); + if (options.cull) gl.disable(gl.CULL_FACE); + } + // Program for the geometry. Derivative based normals. Basic lambert shading. + var program = compile(`attribute vec4 position; varying vec4 Pos; uniform mat4 mv; uniform mat4 p; + void main() { gl_PointSize=12.0; Pos=mv*position; gl_Position = p*Pos; }`, + `#extension GL_OES_standard_derivatives : enable + precision highp float; uniform vec3 color; uniform vec3 color2; varying vec4 Pos; + void main() { vec3 ldir = normalize(Pos.xyz - vec3(2.0,2.0,-4.0)); + vec3 normal = normalize(cross(dFdx(Pos.xyz), dFdy(Pos.xyz))); float l=dot(normal,ldir); + vec3 E = normalize(-Pos.xyz); vec3 R = normalize(reflect(ldir,normal)); + gl_FragColor = vec4(max(0.0,l)*color+vec3(0.5*pow(max(dot(R,E),0.0),20.0))+color2, 1.0); }`); + var programSphere = compile(`attribute vec4 position; varying vec4 Pos; varying vec3 N; uniform mat4 mv; uniform mat4 p; + void main() { gl_PointSize=12.0; Pos=mv*position; N = normalize(position.xzy); gl_Position = p*Pos; }`, + `#extension GL_OES_standard_derivatives : enable + precision highp float; uniform vec3 color; uniform vec3 color2; varying vec4 Pos; varying vec3 N; + void main() { vec3 ldir = normalize(Pos.xyz - vec3(2.0,2.0,-4.0)); + vec3 normal = N; float l=dot(normal,ldir); + vec3 E = normalize(-Pos.xyz); vec3 R = normalize(reflect(ldir,normal)); + gl_FragColor = vec4(max(0.0,l)*color+vec3(0.5*pow(max(dot(R,E),0.0),20.0))+color2, 1.0); }`); + var programPoint = compile(`attribute vec4 position; varying vec4 Pos; uniform mat4 mv; uniform mat4 p; + void main() { gl_PointSize=${((options.pointRadius||1)*(options.devicePixelRatio||devicePixelRatio||1)*8.0).toFixed(2)}; Pos=mv*position; gl_Position = p*Pos; }`, + `precision highp float; uniform vec3 color; uniform vec3 color2; varying vec4 Pos; + void main() { float distanceToCenter = length(gl_PointCoord - vec2(0.5)); if (distanceToCenter>0.5) discard; + gl_FragColor = vec4(color+color2, (distanceToCenter<0.5?1.0:0.0)); }`); + var programline = compile(` + attribute vec4 position; // current point. + attribute vec2 texc; // x = +w or -w, alternating. y = opacity. + attribute vec4 col; // next point. (extrapolated for end point) + uniform vec3 color; // r=aspect g=thickness + uniform mat4 mv,p; // modelview and projection matrix + varying vec2 tc; + void main() { + // Convert to clipspace. + vec4 cp = p*mv*vec4(position.xyz,1.0); + vec2 cs = cp.xy / abs(cp.w); + vec4 np = p*mv*vec4(col.xyz,1.0); + vec2 ns = np.xy / abs(np.w); + // compensate aspect + cs.x *= color.r; + ns.x *= color.r; + // clipspace line direction. + vec2 dir = normalize(cs-ns); + // Calculate screenspace normal. + vec2 normal = vec2( -dir.y, dir.x); + // Line scaling and aspect fix. + normal *= color.g * 5.0; + normal.x /= color.r; + // Pass through texture coordinates for edge softening + tc = vec2(texc.x / abs(texc.x), texc.y); + gl_Position = cp + vec4(normal*texc.x,0.0,0.0); + }`, + `precision highp float; + uniform vec3 color2; + varying vec2 tc; + void main() { +// gl_FragColor = vec4(abs(tc.x),abs(tc.x),abs(tc.x),1.0-abs(tc.x)); + gl_FragColor = vec4(color2,(1.0-pow(abs(tc.x),2.0))*tc.y); + }`); + var programcol = compile(`attribute vec4 position; attribute vec3 col; varying vec3 Col; varying vec4 Pos; uniform mat4 mv; uniform mat4 p; + void main() { gl_PointSize=6.0; Pos=mv*position; gl_Position = p*Pos; Col=col; }`, + `#extension GL_OES_standard_derivatives : enable + precision highp float; uniform vec3 color; uniform vec3 color2; varying vec4 Pos; varying vec3 Col; + void main() { vec3 ldir = normalize(Pos.xyz - vec3(1.0,1.0,2.0)); + vec3 normal = normalize(cross(dFdx(Pos.xyz), dFdy(Pos.xyz))); float l=dot(normal,ldir); + vec3 E = normalize(-Pos.xyz); vec3 R = normalize(reflect(ldir,normal)); + gl_FragColor = vec4(max(0.3,l)*Col+vec3(pow(max(dot(R,E),0.0),20.0))+color2, 1.0); ${options.shader||''} }`); + var programmot = compile(`attribute vec4 position; attribute vec2 texc; attribute vec3 col; varying vec3 Col; varying vec4 Pos; uniform mat4 mv; uniform mat4 p; uniform vec3 color2; + void main() { gl_PointSize=2.0; float blend=fract(color2.x+texc.r)*0.5; Pos=mv*(position*(1.0-blend) + (blend)*vec4(col,1.0)); gl_Position = p*Pos; Col=vec3(length(col-position.xyz)*1.); gl_PointSize = 8.0 - Col.x; Col.y=sin(blend*2.*3.1415); }`, + `precision highp float; uniform vec3 color; uniform vec3 color2; varying vec4 Pos; varying vec3 Col; + void main() { float distanceToCenter = length(gl_PointCoord - vec2(0.5));gl_FragColor = vec4(1.0-pow(Col.x,2.0),0.0,0.0,(.6-Col.x*0.05)*(distanceToCenter<0.5?1.0:0.0)*Col.y); }`); + gl.lineWidth(options.lineWidth||1); // doesn't work yet (nobody supports it) + // Create a font texture, lucida console or otherwise monospaced. + var fw=33, font = Object.assign(document.createElement('canvas'),{width:(19+94)*fw,height:48}), + ctx = Object.assign(font.getContext('2d'),{font:'bold 48px lucida console, monospace'}), + ftx = gl.createTexture(); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, ftx); + for (var i=33; i<127; i++) ctx.fillText(String.fromCharCode(i),(i-33)*fw,40); + var specialChars = "∞≅¹²³₀₁₂₃₄₅₆₇₈₉⋀⋁∆⋅"; specialChars.split('').forEach((x,i)=>ctx.fillText(x,(i-33+127)*fw,40)); + // 2.0 gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,94*fw,32,0,gl.RGBA,gl.UNSIGNED_BYTE,font); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, font); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + // Font rendering program. Renders billboarded fonts, transforms offset passed as color2. + var program2 = compile(`attribute vec4 position; attribute vec2 texc; varying vec2 tex; varying vec4 Pos; uniform mat4 mv; uniform mat4 p; uniform vec3 color2; + void main() { tex=texc; gl_PointSize=6.0; vec4 o=mv*vec4(color2,0.0); Pos=(-1.0/(o.z-mv[3][2]))*position+vec4(mv[3][0],mv[3][1],mv[3][2],0.0)+o; gl_Position = p*Pos; }`, + `precision highp float; uniform vec3 color; varying vec4 Pos; varying vec2 tex; + uniform sampler2D texm; void main() { vec4 c = texture2D(texm,tex); if (c.a<0.01) discard; gl_FragColor = vec4(color,c.a);}`); + // Helpers for line drawing. Convert line segments to triangles. + const line_to_tri = ([ax,ay,az,bx,by,bz]) => [ax,ay,az,ax,ay,az,bx,by,bz,bx,by,bz,ax,ay,az,bx,by,bz]; + const line_to_tri2 = ([ax,ay,az,bx,by,bz]) => [bx,by,bz,bx,by,bz,2*bx-ax,2*by-ay,2*bz-az,2*bx-ax,2*by-ay,2*bz-az,bx,by,bz,2*bx-ax,2*by-ay,2*bz-az]; + // Conformal space needs a bit extra magic to extract euclidean parametric representations. + if (tot==5 && options.conformal) var ni = Element.Coeff(4,1).Add(Element.Coeff(5,1)), no = Element.Coeff(4,0.5).Sub(Element.Coeff(5,0.5)); + var interprete = (x)=>{ + if (!(x instanceof Element)) return { tp:0 }; + if (options.ipns) x=x.Dual; + // tp = { 0:unknown 1:point 2:line, 3:plane, 4:circle, 5:sphere + var X2 = (x.Mul(x)).s, tp=0, weight2, opnix = ni.Wedge(x), ipnix = ni.LDot(x), + attitude, pos, normal, tg,btg,epsilon = 0.000001/(options.scale||1), I3=Element.Coeff(16,-1); + var x2zero = Math.abs(X2) < epsilon, ipnixzero = ipnix.VLength < epsilon, opnixzero = opnix.VLength < epsilon; + if (opnixzero && ipnixzero) { // free flat + } else if (opnixzero && !ipnixzero) { // bound flat (lines) + attitude = no.Wedge(ni).LDot(x); + weight2 = Math.abs(attitude.LDot(attitude).s)**.5; + pos = attitude.LDot(x.Reverse); //Inverse); + pos = [-pos.e15/pos.e45,-pos.e25/pos.e45,-pos.e34/pos.e45]; + if (x.Grade(3).VLength) { + normal = [attitude.e1/weight2,attitude.e2/weight2,attitude.e3/weight2]; tp=2; + } else if (x.Grade(2).VLength) { // point pair with ni + tp = 1; + } else { + normal = Element.LDot(Element.Mul(attitude,1/weight2),I3).Normalized; + var r=normal.Mul(Element.Coeff(3,1)); if (r[0]==-1) r[0]=1; else {r[0]+=1; r=r.Normalized;} + tg = [...r.Mul(Element.Coeff(1,1)).Mul(r.Conjugate)].slice(1,4); + btg = [...r.Mul(Element.Coeff(2,1)).Mul(r.Conjugate)].slice(1,4); + normal = [...normal.slice(1,4)]; tp=3; + } + } else if (!opnixzero && ipnixzero) { // dual bound flat + } else if (x2zero) { // bound vec,biv,tri (points) + if (options.ipns) x=x.Dual; + attitude = ni.Wedge(no).LDot(ni.Wedge(x)); + pos = [...(Element.LDot(1/(ni.LDot(x)).s,x)).slice(1,4)].map(x=>-x); + tp=1; + } else if (!x2zero) { // round (point pair,circle,sphere) + tp = x.Grade(3).VLength?4:x.Grade(2).VLength?6:5; + var nix = ni.Wedge(x), nix2 = (nix.Mul(nix)).s; + attitude = ni.Wedge(no).LDot(nix); + pos = [...(x.Mul(ni).Mul(x)).slice(1,4)].map(x=>-x/(2.0*nix2)); + weight2 = Math.abs((x.LDot(x)).s / nix2)**.5; + if (tp==4) { + if (x.LDot(x).s < 0) { weight2 = -weight2; } + normal = Element.LDot(Element.Mul(attitude,1/weight2),I3).Normalized; + var r=normal.Mul(Element.Coeff(3,1)); if (r[0]==-1) r[0]=1; else {r[0]+=1; r=r.Normalized;} + tg = [...r.Mul(Element.Coeff(1,1)).Mul(r.Conjugate)].slice(1,4); + btg = [...r.Mul(Element.Coeff(2,1)).Mul(r.Conjugate)].slice(1,4); + normal = [...normal.slice(1,4)]; + } else if (tp==6) { + weight2 = (x.LDot(x).s < 0)?-(weight2):weight2; + normal = Element.Mul(attitude.Normalized,weight2).slice(1,4); + } else { + normal = [...((Element.LDot(Element.Mul(attitude,1/weight2),I3)).Normalized).slice(1,4)]; + } + } + return {tp,pos:pos?pos.map(x=>x*(options.scale||1)):[0,0,0],normal,tg,btg,weight2:weight2*(options.scale||1)} + }; + // canvas update will (re)render the content. + var armed=0,sphere,e14 = Element.Coeff(14,1); + canvas.update = (x)=>{ + if (!canvas.parentElement) return; + // restore from still.. + if (options && !options.still && canvas.im && canvas.im.parentElement) { canvas.im.parentElement.insertBefore(canvas,canvas.im); canvas.im.parentElement.removeChild(canvas.im); } + // Start by updating canvas size if needed and viewport. + var s = getComputedStyle(canvas); if (s.width) { canvas.width = parseFloat(s.width)*(options.devicePixelRatio||devicePixelRatio||1); canvas.height = parseFloat(s.height)*(options.devicePixelRatio||devicePixelRatio||1); } + gl.viewport(0,0, canvas.width|0,canvas.height|0); var r=canvas.width/canvas.height; + // Defaults, resolve function input + var a,p=[],l=[],t=[],c=[.5,.5,.5],alpha=0,lastpos=[-1.95,1.5,0,1]; gl.clear(gl.COLOR_BUFFER_BIT+gl.DEPTH_BUFFER_BIT); while (x.call) x=x(); + // Create default camera matrix and initial lastposition (contra-compensated for camera) + M = mtx(options.camera); + var a = new this(); a.set([1,-2,1.90*canvas.height/canvas.width,0],1); a = options.camera.Conjugate.Mul(a.Dual).Mul(options.camera); + lastpos = a.slice(11,14).map((y,i)=>(i<=1?1:-1)*y/a[14]).reverse(); + var linediff = new this(); linediff.set([0,0,-0.12*2000/canvas.width*(options.fontSize||1),0],1); + linediff = options.camera.Conjugate.Mul(linediff.Dual).Mul(options.camera).slice(11,14).map((y,i)=>(i<=1?1:-1)*y/a[14]).reverse(); + // Grid. + if (options.grid) { + const gr = options.gridSize||1; + if (!options.gridLines) { options.gridLines=[[],[],[]]; for (var i=-gr; i<=gr; i+=gr/10) { + options.gridLines[0].push(i,-gr,gr, i,-gr,-gr, gr,-gr,i, -gr,-gr,i); + options.gridLines[1].push(i,gr,gr, i,-gr,gr, gr,i,gr, -gr,i,gr); + options.gridLines[2].push(-gr,i,gr, -gr,i,-gr, -gr,gr,i, -gr,-gr,i); + }} + var ltest = [], ltest2 = [], ttest = []; for (var j=0; j<3; ++j) for (var i=0; i{ while (x.call) x=x.call(); x=interprete(x);l.push.apply(l,x.pos); }); var d = {tp:-1}; } + else if (e instanceof Array && e.length==3) { e.forEach(x=>{ while (x.call) x=x.call(); x=interprete(x);t.push.apply(t,x.pos); }); var d = {tp:-1}; } + else var d = interprete(e); + if (d.tp) lastpos=d.pos; + if (d.tp==1) p.push.apply(p,d.pos); + if (d.tp==2) { l.push.apply(l,d.pos.map((x,i)=>x-d.normal[i]*3)); l.push.apply(l,d.pos.map((x,i)=>x+d.normal[i]*3)); } + if (d.tp==3) { t.push.apply(t,d.pos.map((x,i)=>x+d.tg[i]+d.btg[i])); t.push.apply(t,d.pos.map((x,i)=>x-d.tg[i]+d.btg[i])); t.push.apply(t,d.pos.map((x,i)=>x+d.tg[i]-d.btg[i])); + t.push.apply(t,d.pos.map((x,i)=>x-d.tg[i]+d.btg[i])); t.push.apply(t,d.pos.map((x,i)=>x+d.tg[i]-d.btg[i])); t.push.apply(t,d.pos.map((x,i)=>x-d.tg[i]-d.btg[i])); } + if (d.tp==4) { + var ne=0,la=0; + if (d.weight2<0) { c[0]=1;c[1]=0;c[2]=0; } + for (var j=0; j<65; j++) { + ne = d.pos.map((x,i)=>x+Math.cos(j/32*Math.PI)*d.weight2*d.tg[i]+Math.sin(j/32*Math.PI)*d.weight2*d.btg[i]); if (ne&&la&&(d.weight2>0||j%2==0)) { l.push.apply(l,la); l.push.apply(l,ne); }; la=ne; + } + } + if (d.tp==6) { + if (d.weight2<0) { c[0]=1;c[1]=0;c[2]=0; } + if (options.useUnnaturalLineDisplayForPointPairs) { + l.push.apply(l,d.pos.map((x,i)=>x-d.normal[i]*(options.scale||1))); + l.push.apply(l,d.pos.map((x,i)=>x+d.normal[i]*(options.scale||1))); + } + p.push.apply(p,d.pos.map((x,i)=>x-d.normal[i]*(options.scale||1))); + p.push.apply(p,d.pos.map((x,i)=>x+d.normal[i]*(options.scale||1))); + } + if (d.tp==5) { + if (!sphere) { + var pnts = [], tris=[], S=Math.sin, C=Math.cos, pi=Math.PI, W=96, H=48; + for (var j=0; jx.s); + gl.enable(gl.BLEND); gl.blendFunc(gl.CONSTANT_ALPHA, gl.ONE_MINUS_CONSTANT_ALPHA); gl.blendColor(1,1,1,1-(alpha||0.1)); gl.enable(gl.CULL_FACE) + draw(programSphere,gl.TRIANGLES,undefined,c,[0,0,0],r,undefined,sphere.va); + gl.disable(gl.BLEND); gl.disable(gl.CULL_FACE); + M = oldM; + } + if (i==ll-1 || d.tp==0) { + // render triangles, lines, points. + if (alpha) { gl.enable(gl.BLEND); gl.blendFunc(gl.CONSTANT_ALPHA, gl.ONE_MINUS_CONSTANT_ALPHA); gl.blendColor(1,1,1,1-alpha); } + if (t.length) { draw(program,gl.TRIANGLES,t,c,[0,0,0],r); t.forEach((x,i)=>{ if (i%9==0) lastpos=[0,0,0]; lastpos[i%3]+=x/3; }); t=[]; } + if (l.length) { + var ltest = [], ltest2 = [], ttest = []; for (var li=0; lix%(countx*county))); e.va3.tcount = (countx-1)*county*2*3; + } + if ( e.call && e.length==1 && !e.va2) { var countx=e.dx||256; + var temp=new Float32Array(3*countx),o=new Float32Array(3),et=[]; + for (var ii=0; ii{ + if (e instanceof Array && e.length==3) { tc++; e.forEach(x=>{ while (x.call) x=x.call(); x=interprete(x);et3.push.apply(et3,x.pos); }); var d = {tp:-1}; } + else { + var d = interprete(e); + if (d.tp==1) { pc++; et.push(...d.pos); } + if (d.tp==2) { lc++; et2.push(...d.pos.map((x,i)=>x-d.normal[i]*10),...d.pos.map((x,i)=>x+d.normal[i]*10)); } + } + }); + e.va = createVA(et,undefined); e.va.tcount = pc; + e.va2 = createVA(et2,undefined); e.va2.tcount = lc*2; + e.va3 = createVA(et3,undefined); e.va3.tcount = tc*3; + } + // render the vertex array. + if (e.va && e.va.tcount) { gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); draw(programPoint,gl.POINTS,undefined,[0,0,0],c,r,undefined,e.va); gl.disable(gl.BLEND); }; + if (e.va2 && e.va2.tcount) draw(program,gl.LINES,undefined,[0,0,0],c,r,undefined,e.va2); + if (e.va3 && e.va3.tcount) draw(program,gl.TRIANGLES,undefined,c,[0,0,0],r,undefined,e.va3); + } + if (alpha) gl.disable(gl.BLEND); // no alpha for text printing. + // setup a new color + if (typeof e == "number") { alpha=((e>>>24)&0xff)/255; c[0]=((e>>>16)&0xff)/255; c[1]=((e>>>8)&0xff)/255; c[2]=(e&0xff)/255; } + if (typeof(e)=='string') { + if (options.htmlText) { + if (!x['_'+i]) { console.log('creating div'); Object.defineProperty(x,'_'+i, {value: document.body.appendChild(document.createElement('div')), enumerable:false }) }; + var rc = canvas.getBoundingClientRect(), div = x['_'+i]; + var pos2 = Element.Mul( [[M[0],M[4],M[8],M[12]],[M[1],M[5],M[9],M[13]],[M[2],M[6],M[10],M[14]],[M[3],M[7],M[11],M[15]]], [...lastpos,1]).map(x=>x.s); + pos2 = Element.Mul( [[5,0,0,0],[0,5*(r||2),0,0],[0,0,1,-1],[0,0,2,0]], pos2).map(x=>x.s).map((x,i,a)=>x/a[3]); + Object.assign(div.style,{position:'fixed',pointerEvents:'none',left:rc.left + (rc.right-rc.left)*(pos2[0]/2+0.5),top: rc.top + (rc.bottom-rc.top)*(-pos2[1]/2+0.5) - 20}); + if (div.last != e) { div.innerHTML = e; div.last = e; if (self.renderMathInElement) self.renderMathInElement(div); } + } else { + gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA); + var fw = 113, mapChar = (x)=>{ var c = x.charCodeAt(0)-33; if (c>=94) c = 94+specialChars.indexOf(x); return c/fw; } + draw(program2,gl.TRIANGLES, + [...Array(e.length*6*3)].map((x,i)=>{ var x=0,z=-0.2, o=x+(i/18|0)*1.1; return (0.05*(options.z||5))*[o,-1,z,o+1.2,-1,z,o,1,z,o+1.2,-1,z,o+1.2,1,z,o,1,z][i%18]}),c,lastpos,r, + [...Array(e.length*6*2)].map((x,i)=>{ var o=mapChar(e[i/12|0]); return [o,1,o+1/fw,1,o,0,o+1/fw,1,o+1/fw,0,o,0][i%12]})); gl.disable(gl.BLEND); lastpos[1]+=linediff[1]; lastpos[0]+=linediff[0]; lastpos[2]+=linediff[2]; + } + } + } + continue; + } + // PGA + if (options.dual && e instanceof Element) e = e.Dual; + // Convert planes to polygons. + if (e instanceof Element && e.Grade(1).Length > 0.001) { + var m = Element.Add(1, Element.Mul(e.Normalized, Element.Coeff(3,1))).Normalized, e0 = 0; + e=Element.sw(m,[[-1,-1],[-1,1],[1,1],[-1,-1],[1,1],[1,-1]].map(([x,z])=>Element.Trivector(x*e.Length,e0,z*e.Length,1))); + } + // Convert lines to line segments. + if (e instanceof Element && e.Grade(2).Length) + e=[e.LDot(e14).Wedge(e).Add(e.Wedge(Element.Coeff(1,1)).Mul(Element.Coeff(0,-(options.clip||3)))),e.LDot(e14).Wedge(e).Add(e.Wedge(Element.Coeff(1,1)).Mul(Element.Coeff(0,options.clip||3)))] + .map(x=>x[14]<0?x.Scale(-1):x); + // If euclidean point, store as point, store line segments and triangles. + if (e.e123) p.push.apply(p,e.slice(11,14).map((y,i)=>(i<=1?1:-1)*y/e[14]).reverse()); + if (e instanceof Array && e.length==2) l=l.concat.apply(l,e.map(x=>[...x.slice(11,14).map((y,i)=>(i<=1?1:-1)*y/x[14]).reverse()])); + if (e instanceof Array && e.length%3==0) t=t.concat.apply(t,e.map(x=>[...x.slice(11,14).map((y,i)=>(i<=1?1:-1)*y/x[14]).reverse()])); + // Render orbits of parametrised motors, as well as lists of points.. + function sw_mot_orig(A,R){ + var a0=A[0],a1=A[5],a2=A[6],a3=A[7],a4=A[8],a5=A[9],a6=A[10],a7=A[15]; + R[2] = -2*(a0*a3+a4*a7-a6*a2-a5*a1); + R[1] = -2*(a4*a1-a0*a2-a6*a3+a5*a7); + R[0] = 2*(a0*a1+a4*a2+a5*a3+a6*a7); + return R + } + if ( e.call && e.length==1) { var count=e.dx||64; + for (var ismot,xx,o=new Float32Array(3),ii=0; ii1) l.push(xx[0],xx[1],xx[2]); + var m = e(ii/(count-1)); + if (ii==0) ismot = m[0]||m[5]||m[6]||m[7]||m[8]||m[9]||m[10]; + xx = ismot?sw_mot_orig(m,o):m.slice(11,14).map((y,i)=>(i<=1?1:-1)*y).reverse(); //Element.sw(e(ii/(count-1)),o); + l.push(xx[0],xx[1],xx[2]); + } + } + if ( e.call && e.length==2 && !e.va) { var countx=e.dx||64,county=e.dy||32; + var temp=new Float32Array(3*countx*county),o=new Float32Array(3),et=[]; + for (var pp=0,ii=0; iix%(countx*county))); e.va.tcount = (countx-1)*county*2*3; + } + // Experimental display of motors using particle systems. + if (e instanceof Object && e.motor) { + if (!e.va || e.recalc) { + var seed = 1; function random() { var x = Math.sin(seed++) * 10000; return x - Math.floor(x); } + e.xRange = e.xRange === undefined ? 1:e.xRange; e.yRange = e.yRange === undefined ? 1:e.yRange; e.zRange = e.zRange === undefined ? 1:e.zRange; + var vtx=[], tx=[], vtx2=[]; + for (var i=0; i<(e.zRange===0?5000:60000); i++) { + var p = Element.Trivector(random()*(2*e.xRange)-e.xRange,random()*2*e.yRange-e.yRange,random()*2*e.zRange-e.zRange,1); +// var p2 = Element.sw(e.motor,p); + var p2 = e.motor.Mul(p).Mul(e.motor.Inverse); + tx.push(random(), random()); + vtx.push(...p.slice(11,14).reverse()); vtx2.push(...p2.slice(11,14).reverse()); + } + e.va = createVA(vtx,tx,undefined,vtx2); e.va.tcount = vtx.length/3; + e.recalc = false; + } + var time = performance.now()/1000; + gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); gl.disable(gl.DEPTH_TEST); + draw(programmot, gl.POINTS,t,c,[time%1,0,0],r,undefined,e.va); + gl.disable(gl.BLEND); gl.enable(gl.DEPTH_TEST); + } + // we could also be an object with cached vertex array of triangles .. + else if (e.va || (e instanceof Object && e.data)) { + // Create the vertex array and store it for re-use. + if (!e.va) { + if (e.idx) { + var et = e.data.map(x=>[...x.slice(11,14).map((y,i)=>(i<=1?1:-1)*y/x[14]).reverse()]).flat(); + } else { + var et=[]; e.data.forEach(e=>{if (e instanceof Array && e.length==3) et=et.concat.apply(et,e.map(x=>[...x.slice(11,14).map((y,i)=>(i<=1?1:-1)*y/x[14]).reverse()]));}); + } + e.va = createVA(et,undefined,e.idx,e.color?new Float32Array(e.color):undefined); e.va.tcount = (e.idx && e.idx.length)?e.idx.length:e.data.length*3; + } + // render the vertex array. + var M5 = Element.Scalar(1).Add(Element.Coeff(7,2.5)); + if (e.transform) { + var M1 = mtx(e.transform, false); + var M2 = mtx(M5.Mul(options.camera), false); + M = Array(16).fill(0); + for (var ii=0; ii<4; ++ii) for (var jj=0; jj<4; ++jj) for (var kk=0; kk<4; ++kk) M[ii*4+kk] += M1[ii*4+jj] * M2[jj*4+kk]; + } + if (alpha) { gl.enable(gl.BLEND); gl.blendFunc(gl.CONSTANT_ALPHA, gl.ONE_MINUS_CONSTANT_ALPHA); gl.blendColor(1,1,1,1-alpha); } + draw(e.color?programcol:program,gl.TRIANGLES,t,c,[0,0,0],r,undefined,e.va); + if (alpha) gl.disable(gl.BLEND); + if (e.transform) { M=mtx(options.camera); } + } + // if we're a number (color), label or the last item, we output the collected items. + else if (typeof e=='number' || i==ll-1 || typeof e == 'string') { + // render triangles, lines, points. + if (alpha) { gl.enable(gl.BLEND); gl.blendFunc(gl.CONSTANT_ALPHA, gl.ONE_MINUS_CONSTANT_ALPHA); gl.blendColor(1,1,1,1-alpha); } + if (t.length) { draw(program,gl.TRIANGLES,t,c,[0,0,0],r); t.forEach((x,i)=>{ if (i%9==0) lastpos=[0,0,0]; lastpos[i%3]+=x/3; }); t=[]; } + if (l.length) { + var ltest = [], ltest2 = [], ttest = [], w = (options.lineWidth||1); for (var li=0; li>>24)&0xff)/255; c[0]=((e>>>16)&0xff)/255; c[1]=((e>>>8)&0xff)/255; c[2]=(e&0xff)/255; } + // render a label + if (typeof(e)=='string') { + if (options.htmlText) { + if (!canvas['_'+i]) { console.log('creating div'); Object.defineProperty(canvas,'_'+i, {value: document.body.appendChild(document.createElement('div')), enumerable:false }) }; + var rc = canvas.getBoundingClientRect(), div = canvas['_'+i]; + var pos2 = Element.Mul( [[M[0],M[4],M[8],M[12]],[M[1],M[5],M[9],M[13]],[M[2],M[6],M[10],M[14]],[M[3],M[7],M[11],M[15]]], [...lastpos,1]).map(x=>x.s); + pos2 = Element.Mul( [[5,0,0,0],[0,5*(r||2),0,0],[0,0,1,-1],[0,0,2,0]], pos2).map(x=>x.s).map((x,i,a)=>x/a[3]); + Object.assign(div.style,{position:'fixed',pointerEvents:'none',left:rc.left + (rc.right-rc.left)*(pos2[0]/2+0.5),top: rc.top + (rc.bottom-rc.top)*(-pos2[1]/2+0.5) - 20}); + if (div.last != e) { div.innerHTML = e; div.last = e; if (self.renderMathInElement) self.renderMathInElement(div,{output:'html'}); } + } else { + gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA); gl.disable(gl.DEPTH_TEST); + var fw = 113, mapChar = (x)=>{ var c = x.charCodeAt(0)-33; if (c>=94) c = 94+specialChars.indexOf(x); return c/fw; } + draw(program2,gl.TRIANGLES, + [...Array(e.length*6*3)].map((x,i)=>{ var x=0,z=0.2, o=x+(i/18|0)*1.1; return 0.2*(options.fontSize||1)*2000/canvas.width*[o,-1,z,o+1.2,-1,z,o,1,z,o+1.2,-1,z,o+1.2,1,z,o,1,z][i%18]}),c,lastpos,r, + [...Array(e.length*6*2)].map((x,i)=>{ var o=mapChar(e[i/12|0]); return [o,1,o+1/fw,1,o,0,o+1/fw,1,o+1/fw,0,o,0][i%12]})); gl.disable(gl.BLEND); lastpos[0] += linediff[0];lastpos[1] += linediff[1];lastpos[2] += linediff[2]; + if (!options.noZ) gl.enable(gl.DEPTH_TEST); + } + } + } + }; + // if we're no longer in the page .. stop doing the work. + armed++; if (document.body.contains(canvas)) armed=0; if (armed==2) return; + canvas.value=x; if (options&&!options.animate) canvas.dispatchEvent(new CustomEvent('input')); canvas.options=options; + if (options&&options.animate) { requestAnimationFrame(canvas.update.bind(canvas,f,options)); } + if (options&&options.still) { canvas.value=x; canvas.dispatchEvent(new CustomEvent('input')); canvas.im.style.width=canvas.style.width; canvas.im.style.height=canvas.style.height; canvas.im.src = canvas.toDataURL(); + var p=canvas.parentElement; if (p) { p.insertBefore(canvas.im,canvas); p.removeChild(canvas); } + } + } + // Basic mouse interactivity. needs more love. + var sel=-1; canvas.oncontextmenu = canvas.onmousedown = (e)=>{e.preventDefault(); e.stopPropagation(); if (e.detail===0) return; + var rc = canvas.getBoundingClientRect(), mx=(e.x-rc.left)/(rc.right-rc.left)*2-1, my=((e.y-rc.top)/(rc.bottom-rc.top)*4-2)*canvas.height/canvas.width; + sel = (e.button==2)?-3:-2; canvas.value.forEach((x,i)=>{ + if (tot != 5) { if (x[14]) { + var pos2 = Element.Mul( [[M[0],M[4],M[8],M[12]],[M[1],M[5],M[9],M[13]],[M[2],M[6],M[10],M[14]],[M[3],M[7],M[11],M[15]]], [-x[13]/x[14],x[12]/x[14],x[11]/x[14],1]).map(x=>x.s); + pos2 = Element.Mul( [[5,0,0,0],[0,-5*(2),0,0],[0,0,1,-1],[0,0,2,0]], pos2).map(x=>x.s).map((x,i,a)=>x/a[3]); + if ((mx-pos2[0])**2 + ((my)-pos2[1])**2 < 0.001) sel=i; + }} else { + x = interprete(x); if (x.tp==1) { + var pos2 = Element.Mul( [[M[0],M[4],M[8],M[12]],[M[1],M[5],M[9],M[13]],[M[2],M[6],M[10],M[14]],[M[3],M[7],M[11],M[15]]], [...x.pos,1]).map(x=>x.s); + pos2 = Element.Mul( [[5,0,0,0],[0,5*(r||2),0,0],[0,0,1,-1],[0,0,2,0]], pos2).map(x=>x.s).map((x,i,a)=>x/a[3]); + if ((mx-pos2[0])**2 + ((-my)-pos2[1])**2 < 0.01) sel=i; + } + } + }); + canvas.onwheel=e=>{e.preventDefault(); e.stopPropagation(); options.z = (options.z||5)+e.deltaY/100; if (!options.animate) requestAnimationFrame(canvas.update.bind(canvas,f,options));} + canvas.onmouseup=e=>sel=-1; canvas.onmouseleave=e=>sel=-1; + var tx,ty; canvas.ontouchstart = (e)=>{e.preventDefault(); canvas.focus(); var x = e.changedTouches[0].pageX, y = e.changedTouches[0].pageY; tx=x; ty=y; } + canvas.ontouchmove = function (e) { e.preventDefault(); + var x = e.changedTouches[0].pageX, y = e.changedTouches[0].pageY, mx = (x-(tx||x))/1000, my = -(y-(ty||y))/1000; tx=x; ty=y; + options.h = (options.h||0)+mx; options.p = Math.max(-Math.PI/2,Math.min(Math.PI/2, (options.p||0)+my)); if (!options.animate) requestAnimationFrame(canvas.update.bind(canvas,f,options)); return; + }; + canvas.onmousemove=(e)=>{ + var rc = canvas.getBoundingClientRect(),x; if (sel>=0) { if (tot==5) x=interprete(canvas.value[sel]); else { x=canvas.value[sel]; x={pos:[-x[13]/x[14],-x[12]/x[14],x[11]/x[14]]}; }} + var mx =(e.movementX)/(rc.right-rc.left)*2, my=((e.movementY)/(rc.bottom-rc.top)*2)*canvas.height/canvas.width; + if (sel==-2) { options.h = (options.h||0)+(options.conformal?-1:1)*mx/2; options.p = Math.max(-Math.PI/2,Math.min(Math.PI/2, (options.p||0)-my/2)); if (options.camera) options.camera.set( ( Element.Bivector(0,0,0,0,0,options.p).Exp() ).Mul( Element.Bivector(0,0,0,0,options.h,0).Exp() )); if (!options.animate) requestAnimationFrame(canvas.update.bind(canvas,f,options)); return; }; + if (sel==-3) { var ct = Math.cos(options.h||0), st= Math.sin(options.h||0), ct2 = Math.cos(options.p||0), st2 = Math.sin(options.p||0); + if (e.shiftKey) { options.posy = (options.posy||0)+my; } else { options.posx = (options.posx||0)+mx*ct+my*st; options.posz = (options.posz||0)+mx*-st+my*ct*ct2; } if (!options.animate) requestAnimationFrame(canvas.update.bind(canvas,f,options));return; }; if (sel < 0) return; + if (tot==5) { + x.pos[0] += (e.buttons!=2)?Math.cos((options.h||0))*mx:Math.sin(-(options.h||0))*-my; x.pos[1]+=(e.buttons!=2)?-my:0; x.pos[2]+=(e.buttons!=2)?Math.sin((options.h||0))*mx:Math.cos(-(options.h||0))*-my; + canvas.value[sel].set(Element.Mul(ni,(x.pos[0]**2+x.pos[1]**2+x.pos[2]**2)*0.5).Sub(no)); canvas.value[sel].set(x.pos,1); } + else if (x) { + var [cw,ch] = [rc.width, rc.height]; + var ox = (1/(options.scale || 1)) * ((e.offsetX / cw) - 0.5); + var oy = (1/(options.scale || 1)) * ((e.offsetY / ch) - 0.5) * (ch/cw); + var tb = Element.sw(options.camera,canvas.value[sel]); + var z = -(tb.e012/tb.e123+5)/5*4; tb.e023 = ox*z*tb.e123; tb.e013 = oy*z*tb.e123; + canvas.value[sel].set(Element.sw(options.camera.Reverse, tb)); + } + if (!options.animate) requestAnimationFrame(canvas.update.bind(canvas,f,options)); + } + } + canvas.value = f.call?f():f; canvas.options=options; + if (options&&options.still) { + var i=new Image(); canvas.im = i; return requestAnimationFrame(canvas.update.bind(canvas,f,options)),canvas; + } else return requestAnimationFrame(canvas.update.bind(canvas,f,options)),canvas; + } + + // The inline function is a js to js translator that adds operator overloading and algebraic literals. + // It can be called with a function, a string, or used as a template function. + static inline(intxt) { + // If we are called as a template function. + if (arguments.length>1 || intxt instanceof Array) { + var args=[].slice.call(arguments,1); + return res.inline(new Function(args.map((x,i)=>'_template_'+i).join(),'return ('+intxt.map((x,i)=>(x||'')+(args[i]&&('_template_'+i)||'')).join('')+')')).apply(res,args); + } + // Get the source input text. + var txt = (intxt instanceof Function)?intxt.toString():`function(){return (${intxt})}`; + // Our tokenizer reads the text token by token and stores it in the tok array (as type/token tuples). + var tok = [], resi=[], t, possibleRegex=false, c, tokens = [/^[\s\uFFFF]|^[\u000A\u000D\u2028\u2029]|^\/\/[^\n]*\n|^\/\*[\s\S]*?\*\//g, // 0: whitespace/comments + /^\"\"|^\'\'|^\".*?[^\\]\"|^\'.*?[^\\]\'|^\`[\s\S]*?[^\\]\`/g, // 1: literal strings + /^\d+[.]{0,1}\d*[ei][\+\-_]{0,1}\d*|^\.\d+[ei][\+\-_]{0,1}\d*|^e_\d*/g, // 2: literal numbers in scientific notation (with small hack for i and e_ asciimath) + /^\d+[.]{0,1}\d*[E][+-]{0,1}\d*|^\.\d+[E][+-]{0,1}\d*|^0x\d+|^\d+[.]{0,1}\d*|^\.\d+/g, // 3: literal hex, nonsci numbers + /^\/.*?[^\\]\/[gmisuy]?/g, // 4: regex + /^(\.Normalized|\.Length|\.\.\.|>>>=|===|!==|>>>|<<=|>>=|=>|\|\||[<>\+\-\*%&|^\/!\=]=|\*\*|\+\+|\-\-|<<|>>|\&\&|\^\^|^[{}()\[\];.,<>\+\-\*%|&^!~?:=\/]{1})/g, // 5: punctuator + /^[$_\p{L}][$_\p{L}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\u200C\u200D]*/gu] // 6: identifier + while (txt.length) for (t in tokens) { + if (t == 4 && !possibleRegex) continue; + if (resi = txt.match(tokens[t])) { + c = resi[0]; if (t!=0) {possibleRegex = c == '(' || c == '=' || c == '[' || c == ',' || c == ';';} tok.push([t | 0, c]); txt = txt.slice(c.length); break; + }} // tokenise + // Translate algebraic literals. (scientific e-notation to "this.Coeff" + tok=tok.map(t=>(t[0]==2)?[2,'Element.Coeff('+basis.indexOf((!options.Cayley?simplify:(x)=>x)('e'+t[1].split(/e_|e|i/)[1]||1).replace('-',''))+','+(simplify(t[1].split(/e_|e|i/)[1]||1).match('-')?"-1*":"")+parseFloat(t[1][0]=='e'?1:t[1].split(/e_|e|i/)[0])+')']:t); + // String templates (limited support - needs fundamental changes.). + tok=tok.map(t=>(t[0]==1 && t[1][0]=='`')?[1,t[1].replace(/\$\{(.*?)\}/g,a=>"${"+Element.inline(a.slice(2,-1)).toString().match(/return \((.*)\)/)[1]+"}")]:t); + // We support two syntaxes, standard js or if you pass in a text, asciimath. + var syntax = (intxt instanceof Function)?[[['.Normalized','Normalize',2],['.Length','Length',2]],[['~','Conjugate',1],['!','Dual',1]],[['**','Pow',0,1]],[['^','Wedge'],['&','Vee'],['<<','LDot']],[['*','Mul'],['/','Div']],[['|','Dot']],[['>>>','sw',0,1]],[['-','Sub'],['+','Add']],[['%','%']],[['==','eq'],['!=','neq'],['<','lt'],['>','gt'],['<=','lte'],['>=','gte']]] + :[[['pi','Math.PI'],['sin','Math.sin']],[['ddot','this.Reverse'],['tilde','this.Involute'],['hat','this.Conjugate'],['bar','this.Dual']],[['^','Pow',0,1]],[['^^','Wedge'],['*','LDot']],[['**','Mul'],['/','Div']],[['-','Sub'],['+','Add']],[['<','lt'],['>','gt'],['<=','lte'],['>=','gte']]]; + // For asciimath, some fixed translations apply (like pi->Math.PI) etc .. + tok=tok.map(t=>(t[0]!=6)?t:[].concat.apply([],syntax).filter(x=>x[0]==t[1]).length?[6,[].concat.apply([],syntax).filter(x=>x[0]==t[1])[0][1]]:t); + // Now the token-stream is translated recursively. + function translate(tokens) { + // helpers : first token to the left of x that is not of a type in the skip list. + var left = (x=ti-1,skip=[0])=>{ while(x>=0&&~skip.indexOf(tokens[x][0])) x--; return x; }, + // first token to the right of x that is not of a type in the skip list. + right= (x=ti+1,skip=[0])=>{ while(x{tokens.splice(x,y-x+1,[tp,...(sub||tokens.slice(x,y+1))])}, + // match O-C pairs. returns the 'matching bracket' position + match = (O="(",C=")")=>{var o=1,x=ti+1; while(o){if(tokens[x][1]==O)o++;if(tokens[x][1]==C)o--; x++;}; return x-1;}; + // grouping (resolving brackets). + for (var ti=0,t,si;t=tokens[ti];ti++) if (t[1]=="(") glue(ti,si=match(),7,[[5,"("],...translate(tokens.slice(ti+1,si)),[5,")"]]); + // [] dot call and new + for (var ti=0,t,si; t=tokens[ti];ti++) { + if (t[1]=="[") { glue(ti,si=match("[","]"),7,[[5,"["],...translate(tokens.slice(ti+1,si)),[5,"]"]]); if (ti)ti--;} // matching [] + else if (t[1]==".") { glue(left(),right()); ti--; } // dot operator + else if (t[0]==7 && ti && left()>=0 && tokens[left()][0]>=6 && tokens[left()][1]!="return") { glue(left(),ti--) } // collate ( and [ + else if (t[1]=='new') { glue(ti,right()) }; // collate new keyword + } + // ++ and -- + for (var ti=0,t; t=tokens[ti];ti++) if (t[1]=="++" || t[1]=="--") glue(left(),ti); + // unary - and + are handled separately from syntax .. + for (var ti=0,t,si; t=tokens[ti];ti++) + if (t[1]=="-" && (left()<0 || (tokens[left()]||[])[1]=='return'||(tokens[left()]||[5])[0]==5)) glue(ti,right(),6,["Element.Sub(",tokens[right()],")"]); // unary minus works on all types. + else if (t[1]=="+" && (left()<0 || (tokens[left()]||[])[1]=='return'|| (tokens[left()]||[0])[0]==5 && (tokens[left()]||[0])[1][0]!=".")) glue(ti,ti+1); // unary plus is glued, only on scalars. + // now process all operators in the syntax list .. + for (var si=0,s; s=syntax[si]; si++) for (var ti=s[0][3]?tokens.length-1:0,t; t=tokens[ti];s[0][3]?ti--:ti++) for (var opi=0,op; op=s[opi]; opi++) if (t[1]==op[0]) { + // exception case .. ".Normalized" and ".Length" properties are re-routed (so they work on scalars etc ..) + if (op[2]==2) { var arg=tokens[left()]; glue(ti-1,ti,6,["Element."+op[1],"(",arg,")"]); } + // unary operators (all are to the left) + else if (op[2]) { var arg=tokens[right()]; glue(ti, right(), 6, ["Element."+op[1],"(",arg,")"]); } + // binary operators + else { var l=left(),r=right(),a1=tokens[l],a2=tokens[r]; if (op[0]==op[1]) glue(l,r,6,[a1,op[1],a2]); else glue(l,r,6,["Element."+op[1],"(",a1,",",a2,")"]); ti-=2; } + } + return tokens; + } + // Glue all back together and return as bound function. + return eval( ('('+(function f(t){return t.map(t=>t instanceof Array?f(t):typeof t == "string"?t:"").join('');})(translate(tok))+')') ); + } + } + + if ((p==2 || p==3) && (r==1)) { + res.arrow = res.inline(( from_point, to_point, w=0.03, aspect=0.8, camera=1 )=>{ + from_point = from_point/(-from_point|!1e0); to_point = to_point/(-to_point|!1e0); + var line = ( from_point & to_point ), l = line.Length; + var shape = [[0,w],[l-5*w,w],[l-5*w,aspect*5*w],[l,0],[l-5*w,-aspect*5*w],[l-5*w,-w],[0,-w]].map(([x,y])=>!(1e0+x*1e1+y*1e2)); + var sqrt = R => R==-1?1e12:(1+R).Normalized; + var R = ((to_point - from_point).UnDual).Normalized * 1e1; + var R2 = sqrt(from_point/!1e0) * sqrt(R); + var p2 = R2 >>> 1e3; + if (p2 != 0) { var p1 = (((~(camera+0e1) >>> 1e3)|line)/line).Normalized; return sqrt(p1/p2) * R2 >>> shape; } + return R2 >>> shape; + }) + } + + if (options.dual) { + Object.defineProperty(res.prototype, 'Inverse', {configurable:true, get(){ var s = 1/this.s**2; return this.map((x,i)=>i?-x*s:1/x ); }}); + } else { + // Matrix-free inverses up to 5D. Should translate this to an inline call for readability. + // http://repository.essex.ac.uk/17282/1/TechReport_CES-534.pdf + Object.defineProperty(res.prototype, 'Inverse', {configurable: true, get(){ + // Shirokov inverse .. + if (tot > 5) { + for (var N=2**(((tot+1)/2)|0), Uk=this.Scale(1), k=1; kres.prototype[x] = options.over.inline(res.prototype[x])); + res.prototype.Coeff = function() { for (var i=0,l=arguments.length; ix==0?undefined:(i?'('+x+')'+basis[i]:x.toString())).filter(x=>x).join(' + '); } + } + + // Experimental differential operator. + var _D, _DT, _DA, totd = basis.length; + function makeD(transpose=false){ + _DA = _DA || Algebra({ p:p,q:q,r:r,basis:options.basis,even:options.even,over:Algebra({dual:totd})}); // same algebra, but over dual numbers. + return (func)=>{ + var dfunc = _DA.inline(func); // convert input function to dual algebra + return (val,...args)=>{ // return a new function (the derivative w.r.t 1st param) + if (!(val instanceof res)) val = res.Scalar(val); // allow to be called with scalars. + args = args.map(x=>{ var r = _DA.Scalar(0); for (var i=0; ival.slice()); // call the function in the dual algebra. + if (transpose) for (var i=0; i Date: Sun, 7 Jul 2024 17:56:12 -0700 Subject: [PATCH 063/129] Randomize guess in gradient descent test Randomly perturb the pre-solved part of the guess, and randomly choose the unsolved part. --- .../gram-test/overlapping-pyramids.jl | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/engine-proto/gram-test/overlapping-pyramids.jl b/engine-proto/gram-test/overlapping-pyramids.jl index 1be29e7..97fc119 100644 --- a/engine-proto/gram-test/overlapping-pyramids.jl +++ b/engine-proto/gram-test/overlapping-pyramids.jl @@ -3,6 +3,7 @@ include("Engine.jl") using SparseArrays using AbstractAlgebra using PolynomialRoots +using Random # initialize the partial gram matrix for an arrangement of seven spheres in # which spheres 1 through 5 are mutually tangent, and spheres 3 through 7 are @@ -34,13 +35,17 @@ gram[1, 6] = gram[6, 1] # in this initial guess, the mutual tangency condition is satisfied for spheres # 1 through 5 -guess = sqrt(1/BigFloat(2)) * BigFloat[ - 1 1 -1 -1 0 -0.1 0.3; - 1 -1 1 -1 0 -0.5 0.4; - 1 -1 -1 1 0 0.1 -0.2; - 0 0 0 0 -sqrt(BigFloat(6)) 0.3 -0.2; - 1 1 1 1 2 0.2 0.1; -] +Random.seed!(50793) +guess = hcat( + sqrt(1/BigFloat(2)) * BigFloat[ + 1 1 -1 -1 0; + 1 -1 1 -1 0; + 1 -1 -1 1 0; + 0 0 0 0 -sqrt(BigFloat(6)); + 1 1 1 1 2; + ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)), + Engine.rand_on_shell(fill(BigFloat(-1), 2)) +) # complete the gram matrix using gradient descent L, history = Engine.realize_gram(gram, guess) -- 2.34.1 From 736ac50b075de1d3f4ffef3566a88152c0761e80 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 7 Jul 2024 17:58:55 -0700 Subject: [PATCH 064/129] Test gradient descent for sphere in tetrahedron --- .../gram-test/sphere-in-tetrahedron.jl | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 engine-proto/gram-test/sphere-in-tetrahedron.jl diff --git a/engine-proto/gram-test/sphere-in-tetrahedron.jl b/engine-proto/gram-test/sphere-in-tetrahedron.jl new file mode 100644 index 0000000..ed1adfc --- /dev/null +++ b/engine-proto/gram-test/sphere-in-tetrahedron.jl @@ -0,0 +1,43 @@ +include("Engine.jl") + +using SparseArrays +using AbstractAlgebra +using PolynomialRoots + +# initialize the partial gram matrix for a sphere inscribed in a regular +# tetrahedron +J = Int64[] +K = Int64[] +values = BigFloat[] +for j in 1:5 + for k in 1:5 + push!(J, j) + push!(K, k) + if j == k + push!(values, 1) + elseif (j <= 4 && k <= 4) + push!(values, -1/BigFloat(3)) + else + push!(values, -1) + end + end +end +gram = sparse(J, K, values) + +# set initial guess +Random.seed!(99230) +guess = sqrt(1/BigFloat(3)) * BigFloat[ + 1 1 -1 -1 0 + 1 -1 1 -1 0 + 1 -1 -1 1 0 + 1 1 1 1 -2 + 1 1 1 1 1 +] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)) + +# complete the gram matrix using gradient descent +L, history = Engine.realize_gram(gram, guess) +completed_gram = L'*Engine.Q*L +println("Completed Gram matrix:\n") +display(completed_gram) +println("\nSteps: ", size(history.stepsize, 1)) +println("Loss: ", history.scaled_loss[end], "\n") \ No newline at end of file -- 2.34.1 From 828498b3de397467dbea0d82ffbdf985fb05da2d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 8 Jul 2024 12:56:14 -0700 Subject: [PATCH 065/129] Add sphere and plane utilities to engine --- engine-proto/gram-test/Engine.jl | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index a0a59be..a874485 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -33,6 +33,19 @@ rand_on_shell(rng::AbstractRNG, shells::Array{T}) where T <: Number = rand_on_shell(shells::Array{<:Number}) = rand_on_shell(Random.default_rng(), shells) +# === elements === + +plane(normal, offset) = [normal; offset; offset] + +function sphere(center, radius) + dist_sq = dot(center, center) + return [ + center / radius; + 0.5 * ((dist_sq - 1) / radius - radius); + 0.5 * ((dist_sq + 1) / radius - radius) + ] +end + # === Gram matrix realization === # the Lorentz form -- 2.34.1 From 9efa99e8be12265991cc78077e6d55e691a0f7ee Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 8 Jul 2024 12:56:28 -0700 Subject: [PATCH 066/129] Test gradient descent for circles in triangle --- engine-proto/gram-test/circles-in-triangle.jl | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 engine-proto/gram-test/circles-in-triangle.jl diff --git a/engine-proto/gram-test/circles-in-triangle.jl b/engine-proto/gram-test/circles-in-triangle.jl new file mode 100644 index 0000000..bd6a503 --- /dev/null +++ b/engine-proto/gram-test/circles-in-triangle.jl @@ -0,0 +1,62 @@ +include("Engine.jl") + +using SparseArrays +using AbstractAlgebra +using PolynomialRoots + +# initialize the partial gram matrix for a sphere inscribed in a regular +# tetrahedron +J = Int64[] +K = Int64[] +values = BigFloat[] +for j in 1:8 + for k in 1:8 + filled = false + if j == k + push!(values, 1) + filled = true + elseif (j == 1 || k == 1) + push!(values, 0) + filled = true + elseif (j == 2 || k == 2) + push!(values, -1) + filled = true + end + if filled + push!(J, j) + push!(K, k) + end + end +end +append!(J, [6, 4, 6, 5, 7, 5, 7, 3, 8, 3, 8, 4]) +append!(K, [4, 6, 5, 6, 5, 7, 3, 7, 3, 8, 4, 8]) +append!(values, fill(-1, 12)) +gram = sparse(J, K, values) + +# set initial guess (random) +## Random.seed!(58271) # stuck; step size collapses on step 48 +## Random.seed!(58272) # good convergence +## Random.seed!(58273) # stuck; step size collapses on step 18 +## Random.seed!(58274) # stuck +## Random.seed!(58275) # +## guess = Engine.rand_on_shell(fill(BigFloat(-1), 8)) + +# set initial guess +guess = hcat( + Engine.plane(BigFloat[0, 0, 1], BigFloat(0)), + Engine.sphere(BigFloat[0, 0, 0], BigFloat(1//2)), + Engine.plane(BigFloat[1, 0, 0], BigFloat(1)), + Engine.plane(BigFloat[cos(2pi/3), sin(2pi/3), 0], BigFloat(1)), + Engine.plane(BigFloat[cos(-2pi/3), sin(-2pi/3), 0], BigFloat(1)), + Engine.sphere(BigFloat[-1, 0, 0], BigFloat(1//5)), + Engine.sphere(BigFloat[cos(-pi/3), sin(-pi/3), 0], BigFloat(1//5)), + Engine.sphere(BigFloat[cos(pi/3), sin(pi/3), 0], BigFloat(1//5)) +) + +# complete the gram matrix using gradient descent +L, history = Engine.realize_gram(gram, guess, max_descent_steps = 200) +completed_gram = L'*Engine.Q*L +println("Completed Gram matrix:\n") +display(completed_gram) +println("\nSteps: ", size(history.stepsize, 1)) +println("Loss: ", history.scaled_loss[end], "\n") \ No newline at end of file -- 2.34.1 From 93dd05c3172426685c3bd49f95f4be7554bc1fda Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 8 Jul 2024 14:19:05 -0700 Subject: [PATCH 067/129] Add required package to "sphere in tetrahedron" example --- engine-proto/gram-test/sphere-in-tetrahedron.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/engine-proto/gram-test/sphere-in-tetrahedron.jl b/engine-proto/gram-test/sphere-in-tetrahedron.jl index ed1adfc..3730390 100644 --- a/engine-proto/gram-test/sphere-in-tetrahedron.jl +++ b/engine-proto/gram-test/sphere-in-tetrahedron.jl @@ -3,6 +3,7 @@ include("Engine.jl") using SparseArrays using AbstractAlgebra using PolynomialRoots +using Random # initialize the partial gram matrix for a sphere inscribed in a regular # tetrahedron -- 2.34.1 From 610fc451f08d194835297dc72b04f00013ea4675 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 8 Jul 2024 14:19:25 -0700 Subject: [PATCH 068/129] Track slope in gradient descent history --- engine-proto/gram-test/Engine.jl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index a874485..e8bda9c 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -65,15 +65,17 @@ end # a type for keeping track of gradient descent history struct DescentHistory{T} scaled_loss::Array{T} + slope::Array{T} stepsize::Array{T} backoff_steps::Array{Int64} function DescentHistory{T}( scaled_loss = Array{T}(undef, 0), + slope = Array{T}(undef, 0), stepsize = Array{T}(undef, 0), backoff_steps = Int64[] ) where T - new(scaled_loss, stepsize, backoff_steps) + new(scaled_loss, slope, stepsize, backoff_steps) end end @@ -113,10 +115,11 @@ function realize_gram( neg_grad = 4*Q*L*Δ_proj slope = norm(neg_grad) - # store current position and loss + # store current position, loss, and slope L_last = L loss_last = loss push!(history.scaled_loss, loss / scale_adjustment) + push!(history.slope, slope) # find a good step size using backtracking line search push!(history.stepsize, 0) -- 2.34.1 From 023759a26715e07c2dd00ad274af55ec0f369350 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 8 Jul 2024 14:21:10 -0700 Subject: [PATCH 069/129] Start "circles in triangle" from a very close guess --- engine-proto/gram-test/circles-in-triangle.jl | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/engine-proto/gram-test/circles-in-triangle.jl b/engine-proto/gram-test/circles-in-triangle.jl index bd6a503..2bca4f2 100644 --- a/engine-proto/gram-test/circles-in-triangle.jl +++ b/engine-proto/gram-test/circles-in-triangle.jl @@ -31,6 +31,11 @@ end append!(J, [6, 4, 6, 5, 7, 5, 7, 3, 8, 3, 8, 4]) append!(K, [4, 6, 5, 6, 5, 7, 3, 7, 3, 8, 4, 8]) append!(values, fill(-1, 12)) +#= make construction rigid +append!(J, [3, 4, 4, 5]) +append!(K, [4, 3, 5, 4]) +append!(values, fill(-0.5, 4)) +=# gram = sparse(J, K, values) # set initial guess (random) @@ -42,6 +47,7 @@ gram = sparse(J, K, values) ## guess = Engine.rand_on_shell(fill(BigFloat(-1), 8)) # set initial guess +#= guess = hcat( Engine.plane(BigFloat[0, 0, 1], BigFloat(0)), Engine.sphere(BigFloat[0, 0, 0], BigFloat(1//2)), @@ -52,6 +58,17 @@ guess = hcat( Engine.sphere(BigFloat[cos(-pi/3), sin(-pi/3), 0], BigFloat(1//5)), Engine.sphere(BigFloat[cos(pi/3), sin(pi/3), 0], BigFloat(1//5)) ) +=# +guess = hcat( + Engine.plane(BigFloat[0, 0, 1], BigFloat(0)), + Engine.sphere(BigFloat[0, 0, 0], BigFloat(0.9)), + Engine.plane(BigFloat[1, 0, 0], BigFloat(1)), + Engine.plane(BigFloat[cos(2pi/3), sin(2pi/3), 0], BigFloat(1)), + Engine.plane(BigFloat[cos(-2pi/3), sin(-2pi/3), 0], BigFloat(1)), + Engine.sphere(4//3*BigFloat[-1, 0, 0], BigFloat(1//3)), + Engine.sphere(4//3*BigFloat[cos(-pi/3), sin(-pi/3), 0], BigFloat(1//3)), + Engine.sphere(4//3*BigFloat[cos(pi/3), sin(pi/3), 0], BigFloat(1//3)) +) # complete the gram matrix using gradient descent L, history = Engine.realize_gram(gram, guess, max_descent_steps = 200) -- 2.34.1 From 77bc124170e2ee72fe94ec55d92af7f689aa24f7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 9 Jul 2024 14:00:24 -0700 Subject: [PATCH 070/129] Change loss function to match gradient --- engine-proto/gram-test/Engine.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index e8bda9c..2fb41e0 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -104,7 +104,7 @@ function realize_gram( # do gradient descent Δ_proj = proj_diff(gram, L'*Q*L) - loss = norm(Δ_proj) + loss = dot(Δ_proj, Δ_proj) for step in 1:max_descent_steps # stop if the loss is tolerably low if loss < tol @@ -128,7 +128,7 @@ function realize_gram( history.stepsize[end] = stepsize L = L_last + stepsize * neg_grad Δ_proj = proj_diff(gram, L'*Q*L) - loss = norm(Δ_proj) + loss = dot(Δ_proj, Δ_proj) improvement = loss_last - loss if improvement >= target_improvement * stepsize * slope history.backoff_steps[end] = backoff_steps -- 2.34.1 From f84d475580391e5ef9b5ef2d0eca02af9c121087 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 9 Jul 2024 14:01:30 -0700 Subject: [PATCH 071/129] Visualize neighborhoods of global minima --- engine-proto/gram-test/basin-shapes.jl | 99 ++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 engine-proto/gram-test/basin-shapes.jl diff --git a/engine-proto/gram-test/basin-shapes.jl b/engine-proto/gram-test/basin-shapes.jl new file mode 100644 index 0000000..5c03c01 --- /dev/null +++ b/engine-proto/gram-test/basin-shapes.jl @@ -0,0 +1,99 @@ +include("Engine.jl") + +using LinearAlgebra +using SparseArrays + +function sphere_in_tetrahedron_shape() + # initialize the partial gram matrix for a sphere inscribed in a regular + # tetrahedron + J = Int64[] + K = Int64[] + values = BigFloat[] + for j in 1:5 + for k in 1:5 + push!(J, j) + push!(K, k) + if j == k + push!(values, 1) + elseif (j <= 4 && k <= 4) + push!(values, -1/BigFloat(3)) + else + push!(values, -1) + end + end + end + gram = sparse(J, K, values) + + # plot loss along a slice + loss_lin = [] + loss_sq = [] + mesh = range(0.9, 1.1, 101) + for t in mesh + L = hcat( + Engine.plane(normalize(BigFloat[ 1, 1, 1]), BigFloat(1)), + Engine.plane(normalize(BigFloat[ 1, -1, -1]), BigFloat(1)), + Engine.plane(normalize(BigFloat[-1, 1, -1]), BigFloat(1)), + Engine.plane(normalize(BigFloat[-1, -1, 1]), BigFloat(1)), + Engine.sphere(BigFloat[0, 0, 0], BigFloat(t)) + ) + Δ_proj = Engine.proj_diff(gram, L'*Engine.Q*L) + push!(loss_lin, norm(Δ_proj)) + push!(loss_sq, dot(Δ_proj, Δ_proj)) + end + mesh, loss_lin, loss_sq +end + +function circles_in_triangle_shape() + # initialize the partial gram matrix for a sphere inscribed in a regular + # tetrahedron + J = Int64[] + K = Int64[] + values = BigFloat[] + for j in 1:8 + for k in 1:8 + filled = false + if j == k + push!(values, 1) + filled = true + elseif (j == 1 || k == 1) + push!(values, 0) + filled = true + elseif (j == 2 || k == 2) + push!(values, -1) + filled = true + end + #=elseif (j <= 5 && j != 2 && k == 9 || k == 9 && k <= 5 && k != 2) + push!(values, 0) + filled = true + end=# + if filled + push!(J, j) + push!(K, k) + end + end + end + append!(J, [6, 4, 6, 5, 7, 5, 7, 3, 8, 3, 8, 4]) + append!(K, [4, 6, 5, 6, 5, 7, 3, 7, 3, 8, 4, 8]) + append!(values, fill(-1, 12)) + + # plot loss along a slice + loss_lin = [] + loss_sq = [] + mesh = range(0.99, 1.01, 101) + for t in mesh + L = hcat( + Engine.plane(BigFloat[0, 0, 1], BigFloat(0)), + Engine.sphere(BigFloat[0, 0, 0], BigFloat(t)), + Engine.plane(BigFloat[1, 0, 0], BigFloat(1)), + Engine.plane(BigFloat[cos(2pi/3), sin(2pi/3), 0], BigFloat(1)), + Engine.plane(BigFloat[cos(-2pi/3), sin(-2pi/3), 0], BigFloat(1)), + Engine.sphere(4//3*BigFloat[-1, 0, 0], BigFloat(1//3)), + Engine.sphere(4//3*BigFloat[cos(-pi/3), sin(-pi/3), 0], BigFloat(1//3)), + Engine.sphere(4//3*BigFloat[cos(pi/3), sin(pi/3), 0], BigFloat(1//3)) + ) + Δ_proj = Engine.proj_diff(gram, L'*Engine.Q*L) + push!(loss_lin, norm(Δ_proj)) + push!(loss_sq, dot(Δ_proj, Δ_proj)) + end + mesh, loss_lin, loss_sq +end \ No newline at end of file -- 2.34.1 From 5652719642b236d36205b167a49510c9d21ae87b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 9 Jul 2024 14:10:23 -0700 Subject: [PATCH 072/129] Require triangle sides to be planar --- engine-proto/gram-test/circles-in-triangle.jl | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/engine-proto/gram-test/circles-in-triangle.jl b/engine-proto/gram-test/circles-in-triangle.jl index 2bca4f2..9dc3fac 100644 --- a/engine-proto/gram-test/circles-in-triangle.jl +++ b/engine-proto/gram-test/circles-in-triangle.jl @@ -1,20 +1,28 @@ include("Engine.jl") using SparseArrays -using AbstractAlgebra -using PolynomialRoots # initialize the partial gram matrix for a sphere inscribed in a regular # tetrahedron J = Int64[] K = Int64[] values = BigFloat[] -for j in 1:8 - for k in 1:8 +for j in 1:9 + for k in 1:9 filled = false if j == k - push!(values, 1) + push!(values, j < 9 ? 1 : 0) filled = true + elseif (j == 9) + if (k <= 5 && k != 2) + push!(values, 0) + filled = true + end + elseif (k == 9) + if (j <= 5 && j != 2) + push!(values, 0) + filled = true + end elseif (j == 1 || k == 1) push!(values, 0) filled = true @@ -56,7 +64,8 @@ guess = hcat( Engine.plane(BigFloat[cos(-2pi/3), sin(-2pi/3), 0], BigFloat(1)), Engine.sphere(BigFloat[-1, 0, 0], BigFloat(1//5)), Engine.sphere(BigFloat[cos(-pi/3), sin(-pi/3), 0], BigFloat(1//5)), - Engine.sphere(BigFloat[cos(pi/3), sin(pi/3), 0], BigFloat(1//5)) + Engine.sphere(BigFloat[cos(pi/3), sin(pi/3), 0], BigFloat(1//5)), + BigFloat[0, 0, 0, 1, 1] ) =# guess = hcat( @@ -67,7 +76,8 @@ guess = hcat( Engine.plane(BigFloat[cos(-2pi/3), sin(-2pi/3), 0], BigFloat(1)), Engine.sphere(4//3*BigFloat[-1, 0, 0], BigFloat(1//3)), Engine.sphere(4//3*BigFloat[cos(-pi/3), sin(-pi/3), 0], BigFloat(1//3)), - Engine.sphere(4//3*BigFloat[cos(pi/3), sin(pi/3), 0], BigFloat(1//3)) + Engine.sphere(4//3*BigFloat[cos(pi/3), sin(pi/3), 0], BigFloat(1//3)), + BigFloat[0, 0, 0, 1, 1] ) # complete the gram matrix using gradient descent -- 2.34.1 From 4d5ea062a3dc47ae92472dda23e7f8bdccc6c614 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 9 Jul 2024 15:00:13 -0700 Subject: [PATCH 073/129] Record gradient and last line search in history --- engine-proto/gram-test/Engine.jl | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index 2fb41e0..0539326 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -65,17 +65,23 @@ end # a type for keeping track of gradient descent history struct DescentHistory{T} scaled_loss::Array{T} + neg_grad::Array{Matrix{T}} slope::Array{T} stepsize::Array{T} backoff_steps::Array{Int64} + last_line_L::Array{Matrix{T}} + last_line_loss::Array{T} function DescentHistory{T}( scaled_loss = Array{T}(undef, 0), + neg_grad = Array{Matrix{T}}(undef, 0), slope = Array{T}(undef, 0), stepsize = Array{T}(undef, 0), - backoff_steps = Int64[] + backoff_steps = Int64[], + last_line_L = Array{Matrix{T}}(undef, 0), + last_line_loss = Array{T}(undef, 0) ) where T - new(scaled_loss, slope, stepsize, backoff_steps) + new(scaled_loss, neg_grad, slope, stepsize, backoff_steps, last_line_L, last_line_loss) end end @@ -119,23 +125,33 @@ function realize_gram( L_last = L loss_last = loss push!(history.scaled_loss, loss / scale_adjustment) + push!(history.neg_grad, neg_grad) push!(history.slope, slope) # find a good step size using backtracking line search push!(history.stepsize, 0) push!(history.backoff_steps, max_backoff_steps) + empty!(history.last_line_L) + empty!(history.last_line_loss) for backoff_steps in 0:max_backoff_steps history.stepsize[end] = stepsize L = L_last + stepsize * neg_grad Δ_proj = proj_diff(gram, L'*Q*L) loss = dot(Δ_proj, Δ_proj) improvement = loss_last - loss + push!(history.last_line_L, L) + push!(history.last_line_loss, loss / scale_adjustment) if improvement >= target_improvement * stepsize * slope history.backoff_steps[end] = backoff_steps break end stepsize *= backoff end + + # [DEBUG] if we've hit a wall, quit + if history.backoff_steps[end] == max_backoff_steps + break + end end # return the factorization and its history -- 2.34.1 From d538cbf716607f9b53a348c8dfa202363575abf0 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 10 Jul 2024 23:31:44 -0700 Subject: [PATCH 074/129] Correct improvement threshold by using unit step Our formula for the improvement theshold works when the step size is an absolute distance. However, in commit `4d5ea06`, the step size was measured relative to the current gradient instead. This commit scales the base step to unit length, so now the step size really is an absolute distance. --- engine-proto/gram-test/Engine.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index 0539326..a160291 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -120,6 +120,7 @@ function realize_gram( # find negative gradient of loss function neg_grad = 4*Q*L*Δ_proj slope = norm(neg_grad) + dir = neg_grad / slope # store current position, loss, and slope L_last = L @@ -135,7 +136,7 @@ function realize_gram( empty!(history.last_line_loss) for backoff_steps in 0:max_backoff_steps history.stepsize[end] = stepsize - L = L_last + stepsize * neg_grad + L = L_last + stepsize * dir Δ_proj = proj_diff(gram, L'*Q*L) loss = dot(Δ_proj, Δ_proj) improvement = loss_last - loss -- 2.34.1 From 3910b9f740b7a775b5642892ac2b0575c1c73b22 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 11 Jul 2024 13:43:52 -0700 Subject: [PATCH 075/129] Use Newton's method for polishing --- engine-proto/gram-test/Engine.jl | 85 ++++++++++++++++++- engine-proto/gram-test/circles-in-triangle.jl | 19 +++-- .../gram-test/overlapping-pyramids.jl | 9 +- .../gram-test/sphere-in-tetrahedron.jl | 8 +- 4 files changed, 103 insertions(+), 18 deletions(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index a160291..e6f0d97 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -51,11 +51,21 @@ end # the Lorentz form Q = diagm([1, 1, 1, 1, -1]) +# project a matrix onto the subspace of matrices whose entries vanish at the +# given indices +function proj_to_entries(mat, indices) + result = zeros(size(mat)) + for (j, k) in indices + result[j, k] = mat[j, k] + end + result +end + # the difference between the matrices `target` and `attempt`, projected onto the # subspace of matrices whose entries vanish at each empty index of `target` function proj_diff(target::SparseMatrixCSC{T, <:Any}, attempt::Matrix{T}) where T J, K, values = findnz(target) - result = zeros(size(target)...) + result = zeros(size(target)) for (j, k, val) in zip(J, K, values) result[j, k] = val - attempt[j, k] end @@ -87,7 +97,7 @@ end # seek a matrix `L` for which `L'QL` matches the sparse matrix `gram` at every # explicit entry of `gram`. use gradient descent starting from `guess` -function realize_gram( +function realize_gram_gradient( gram::SparseMatrixCSC{T, <:Any}, guess::Matrix{T}; scaled_tol = 1e-30, @@ -111,7 +121,7 @@ function realize_gram( # do gradient descent Δ_proj = proj_diff(gram, L'*Q*L) loss = dot(Δ_proj, Δ_proj) - for step in 1:max_descent_steps + for _ in 1:max_descent_steps # stop if the loss is tolerably low if loss < tol break @@ -160,4 +170,73 @@ function realize_gram( L, history end +function basis_matrix(::Type{T}, j, k, dims) where T + result = zeros(T, dims) + result[j, k] = one(T) + result +end + +# seek a matrix `L` for which `L'QL` matches the sparse matrix `gram` at every +# explicit entry of `gram`. use Newton's method starting from `guess` +function realize_gram_newton( + gram::SparseMatrixCSC{T, <:Any}, + guess::Matrix{T}; + scaled_tol = 1e-30, + rate = 1, + max_steps = 100 +) where T <: Number + # start history + history = DescentHistory{T}() + + # find the dimension of the search space + dims = size(guess) + element_dim, construction_dim = dims + total_dim = element_dim * construction_dim + + # list the constrained entries of the gram matrix + J, K, _ = findnz(gram) + constrained = zip(J, K) + + # scale the tolerance + scale_adjustment = sqrt(T(length(constrained))) + tol = scale_adjustment * scaled_tol + + # use newton's method + L = copy(guess) + for step in 0:max_steps + # evaluate the loss function + Δ_proj = proj_diff(gram, L'*Q*L) + loss = dot(Δ_proj, Δ_proj) + + # store the current loss + push!(history.scaled_loss, loss / scale_adjustment) + + # stop if the loss is tolerably low + if loss < tol || step > max_steps + break + end + + # find the negative gradient of loss function + neg_grad = 4*Q*L*Δ_proj + + # find the negative Hessian of the loss function + hess = Matrix{T}(undef, total_dim, total_dim) + indices = [(j, k) for k in 1:construction_dim for j in 1:element_dim] + for (j, k) in indices + basis_mat = basis_matrix(T, j, k, dims) + neg_dΔ = basis_mat'*Q*L + L'*Q*basis_mat + neg_dΔ_proj = proj_to_entries(neg_dΔ, constrained) + deriv_grad = 4*Q*(-basis_mat*Δ_proj + L*neg_dΔ_proj) + hess[:, (k-1)*element_dim + j] = reshape(deriv_grad, total_dim) + end + + # compute the newton step + step = hess \ reshape(neg_grad, total_dim) + L += rate * reshape(step, dims) + end + + # return the factorization and its history + L, history +end + end \ No newline at end of file diff --git a/engine-proto/gram-test/circles-in-triangle.jl b/engine-proto/gram-test/circles-in-triangle.jl index 9dc3fac..b031711 100644 --- a/engine-proto/gram-test/circles-in-triangle.jl +++ b/engine-proto/gram-test/circles-in-triangle.jl @@ -55,7 +55,6 @@ gram = sparse(J, K, values) ## guess = Engine.rand_on_shell(fill(BigFloat(-1), 8)) # set initial guess -#= guess = hcat( Engine.plane(BigFloat[0, 0, 1], BigFloat(0)), Engine.sphere(BigFloat[0, 0, 0], BigFloat(1//2)), @@ -67,7 +66,7 @@ guess = hcat( Engine.sphere(BigFloat[cos(pi/3), sin(pi/3), 0], BigFloat(1//5)), BigFloat[0, 0, 0, 1, 1] ) -=# +#= guess = hcat( Engine.plane(BigFloat[0, 0, 1], BigFloat(0)), Engine.sphere(BigFloat[0, 0, 0], BigFloat(0.9)), @@ -79,11 +78,19 @@ guess = hcat( Engine.sphere(4//3*BigFloat[cos(pi/3), sin(pi/3), 0], BigFloat(1//3)), BigFloat[0, 0, 0, 1, 1] ) +=# -# complete the gram matrix using gradient descent -L, history = Engine.realize_gram(gram, guess, max_descent_steps = 200) +# complete the gram matrix using gradient descent followed by Newton's method +L, history = Engine.realize_gram_gradient(gram, guess, scaled_tol = 0.01) +L_pol, history_pol = Engine.realize_gram_newton(gram, L, rate = 0.3, scaled_tol = 1e-9) +L_pol2, history_pol2 = Engine.realize_gram_newton(gram, L_pol) completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") display(completed_gram) -println("\nSteps: ", size(history.stepsize, 1)) -println("Loss: ", history.scaled_loss[end], "\n") \ No newline at end of file +println( + "\nSteps: ", + size(history.scaled_loss, 1), + " + ", size(history_pol.scaled_loss, 1), + " + ", size(history_pol2.scaled_loss, 1) +) +println("Loss: ", history_pol2.scaled_loss[end], "\n") \ No newline at end of file diff --git a/engine-proto/gram-test/overlapping-pyramids.jl b/engine-proto/gram-test/overlapping-pyramids.jl index 97fc119..51606e1 100644 --- a/engine-proto/gram-test/overlapping-pyramids.jl +++ b/engine-proto/gram-test/overlapping-pyramids.jl @@ -47,13 +47,14 @@ guess = hcat( Engine.rand_on_shell(fill(BigFloat(-1), 2)) ) -# complete the gram matrix using gradient descent -L, history = Engine.realize_gram(gram, guess) +# complete the gram matrix using gradient descent followed by Newton's method +L, history = Engine.realize_gram_gradient(gram, guess, scaled_tol = 0.01) +L_pol, history_pol = Engine.realize_gram_newton(gram, L) completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") display(completed_gram) -println("\nSteps: ", size(history.stepsize, 1)) -println("Loss: ", history.scaled_loss[end], "\n") +println("\nSteps: ", size(history.scaled_loss, 1), " + ", size(history_pol.scaled_loss, 1)) +println("Loss: ", history_pol.scaled_loss[end], "\n") # === algebraic check === diff --git a/engine-proto/gram-test/sphere-in-tetrahedron.jl b/engine-proto/gram-test/sphere-in-tetrahedron.jl index 3730390..dc5588c 100644 --- a/engine-proto/gram-test/sphere-in-tetrahedron.jl +++ b/engine-proto/gram-test/sphere-in-tetrahedron.jl @@ -1,8 +1,6 @@ include("Engine.jl") using SparseArrays -using AbstractAlgebra -using PolynomialRoots using Random # initialize the partial gram matrix for a sphere inscribed in a regular @@ -35,10 +33,10 @@ guess = sqrt(1/BigFloat(3)) * BigFloat[ 1 1 1 1 1 ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)) -# complete the gram matrix using gradient descent -L, history = Engine.realize_gram(gram, guess) +# complete the gram matrix using Newton's method +L, history = Engine.realize_gram_newton(gram, guess) completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") display(completed_gram) -println("\nSteps: ", size(history.stepsize, 1)) +println("\nSteps: ", size(history.scaled_loss, 1)) println("Loss: ", history.scaled_loss[end], "\n") \ No newline at end of file -- 2.34.1 From 25b09ebf925bc9a03f3f52014b7d0a8a3693b12e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 15 Jul 2024 11:32:04 -0700 Subject: [PATCH 076/129] Sketch backtracking Newton's method This code is a mess, but I'm committing it to record a working state before I start trying to clean up. --- engine-proto/gram-test/Engine.jl | 237 +++++++++++++++++- engine-proto/gram-test/circles-in-triangle.jl | 9 +- .../gram-test/overlapping-pyramids.jl | 11 +- .../gram-test/sphere-in-tetrahedron.jl | 5 +- 4 files changed, 254 insertions(+), 8 deletions(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index e6f0d97..78d1409 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -1,8 +1,10 @@ module Engine using LinearAlgebra +using GenericLinearAlgebra using SparseArrays using Random +using Optim export rand_on_shell, Q, DescentHistory, realize_gram @@ -76,8 +78,11 @@ end struct DescentHistory{T} scaled_loss::Array{T} neg_grad::Array{Matrix{T}} + base_step::Array{Matrix{T}} + hess::Array{Hermitian{T, Matrix{T}}} slope::Array{T} stepsize::Array{T} + used_grad::Array{Bool} backoff_steps::Array{Int64} last_line_L::Array{Matrix{T}} last_line_loss::Array{T} @@ -85,13 +90,16 @@ struct DescentHistory{T} function DescentHistory{T}( scaled_loss = Array{T}(undef, 0), neg_grad = Array{Matrix{T}}(undef, 0), + hess = Array{Hermitian{T, Matrix{T}}}(undef, 0), + base_step = Array{Matrix{T}}(undef, 0), slope = Array{T}(undef, 0), stepsize = Array{T}(undef, 0), + used_grad = Bool[], backoff_steps = Int64[], last_line_L = Array{Matrix{T}}(undef, 0), last_line_loss = Array{T}(undef, 0) ) where T - new(scaled_loss, neg_grad, slope, stepsize, backoff_steps, last_line_L, last_line_loss) + new(scaled_loss, neg_grad, hess, base_step, slope, stepsize, used_grad, backoff_steps, last_line_L, last_line_loss) end end @@ -101,7 +109,7 @@ function realize_gram_gradient( gram::SparseMatrixCSC{T, <:Any}, guess::Matrix{T}; scaled_tol = 1e-30, - target_improvement = 0.5, + min_efficiency = 0.5, init_stepsize = 1.0, backoff = 0.9, max_descent_steps = 600, @@ -152,7 +160,7 @@ function realize_gram_gradient( improvement = loss_last - loss push!(history.last_line_L, L) push!(history.last_line_loss, loss / scale_adjustment) - if improvement >= target_improvement * stepsize * slope + if improvement >= min_efficiency * stepsize * slope history.backoff_steps[end] = backoff_steps break end @@ -201,7 +209,7 @@ function realize_gram_newton( scale_adjustment = sqrt(T(length(constrained))) tol = scale_adjustment * scaled_tol - # use newton's method + # use Newton's method L = copy(guess) for step in 0:max_steps # evaluate the loss function @@ -229,8 +237,10 @@ function realize_gram_newton( deriv_grad = 4*Q*(-basis_mat*Δ_proj + L*neg_dΔ_proj) hess[:, (k-1)*element_dim + j] = reshape(deriv_grad, total_dim) end + hess = Hermitian(hess) + push!(history.hess, hess) - # compute the newton step + # compute the Newton step step = hess \ reshape(neg_grad, total_dim) L += rate * reshape(step, dims) end @@ -239,4 +249,221 @@ function realize_gram_newton( L, history end +LinearAlgebra.eigen!(A::Symmetric{BigFloat, Matrix{BigFloat}}; sortby::Nothing) = + eigen!(Hermitian(A)) + +function realize_gram_optim( + gram::SparseMatrixCSC{T, <:Any}, + guess::Matrix{T} +) where T <: Number + # find the dimension of the search space + dims = size(guess) + element_dim, construction_dim = dims + total_dim = element_dim * construction_dim + + # list the constrained entries of the gram matrix + J, K, _ = findnz(gram) + constrained = zip(J, K) + + # scale the loss function + scale_adjustment = length(constrained) + + function loss(L_vec) + L = reshape(L_vec, dims) + Δ_proj = proj_diff(gram, L'*Q*L) + dot(Δ_proj, Δ_proj) / scale_adjustment + end + + function loss_grad!(storage, L_vec) + L = reshape(L_vec, dims) + Δ_proj = proj_diff(gram, L'*Q*L) + storage .= reshape(-4*Q*L*Δ_proj, total_dim) / scale_adjustment + end + + function loss_hess!(storage, L_vec) + L = reshape(L_vec, dims) + Δ_proj = proj_diff(gram, L'*Q*L) + indices = [(j, k) for k in 1:construction_dim for j in 1:element_dim] + for (j, k) in indices + basis_mat = basis_matrix(T, j, k, dims) + neg_dΔ = basis_mat'*Q*L + L'*Q*basis_mat + neg_dΔ_proj = proj_to_entries(neg_dΔ, constrained) + deriv_grad = 4*Q*(-basis_mat*Δ_proj + L*neg_dΔ_proj) / scale_adjustment + storage[:, (k-1)*element_dim + j] = reshape(deriv_grad, total_dim) + end + end + + optimize( + loss, loss_grad!, loss_hess!, + reshape(guess, total_dim), + NewtonTrustRegion() + ) +end + +# seek a matrix `L` for which `L'QL` matches the sparse matrix `gram` at every +# explicit entry of `gram`. use gradient descent starting from `guess` +function realize_gram( + gram::SparseMatrixCSC{T, <:Any}, + guess::Matrix{T}; + scaled_tol = 1e-30, + min_efficiency = 0.5, + init_rate = 1.0, + backoff = 0.9, + reg_scale = 1.1, + max_descent_steps = 200, + max_backoff_steps = 110 +) where T <: Number + # start history + history = DescentHistory{T}() + + # find the dimension of the search space + dims = size(guess) + element_dim, construction_dim = dims + total_dim = element_dim * construction_dim + + # list the constrained entries of the gram matrix + J, K, _ = findnz(gram) + constrained = zip(J, K) + + # scale the tolerance + scale_adjustment = sqrt(T(length(constrained))) + tol = scale_adjustment * scaled_tol + + # initialize variables + grad_rate = init_rate + L = copy(guess) + + # use Newton's method with backtracking and gradient descent backup + Δ_proj = proj_diff(gram, L'*Q*L) + loss = dot(Δ_proj, Δ_proj) + for step in 1:max_descent_steps + # stop if the loss is tolerably low + if loss < tol + break + end + + # find the negative gradient of loss function + neg_grad = 4*Q*L*Δ_proj + + # find the negative Hessian of the loss function + hess = Matrix{T}(undef, total_dim, total_dim) + indices = [(j, k) for k in 1:construction_dim for j in 1:element_dim] + for (j, k) in indices + basis_mat = basis_matrix(T, j, k, dims) + neg_dΔ = basis_mat'*Q*L + L'*Q*basis_mat + neg_dΔ_proj = proj_to_entries(neg_dΔ, constrained) + deriv_grad = 4*Q*(-basis_mat*Δ_proj + L*neg_dΔ_proj) + hess[:, (k-1)*element_dim + j] = reshape(deriv_grad, total_dim) + end + hess = Hermitian(hess) + push!(history.hess, hess) + + # choose a base step: the Newton step if the Hessian is non-singular, and + # the gradient descent direction otherwise + #= + sing = false + base_step = try + reshape(hess \ reshape(neg_grad, total_dim), dims) + catch ex + if isa(ex, SingularException) + sing = true + normalize(neg_grad) + else + throw(ex) + end + end + =# + #= + if !sing + rate = one(T) + end + =# + #= + if cond(Float64.(hess)) < 1e5 + sing = false + base_step = reshape(hess \ reshape(neg_grad, total_dim), dims) + else + sing = true + base_step = normalize(neg_grad) + end + =# + #= + if cond(Float64.(hess)) > 1e3 + sing = true + hess += big"1e-5"*I + else + sing = false + end + base_step = reshape(hess \ reshape(neg_grad, total_dim), dims) + =# + min_eigval = minimum(eigvals(hess)) + if min_eigval < 0 + hess -= reg_scale * min_eigval * I + end + push!(history.used_grad, false) + base_step = reshape(hess \ reshape(neg_grad, total_dim), dims) + push!(history.base_step, base_step) + #= + push!(history.used_grad, sing) + =# + + # store the current position, loss, and slope + L_last = L + loss_last = loss + push!(history.scaled_loss, loss / scale_adjustment) + push!(history.neg_grad, neg_grad) + push!(history.slope, norm(neg_grad)) + + # find a good step size using backtracking line search + push!(history.stepsize, 0) + push!(history.backoff_steps, max_backoff_steps) + empty!(history.last_line_L) + empty!(history.last_line_loss) + rate = one(T) + for backoff_steps in 0:max_backoff_steps + history.stepsize[end] = rate + + # try Newton step, but not on the first step. doing at least one step of + # gradient descent seems to help prevent getting stuck, for some reason? + if step > 0 + L = L_last + rate * base_step + Δ_proj = proj_diff(gram, L'*Q*L) + loss = dot(Δ_proj, Δ_proj) + improvement = loss_last - loss + push!(history.last_line_L, L) + push!(history.last_line_loss, loss / scale_adjustment) + if improvement >= min_efficiency * rate * dot(neg_grad, base_step) + history.backoff_steps[end] = backoff_steps + break + end + end + + # try gradient descent step + slope = norm(neg_grad) + dir = neg_grad / slope + L = L_last + rate * grad_rate * dir + Δ_proj = proj_diff(gram, L'*Q*L) + loss = dot(Δ_proj, Δ_proj) + improvement = loss_last - loss + if improvement >= min_efficiency * rate * grad_rate * slope + grad_rate *= rate + history.used_grad[end] = true + history.backoff_steps[end] = backoff_steps + break + end + + rate *= backoff + end + + # [DEBUG] if we've hit a wall, quit + if history.backoff_steps[end] == max_backoff_steps + return L_last, history + end + end + + # return the factorization and its history + push!(history.scaled_loss, loss / scale_adjustment) + L, history +end + end \ No newline at end of file diff --git a/engine-proto/gram-test/circles-in-triangle.jl b/engine-proto/gram-test/circles-in-triangle.jl index b031711..2f6caa7 100644 --- a/engine-proto/gram-test/circles-in-triangle.jl +++ b/engine-proto/gram-test/circles-in-triangle.jl @@ -81,16 +81,23 @@ guess = hcat( =# # complete the gram matrix using gradient descent followed by Newton's method +#= L, history = Engine.realize_gram_gradient(gram, guess, scaled_tol = 0.01) L_pol, history_pol = Engine.realize_gram_newton(gram, L, rate = 0.3, scaled_tol = 1e-9) L_pol2, history_pol2 = Engine.realize_gram_newton(gram, L_pol) +=# +L, history = Engine.realize_gram(Float64.(gram), Float64.(guess)) completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") display(completed_gram) +#= println( "\nSteps: ", size(history.scaled_loss, 1), " + ", size(history_pol.scaled_loss, 1), " + ", size(history_pol2.scaled_loss, 1) ) -println("Loss: ", history_pol2.scaled_loss[end], "\n") \ No newline at end of file +println("Loss: ", history_pol2.scaled_loss[end], "\n") +=# +println("\nSteps: ", size(history.scaled_loss, 1)) +println("Loss: ", history.scaled_loss[end], "\n") \ No newline at end of file diff --git a/engine-proto/gram-test/overlapping-pyramids.jl b/engine-proto/gram-test/overlapping-pyramids.jl index 51606e1..ae6fb88 100644 --- a/engine-proto/gram-test/overlapping-pyramids.jl +++ b/engine-proto/gram-test/overlapping-pyramids.jl @@ -47,17 +47,25 @@ guess = hcat( Engine.rand_on_shell(fill(BigFloat(-1), 2)) ) -# complete the gram matrix using gradient descent followed by Newton's method +# complete the gram matrix +#= L, history = Engine.realize_gram_gradient(gram, guess, scaled_tol = 0.01) L_pol, history_pol = Engine.realize_gram_newton(gram, L) +=# +L, history = Engine.realize_gram(Float64.(gram), Float64.(guess)) completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") display(completed_gram) +#= println("\nSteps: ", size(history.scaled_loss, 1), " + ", size(history_pol.scaled_loss, 1)) println("Loss: ", history_pol.scaled_loss[end], "\n") +=# +println("\nSteps: ", size(history.scaled_loss, 1)) +println("Loss: ", history.scaled_loss[end], "\n") # === algebraic check === +#= R, gens = polynomial_ring(AbstractAlgebra.Rationals{BigInt}(), ["x", "t₁", "t₂", "t₃"]) x = gens[1] t = gens[2:4] @@ -85,3 +93,4 @@ x_constraint = 25//16 * to_univariate(S, evaluate(rank_constraints[1], [2], [ind t₂_constraint = 25//16 * to_univariate(S, evaluate(rank_constraints[3], [2], [indep_val])) x_vals = PolynomialRoots.roots(x_constraint.coeffs) t₂_vals = PolynomialRoots.roots(t₂_constraint.coeffs) +=# \ No newline at end of file diff --git a/engine-proto/gram-test/sphere-in-tetrahedron.jl b/engine-proto/gram-test/sphere-in-tetrahedron.jl index dc5588c..6b428cb 100644 --- a/engine-proto/gram-test/sphere-in-tetrahedron.jl +++ b/engine-proto/gram-test/sphere-in-tetrahedron.jl @@ -33,8 +33,11 @@ guess = sqrt(1/BigFloat(3)) * BigFloat[ 1 1 1 1 1 ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)) -# complete the gram matrix using Newton's method +# complete the gram matrix +#= L, history = Engine.realize_gram_newton(gram, guess) +=# +L, history = Engine.realize_gram(gram, guess, max_descent_steps = 50) completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") display(completed_gram) -- 2.34.1 From 7b3efbc385c5561ee19dfc6b3322d7e79748d65e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 15 Jul 2024 13:15:15 -0700 Subject: [PATCH 077/129] Clean up backtracking gradient descent code Drop experimental singularity handling strategies. Reduce the default tolerance to within 64-bit floating point precision. Report success. --- engine-proto/gram-test/Engine.jl | 96 ++++--------------- engine-proto/gram-test/circles-in-triangle.jl | 9 +- .../gram-test/overlapping-pyramids.jl | 9 +- .../gram-test/sphere-in-tetrahedron.jl | 9 +- 4 files changed, 41 insertions(+), 82 deletions(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index 78d1409..e52982d 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -6,7 +6,9 @@ using SparseArrays using Random using Optim -export rand_on_shell, Q, DescentHistory, realize_gram +export + rand_on_shell, Q, DescentHistory, + realize_gram_gradient, realize_gram_newton, realize_gram_optim, realize_gram # === guessing === @@ -82,7 +84,7 @@ struct DescentHistory{T} hess::Array{Hermitian{T, Matrix{T}}} slope::Array{T} stepsize::Array{T} - used_grad::Array{Bool} + positive::Array{Bool} backoff_steps::Array{Int64} last_line_L::Array{Matrix{T}} last_line_loss::Array{T} @@ -94,12 +96,12 @@ struct DescentHistory{T} base_step = Array{Matrix{T}}(undef, 0), slope = Array{T}(undef, 0), stepsize = Array{T}(undef, 0), - used_grad = Bool[], + positive = Bool[], backoff_steps = Int64[], last_line_L = Array{Matrix{T}}(undef, 0), last_line_loss = Array{T}(undef, 0) ) where T - new(scaled_loss, neg_grad, hess, base_step, slope, stepsize, used_grad, backoff_steps, last_line_L, last_line_loss) + new(scaled_loss, neg_grad, hess, base_step, slope, stepsize, positive, backoff_steps, last_line_L, last_line_loss) end end @@ -305,7 +307,7 @@ end function realize_gram( gram::SparseMatrixCSC{T, <:Any}, guess::Matrix{T}; - scaled_tol = 1e-30, + scaled_tol = 1e-16, min_efficiency = 0.5, init_rate = 1.0, backoff = 0.9, @@ -358,54 +360,14 @@ function realize_gram( hess = Hermitian(hess) push!(history.hess, hess) - # choose a base step: the Newton step if the Hessian is non-singular, and - # the gradient descent direction otherwise - #= - sing = false - base_step = try - reshape(hess \ reshape(neg_grad, total_dim), dims) - catch ex - if isa(ex, SingularException) - sing = true - normalize(neg_grad) - else - throw(ex) - end - end - =# - #= - if !sing - rate = one(T) - end - =# - #= - if cond(Float64.(hess)) < 1e5 - sing = false - base_step = reshape(hess \ reshape(neg_grad, total_dim), dims) - else - sing = true - base_step = normalize(neg_grad) - end - =# - #= - if cond(Float64.(hess)) > 1e3 - sing = true - hess += big"1e-5"*I - else - sing = false - end - base_step = reshape(hess \ reshape(neg_grad, total_dim), dims) - =# + # regularize the Hessian min_eigval = minimum(eigvals(hess)) - if min_eigval < 0 + push!(history.positive, min_eigval > 0) + if min_eigval <= 0 hess -= reg_scale * min_eigval * I end - push!(history.used_grad, false) base_step = reshape(hess \ reshape(neg_grad, total_dim), dims) push!(history.base_step, base_step) - #= - push!(history.used_grad, sing) - =# # store the current position, loss, and slope L_last = L @@ -420,50 +382,32 @@ function realize_gram( empty!(history.last_line_L) empty!(history.last_line_loss) rate = one(T) + step_success = false for backoff_steps in 0:max_backoff_steps history.stepsize[end] = rate - - # try Newton step, but not on the first step. doing at least one step of - # gradient descent seems to help prevent getting stuck, for some reason? - if step > 0 - L = L_last + rate * base_step - Δ_proj = proj_diff(gram, L'*Q*L) - loss = dot(Δ_proj, Δ_proj) - improvement = loss_last - loss - push!(history.last_line_L, L) - push!(history.last_line_loss, loss / scale_adjustment) - if improvement >= min_efficiency * rate * dot(neg_grad, base_step) - history.backoff_steps[end] = backoff_steps - break - end - end - - # try gradient descent step - slope = norm(neg_grad) - dir = neg_grad / slope - L = L_last + rate * grad_rate * dir + L = L_last + rate * base_step Δ_proj = proj_diff(gram, L'*Q*L) loss = dot(Δ_proj, Δ_proj) improvement = loss_last - loss - if improvement >= min_efficiency * rate * grad_rate * slope - grad_rate *= rate - history.used_grad[end] = true + push!(history.last_line_L, L) + push!(history.last_line_loss, loss / scale_adjustment) + if improvement >= min_efficiency * rate * dot(neg_grad, base_step) history.backoff_steps[end] = backoff_steps + step_success = true break end - rate *= backoff end - # [DEBUG] if we've hit a wall, quit - if history.backoff_steps[end] == max_backoff_steps - return L_last, history + # if we've hit a wall, quit + if !step_success + return L_last, false, history end end # return the factorization and its history push!(history.scaled_loss, loss / scale_adjustment) - L, history + L, true, history end end \ No newline at end of file diff --git a/engine-proto/gram-test/circles-in-triangle.jl b/engine-proto/gram-test/circles-in-triangle.jl index 2f6caa7..a919b8e 100644 --- a/engine-proto/gram-test/circles-in-triangle.jl +++ b/engine-proto/gram-test/circles-in-triangle.jl @@ -86,7 +86,7 @@ L, history = Engine.realize_gram_gradient(gram, guess, scaled_tol = 0.01) L_pol, history_pol = Engine.realize_gram_newton(gram, L, rate = 0.3, scaled_tol = 1e-9) L_pol2, history_pol2 = Engine.realize_gram_newton(gram, L_pol) =# -L, history = Engine.realize_gram(Float64.(gram), Float64.(guess)) +L, success, history = Engine.realize_gram(Float64.(gram), Float64.(guess)) completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") display(completed_gram) @@ -99,5 +99,10 @@ println( ) println("Loss: ", history_pol2.scaled_loss[end], "\n") =# -println("\nSteps: ", size(history.scaled_loss, 1)) +if success + println("\nTarget accuracy achieved!") +else + println("\nFailed to reach target accuracy") +end +println("Steps: ", size(history.scaled_loss, 1)) println("Loss: ", history.scaled_loss[end], "\n") \ No newline at end of file diff --git a/engine-proto/gram-test/overlapping-pyramids.jl b/engine-proto/gram-test/overlapping-pyramids.jl index ae6fb88..3c16e35 100644 --- a/engine-proto/gram-test/overlapping-pyramids.jl +++ b/engine-proto/gram-test/overlapping-pyramids.jl @@ -52,7 +52,7 @@ guess = hcat( L, history = Engine.realize_gram_gradient(gram, guess, scaled_tol = 0.01) L_pol, history_pol = Engine.realize_gram_newton(gram, L) =# -L, history = Engine.realize_gram(Float64.(gram), Float64.(guess)) +L, success, history = Engine.realize_gram(Float64.(gram), Float64.(guess)) completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") display(completed_gram) @@ -60,7 +60,12 @@ display(completed_gram) println("\nSteps: ", size(history.scaled_loss, 1), " + ", size(history_pol.scaled_loss, 1)) println("Loss: ", history_pol.scaled_loss[end], "\n") =# -println("\nSteps: ", size(history.scaled_loss, 1)) +if success + println("\nTarget accuracy achieved!") +else + println("\nFailed to reach target accuracy") +end +println("Steps: ", size(history.scaled_loss, 1)) println("Loss: ", history.scaled_loss[end], "\n") # === algebraic check === diff --git a/engine-proto/gram-test/sphere-in-tetrahedron.jl b/engine-proto/gram-test/sphere-in-tetrahedron.jl index 6b428cb..273d3ad 100644 --- a/engine-proto/gram-test/sphere-in-tetrahedron.jl +++ b/engine-proto/gram-test/sphere-in-tetrahedron.jl @@ -37,9 +37,14 @@ guess = sqrt(1/BigFloat(3)) * BigFloat[ #= L, history = Engine.realize_gram_newton(gram, guess) =# -L, history = Engine.realize_gram(gram, guess, max_descent_steps = 50) +L, success, history = Engine.realize_gram(gram, guess) completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") display(completed_gram) -println("\nSteps: ", size(history.scaled_loss, 1)) +if success + println("\nTarget accuracy achieved!") +else + println("\nFailed to reach target accuracy") +end +println("Steps: ", size(history.scaled_loss, 1)) println("Loss: ", history.scaled_loss[end], "\n") \ No newline at end of file -- 2.34.1 From 53d8c380479546ed2b0cf2735502ba4afb671e1c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 15 Jul 2024 14:08:57 -0700 Subject: [PATCH 078/129] Preserve explicit zeros in Gram matrix conversion In previous commits, the `circles-in-triangle` example converged much more slowly in BigFloat precision than in Float64 precision. This turned out to be a sign of a bug in the Float64 computation: converting the Gram matrix using `Float64.()` dropped the explicit zeros, removing many constraints and making the problem much easier to solve. This commit corrects the Gram matrix conversion. The Float64 search now solves the same problem as the BigFloat search, with comparable performance. --- engine-proto/gram-test/Engine.jl | 5 +++++ engine-proto/gram-test/circles-in-triangle.jl | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index e52982d..01ca9a2 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -254,6 +254,11 @@ end LinearAlgebra.eigen!(A::Symmetric{BigFloat, Matrix{BigFloat}}; sortby::Nothing) = eigen!(Hermitian(A)) +function convertnz(type, mat) + J, K, values = findnz(mat) + sparse(J, K, type.(values)) +end + function realize_gram_optim( gram::SparseMatrixCSC{T, <:Any}, guess::Matrix{T} diff --git a/engine-proto/gram-test/circles-in-triangle.jl b/engine-proto/gram-test/circles-in-triangle.jl index a919b8e..aa24c0f 100644 --- a/engine-proto/gram-test/circles-in-triangle.jl +++ b/engine-proto/gram-test/circles-in-triangle.jl @@ -86,7 +86,7 @@ L, history = Engine.realize_gram_gradient(gram, guess, scaled_tol = 0.01) L_pol, history_pol = Engine.realize_gram_newton(gram, L, rate = 0.3, scaled_tol = 1e-9) L_pol2, history_pol2 = Engine.realize_gram_newton(gram, L_pol) =# -L, success, history = Engine.realize_gram(Float64.(gram), Float64.(guess)) +L, success, history = Engine.realize_gram(Engine.convertnz(Float64, gram), Float64.(guess)) completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") display(completed_gram) -- 2.34.1 From 94e0d321d5175f15e6a0a1f356ec8eff0c9b4b4a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 15 Jul 2024 14:31:30 -0700 Subject: [PATCH 079/129] Switch back to BigFloat precision in examples --- engine-proto/gram-test/Engine.jl | 2 +- engine-proto/gram-test/circles-in-triangle.jl | 2 +- engine-proto/gram-test/overlapping-pyramids.jl | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index 01ca9a2..0a4303b 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -312,7 +312,7 @@ end function realize_gram( gram::SparseMatrixCSC{T, <:Any}, guess::Matrix{T}; - scaled_tol = 1e-16, + scaled_tol = 1e-30, min_efficiency = 0.5, init_rate = 1.0, backoff = 0.9, diff --git a/engine-proto/gram-test/circles-in-triangle.jl b/engine-proto/gram-test/circles-in-triangle.jl index aa24c0f..12975c2 100644 --- a/engine-proto/gram-test/circles-in-triangle.jl +++ b/engine-proto/gram-test/circles-in-triangle.jl @@ -86,7 +86,7 @@ L, history = Engine.realize_gram_gradient(gram, guess, scaled_tol = 0.01) L_pol, history_pol = Engine.realize_gram_newton(gram, L, rate = 0.3, scaled_tol = 1e-9) L_pol2, history_pol2 = Engine.realize_gram_newton(gram, L_pol) =# -L, success, history = Engine.realize_gram(Engine.convertnz(Float64, gram), Float64.(guess)) +L, success, history = Engine.realize_gram(gram, guess) completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") display(completed_gram) diff --git a/engine-proto/gram-test/overlapping-pyramids.jl b/engine-proto/gram-test/overlapping-pyramids.jl index 3c16e35..ee4c1fc 100644 --- a/engine-proto/gram-test/overlapping-pyramids.jl +++ b/engine-proto/gram-test/overlapping-pyramids.jl @@ -52,7 +52,7 @@ guess = hcat( L, history = Engine.realize_gram_gradient(gram, guess, scaled_tol = 0.01) L_pol, history_pol = Engine.realize_gram_newton(gram, L) =# -L, success, history = Engine.realize_gram(Float64.(gram), Float64.(guess)) +L, success, history = Engine.realize_gram(gram, guess) completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") display(completed_gram) -- 2.34.1 From b185fd4b83ff494e36967008ae2f207a190eb610 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 15 Jul 2024 15:52:38 -0700 Subject: [PATCH 080/129] Switch to backtracking Newton's method in Optim This performs much better than the trust region Newton's method for the actual `circles-in-triangle` problem. (The trust region method performs better for the simplified problem produced by the conversion bug.) --- engine-proto/gram-test/Engine.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index 0a4303b..10f4b02 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -303,7 +303,7 @@ function realize_gram_optim( optimize( loss, loss_grad!, loss_hess!, reshape(guess, total_dim), - NewtonTrustRegion() + Newton() ) end -- 2.34.1 From 1ce609836bf45c16131bdec879c6c3fb613385af Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 15 Jul 2024 22:11:54 -0700 Subject: [PATCH 081/129] Implement frozen variables --- engine-proto/gram-test/Engine.jl | 30 +++++++++++++++++-- engine-proto/gram-test/circles-in-triangle.jl | 3 +- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index 10f4b02..dccb514 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -311,7 +311,8 @@ end # explicit entry of `gram`. use gradient descent starting from `guess` function realize_gram( gram::SparseMatrixCSC{T, <:Any}, - guess::Matrix{T}; + guess::Matrix{T}, + frozen = nothing; scaled_tol = 1e-30, min_efficiency = 0.5, init_rate = 1.0, @@ -336,6 +337,15 @@ function realize_gram( scale_adjustment = sqrt(T(length(constrained))) tol = scale_adjustment * scaled_tol + # list the un-frozen indices + has_frozen = !isnothing(frozen) + if has_frozen + is_unfrozen = fill(true, size(guess)) + is_unfrozen[frozen] .= false + unfrozen = findall(is_unfrozen) + unfrozen_stacked = reshape(is_unfrozen, total_dim) + end + # initialize variables grad_rate = init_rate L = copy(guess) @@ -371,7 +381,23 @@ function realize_gram( if min_eigval <= 0 hess -= reg_scale * min_eigval * I end - base_step = reshape(hess \ reshape(neg_grad, total_dim), dims) + + # compute the Newton step + neg_grad_stacked = reshape(neg_grad, total_dim) + if has_frozen + hess = hess[unfrozen_stacked, unfrozen_stacked] + neg_grad_compressed = neg_grad_stacked[unfrozen_stacked] + else + neg_grad_compressed = neg_grad_stacked + end + base_step_compressed = hess \ neg_grad_compressed + if has_frozen + base_step_stacked = zeros(total_dim) + base_step_stacked[unfrozen_stacked] .= base_step_compressed + else + base_step_stacked = base_step_compressed + end + base_step = reshape(base_step_stacked, dims) push!(history.base_step, base_step) # store the current position, loss, and slope diff --git a/engine-proto/gram-test/circles-in-triangle.jl b/engine-proto/gram-test/circles-in-triangle.jl index 12975c2..b173ed9 100644 --- a/engine-proto/gram-test/circles-in-triangle.jl +++ b/engine-proto/gram-test/circles-in-triangle.jl @@ -66,6 +66,7 @@ guess = hcat( Engine.sphere(BigFloat[cos(pi/3), sin(pi/3), 0], BigFloat(1//5)), BigFloat[0, 0, 0, 1, 1] ) +frozen = [CartesianIndex(j, 9) for j in 4:5] #= guess = hcat( Engine.plane(BigFloat[0, 0, 1], BigFloat(0)), @@ -86,7 +87,7 @@ L, history = Engine.realize_gram_gradient(gram, guess, scaled_tol = 0.01) L_pol, history_pol = Engine.realize_gram_newton(gram, L, rate = 0.3, scaled_tol = 1e-9) L_pol2, history_pol2 = Engine.realize_gram_newton(gram, L_pol) =# -L, success, history = Engine.realize_gram(gram, guess) +L, success, history = Engine.realize_gram(gram, guess, frozen) completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") display(completed_gram) -- 2.34.1 From 7c77481f5e9990bc5a9723b64a7bd7532943ce4d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 15 Jul 2024 23:39:05 -0700 Subject: [PATCH 082/129] Don't constrain self-product of frozen vector --- engine-proto/gram-test/circles-in-triangle.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/engine-proto/gram-test/circles-in-triangle.jl b/engine-proto/gram-test/circles-in-triangle.jl index b173ed9..dd01ebf 100644 --- a/engine-proto/gram-test/circles-in-triangle.jl +++ b/engine-proto/gram-test/circles-in-triangle.jl @@ -10,19 +10,19 @@ values = BigFloat[] for j in 1:9 for k in 1:9 filled = false - if j == k - push!(values, j < 9 ? 1 : 0) - filled = true - elseif (j == 9) + if j == 9 if (k <= 5 && k != 2) push!(values, 0) filled = true end - elseif (k == 9) + elseif k == 9 if (j <= 5 && j != 2) push!(values, 0) filled = true end + elseif j == k + push!(values, 1) + filled = true elseif (j == 1 || k == 1) push!(values, 0) filled = true -- 2.34.1 From e6cf08a9b3535bbb2c2b364f669e76534a123dce Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 15 Jul 2024 23:54:59 -0700 Subject: [PATCH 083/129] Make tetrahedron faces planar --- .../gram-test/sphere-in-tetrahedron.jl | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/engine-proto/gram-test/sphere-in-tetrahedron.jl b/engine-proto/gram-test/sphere-in-tetrahedron.jl index 273d3ad..1d02d45 100644 --- a/engine-proto/gram-test/sphere-in-tetrahedron.jl +++ b/engine-proto/gram-test/sphere-in-tetrahedron.jl @@ -8,16 +8,32 @@ using Random J = Int64[] K = Int64[] values = BigFloat[] -for j in 1:5 - for k in 1:5 - push!(J, j) - push!(K, k) - if j == k +for j in 1:6 + for k in 1:6 + filled = false + if j == 6 + if k <= 4 + push!(values, 0) + filled = true + end + elseif k == 6 + if j <= 4 + push!(values, 0) + filled = true + end + elseif j == k push!(values, 1) + filled = true elseif (j <= 4 && k <= 4) push!(values, -1/BigFloat(3)) + filled = true else push!(values, -1) + filled = true + end + if filled + push!(J, j) + push!(K, k) end end end @@ -25,19 +41,23 @@ gram = sparse(J, K, values) # set initial guess Random.seed!(99230) -guess = sqrt(1/BigFloat(3)) * BigFloat[ - 1 1 -1 -1 0 - 1 -1 1 -1 0 - 1 -1 -1 1 0 - 1 1 1 1 -2 - 1 1 1 1 1 -] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)) +guess = hcat( + sqrt(1/BigFloat(3)) * BigFloat[ + 1 1 -1 -1 0 + 1 -1 1 -1 0 + 1 -1 -1 1 0 + 1 1 1 1 -2 + 1 1 1 1 1 + ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)), + BigFloat[0, 0, 0, 1, 1] +) +frozen = [CartesianIndex(j, 6) for j in 1:5] # complete the gram matrix #= L, history = Engine.realize_gram_newton(gram, guess) =# -L, success, history = Engine.realize_gram(gram, guess) +L, success, history = Engine.realize_gram(gram, guess, frozen) completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") display(completed_gram) -- 2.34.1 From bde42ebac0548d1665de75af7722301229f95d24 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 14:30:43 -0700 Subject: [PATCH 084/129] Switch engine to light cone basis --- engine-proto/ConstructionViewer.jl | 6 ++++-- engine-proto/gram-test/Engine.jl | 14 ++++++++++++-- engine-proto/gram-test/circles-in-triangle.jl | 2 +- engine-proto/gram-test/overlapping-pyramids.jl | 2 +- engine-proto/gram-test/sphere-in-tetrahedron.jl | 2 +- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/engine-proto/ConstructionViewer.jl b/engine-proto/ConstructionViewer.jl index 29af212..c4845fa 100644 --- a/engine-proto/ConstructionViewer.jl +++ b/engine-proto/ConstructionViewer.jl @@ -4,6 +4,8 @@ using Blink using Colors using Printf +using Main.Engine + export ConstructionViewer, display!, opentools!, closetools! # === Blink utilities === @@ -133,7 +135,7 @@ mprod(v, w) = function display!(viewer::ConstructionViewer, elements::Matrix) # load elements elements_full = [] - for elt in eachcol(elements) + for elt in eachcol(Engine.unmix * elements) if mprod(elt, elt) < 0.5 elt_full = [0; elt; fill(0, 26)] else @@ -206,7 +208,7 @@ end # ~~~ sandbox setup ~~~ # in the default view, e4 + e5 is the point at infinity -elements = sqrt(0.5) * BigFloat[ +elements = Engine.nullmix * sqrt(0.5) * BigFloat[ 1 1 -1 -1 0; 1 -1 1 -1 0; 1 -1 -1 1 0; diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index dccb514..93102f8 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -39,11 +39,16 @@ rand_on_shell(shells::Array{<:Number}) = rand_on_shell(Random.default_rng(), she # === elements === +## [temp] in light cone coordinates +point(pos) = [pos; 1; dot(pos, pos)] + +## [temp] in standard coordinates plane(normal, offset) = [normal; offset; offset] +## [temp] in standard coordinates function sphere(center, radius) dist_sq = dot(center, center) - return [ + [ center / radius; 0.5 * ((dist_sq - 1) / radius - radius); 0.5 * ((dist_sq + 1) / radius - radius) @@ -52,8 +57,13 @@ end # === Gram matrix realization === +# basis changes +nullmix = [Matrix{Int64}(I, 3, 3) zeros(Int64, 3, 2); zeros(Int64, 2, 3) [1 -1; 1 1]//2] +unmix = [Matrix{Int64}(I, 3, 3) zeros(Int64, 3, 2); zeros(Int64, 2, 3) [1 1; -1 1]] + # the Lorentz form -Q = diagm([1, 1, 1, 1, -1]) +## [old] Q = diagm([1, 1, 1, 1, -1]) +Q = [Matrix{Int64}(I, 3, 3) zeros(Int64, 3, 2); zeros(Int64, 2, 3) [0 2; 2 0]] # project a matrix onto the subspace of matrices whose entries vanish at the # given indices diff --git a/engine-proto/gram-test/circles-in-triangle.jl b/engine-proto/gram-test/circles-in-triangle.jl index dd01ebf..ec6e1e0 100644 --- a/engine-proto/gram-test/circles-in-triangle.jl +++ b/engine-proto/gram-test/circles-in-triangle.jl @@ -55,7 +55,7 @@ gram = sparse(J, K, values) ## guess = Engine.rand_on_shell(fill(BigFloat(-1), 8)) # set initial guess -guess = hcat( +guess = Engine.nullmix * hcat( Engine.plane(BigFloat[0, 0, 1], BigFloat(0)), Engine.sphere(BigFloat[0, 0, 0], BigFloat(1//2)), Engine.plane(BigFloat[1, 0, 0], BigFloat(1)), diff --git a/engine-proto/gram-test/overlapping-pyramids.jl b/engine-proto/gram-test/overlapping-pyramids.jl index ee4c1fc..706a334 100644 --- a/engine-proto/gram-test/overlapping-pyramids.jl +++ b/engine-proto/gram-test/overlapping-pyramids.jl @@ -36,7 +36,7 @@ gram[1, 6] = gram[6, 1] # in this initial guess, the mutual tangency condition is satisfied for spheres # 1 through 5 Random.seed!(50793) -guess = hcat( +guess = Engine.nullmix * hcat( sqrt(1/BigFloat(2)) * BigFloat[ 1 1 -1 -1 0; 1 -1 1 -1 0; diff --git a/engine-proto/gram-test/sphere-in-tetrahedron.jl b/engine-proto/gram-test/sphere-in-tetrahedron.jl index 1d02d45..6bfb2c0 100644 --- a/engine-proto/gram-test/sphere-in-tetrahedron.jl +++ b/engine-proto/gram-test/sphere-in-tetrahedron.jl @@ -41,7 +41,7 @@ gram = sparse(J, K, values) # set initial guess Random.seed!(99230) -guess = hcat( +guess = Engine.nullmix * hcat( sqrt(1/BigFloat(3)) * BigFloat[ 1 1 -1 -1 0 1 -1 1 -1 0 -- 2.34.1 From 2038103d805dce9e39f61bac3b2e3cfbf1b11c2c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 15:37:14 -0700 Subject: [PATCH 085/129] Write examples directly in light cone basis --- engine-proto/ConstructionViewer.jl | 18 ++++++++------- engine-proto/gram-test/Engine.jl | 11 ++++----- engine-proto/gram-test/circles-in-triangle.jl | 4 ++-- .../gram-test/overlapping-pyramids.jl | 23 +++++++++++-------- .../gram-test/sphere-in-tetrahedron.jl | 8 +++---- 5 files changed, 33 insertions(+), 31 deletions(-) diff --git a/engine-proto/ConstructionViewer.jl b/engine-proto/ConstructionViewer.jl index c4845fa..8cfa632 100644 --- a/engine-proto/ConstructionViewer.jl +++ b/engine-proto/ConstructionViewer.jl @@ -207,14 +207,16 @@ end # ~~~ sandbox setup ~~~ -# in the default view, e4 + e5 is the point at infinity -elements = Engine.nullmix * sqrt(0.5) * BigFloat[ - 1 1 -1 -1 0; - 1 -1 1 -1 0; - 1 -1 -1 1 0; - 0 0 0 0 -sqrt(6); - 1 1 1 1 2 -] +elements = begin + const a = sqrt(BigFloat(3)/2) + sqrt(0.5) * BigFloat[ + 1 1 -1 -1 0 + 1 -1 1 -1 0 + 1 -1 -1 1 0 + -0.5 -0.5 -0.5 -0.5 -a-1 + 0.5 0.5 0.5 0.5 -a+1 + ] +end # show construction viewer = Viewer.ConstructionViewer() diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index 93102f8..920b043 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -29,7 +29,7 @@ function rand_on_shell(rng::AbstractRNG, shell::T) where T <: Number space_part = rand_on_sphere(rng, T, 4) rapidity = randn(rng, T) sig = sign(shell) - [sconh(rapidity, sig)*space_part; sconh(rapidity, -sig)] + nullmix * [sconh(rapidity, sig)*space_part; sconh(rapidity, -sig)] end rand_on_shell(rng::AbstractRNG, shells::Array{T}) where T <: Number = @@ -39,19 +39,16 @@ rand_on_shell(shells::Array{<:Number}) = rand_on_shell(Random.default_rng(), she # === elements === -## [temp] in light cone coordinates point(pos) = [pos; 1; dot(pos, pos)] -## [temp] in standard coordinates -plane(normal, offset) = [normal; offset; offset] +plane(normal, offset) = [normal; 0; offset] -## [temp] in standard coordinates function sphere(center, radius) dist_sq = dot(center, center) [ center / radius; - 0.5 * ((dist_sq - 1) / radius - radius); - 0.5 * ((dist_sq + 1) / radius - radius) + -0.5 / radius; + 0.5 * (dist_sq / radius - radius) ] end diff --git a/engine-proto/gram-test/circles-in-triangle.jl b/engine-proto/gram-test/circles-in-triangle.jl index ec6e1e0..fc5e13d 100644 --- a/engine-proto/gram-test/circles-in-triangle.jl +++ b/engine-proto/gram-test/circles-in-triangle.jl @@ -55,7 +55,7 @@ gram = sparse(J, K, values) ## guess = Engine.rand_on_shell(fill(BigFloat(-1), 8)) # set initial guess -guess = Engine.nullmix * hcat( +guess = hcat( Engine.plane(BigFloat[0, 0, 1], BigFloat(0)), Engine.sphere(BigFloat[0, 0, 0], BigFloat(1//2)), Engine.plane(BigFloat[1, 0, 0], BigFloat(1)), @@ -64,7 +64,7 @@ guess = Engine.nullmix * hcat( Engine.sphere(BigFloat[-1, 0, 0], BigFloat(1//5)), Engine.sphere(BigFloat[cos(-pi/3), sin(-pi/3), 0], BigFloat(1//5)), Engine.sphere(BigFloat[cos(pi/3), sin(pi/3), 0], BigFloat(1//5)), - BigFloat[0, 0, 0, 1, 1] + BigFloat[0, 0, 0, 0, 1] ) frozen = [CartesianIndex(j, 9) for j in 4:5] #= diff --git a/engine-proto/gram-test/overlapping-pyramids.jl b/engine-proto/gram-test/overlapping-pyramids.jl index 706a334..8edb981 100644 --- a/engine-proto/gram-test/overlapping-pyramids.jl +++ b/engine-proto/gram-test/overlapping-pyramids.jl @@ -36,16 +36,19 @@ gram[1, 6] = gram[6, 1] # in this initial guess, the mutual tangency condition is satisfied for spheres # 1 through 5 Random.seed!(50793) -guess = Engine.nullmix * hcat( - sqrt(1/BigFloat(2)) * BigFloat[ - 1 1 -1 -1 0; - 1 -1 1 -1 0; - 1 -1 -1 1 0; - 0 0 0 0 -sqrt(BigFloat(6)); - 1 1 1 1 2; - ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)), - Engine.rand_on_shell(fill(BigFloat(-1), 2)) -) +guess = begin + const a = sqrt(BigFloat(3)/2) + hcat( + sqrt(1/BigFloat(2)) * BigFloat[ + 1 1 -1 -1 0 + 1 -1 1 -1 0 + 1 -1 -1 1 0 + -0.5 -0.5 -0.5 -0.5 -a-1 + 0.5 0.5 0.5 0.5 -a+1 + ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)), + Engine.rand_on_shell(fill(BigFloat(-1), 2)) + ) +end # complete the gram matrix #= diff --git a/engine-proto/gram-test/sphere-in-tetrahedron.jl b/engine-proto/gram-test/sphere-in-tetrahedron.jl index 6bfb2c0..1c0dda8 100644 --- a/engine-proto/gram-test/sphere-in-tetrahedron.jl +++ b/engine-proto/gram-test/sphere-in-tetrahedron.jl @@ -41,15 +41,15 @@ gram = sparse(J, K, values) # set initial guess Random.seed!(99230) -guess = Engine.nullmix * hcat( +guess = hcat( sqrt(1/BigFloat(3)) * BigFloat[ 1 1 -1 -1 0 1 -1 1 -1 0 1 -1 -1 1 0 - 1 1 1 1 -2 - 1 1 1 1 1 + 0 0 0 0 -1.5 + 1 1 1 1 -0.5 ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)), - BigFloat[0, 0, 0, 1, 1] + BigFloat[0, 0, 0, 0, 1] ) frozen = [CartesianIndex(j, 6) for j in 1:5] -- 2.34.1 From 4728959ae049f728c204e206236865427bad762c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 17:22:33 -0700 Subject: [PATCH 086/129] Give spheres positive radii in examples This changes the meaning of `indep_val` in the overlapping pyramids example, so we adjust `indep_val` to get a nice-looking construction. --- engine-proto/ConstructionViewer.jl | 4 ++-- engine-proto/gram-test/overlapping-pyramids.jl | 12 +++--------- engine-proto/gram-test/sphere-in-tetrahedron.jl | 6 +++--- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/engine-proto/ConstructionViewer.jl b/engine-proto/ConstructionViewer.jl index 8cfa632..0ce6a6d 100644 --- a/engine-proto/ConstructionViewer.jl +++ b/engine-proto/ConstructionViewer.jl @@ -213,8 +213,8 @@ elements = begin 1 1 -1 -1 0 1 -1 1 -1 0 1 -1 -1 1 0 - -0.5 -0.5 -0.5 -0.5 -a-1 - 0.5 0.5 0.5 0.5 -a+1 + 0.5 0.5 0.5 0.5 a+1 + -0.5 -0.5 -0.5 -0.5 a-1 ] end diff --git a/engine-proto/gram-test/overlapping-pyramids.jl b/engine-proto/gram-test/overlapping-pyramids.jl index 8edb981..757a18c 100644 --- a/engine-proto/gram-test/overlapping-pyramids.jl +++ b/engine-proto/gram-test/overlapping-pyramids.jl @@ -23,13 +23,7 @@ end gram = sparse(J, K, values) # set the independent variable -# -# using gram[6, 2] or gram[7, 1] as the independent variable seems to stall -# convergence, even if its value comes from a known solution, like -# -# gram[6, 2] = 0.9936131705272925 -# -indep_val = -9//5 +indep_val = 2//5 gram[6, 1] = BigFloat(indep_val) gram[1, 6] = gram[6, 1] @@ -43,8 +37,8 @@ guess = begin 1 1 -1 -1 0 1 -1 1 -1 0 1 -1 -1 1 0 - -0.5 -0.5 -0.5 -0.5 -a-1 - 0.5 0.5 0.5 0.5 -a+1 + 0.5 0.5 0.5 0.5 a+1 + -0.5 -0.5 -0.5 -0.5 a-1 ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)), Engine.rand_on_shell(fill(BigFloat(-1), 2)) ) diff --git a/engine-proto/gram-test/sphere-in-tetrahedron.jl b/engine-proto/gram-test/sphere-in-tetrahedron.jl index 1c0dda8..d703321 100644 --- a/engine-proto/gram-test/sphere-in-tetrahedron.jl +++ b/engine-proto/gram-test/sphere-in-tetrahedron.jl @@ -28,7 +28,7 @@ for j in 1:6 push!(values, -1/BigFloat(3)) filled = true else - push!(values, -1) + push!(values, 1) filled = true end if filled @@ -46,8 +46,8 @@ guess = hcat( 1 1 -1 -1 0 1 -1 1 -1 0 1 -1 -1 1 0 - 0 0 0 0 -1.5 - 1 1 1 1 -0.5 + 0 0 0 0 1.5 + 1 1 1 1 0.5 ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)), BigFloat[0, 0, 0, 0, 1] ) -- 2.34.1 From ea640f48617b3423edb70344cf504fbdb2b16c1e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 17:33:32 -0700 Subject: [PATCH 087/129] Start tetrahedron radius ratio example Add the vertices of the tetrahedron to the `sphere-in-tetrahedron` example. --- .../gram-test/tetrahedron-radius-ratio.jl | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 engine-proto/gram-test/tetrahedron-radius-ratio.jl diff --git a/engine-proto/gram-test/tetrahedron-radius-ratio.jl b/engine-proto/gram-test/tetrahedron-radius-ratio.jl new file mode 100644 index 0000000..bde7272 --- /dev/null +++ b/engine-proto/gram-test/tetrahedron-radius-ratio.jl @@ -0,0 +1,87 @@ +include("Engine.jl") + +using SparseArrays +using Random + +# initialize the partial gram matrix for a sphere inscribed in a regular +# tetrahedron +J = Int64[] +K = Int64[] +values = BigFloat[] +for j in 1:10 + for k in 1:10 + filled = false + if j == 10 + if k <= 4 + push!(values, 0) + filled = true + end + elseif k == 10 + if j <= 4 + push!(values, 0) + filled = true + end + elseif j == k + push!(values, j <= 5 ? 1 : 0) + filled = true + elseif j <= 4 + if k <= 4 + push!(values, -1/BigFloat(3)) + filled = true + elseif k == 5 + push!(values, 1) + filled = true + elseif k <= 9 && k - j != 5 + push!(values, 0) + filled = true + end + elseif k <= 4 + if j == 5 + push!(values, 1) + filled = true + elseif j <= 9 && j - k != 5 + push!(values, 0) + filled = true + end + end + if filled + push!(J, j) + push!(K, k) + end + end +end +gram = sparse(J, K, values) + +# set initial guess +Random.seed!(99230) +guess = hcat( + sqrt(1/BigFloat(3)) * BigFloat[ + 1 1 -1 -1 0 + 1 -1 1 -1 0 + 1 -1 -1 1 0 + 0 0 0 0 1.5 + 1 1 1 1 0.5 + ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)), + Engine.point([-1, -1, -1]), + Engine.point([ 1, -1, -1]), + Engine.point([-1, 1, -1]), + Engine.point([ 1, -1, 1]), + BigFloat[0, 0, 0, 0, 1] +) +frozen = vcat( + [CartesianIndex(4, k) for k in 6:9], + [CartesianIndex(j, 10) for j in 1:5] +) + +# complete the gram matrix +L, success, history = Engine.realize_gram(gram, guess, frozen) +completed_gram = L'*Engine.Q*L +println("Completed Gram matrix:\n") +display(completed_gram) +if success + println("\nTarget accuracy achieved!") +else + println("\nFailed to reach target accuracy") +end +println("Steps: ", size(history.scaled_loss, 1)) +println("Loss: ", history.scaled_loss[end], "\n") \ No newline at end of file -- 2.34.1 From 5abd4ca6e16b1efc2c1ca8ad3d1610c920c41c5a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 17:49:43 -0700 Subject: [PATCH 088/129] Revert "Give spheres positive radii in examples" This reverts commit 4728959ae049f728c204e206236865427bad762c, which actually gave the spheres negative radii! I got confused by the sign convention differences between the notes and the engine. --- engine-proto/ConstructionViewer.jl | 4 ++-- engine-proto/gram-test/overlapping-pyramids.jl | 12 +++++++++--- engine-proto/gram-test/sphere-in-tetrahedron.jl | 6 +++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/engine-proto/ConstructionViewer.jl b/engine-proto/ConstructionViewer.jl index 0ce6a6d..8cfa632 100644 --- a/engine-proto/ConstructionViewer.jl +++ b/engine-proto/ConstructionViewer.jl @@ -213,8 +213,8 @@ elements = begin 1 1 -1 -1 0 1 -1 1 -1 0 1 -1 -1 1 0 - 0.5 0.5 0.5 0.5 a+1 - -0.5 -0.5 -0.5 -0.5 a-1 + -0.5 -0.5 -0.5 -0.5 -a-1 + 0.5 0.5 0.5 0.5 -a+1 ] end diff --git a/engine-proto/gram-test/overlapping-pyramids.jl b/engine-proto/gram-test/overlapping-pyramids.jl index 757a18c..8edb981 100644 --- a/engine-proto/gram-test/overlapping-pyramids.jl +++ b/engine-proto/gram-test/overlapping-pyramids.jl @@ -23,7 +23,13 @@ end gram = sparse(J, K, values) # set the independent variable -indep_val = 2//5 +# +# using gram[6, 2] or gram[7, 1] as the independent variable seems to stall +# convergence, even if its value comes from a known solution, like +# +# gram[6, 2] = 0.9936131705272925 +# +indep_val = -9//5 gram[6, 1] = BigFloat(indep_val) gram[1, 6] = gram[6, 1] @@ -37,8 +43,8 @@ guess = begin 1 1 -1 -1 0 1 -1 1 -1 0 1 -1 -1 1 0 - 0.5 0.5 0.5 0.5 a+1 - -0.5 -0.5 -0.5 -0.5 a-1 + -0.5 -0.5 -0.5 -0.5 -a-1 + 0.5 0.5 0.5 0.5 -a+1 ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)), Engine.rand_on_shell(fill(BigFloat(-1), 2)) ) diff --git a/engine-proto/gram-test/sphere-in-tetrahedron.jl b/engine-proto/gram-test/sphere-in-tetrahedron.jl index d703321..1c0dda8 100644 --- a/engine-proto/gram-test/sphere-in-tetrahedron.jl +++ b/engine-proto/gram-test/sphere-in-tetrahedron.jl @@ -28,7 +28,7 @@ for j in 1:6 push!(values, -1/BigFloat(3)) filled = true else - push!(values, 1) + push!(values, -1) filled = true end if filled @@ -46,8 +46,8 @@ guess = hcat( 1 1 -1 -1 0 1 -1 1 -1 0 1 -1 -1 1 0 - 0 0 0 0 1.5 - 1 1 1 1 0.5 + 0 0 0 0 -1.5 + 1 1 1 1 -0.5 ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)), BigFloat[0, 0, 0, 0, 1] ) -- 2.34.1 From 6d233b5ee90c32cbc83f8e242ddbdc0288abec9f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 18:08:36 -0700 Subject: [PATCH 089/129] Tetrahedron radius ratio: correct signs --- engine-proto/gram-test/tetrahedron-radius-ratio.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/engine-proto/gram-test/tetrahedron-radius-ratio.jl b/engine-proto/gram-test/tetrahedron-radius-ratio.jl index bde7272..6172aa3 100644 --- a/engine-proto/gram-test/tetrahedron-radius-ratio.jl +++ b/engine-proto/gram-test/tetrahedron-radius-ratio.jl @@ -29,7 +29,7 @@ for j in 1:10 push!(values, -1/BigFloat(3)) filled = true elseif k == 5 - push!(values, 1) + push!(values, -1) filled = true elseif k <= 9 && k - j != 5 push!(values, 0) @@ -37,7 +37,7 @@ for j in 1:10 end elseif k <= 4 if j == 5 - push!(values, 1) + push!(values, -1) filled = true elseif j <= 9 && j - k != 5 push!(values, 0) @@ -59,8 +59,8 @@ guess = hcat( 1 1 -1 -1 0 1 -1 1 -1 0 1 -1 -1 1 0 - 0 0 0 0 1.5 - 1 1 1 1 0.5 + 0 0 0 0 -1.5 + 1 1 1 1 -0.5 ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)), Engine.point([-1, -1, -1]), Engine.point([ 1, -1, -1]), -- 2.34.1 From d51d43f481e9819b2a312e82da13235a6dd638ad Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 18:27:22 -0700 Subject: [PATCH 090/129] Correct point utility --- engine-proto/gram-test/Engine.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index 920b043..bedda00 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -39,7 +39,7 @@ rand_on_shell(shells::Array{<:Number}) = rand_on_shell(Random.default_rng(), she # === elements === -point(pos) = [pos; 1; dot(pos, pos)] +point(pos) = [pos; -1; 0.25 * dot(pos, pos)] plane(normal, offset) = [normal; 0; offset] -- 2.34.1 From 6e719f9943754b1b120e728738390b26752c54c7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 18:27:58 -0700 Subject: [PATCH 091/129] Tetrahedron radius ratio: correct vertex guesses --- engine-proto/gram-test/tetrahedron-radius-ratio.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engine-proto/gram-test/tetrahedron-radius-ratio.jl b/engine-proto/gram-test/tetrahedron-radius-ratio.jl index 6172aa3..bb89268 100644 --- a/engine-proto/gram-test/tetrahedron-radius-ratio.jl +++ b/engine-proto/gram-test/tetrahedron-radius-ratio.jl @@ -63,9 +63,9 @@ guess = hcat( 1 1 1 1 -0.5 ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)), Engine.point([-1, -1, -1]), - Engine.point([ 1, -1, -1]), - Engine.point([-1, 1, -1]), + Engine.point([-1, 1, 1]), Engine.point([ 1, -1, 1]), + Engine.point([ 1, 1, -1]), BigFloat[0, 0, 0, 0, 1] ) frozen = vcat( -- 2.34.1 From a02b76544a9930d09a86d36e0ea4560c4f339c45 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 18:55:36 -0700 Subject: [PATCH 092/129] Tetrahedron radius ratio: add circumscribed sphere --- .../gram-test/tetrahedron-radius-ratio.jl | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/engine-proto/gram-test/tetrahedron-radius-ratio.jl b/engine-proto/gram-test/tetrahedron-radius-ratio.jl index bb89268..957f031 100644 --- a/engine-proto/gram-test/tetrahedron-radius-ratio.jl +++ b/engine-proto/gram-test/tetrahedron-radius-ratio.jl @@ -8,21 +8,21 @@ using Random J = Int64[] K = Int64[] values = BigFloat[] -for j in 1:10 - for k in 1:10 +for j in 1:11 + for k in 1:11 filled = false - if j == 10 + if j == 11 if k <= 4 push!(values, 0) filled = true end - elseif k == 10 + elseif k == 11 if j <= 4 push!(values, 0) filled = true end elseif j == k - push!(values, j <= 5 ? 1 : 0) + push!(values, j <= 6 ? 1 : 0) filled = true elseif j <= 4 if k <= 4 @@ -31,7 +31,7 @@ for j in 1:10 elseif k == 5 push!(values, -1) filled = true - elseif k <= 9 && k - j != 5 + elseif 7 <= k <= 10 && k - j != 6 push!(values, 0) filled = true end @@ -39,10 +39,13 @@ for j in 1:10 if j == 5 push!(values, -1) filled = true - elseif j <= 9 && j - k != 5 + elseif 7 <= j <= 10 && j - k != 6 push!(values, 0) filled = true end + elseif j == 6 && 7 <= k <= 10 || k == 6 && 7 <= j <= 10 + push!(values, 0) + filled = true end if filled push!(J, j) @@ -56,12 +59,12 @@ gram = sparse(J, K, values) Random.seed!(99230) guess = hcat( sqrt(1/BigFloat(3)) * BigFloat[ - 1 1 -1 -1 0 - 1 -1 1 -1 0 - 1 -1 -1 1 0 - 0 0 0 0 -1.5 - 1 1 1 1 -0.5 - ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)), + 1 1 -1 -1 0 0 + 1 -1 1 -1 0 0 + 1 -1 -1 1 0 0 + 0 0 0 0 -1.5 -3 + 1 1 1 1 -0.5 -1 + ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 6)), Engine.point([-1, -1, -1]), Engine.point([-1, 1, 1]), Engine.point([ 1, -1, 1]), @@ -69,8 +72,8 @@ guess = hcat( BigFloat[0, 0, 0, 0, 1] ) frozen = vcat( - [CartesianIndex(4, k) for k in 6:9], - [CartesianIndex(j, 10) for j in 1:5] + [CartesianIndex(4, k) for k in 7:10], + [CartesianIndex(j, 11) for j in 1:5] ) # complete the gram matrix -- 2.34.1 From 96ffc59642ca656193193ba6ec9d87e12cb0036f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 19:01:34 -0700 Subject: [PATCH 093/129] Tetrahedron radius ratio: tweak guess Jiggle the vertex guesses. Put the circumscribed sphere guess on-shell. --- engine-proto/gram-test/tetrahedron-radius-ratio.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/engine-proto/gram-test/tetrahedron-radius-ratio.jl b/engine-proto/gram-test/tetrahedron-radius-ratio.jl index 957f031..9e79c05 100644 --- a/engine-proto/gram-test/tetrahedron-radius-ratio.jl +++ b/engine-proto/gram-test/tetrahedron-radius-ratio.jl @@ -62,13 +62,13 @@ guess = hcat( 1 1 -1 -1 0 0 1 -1 1 -1 0 0 1 -1 -1 1 0 0 - 0 0 0 0 -1.5 -3 - 1 1 1 1 -0.5 -1 + 0 0 0 0 -1.5 -0.5 + 1 1 1 1 -0.5 -1.5 ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 6)), - Engine.point([-1, -1, -1]), - Engine.point([-1, 1, 1]), - Engine.point([ 1, -1, 1]), - Engine.point([ 1, 1, -1]), + Engine.point([-1, -1, -1] + 0.3*randn(3)), + Engine.point([-1, 1, 1] + 0.3*randn(3)), + Engine.point([ 1, -1, 1] + 0.3*randn(3)), + Engine.point([ 1, 1, -1] + 0.3*randn(3)), BigFloat[0, 0, 0, 0, 1] ) frozen = vcat( -- 2.34.1 From 01f44324c12108fd04ff33577a32eb3d770dc4a5 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 22:45:17 -0700 Subject: [PATCH 094/129] Tetrahedron radius ratio: find radius ratio --- engine-proto/gram-test/tetrahedron-radius-ratio.jl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/engine-proto/gram-test/tetrahedron-radius-ratio.jl b/engine-proto/gram-test/tetrahedron-radius-ratio.jl index 9e79c05..4218cb7 100644 --- a/engine-proto/gram-test/tetrahedron-radius-ratio.jl +++ b/engine-proto/gram-test/tetrahedron-radius-ratio.jl @@ -1,5 +1,6 @@ include("Engine.jl") +using LinearAlgebra using SparseArrays using Random @@ -87,4 +88,9 @@ else println("\nFailed to reach target accuracy") end println("Steps: ", size(history.scaled_loss, 1)) -println("Loss: ", history.scaled_loss[end], "\n") \ No newline at end of file +println("Loss: ", history.scaled_loss[end]) +if success + infty = BigFloat[0, 0, 0, 0, 1] + radius_ratio = dot(infty, Engine.Q * L[:,5]) / dot(infty, Engine.Q * L[:,6]) + println("\nCircumradius / inradius: ", radius_ratio) +end \ No newline at end of file -- 2.34.1 From 69a704d4145d581ec0c3599a05a1ae88b42698af Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 23:07:34 -0700 Subject: [PATCH 095/129] Use notes' sign convention for light cone basis --- engine-proto/ConstructionViewer.jl | 4 ++-- engine-proto/gram-test/Engine.jl | 10 +++++----- engine-proto/gram-test/overlapping-pyramids.jl | 4 ++-- engine-proto/gram-test/sphere-in-tetrahedron.jl | 2 +- engine-proto/gram-test/tetrahedron-radius-ratio.jl | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/engine-proto/ConstructionViewer.jl b/engine-proto/ConstructionViewer.jl index 8cfa632..c9b0b7a 100644 --- a/engine-proto/ConstructionViewer.jl +++ b/engine-proto/ConstructionViewer.jl @@ -213,8 +213,8 @@ elements = begin 1 1 -1 -1 0 1 -1 1 -1 0 1 -1 -1 1 0 - -0.5 -0.5 -0.5 -0.5 -a-1 - 0.5 0.5 0.5 0.5 -a+1 + 0.5 0.5 0.5 0.5 1+a + 0.5 0.5 0.5 0.5 1-a ] end diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index bedda00..73c5c31 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -39,7 +39,7 @@ rand_on_shell(shells::Array{<:Number}) = rand_on_shell(Random.default_rng(), she # === elements === -point(pos) = [pos; -1; 0.25 * dot(pos, pos)] +point(pos) = [pos; 1; 0.25 * dot(pos, pos)] plane(normal, offset) = [normal; 0; offset] @@ -47,7 +47,7 @@ function sphere(center, radius) dist_sq = dot(center, center) [ center / radius; - -0.5 / radius; + 0.5 / radius; 0.5 * (dist_sq / radius - radius) ] end @@ -55,12 +55,12 @@ end # === Gram matrix realization === # basis changes -nullmix = [Matrix{Int64}(I, 3, 3) zeros(Int64, 3, 2); zeros(Int64, 2, 3) [1 -1; 1 1]//2] -unmix = [Matrix{Int64}(I, 3, 3) zeros(Int64, 3, 2); zeros(Int64, 2, 3) [1 1; -1 1]] +nullmix = [Matrix{Int64}(I, 3, 3) zeros(Int64, 3, 2); zeros(Int64, 2, 3) [-1 1; 1 1]//2] +unmix = [Matrix{Int64}(I, 3, 3) zeros(Int64, 3, 2); zeros(Int64, 2, 3) [-1 1; 1 1]] # the Lorentz form ## [old] Q = diagm([1, 1, 1, 1, -1]) -Q = [Matrix{Int64}(I, 3, 3) zeros(Int64, 3, 2); zeros(Int64, 2, 3) [0 2; 2 0]] +Q = [Matrix{Int64}(I, 3, 3) zeros(Int64, 3, 2); zeros(Int64, 2, 3) [0 -2; -2 0]] # project a matrix onto the subspace of matrices whose entries vanish at the # given indices diff --git a/engine-proto/gram-test/overlapping-pyramids.jl b/engine-proto/gram-test/overlapping-pyramids.jl index 8edb981..0d1f018 100644 --- a/engine-proto/gram-test/overlapping-pyramids.jl +++ b/engine-proto/gram-test/overlapping-pyramids.jl @@ -43,8 +43,8 @@ guess = begin 1 1 -1 -1 0 1 -1 1 -1 0 1 -1 -1 1 0 - -0.5 -0.5 -0.5 -0.5 -a-1 - 0.5 0.5 0.5 0.5 -a+1 + 0.5 0.5 0.5 0.5 1+a + 0.5 0.5 0.5 0.5 1-a ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)), Engine.rand_on_shell(fill(BigFloat(-1), 2)) ) diff --git a/engine-proto/gram-test/sphere-in-tetrahedron.jl b/engine-proto/gram-test/sphere-in-tetrahedron.jl index 1c0dda8..631f0e5 100644 --- a/engine-proto/gram-test/sphere-in-tetrahedron.jl +++ b/engine-proto/gram-test/sphere-in-tetrahedron.jl @@ -46,7 +46,7 @@ guess = hcat( 1 1 -1 -1 0 1 -1 1 -1 0 1 -1 -1 1 0 - 0 0 0 0 -1.5 + 0 0 0 0 1.5 1 1 1 1 -0.5 ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 5)), BigFloat[0, 0, 0, 0, 1] diff --git a/engine-proto/gram-test/tetrahedron-radius-ratio.jl b/engine-proto/gram-test/tetrahedron-radius-ratio.jl index 4218cb7..ed3ceb0 100644 --- a/engine-proto/gram-test/tetrahedron-radius-ratio.jl +++ b/engine-proto/gram-test/tetrahedron-radius-ratio.jl @@ -63,7 +63,7 @@ guess = hcat( 1 1 -1 -1 0 0 1 -1 1 -1 0 0 1 -1 -1 1 0 0 - 0 0 0 0 -1.5 -0.5 + 0 0 0 0 1.5 0.5 1 1 1 1 -0.5 -1.5 ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 6)), Engine.point([-1, -1, -1] + 0.3*randn(3)), -- 2.34.1 From d0340c0b658b428eeb6293edf1006c7ce9d7f093 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 23:37:28 -0700 Subject: [PATCH 096/129] Correct point utility again The balance between the light cone basis vectors was wrong, throwing the point's coordinates off by a factor of two. --- engine-proto/gram-test/Engine.jl | 2 +- engine-proto/gram-test/tetrahedron-radius-ratio.jl | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index 73c5c31..2662a17 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -39,7 +39,7 @@ rand_on_shell(shells::Array{<:Number}) = rand_on_shell(Random.default_rng(), she # === elements === -point(pos) = [pos; 1; 0.25 * dot(pos, pos)] +point(pos) = [pos; 0.5; 0.5 * dot(pos, pos)] plane(normal, offset) = [normal; 0; offset] diff --git a/engine-proto/gram-test/tetrahedron-radius-ratio.jl b/engine-proto/gram-test/tetrahedron-radius-ratio.jl index ed3ceb0..c284078 100644 --- a/engine-proto/gram-test/tetrahedron-radius-ratio.jl +++ b/engine-proto/gram-test/tetrahedron-radius-ratio.jl @@ -65,11 +65,11 @@ guess = hcat( 1 -1 -1 1 0 0 0 0 0 0 1.5 0.5 1 1 1 1 -0.5 -1.5 - ] + 0.2*Engine.rand_on_shell(fill(BigFloat(-1), 6)), - Engine.point([-1, -1, -1] + 0.3*randn(3)), - Engine.point([-1, 1, 1] + 0.3*randn(3)), - Engine.point([ 1, -1, 1] + 0.3*randn(3)), - Engine.point([ 1, 1, -1] + 0.3*randn(3)), + ] + 0.0*Engine.rand_on_shell(fill(BigFloat(-1), 6)), + Engine.point([-0.5, -0.5, -0.5] + 0.3*randn(3)), + Engine.point([-0.5, 0.5, 0.5] + 0.3*randn(3)), + Engine.point([ 0.5, -0.5, 0.5] + 0.3*randn(3)), + Engine.point([ 0.5, 0.5, -0.5] + 0.3*randn(3)), BigFloat[0, 0, 0, 0, 1] ) frozen = vcat( -- 2.34.1 From 74c7f64b0c708b013db0175f0e729c8f3fc2bbc8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 00:03:12 -0700 Subject: [PATCH 097/129] Correct sign of normal in plane utility Clarify the relevant notes too. --- engine-proto/gram-test/Engine.jl | 2 +- engine-proto/gram-test/circles-in-triangle.jl | 6 +++--- notes/inversive.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index 2662a17..40b77b0 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -41,7 +41,7 @@ rand_on_shell(shells::Array{<:Number}) = rand_on_shell(Random.default_rng(), she point(pos) = [pos; 0.5; 0.5 * dot(pos, pos)] -plane(normal, offset) = [normal; 0; offset] +plane(normal, offset) = [-normal; 0; -offset] function sphere(center, radius) dist_sq = dot(center, center) diff --git a/engine-proto/gram-test/circles-in-triangle.jl b/engine-proto/gram-test/circles-in-triangle.jl index fc5e13d..ca49574 100644 --- a/engine-proto/gram-test/circles-in-triangle.jl +++ b/engine-proto/gram-test/circles-in-triangle.jl @@ -58,9 +58,9 @@ gram = sparse(J, K, values) guess = hcat( Engine.plane(BigFloat[0, 0, 1], BigFloat(0)), Engine.sphere(BigFloat[0, 0, 0], BigFloat(1//2)), - Engine.plane(BigFloat[1, 0, 0], BigFloat(1)), - Engine.plane(BigFloat[cos(2pi/3), sin(2pi/3), 0], BigFloat(1)), - Engine.plane(BigFloat[cos(-2pi/3), sin(-2pi/3), 0], BigFloat(1)), + Engine.plane(-BigFloat[1, 0, 0], BigFloat(-1)), + Engine.plane(-BigFloat[cos(2pi/3), sin(2pi/3), 0], BigFloat(-1)), + Engine.plane(-BigFloat[cos(-2pi/3), sin(-2pi/3), 0], BigFloat(-1)), Engine.sphere(BigFloat[-1, 0, 0], BigFloat(1//5)), Engine.sphere(BigFloat[cos(-pi/3), sin(-pi/3), 0], BigFloat(1//5)), Engine.sphere(BigFloat[cos(pi/3), sin(pi/3), 0], BigFloat(1//5)), diff --git a/notes/inversive.md b/notes/inversive.md index 6de7ef2..9cb2e19 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -7,7 +7,7 @@ These coordinates are of form $I=(c, r, x, y, z)$ where we think of $c$ as the c | Entity or Relationship | Representation | Comments/questions | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Sphere s with radius r>0 centered on P = (x,y,z) | $I_s = (1/c, 1/r, x/r, y/r, z/r)$ satisfying $Q(I_s,I_s) = -1$, i.e., $c = r/(\|P\|^2 - r^2)$. | Can also write $I_s = (\|P\|^2/r - r, 1/r, x/r. y/r, z/r)$ -- so there is no trouble if $\|E_{I_s}\| = r$, just get first coordinate to be 0. | -| Plane p with unit normal (x,y,z), a distance s from origin | $I_p = (2s, 0, x, y, z)$ | Note $Q(I_p, I_p)$ is still -1. Also, there are two representations for each plane through the origin, namely $(0,0,x,y,z)$ and $(0,0,-x,-y,-z)$ | +| Plane p with unit normal (x,y,z) through the point s(x,y,z) | $I_p = (-2s, 0, -x, -y, -z)$ | Note $Q(I_p, I_p)$ is still −1. | | Point P with Euclidean coordinates (x,y,z) | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$.  Because of this we might choose  some other scaling of the inversive coordinates, say $(\||P\||,1/\||P\||,x/\||P\||,y/\||P\||,z/\||P\||)$ instead, but that fails at the origin, and likely won't have some of the other nice properties listed below.  Note that scaling just the co-radius by $s$ and the radius by $1/s$ (which still preserves $Q=0$) dilates by a factor of $s$ about the origin, so that $(\|P\|, \|P\|, x, y, z)$, which might look symmetric, would actually have to represent the Euclidean point $(x/\||P\||, y/\||P\||, z/\||P\||)$ . | | ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by the above case. | | P lies on sphere or plane given by I | $Q(I_P, I) = 0$ | | -- 2.34.1 From 24dae6807bf9799d39a94088d02f7980d826d057 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 00:16:23 -0700 Subject: [PATCH 098/129] Clarify notes on tangency --- notes/inversive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/inversive.md b/notes/inversive.md index 9cb2e19..acac4cd 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -11,7 +11,7 @@ These coordinates are of form $I=(c, r, x, y, z)$ where we think of $c$ as the c | Point P with Euclidean coordinates (x,y,z) | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$.  Because of this we might choose  some other scaling of the inversive coordinates, say $(\||P\||,1/\||P\||,x/\||P\||,y/\||P\||,z/\||P\||)$ instead, but that fails at the origin, and likely won't have some of the other nice properties listed below.  Note that scaling just the co-radius by $s$ and the radius by $1/s$ (which still preserves $Q=0$) dilates by a factor of $s$ about the origin, so that $(\|P\|, \|P\|, x, y, z)$, which might look symmetric, would actually have to represent the Euclidean point $(x/\||P\||, y/\||P\||, z/\||P\||)$ . | | ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by the above case. | | P lies on sphere or plane given by I | $Q(I_P, I) = 0$ | | -| Sphere/planes represented by I and J are tangent | $Q(I,J) = 1$ (??, see note at right) | Seems as though this must be $Q(I,J) = \pm1$  ? For example, the $xy$ plane represented by (0,0,0,0,1)  is tangent to the unit circle centered at (0,0,1) rep'd by (0,1,0,0,1), but their Q-product is -1. And in general you can reflect any sphere tangent to any plane through the plane and it should flip the sign of $Q(I,J)$, if I am not mistaken. | +| Sphere/planes represented by I and J are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1)$. Accordingly, their $Q$-product is −1. | | Sphere/planes represented by I and J intersect (respectively, don't intersect) | $\|Q(I,J)\| < (\text{resp. }>)\; 1$ | Follows from the angle formula, at least conceptually. | | P is center of sphere represented by I | Well, $Q(I_P, I)$ comes out to be $(\|P\|^2/r - r + \|P\|^2/r)/2 - \|P\|^2/r$ or just $-r/2$ . | Is it if and only if ?   No this probably doesn't work because center is not conformal quantity. | | Distance between P and R is d | $Q(I_P, I_R) = d^2/2$ | | -- 2.34.1 From 3764fde2f6ca75cd56686030d019b11c44182e12 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 00:27:10 -0700 Subject: [PATCH 099/129] Clean up formatting of notes --- notes/inversive.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/notes/inversive.md b/notes/inversive.md index acac4cd..c845e3d 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -6,22 +6,22 @@ These coordinates are of form $I=(c, r, x, y, z)$ where we think of $c$ as the c | Entity or Relationship | Representation | Comments/questions | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Sphere s with radius r>0 centered on P = (x,y,z) | $I_s = (1/c, 1/r, x/r, y/r, z/r)$ satisfying $Q(I_s,I_s) = -1$, i.e., $c = r/(\|P\|^2 - r^2)$. | Can also write $I_s = (\|P\|^2/r - r, 1/r, x/r. y/r, z/r)$ -- so there is no trouble if $\|E_{I_s}\| = r$, just get first coordinate to be 0. | +| Sphere s with radius r>0 centered on P = (x,y,z) | $I_s = (1/c, 1/r, x/r, y/r, z/r)$ satisfying $Q(I_s,I_s) = -1$, i.e., $c = r/(\|P\|^2 - r^2)$. | Can also write $I_s = (\|P\|^2/r - r, 1/r, x/r. y/r, z/r)$—so there is no trouble if $\|E_{I_s}\| = r$, just get first coordinate to be 0. | | Plane p with unit normal (x,y,z) through the point s(x,y,z) | $I_p = (-2s, 0, -x, -y, -z)$ | Note $Q(I_p, I_p)$ is still −1. | | Point P with Euclidean coordinates (x,y,z) | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$.  Because of this we might choose  some other scaling of the inversive coordinates, say $(\||P\||,1/\||P\||,x/\||P\||,y/\||P\||,z/\||P\||)$ instead, but that fails at the origin, and likely won't have some of the other nice properties listed below.  Note that scaling just the co-radius by $s$ and the radius by $1/s$ (which still preserves $Q=0$) dilates by a factor of $s$ about the origin, so that $(\|P\|, \|P\|, x, y, z)$, which might look symmetric, would actually have to represent the Euclidean point $(x/\||P\||, y/\||P\||, z/\||P\||)$ . | | ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by the above case. | | P lies on sphere or plane given by I | $Q(I_P, I) = 0$ | | -| Sphere/planes represented by I and J are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1)$. Accordingly, their $Q$-product is −1. | +| Sphere/planes represented by I and J are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1)$. Accordingly, their $Q$-product is −1. | | Sphere/planes represented by I and J intersect (respectively, don't intersect) | $\|Q(I,J)\| < (\text{resp. }>)\; 1$ | Follows from the angle formula, at least conceptually. | | P is center of sphere represented by I | Well, $Q(I_P, I)$ comes out to be $(\|P\|^2/r - r + \|P\|^2/r)/2 - \|P\|^2/r$ or just $-r/2$ . | Is it if and only if ?   No this probably doesn't work because center is not conformal quantity. | | Distance between P and R is d | $Q(I_P, I_R) = d^2/2$ | | | Distance between P and sphere/plane rep by I | | In the very simple case of a plane $I$ rep'd by $(2s, 0, x, y, z)$ and a point $P$ that lies on its perpendicular through the origin, rep'd by $(r^2, 1, rx, ry, rz)$ we get $Q(I, I_p) = s-r$, which is indeed the signed distance between $I$ and $P$. Not sure if this generalizes to other combinations? | -| Distance between sphere/planes rep by I and J | Note that for any two Euclidean-concentric spheres rep by $I$ and $J$ with radius $r$ and $s,$ $Q(I,J) = -\frac12\left(\frac rs  + \frac sr\right)$ depends only on the ratio of $r$ and $s$. So this can't give something that determines the Euclidean distance between the two spheres, which presumably grows as the two spheres are blown up proportionally. For another example, for any two parallel planes, $Q(I,J) = \pm1$. | Alex had said: Q(I,J)=cosh^2 (d/2) maybe where d is distance in usual hyperbolic metric. Or maybe cosh d. That may be right depending on what's meant by the hyperbolic metric there, but it seems like it won't determine a reasonable Euclidean distance between planes, which should differ between different pairs of parallel planes. | +| Distance between sphere/planes rep by I and J | Note that for any two Euclidean-concentric spheres rep by $I$ and $J$ with radius $r$ and $s,$ $Q(I,J) = -\frac12\left(\frac rs  + \frac sr\right)$ depends only on the ratio of $r$ and $s$. So this can't give something that determines the Euclidean distance between the two spheres, which presumably grows as the two spheres are blown up proportionally. For another example, for any two parallel planes, $Q(I,J) = \pm1$. | Alex had said: $Q(I,J)=\cosh(d/2)^2$ maybe where d is distance in usual hyperbolic metric. Or maybe $\cosh(d)$. That may be right depending on what's meant by the hyperbolic metric there, but it seems like it won't determine a reasonable Euclidean distance between planes, which should differ between different pairs of parallel planes. | | Sphere centered on P through R | | Probably just calculate distance etc. | -| Plane rep'd by I goes through center of sphere rep'd by J | I think this is equivalent to the plane being perpendicular to the sphere, i.e.$Q(I,J) = 0$. | | -| Dihedral angle between planes (or spheres?) rep by I and J | $\theta = \arccos(Q(I,J))$ | Aaron Fenyes points out: The angle between spheres in $S^3$ matches the angle between the planes they bound in $R^{(1,4)}$, which matches the angle between the spacelike vectors perpendicular to those planes. So we should have $Q(I,J) = \cos\theta$. Note that when the spheres do not intersect, we can interpret this as the "imaginary angle" between them, via $\cosh t = \cos it$. | -| R, P, S are collinear | Maybe just cross product of two differences is 0. Or, $R,P,S,\infty$ lie on a circle, or equivalently, $I_R,I_P,I_S,I_\infty$ span a plane (rather than a three-space). | Not a conformal property, but $R,P,S,\infty$ lying on a circle _is_. | -| Plane through noncollinear R, P, S | Should be, just solve Q(I, I_R) = 0 etc. | | +| Plane rep'd by I goes through center of sphere rep'd by J | I think this is equivalent to the plane being perpendicular to the sphere, i.e. $Q(I,J) = 0$. | | +| Dihedral angle between planes (or spheres?) rep by I and J | $\theta = \arccos(Q(I,J))$ | Aaron Fenyes points out: The angle between spheres in $S^3$ matches the angle between the planes they bound in $R^{(1,4)}$, which matches the angle between the spacelike vectors perpendicular to those planes. So we should have $Q(I,J) = \cos(\theta)$. Note that when the spheres do not intersect, we can interpret this as the "imaginary angle" between them, via $\cosh(t) = \cos(it)$. | +| R, P, S are collinear | Maybe just cross product of two differences is 0. Or, $R,P,S,\infty$ lie on a circle, or equivalently, $I_R,I_P,I_S,I_\infty$ span a plane (rather than a three-space). | $R,P,S$ lying on a line isn't a conformal property, but $R,P,S,\infty$ lying on a circle is. | +| Plane through noncollinear R, P, S | Should be, just solve $Q(I, I_R) = 0$ etc. | | | circle | Maybe concentric sphere and the containing plane? Note it is easy to constrain the relationship between those two: they must be perpendicular. | Defn: circle is intersection of two spheres. That does cover lines. But you lose the canonicalness | | line | Maybe two perpendicular containing planes? Maybe the plane perpendicular to the line and through origin, together with the point of the line on that plane? Or maybe just as a bag of collinear points? | The first is the limiting case of the possible circle rep, but it is not canonical. The second appears to be canonical, but I don't see a circle rep that corresponds to it. | -- 2.34.1 From a7f9545a3704002c328e71a5ac1b601a14e74ca5 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 00:43:00 -0700 Subject: [PATCH 100/129] Circles in triangle: correct frozen variables Since the self-product of the point at infinity is left unspecified, the first three components can vary without violating any constraints. To keep the point at infinity where it's supposed to be, we freeze all of its components. --- engine-proto/gram-test/circles-in-triangle.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine-proto/gram-test/circles-in-triangle.jl b/engine-proto/gram-test/circles-in-triangle.jl index ca49574..457ac0d 100644 --- a/engine-proto/gram-test/circles-in-triangle.jl +++ b/engine-proto/gram-test/circles-in-triangle.jl @@ -66,7 +66,7 @@ guess = hcat( Engine.sphere(BigFloat[cos(pi/3), sin(pi/3), 0], BigFloat(1//5)), BigFloat[0, 0, 0, 0, 1] ) -frozen = [CartesianIndex(j, 9) for j in 4:5] +frozen = [CartesianIndex(j, 9) for j in 1:5] #= guess = hcat( Engine.plane(BigFloat[0, 0, 1], BigFloat(0)), -- 2.34.1 From 9007c8bc7c906067e03f040180a2bc48b179053a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 00:43:44 -0700 Subject: [PATCH 101/129] Circles in triangle: jiggle the guess --- engine-proto/gram-test/circles-in-triangle.jl | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/engine-proto/gram-test/circles-in-triangle.jl b/engine-proto/gram-test/circles-in-triangle.jl index 457ac0d..c1f8bf2 100644 --- a/engine-proto/gram-test/circles-in-triangle.jl +++ b/engine-proto/gram-test/circles-in-triangle.jl @@ -1,6 +1,7 @@ include("Engine.jl") using SparseArrays +using Random # initialize the partial gram matrix for a sphere inscribed in a regular # tetrahedron @@ -55,15 +56,16 @@ gram = sparse(J, K, values) ## guess = Engine.rand_on_shell(fill(BigFloat(-1), 8)) # set initial guess +Random.seed!(58271) guess = hcat( Engine.plane(BigFloat[0, 0, 1], BigFloat(0)), - Engine.sphere(BigFloat[0, 0, 0], BigFloat(1//2)), - Engine.plane(-BigFloat[1, 0, 0], BigFloat(-1)), - Engine.plane(-BigFloat[cos(2pi/3), sin(2pi/3), 0], BigFloat(-1)), - Engine.plane(-BigFloat[cos(-2pi/3), sin(-2pi/3), 0], BigFloat(-1)), - Engine.sphere(BigFloat[-1, 0, 0], BigFloat(1//5)), - Engine.sphere(BigFloat[cos(-pi/3), sin(-pi/3), 0], BigFloat(1//5)), - Engine.sphere(BigFloat[cos(pi/3), sin(pi/3), 0], BigFloat(1//5)), + Engine.sphere(BigFloat[0, 0, 0], BigFloat(1//2)) + 0.1*Engine.rand_on_shell([BigFloat(-1)]), + Engine.plane(-BigFloat[1, 0, 0], BigFloat(-1)) + 0.1*Engine.rand_on_shell([BigFloat(-1)]), + Engine.plane(-BigFloat[cos(2pi/3), sin(2pi/3), 0], BigFloat(-1)) + 0.1*Engine.rand_on_shell([BigFloat(-1)]), + Engine.plane(-BigFloat[cos(-2pi/3), sin(-2pi/3), 0], BigFloat(-1)) + 0.1*Engine.rand_on_shell([BigFloat(-1)]), + Engine.sphere(BigFloat[-1, 0, 0], BigFloat(1//5)) + 0.1*Engine.rand_on_shell([BigFloat(-1)]), + Engine.sphere(BigFloat[cos(-pi/3), sin(-pi/3), 0], BigFloat(1//5)) + 0.1*Engine.rand_on_shell([BigFloat(-1)]), + Engine.sphere(BigFloat[cos(pi/3), sin(pi/3), 0], BigFloat(1//5)) + 0.1*Engine.rand_on_shell([BigFloat(-1)]), BigFloat[0, 0, 0, 0, 1] ) frozen = [CartesianIndex(j, 9) for j in 1:5] -- 2.34.1 From b040bbb7feff00f3399eb1a85f938d32c814608c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 00:50:48 -0700 Subject: [PATCH 102/129] Drop old code from examples --- engine-proto/gram-test/circles-in-triangle.jl | 37 +------------------ .../gram-test/overlapping-pyramids.jl | 10 +---- .../gram-test/sphere-in-tetrahedron.jl | 5 +-- .../gram-test/tetrahedron-radius-ratio.jl | 2 +- 4 files changed, 4 insertions(+), 50 deletions(-) diff --git a/engine-proto/gram-test/circles-in-triangle.jl b/engine-proto/gram-test/circles-in-triangle.jl index c1f8bf2..f7248df 100644 --- a/engine-proto/gram-test/circles-in-triangle.jl +++ b/engine-proto/gram-test/circles-in-triangle.jl @@ -47,14 +47,6 @@ append!(values, fill(-0.5, 4)) =# gram = sparse(J, K, values) -# set initial guess (random) -## Random.seed!(58271) # stuck; step size collapses on step 48 -## Random.seed!(58272) # good convergence -## Random.seed!(58273) # stuck; step size collapses on step 18 -## Random.seed!(58274) # stuck -## Random.seed!(58275) # -## guess = Engine.rand_on_shell(fill(BigFloat(-1), 8)) - # set initial guess Random.seed!(58271) guess = hcat( @@ -69,39 +61,12 @@ guess = hcat( BigFloat[0, 0, 0, 0, 1] ) frozen = [CartesianIndex(j, 9) for j in 1:5] -#= -guess = hcat( - Engine.plane(BigFloat[0, 0, 1], BigFloat(0)), - Engine.sphere(BigFloat[0, 0, 0], BigFloat(0.9)), - Engine.plane(BigFloat[1, 0, 0], BigFloat(1)), - Engine.plane(BigFloat[cos(2pi/3), sin(2pi/3), 0], BigFloat(1)), - Engine.plane(BigFloat[cos(-2pi/3), sin(-2pi/3), 0], BigFloat(1)), - Engine.sphere(4//3*BigFloat[-1, 0, 0], BigFloat(1//3)), - Engine.sphere(4//3*BigFloat[cos(-pi/3), sin(-pi/3), 0], BigFloat(1//3)), - Engine.sphere(4//3*BigFloat[cos(pi/3), sin(pi/3), 0], BigFloat(1//3)), - BigFloat[0, 0, 0, 1, 1] -) -=# -# complete the gram matrix using gradient descent followed by Newton's method -#= -L, history = Engine.realize_gram_gradient(gram, guess, scaled_tol = 0.01) -L_pol, history_pol = Engine.realize_gram_newton(gram, L, rate = 0.3, scaled_tol = 1e-9) -L_pol2, history_pol2 = Engine.realize_gram_newton(gram, L_pol) -=# +# complete the gram matrix using Newton's method with backtracking L, success, history = Engine.realize_gram(gram, guess, frozen) completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") display(completed_gram) -#= -println( - "\nSteps: ", - size(history.scaled_loss, 1), - " + ", size(history_pol.scaled_loss, 1), - " + ", size(history_pol2.scaled_loss, 1) -) -println("Loss: ", history_pol2.scaled_loss[end], "\n") -=# if success println("\nTarget accuracy achieved!") else diff --git a/engine-proto/gram-test/overlapping-pyramids.jl b/engine-proto/gram-test/overlapping-pyramids.jl index 0d1f018..c530296 100644 --- a/engine-proto/gram-test/overlapping-pyramids.jl +++ b/engine-proto/gram-test/overlapping-pyramids.jl @@ -50,19 +50,11 @@ guess = begin ) end -# complete the gram matrix -#= -L, history = Engine.realize_gram_gradient(gram, guess, scaled_tol = 0.01) -L_pol, history_pol = Engine.realize_gram_newton(gram, L) -=# +# complete the gram matrix using Newton's method with backtracking L, success, history = Engine.realize_gram(gram, guess) completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") display(completed_gram) -#= -println("\nSteps: ", size(history.scaled_loss, 1), " + ", size(history_pol.scaled_loss, 1)) -println("Loss: ", history_pol.scaled_loss[end], "\n") -=# if success println("\nTarget accuracy achieved!") else diff --git a/engine-proto/gram-test/sphere-in-tetrahedron.jl b/engine-proto/gram-test/sphere-in-tetrahedron.jl index 631f0e5..1c36e99 100644 --- a/engine-proto/gram-test/sphere-in-tetrahedron.jl +++ b/engine-proto/gram-test/sphere-in-tetrahedron.jl @@ -53,10 +53,7 @@ guess = hcat( ) frozen = [CartesianIndex(j, 6) for j in 1:5] -# complete the gram matrix -#= -L, history = Engine.realize_gram_newton(gram, guess) -=# +# complete the gram matrix using Newton's method with backtracking L, success, history = Engine.realize_gram(gram, guess, frozen) completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") diff --git a/engine-proto/gram-test/tetrahedron-radius-ratio.jl b/engine-proto/gram-test/tetrahedron-radius-ratio.jl index c284078..7ceb794 100644 --- a/engine-proto/gram-test/tetrahedron-radius-ratio.jl +++ b/engine-proto/gram-test/tetrahedron-radius-ratio.jl @@ -77,7 +77,7 @@ frozen = vcat( [CartesianIndex(j, 11) for j in 1:5] ) -# complete the gram matrix +# complete the gram matrix using Newton's method with backtracking L, success, history = Engine.realize_gram(gram, guess, frozen) completed_gram = L'*Engine.Q*L println("Completed Gram matrix:\n") -- 2.34.1 From b24dcc9af8c26241825093762a8e8b984ece5f23 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 01:04:40 -0700 Subject: [PATCH 103/129] Report success correctly when step limit is reached --- engine-proto/gram-test/Engine.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index 40b77b0..ac5fe54 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -445,7 +445,7 @@ function realize_gram( # return the factorization and its history push!(history.scaled_loss, loss / scale_adjustment) - L, true, history + L, loss < tol, history end end \ No newline at end of file -- 2.34.1 From 33c09917d0da99ad8d973254b37272ae0b4ef47e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 01:05:13 -0700 Subject: [PATCH 104/129] Correct scope of guess constants --- engine-proto/ConstructionViewer.jl | 4 ++-- engine-proto/gram-test/overlapping-pyramids.jl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/engine-proto/ConstructionViewer.jl b/engine-proto/ConstructionViewer.jl index c9b0b7a..b9c8ffb 100644 --- a/engine-proto/ConstructionViewer.jl +++ b/engine-proto/ConstructionViewer.jl @@ -207,8 +207,8 @@ end # ~~~ sandbox setup ~~~ -elements = begin - const a = sqrt(BigFloat(3)/2) +elements = let + a = sqrt(BigFloat(3)/2) sqrt(0.5) * BigFloat[ 1 1 -1 -1 0 1 -1 1 -1 0 diff --git a/engine-proto/gram-test/overlapping-pyramids.jl b/engine-proto/gram-test/overlapping-pyramids.jl index c530296..cf4b88d 100644 --- a/engine-proto/gram-test/overlapping-pyramids.jl +++ b/engine-proto/gram-test/overlapping-pyramids.jl @@ -36,8 +36,8 @@ gram[1, 6] = gram[6, 1] # in this initial guess, the mutual tangency condition is satisfied for spheres # 1 through 5 Random.seed!(50793) -guess = begin - const a = sqrt(BigFloat(3)/2) +guess = let + a = sqrt(BigFloat(3)/2) hcat( sqrt(1/BigFloat(2)) * BigFloat[ 1 1 -1 -1 0 -- 2.34.1 From 71c10adbdd2f82d9979b3fae32be3da2720e1efe Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 01:12:49 -0700 Subject: [PATCH 105/129] Overlapping pyramids: drop outdated comment --- engine-proto/gram-test/overlapping-pyramids.jl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/engine-proto/gram-test/overlapping-pyramids.jl b/engine-proto/gram-test/overlapping-pyramids.jl index cf4b88d..a4ae01a 100644 --- a/engine-proto/gram-test/overlapping-pyramids.jl +++ b/engine-proto/gram-test/overlapping-pyramids.jl @@ -23,12 +23,6 @@ end gram = sparse(J, K, values) # set the independent variable -# -# using gram[6, 2] or gram[7, 1] as the independent variable seems to stall -# convergence, even if its value comes from a known solution, like -# -# gram[6, 2] = 0.9936131705272925 -# indep_val = -9//5 gram[6, 1] = BigFloat(indep_val) gram[1, 6] = gram[6, 1] -- 2.34.1 From 19a4d49497061b4c0523f32e0c5d7ed5af045af3 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 01:48:05 -0700 Subject: [PATCH 106/129] Clean up example formatting --- engine-proto/gram-test/circles-in-triangle.jl | 8 ++++---- engine-proto/gram-test/sphere-in-tetrahedron.jl | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/engine-proto/gram-test/circles-in-triangle.jl b/engine-proto/gram-test/circles-in-triangle.jl index f7248df..1bd22a7 100644 --- a/engine-proto/gram-test/circles-in-triangle.jl +++ b/engine-proto/gram-test/circles-in-triangle.jl @@ -12,22 +12,22 @@ for j in 1:9 for k in 1:9 filled = false if j == 9 - if (k <= 5 && k != 2) + if k <= 5 && k != 2 push!(values, 0) filled = true end elseif k == 9 - if (j <= 5 && j != 2) + if j <= 5 && j != 2 push!(values, 0) filled = true end elseif j == k push!(values, 1) filled = true - elseif (j == 1 || k == 1) + elseif j == 1 || k == 1 push!(values, 0) filled = true - elseif (j == 2 || k == 2) + elseif j == 2 || k == 2 push!(values, -1) filled = true end diff --git a/engine-proto/gram-test/sphere-in-tetrahedron.jl b/engine-proto/gram-test/sphere-in-tetrahedron.jl index 1c36e99..97f0720 100644 --- a/engine-proto/gram-test/sphere-in-tetrahedron.jl +++ b/engine-proto/gram-test/sphere-in-tetrahedron.jl @@ -24,7 +24,7 @@ for j in 1:6 elseif j == k push!(values, 1) filled = true - elseif (j <= 4 && k <= 4) + elseif j <= 4 && k <= 4 push!(values, -1/BigFloat(3)) filled = true else -- 2.34.1 From a26f1e3927472cdd1b2453509ba0b29a1d4ae702 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 03:16:57 -0700 Subject: [PATCH 107/129] Add Irisawa hexlet example Hat tip Romy, who sent me the article on sangaku that led me to this problem. --- engine-proto/gram-test/irisawa-hexlet.jl | 77 ++++++++++++++ engine-proto/gram-test/irisawa-hexlet_bad.jl | 105 +++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 engine-proto/gram-test/irisawa-hexlet.jl create mode 100644 engine-proto/gram-test/irisawa-hexlet_bad.jl diff --git a/engine-proto/gram-test/irisawa-hexlet.jl b/engine-proto/gram-test/irisawa-hexlet.jl new file mode 100644 index 0000000..8b43ad4 --- /dev/null +++ b/engine-proto/gram-test/irisawa-hexlet.jl @@ -0,0 +1,77 @@ +include("Engine.jl") + +using SparseArrays + +# this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article below +# includes a nice translation of the problem statement, which was recorded in +# Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and Present_) +# +# "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki +# https://www.nippon.com/en/japan-topics/c12801/ +# + +# initialize the partial gram matrix +J = Int64[] +K = Int64[] +values = BigFloat[] +for s in 1:9 + # each sphere is represented by a spacelike vector + push!(J, s) + push!(K, s) + push!(values, 1) + + # the circumscribing sphere is internally tangent to all of the other spheres + if s > 1 + append!(J, [1, s]) + append!(K, [s, 1]) + append!(values, [1, 1]) + end + + if s > 3 + # each chain sphere is externally tangent to the two nucleus spheres + for n in 2:3 + append!(J, [s, n]) + append!(K, [n, s]) + append!(values, [-1, -1]) + end + + # each chain sphere is externally tangent to the next sphere in the chain + s_next = 4 + mod(s-3, 6) + append!(J, [s, s_next]) + append!(K, [s_next, s]) + append!(values, [-1, -1]) + end +end +gram = sparse(J, K, values) + +# make an initial guess +guess = hcat( + Engine.sphere(BigFloat[0, 0, 0], BigFloat(15)), + Engine.sphere(BigFloat[0, 0, -9], BigFloat(5)), + Engine.sphere(BigFloat[0, 0, 11], BigFloat(3)), + ( + Engine.sphere(9*BigFloat[cos(k*π/3), sin(k*π/3), 0], BigFloat(2.5)) + for k in 1:6 + )... +) +frozen = [CartesianIndex(4, k) for k in 1:4] + +# complete the gram matrix using Newton's method with backtracking +L, success, history = Engine.realize_gram(gram, guess, frozen) +completed_gram = L'*Engine.Q*L +println("Completed Gram matrix:\n") +display(completed_gram) +if success + println("\nTarget accuracy achieved!") +else + println("\nFailed to reach target accuracy") +end +println("Steps: ", size(history.scaled_loss, 1)) +println("Loss: ", history.scaled_loss[end], "\n") +if success + println("Chain diameters:") + println(" ", 1 / L[4,4], " sun (given)") + for k in 5:9 + println(" ", 1 / L[4,k], " sun") + end +end \ No newline at end of file diff --git a/engine-proto/gram-test/irisawa-hexlet_bad.jl b/engine-proto/gram-test/irisawa-hexlet_bad.jl new file mode 100644 index 0000000..8786778 --- /dev/null +++ b/engine-proto/gram-test/irisawa-hexlet_bad.jl @@ -0,0 +1,105 @@ +include("Engine.jl") + +using SparseArrays + +# --- construct the nucleus spheres --- + +println("--- Nucleus spheres ---\n") + +# initialize the partial gram matrix for the circumscribing and nucleus spheres +J = Int64[] +K = Int64[] +values = BigFloat[] +for n in 1:3 + push!(J, n) + push!(K, n) + push!(values, 1) + if n > 1 + append!(J, [1, n]) + append!(K, [n, 1]) + append!(values, [1, 1]) + end +end +gram_nuc = sparse(J, K, values) + +# make an initial guess +guess_nuc = hcat( + Engine.sphere(BigFloat[0, 0, 0], BigFloat(15)), + Engine.sphere(BigFloat[0, 0, -10], BigFloat(5)), + Engine.sphere(BigFloat[0, 0, 11], BigFloat(3)), +) +frozen_nuc = [CartesianIndex(4, k) for k in 1:3] + +# complete the gram matrix using Newton's method with backtracking +L_nuc, success_nuc, history_nuc = Engine.realize_gram(gram_nuc, guess_nuc, frozen_nuc) +completed_gram_nuc = L_nuc'*Engine.Q*L_nuc +println("Completed Gram matrix:\n") +display(completed_gram_nuc) +if success_nuc + println("\nTarget accuracy achieved!") +else + println("\nFailed to reach target accuracy") +end +println("Steps: ", size(history_nuc.scaled_loss, 1)) +println("Loss: ", history_nuc.scaled_loss[end], "\n") + +# --- construct the chain of spheres --- + +# initialize the partial gram matrix for the chain of spheres +J = Int64[] +K = Int64[] +values = BigFloat[] +for a in 4:9 + push!(J, a) + push!(K, a) + push!(values, 1) + + # each chain sphere is internally tangent to the circumscribing sphere + append!(J, [a, 1]) + append!(K, [1, a]) + append!(values, [1, 1]) + + # each chain sphere is externally tangent to the nucleus spheres + for n in 2:3 + append!(J, [a, n]) + append!(K, [n, a]) + append!(values, [-1, -1]) + end + + # each chain sphere is externally tangent to the next sphere in the chain + #= + a_next = 4 + mod(a-3, 6) + append!(J, [a, a_next]) + append!(K, [a_next, a]) + append!(values, [-1, -1]) + =# +end +gram_chain = sparse(J, K, values) + +if success_nuc + println("--- Chain spheres ---\n") + + # make an initial guess, with the circumscribing and nucleus spheres included + # as frozen elements + guess_chain = hcat( + L_nuc, + ( + Engine.sphere(10*BigFloat[cos(k*π/3), sin(k*π/3), 0], BigFloat(2.5)) + for k in 1:6 + )... + ) + frozen_chain = [CartesianIndex(j, k) for k in 1:3 for j in 1:5] + + # complete the gram matrix using Newton's method with backtracking + L_chain, success_chain, history_chain = Engine.realize_gram(gram_chain, guess_chain, frozen_chain) + completed_gram_chain = L_chain'*Engine.Q*L_chain + println("Completed Gram matrix:\n") + display(completed_gram_chain) + if success_chain + println("\nTarget accuracy achieved!") + else + println("\nFailed to reach target accuracy") + end + println("Steps: ", size(history_chain.scaled_loss, 1)) + println("Loss: ", history_chain.scaled_loss[end], "\n") +end \ No newline at end of file -- 2.34.1 From 8a77cd74846f972cd1721fbefa130618ac03e048 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 03:21:46 -0700 Subject: [PATCH 108/129] Irisawa hexlet: drop unviable approach The approach in the deleted file can't work, because the "sun" and "moon" spheres can't be placed arbitrarily. --- engine-proto/gram-test/irisawa-hexlet_bad.jl | 105 ------------------- 1 file changed, 105 deletions(-) delete mode 100644 engine-proto/gram-test/irisawa-hexlet_bad.jl diff --git a/engine-proto/gram-test/irisawa-hexlet_bad.jl b/engine-proto/gram-test/irisawa-hexlet_bad.jl deleted file mode 100644 index 8786778..0000000 --- a/engine-proto/gram-test/irisawa-hexlet_bad.jl +++ /dev/null @@ -1,105 +0,0 @@ -include("Engine.jl") - -using SparseArrays - -# --- construct the nucleus spheres --- - -println("--- Nucleus spheres ---\n") - -# initialize the partial gram matrix for the circumscribing and nucleus spheres -J = Int64[] -K = Int64[] -values = BigFloat[] -for n in 1:3 - push!(J, n) - push!(K, n) - push!(values, 1) - if n > 1 - append!(J, [1, n]) - append!(K, [n, 1]) - append!(values, [1, 1]) - end -end -gram_nuc = sparse(J, K, values) - -# make an initial guess -guess_nuc = hcat( - Engine.sphere(BigFloat[0, 0, 0], BigFloat(15)), - Engine.sphere(BigFloat[0, 0, -10], BigFloat(5)), - Engine.sphere(BigFloat[0, 0, 11], BigFloat(3)), -) -frozen_nuc = [CartesianIndex(4, k) for k in 1:3] - -# complete the gram matrix using Newton's method with backtracking -L_nuc, success_nuc, history_nuc = Engine.realize_gram(gram_nuc, guess_nuc, frozen_nuc) -completed_gram_nuc = L_nuc'*Engine.Q*L_nuc -println("Completed Gram matrix:\n") -display(completed_gram_nuc) -if success_nuc - println("\nTarget accuracy achieved!") -else - println("\nFailed to reach target accuracy") -end -println("Steps: ", size(history_nuc.scaled_loss, 1)) -println("Loss: ", history_nuc.scaled_loss[end], "\n") - -# --- construct the chain of spheres --- - -# initialize the partial gram matrix for the chain of spheres -J = Int64[] -K = Int64[] -values = BigFloat[] -for a in 4:9 - push!(J, a) - push!(K, a) - push!(values, 1) - - # each chain sphere is internally tangent to the circumscribing sphere - append!(J, [a, 1]) - append!(K, [1, a]) - append!(values, [1, 1]) - - # each chain sphere is externally tangent to the nucleus spheres - for n in 2:3 - append!(J, [a, n]) - append!(K, [n, a]) - append!(values, [-1, -1]) - end - - # each chain sphere is externally tangent to the next sphere in the chain - #= - a_next = 4 + mod(a-3, 6) - append!(J, [a, a_next]) - append!(K, [a_next, a]) - append!(values, [-1, -1]) - =# -end -gram_chain = sparse(J, K, values) - -if success_nuc - println("--- Chain spheres ---\n") - - # make an initial guess, with the circumscribing and nucleus spheres included - # as frozen elements - guess_chain = hcat( - L_nuc, - ( - Engine.sphere(10*BigFloat[cos(k*π/3), sin(k*π/3), 0], BigFloat(2.5)) - for k in 1:6 - )... - ) - frozen_chain = [CartesianIndex(j, k) for k in 1:3 for j in 1:5] - - # complete the gram matrix using Newton's method with backtracking - L_chain, success_chain, history_chain = Engine.realize_gram(gram_chain, guess_chain, frozen_chain) - completed_gram_chain = L_chain'*Engine.Q*L_chain - println("Completed Gram matrix:\n") - display(completed_gram_chain) - if success_chain - println("\nTarget accuracy achieved!") - else - println("\nFailed to reach target accuracy") - end - println("Steps: ", size(history_chain.scaled_loss, 1)) - println("Loss: ", history_chain.scaled_loss[end], "\n") -end \ No newline at end of file -- 2.34.1 From 9d69a900e28e0694b984ed326d3c9b6d6edca668 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 03:39:41 -0700 Subject: [PATCH 109/129] Irisawa hexlet: use Abe's terminology in comments Abe uses the names "sun" and "moon" for what Wikipedia calls the nucleus spheres. --- engine-proto/gram-test/irisawa-hexlet.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engine-proto/gram-test/irisawa-hexlet.jl b/engine-proto/gram-test/irisawa-hexlet.jl index 8b43ad4..67def8c 100644 --- a/engine-proto/gram-test/irisawa-hexlet.jl +++ b/engine-proto/gram-test/irisawa-hexlet.jl @@ -28,14 +28,14 @@ for s in 1:9 end if s > 3 - # each chain sphere is externally tangent to the two nucleus spheres + # each chain sphere is externally tangent to the "sun" and "moon" spheres for n in 2:3 append!(J, [s, n]) append!(K, [n, s]) append!(values, [-1, -1]) end - # each chain sphere is externally tangent to the next sphere in the chain + # each chain sphere is externally tangent to the next chain sphere s_next = 4 + mod(s-3, 6) append!(J, [s, s_next]) append!(K, [s_next, s]) -- 2.34.1 From d7dbee4c05b93a0cf6edda8d46eca7bb1a2ee681 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 28 Jul 2024 20:50:04 -0700 Subject: [PATCH 110/129] Stow algebraic engine prototype We're using the Gram matrix engine for the next stage of development, so the algebraic engine shouldn't be at the top level anymore. --- engine-proto/{ => alg-test}/ConstructionViewer.jl | 0 engine-proto/{ => alg-test}/Engine.Algebraic.jl | 0 engine-proto/{ => alg-test}/Engine.Numerical.jl | 0 engine-proto/{ => alg-test}/Engine.jl | 0 engine-proto/{ => alg-test}/HittingSet.jl | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename engine-proto/{ => alg-test}/ConstructionViewer.jl (100%) rename engine-proto/{ => alg-test}/Engine.Algebraic.jl (100%) rename engine-proto/{ => alg-test}/Engine.Numerical.jl (100%) rename engine-proto/{ => alg-test}/Engine.jl (100%) rename engine-proto/{ => alg-test}/HittingSet.jl (100%) diff --git a/engine-proto/ConstructionViewer.jl b/engine-proto/alg-test/ConstructionViewer.jl similarity index 100% rename from engine-proto/ConstructionViewer.jl rename to engine-proto/alg-test/ConstructionViewer.jl diff --git a/engine-proto/Engine.Algebraic.jl b/engine-proto/alg-test/Engine.Algebraic.jl similarity index 100% rename from engine-proto/Engine.Algebraic.jl rename to engine-proto/alg-test/Engine.Algebraic.jl diff --git a/engine-proto/Engine.Numerical.jl b/engine-proto/alg-test/Engine.Numerical.jl similarity index 100% rename from engine-proto/Engine.Numerical.jl rename to engine-proto/alg-test/Engine.Numerical.jl diff --git a/engine-proto/Engine.jl b/engine-proto/alg-test/Engine.jl similarity index 100% rename from engine-proto/Engine.jl rename to engine-proto/alg-test/Engine.jl diff --git a/engine-proto/HittingSet.jl b/engine-proto/alg-test/HittingSet.jl similarity index 100% rename from engine-proto/HittingSet.jl rename to engine-proto/alg-test/HittingSet.jl -- 2.34.1 From 8084fdeab011f9ac576959b879c0af9abb409b43 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 18 Sep 2024 19:50:23 -0700 Subject: [PATCH 111/129] doc: Slight mods to the inversive notes --- notes/inversive.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/notes/inversive.md b/notes/inversive.md index c845e3d..15aee59 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -4,26 +4,26 @@ These coordinates are of form $I=(c, r, x, y, z)$ where we think of $c$ as the co-radius, $r$ as the radius, and $x, y, z$ as the "Euclidean" part, which we abbreviate $E_I$. There is an underlying basic quadratic form $Q(I_1,I_2) = (c_1r_2+c_2r_1)/2 - x_1x_2 -y_1y_2-z_1z_2$ which aids in calculation/verification of coordinates in this representation. We have: -| Entity or Relationship | Representation | Comments/questions | -| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Sphere s with radius r>0 centered on P = (x,y,z) | $I_s = (1/c, 1/r, x/r, y/r, z/r)$ satisfying $Q(I_s,I_s) = -1$, i.e., $c = r/(\|P\|^2 - r^2)$. | Can also write $I_s = (\|P\|^2/r - r, 1/r, x/r. y/r, z/r)$—so there is no trouble if $\|E_{I_s}\| = r$, just get first coordinate to be 0. | -| Plane p with unit normal (x,y,z) through the point s(x,y,z) | $I_p = (-2s, 0, -x, -y, -z)$ | Note $Q(I_p, I_p)$ is still −1. | -| Point P with Euclidean coordinates (x,y,z) | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$.  Because of this we might choose  some other scaling of the inversive coordinates, say $(\||P\||,1/\||P\||,x/\||P\||,y/\||P\||,z/\||P\||)$ instead, but that fails at the origin, and likely won't have some of the other nice properties listed below.  Note that scaling just the co-radius by $s$ and the radius by $1/s$ (which still preserves $Q=0$) dilates by a factor of $s$ about the origin, so that $(\|P\|, \|P\|, x, y, z)$, which might look symmetric, would actually have to represent the Euclidean point $(x/\||P\||, y/\||P\||, z/\||P\||)$ . | -| ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by the above case. | -| P lies on sphere or plane given by I | $Q(I_P, I) = 0$ | | -| Sphere/planes represented by I and J are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1)$. Accordingly, their $Q$-product is −1. | -| Sphere/planes represented by I and J intersect (respectively, don't intersect) | $\|Q(I,J)\| < (\text{resp. }>)\; 1$ | Follows from the angle formula, at least conceptually. | -| P is center of sphere represented by I | Well, $Q(I_P, I)$ comes out to be $(\|P\|^2/r - r + \|P\|^2/r)/2 - \|P\|^2/r$ or just $-r/2$ . | Is it if and only if ?   No this probably doesn't work because center is not conformal quantity. | -| Distance between P and R is d | $Q(I_P, I_R) = d^2/2$ | | -| Distance between P and sphere/plane rep by I | | In the very simple case of a plane $I$ rep'd by $(2s, 0, x, y, z)$ and a point $P$ that lies on its perpendicular through the origin, rep'd by $(r^2, 1, rx, ry, rz)$ we get $Q(I, I_p) = s-r$, which is indeed the signed distance between $I$ and $P$. Not sure if this generalizes to other combinations? | -| Distance between sphere/planes rep by I and J | Note that for any two Euclidean-concentric spheres rep by $I$ and $J$ with radius $r$ and $s,$ $Q(I,J) = -\frac12\left(\frac rs  + \frac sr\right)$ depends only on the ratio of $r$ and $s$. So this can't give something that determines the Euclidean distance between the two spheres, which presumably grows as the two spheres are blown up proportionally. For another example, for any two parallel planes, $Q(I,J) = \pm1$. | Alex had said: $Q(I,J)=\cosh(d/2)^2$ maybe where d is distance in usual hyperbolic metric. Or maybe $\cosh(d)$. That may be right depending on what's meant by the hyperbolic metric there, but it seems like it won't determine a reasonable Euclidean distance between planes, which should differ between different pairs of parallel planes. | -| Sphere centered on P through R | | Probably just calculate distance etc. | -| Plane rep'd by I goes through center of sphere rep'd by J | I think this is equivalent to the plane being perpendicular to the sphere, i.e. $Q(I,J) = 0$. | | -| Dihedral angle between planes (or spheres?) rep by I and J | $\theta = \arccos(Q(I,J))$ | Aaron Fenyes points out: The angle between spheres in $S^3$ matches the angle between the planes they bound in $R^{(1,4)}$, which matches the angle between the spacelike vectors perpendicular to those planes. So we should have $Q(I,J) = \cos(\theta)$. Note that when the spheres do not intersect, we can interpret this as the "imaginary angle" between them, via $\cosh(t) = \cos(it)$. | -| R, P, S are collinear | Maybe just cross product of two differences is 0. Or, $R,P,S,\infty$ lie on a circle, or equivalently, $I_R,I_P,I_S,I_\infty$ span a plane (rather than a three-space). | $R,P,S$ lying on a line isn't a conformal property, but $R,P,S,\infty$ lying on a circle is. | -| Plane through noncollinear R, P, S | Should be, just solve $Q(I, I_R) = 0$ etc. | | -| circle | Maybe concentric sphere and the containing plane? Note it is easy to constrain the relationship between those two: they must be perpendicular. | Defn: circle is intersection of two spheres. That does cover lines. But you lose the canonicalness | -| line | Maybe two perpendicular containing planes? Maybe the plane perpendicular to the line and through origin, together with the point of the line on that plane? Or maybe just as a bag of collinear points? | The first is the limiting case of the possible circle rep, but it is not canonical. The second appears to be canonical, but I don't see a circle rep that corresponds to it. | +| Entity or Relationship | Representation | Comments/questions | +| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Sphere s with radius r>0 centered on P = (x,y,z) | $I_s = (1/c, 1/r, x/r, y/r, z/r)$ satisfying $Q(I_s,I_s) = -1$, i.e., $c = r/(\|P\|^2 - r^2)$. | Can also write $I_s = (\|P\|^2/r - r, 1/r, x/r, y/r, z/r)$ -- so there is no trouble if $\|P\| = r$; we just get first coordinate to be 0. | +| Plane p with unit normal $(x,y,z)$ through the (Euclidean) point $(sx,sy,sz)$ | $I_p = (-2s, 0, -x, -y, -z)$ | Note $Q(I_p, I_p)$ is still −1. This plane is at distance $s$ from the origin. | +| Point P with Euclidean coordinates (x,y,z) | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$.  Because of this we might choose  some other scaling of the inversive coordinates, say $(\| | +| ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by the above case. | +| P lies on sphere or plane given by I | $Q(I_P, I) = 0$ | | +| Sphere/planes represented by I and J are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1)$. Accordingly, their $Q$-product is −1. | +| Sphere/planes represented by I and J intersect (respectively, don't intersect) | $\|Q(I,J)\| < (\text{resp. }>)\; 1$ | Follows from the angle formula, at least conceptually. | +| P is center of sphere represented by I | Well, $Q(I_P, I)$ comes out to be $(\|P\|^2/r - r + \|P\|^2/r)/2 - \|P\|^2/r$ or just $-r/2$ . | Is it if and only if ?   No this probably doesn't work because center is not conformal quantity. | +| Distance between P and R is d | $Q(I_P, I_R) = d^2/2$ | | +| Distance between P and sphere/plane rep by I | | In the very simple case of a plane $I$ rep'd by $(2s, 0, x, y, z)$ and a point $P$ that lies on its perpendicular through the origin, rep'd by $(r^2, 1, rx, ry, rz)$ we get $Q(I, I_p) = s-r$, which is indeed the signed distance between $I$ and $P$. Not sure if this generalizes to other combinations? | +| Distance between sphere/planes rep by I and J | Note that for any two Euclidean-concentric spheres rep by $I$ and $J$ with radius $r$ and $s,$ $Q(I,J) = -\frac12\left(\frac rs  + \frac sr\right)$ depends only on the ratio of $r$ and $s$. So this can't give something that determines the Euclidean distance between the two spheres, which presumably grows as the two spheres are blown up proportionally. For another example, for any two parallel planes, $Q(I,J) = \pm1$. | Alex had said: $Q(I,J)=\cosh(d/2)^2$ maybe where d is distance in usual hyperbolic metric. Or maybe $\cosh(d)$. That may be right depending on what's meant by the hyperbolic metric there, but it seems like it won't determine a reasonable Euclidean distance between planes, which should differ between different pairs of parallel planes. | +| Sphere centered on P through R | | Probably just calculate distance etc. | +| Plane rep'd by I goes through center of sphere rep'd by J | I think this is equivalent to the plane being perpendicular to the sphere, i.e. $Q(I,J) = 0$. | | +| Dihedral angle between planes (or spheres?) rep by I and J | $\theta = \arccos(Q(I,J))$ | Aaron Fenyes points out: The angle between spheres in $S^3$ matches the angle between the planes they bound in $R^{(1,4)}$, which matches the angle between the spacelike vectors perpendicular to those planes. So we should have $Q(I,J) = \cos(\theta)$. Note that when the spheres do not intersect, we can interpret this as the "imaginary angle" between them, via $\cosh(t) = \cos(it)$. | +| R, P, S are collinear | Maybe just cross product of two differences is 0. Or, $R,P,S,\infty$ lie on a circle, or equivalently, $I_R,I_P,I_S,I_\infty$ span a plane (rather than a three-space). | $R,P,S$ lying on a line isn't a conformal property, but $R,P,S,\infty$ lying on a circle is. | +| Plane through noncollinear R, P, S | Should be, just solve $Q(I, I_R) = 0$ etc. | | +| circle | Maybe concentric sphere and the containing plane? Note it is easy to constrain the relationship between those two: they must be perpendicular. | Defn: circle is intersection of two spheres. That does cover lines. But you lose the canonicalness | +| line | Maybe two perpendicular containing planes? Maybe the plane perpendicular to the line and through origin, together with the point of the line on that plane? Or maybe just as a bag of collinear points? | The first is the limiting case of the possible circle rep, but it is not canonical. The second appears to be canonical, but I don't see a circle rep that corresponds to it. | The unification of spheres/planes is indeed attractive for a project like Dyna3. The relationship between this representation and Geometric Algebras is a bit murky; likely it somehow fits under the Geometric Algebra umbrella. -- 2.34.1 From bd3e3506e57ecbe13cef98db8293c373a4a9c4c1 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 18 Sep 2024 19:52:03 -0700 Subject: [PATCH 112/129] doc: typo in inversive notes --- notes/inversive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/inversive.md b/notes/inversive.md index 15aee59..7867268 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -7,7 +7,7 @@ These coordinates are of form $I=(c, r, x, y, z)$ where we think of $c$ as the c | Entity or Relationship | Representation | Comments/questions | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Sphere s with radius r>0 centered on P = (x,y,z) | $I_s = (1/c, 1/r, x/r, y/r, z/r)$ satisfying $Q(I_s,I_s) = -1$, i.e., $c = r/(\|P\|^2 - r^2)$. | Can also write $I_s = (\|P\|^2/r - r, 1/r, x/r, y/r, z/r)$ -- so there is no trouble if $\|P\| = r$; we just get first coordinate to be 0. | -| Plane p with unit normal $(x,y,z)$ through the (Euclidean) point $(sx,sy,sz)$ | $I_p = (-2s, 0, -x, -y, -z)$ | Note $Q(I_p, I_p)$ is still −1. This plane is at distance $s$ from the origin. | +| Plane p with unit normal $(x,y,z)$ through the (Euclidean) point $(sx,sy,sz)$ | $I_p = (-2s, 0, -x, -y, -z)$ | Note $Q(I_p, I_p)$ is still −1. This plane is at distance $s$ from the origin. | | Point P with Euclidean coordinates (x,y,z) | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$.  Because of this we might choose  some other scaling of the inversive coordinates, say $(\| | | ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by the above case. | | P lies on sphere or plane given by I | $Q(I_P, I) = 0$ | | -- 2.34.1 From a182b663016b4301b52260d1480899cf44505170 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 18 Sep 2024 20:11:20 -0700 Subject: [PATCH 113/129] doc: More elaboration of plane coordinates in inversive notes --- notes/inversive.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notes/inversive.md b/notes/inversive.md index 7867268..7a5318d 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -7,8 +7,8 @@ These coordinates are of form $I=(c, r, x, y, z)$ where we think of $c$ as the c | Entity or Relationship | Representation | Comments/questions | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Sphere s with radius r>0 centered on P = (x,y,z) | $I_s = (1/c, 1/r, x/r, y/r, z/r)$ satisfying $Q(I_s,I_s) = -1$, i.e., $c = r/(\|P\|^2 - r^2)$. | Can also write $I_s = (\|P\|^2/r - r, 1/r, x/r, y/r, z/r)$ -- so there is no trouble if $\|P\| = r$; we just get first coordinate to be 0. | -| Plane p with unit normal $(x,y,z)$ through the (Euclidean) point $(sx,sy,sz)$ | $I_p = (-2s, 0, -x, -y, -z)$ | Note $Q(I_p, I_p)$ is still −1. This plane is at distance $s$ from the origin. | -| Point P with Euclidean coordinates (x,y,z) | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$.  Because of this we might choose  some other scaling of the inversive coordinates, say $(\| | +| Plane p with unit normal $(x,y,z)$ through the (Euclidean) point $(sx,sy,sz)$ | $I_p = (-2s, 0, -x, -y, -z)$ | Note $Q(I_p, I_p)$ is still −1. This plane is at distance $s$ from the origin. And note that these are _oriented_ planes. For example, $(-2, 0, -1/\sqrt3, -1/\sqrt3, -1/\sqrt3)$ and $(2, 0, 1/\sqrt3, 1/\sqrt3, 1/\sqrt3)$ represent planes that coincide in space, just the former has normal pointing away from the origin and the latter pointing toward it. | +| Point P with Euclidean coordinates (x,y,z) | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$. Because of this we might choose some other scaling of the inversive coordinates, say ... | | ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by the above case. | | P lies on sphere or plane given by I | $Q(I_P, I) = 0$ | | | Sphere/planes represented by I and J are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1)$. Accordingly, their $Q$-product is −1. | -- 2.34.1 From 23ecca3963ad481ad29ee984ff21c38a16539484 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 18 Sep 2024 20:27:04 -0700 Subject: [PATCH 114/129] doc: Another note about inversive coordinates --- notes/inversive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/inversive.md b/notes/inversive.md index 7a5318d..31eb898 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -10,7 +10,7 @@ These coordinates are of form $I=(c, r, x, y, z)$ where we think of $c$ as the c | Plane p with unit normal $(x,y,z)$ through the (Euclidean) point $(sx,sy,sz)$ | $I_p = (-2s, 0, -x, -y, -z)$ | Note $Q(I_p, I_p)$ is still −1. This plane is at distance $s$ from the origin. And note that these are _oriented_ planes. For example, $(-2, 0, -1/\sqrt3, -1/\sqrt3, -1/\sqrt3)$ and $(2, 0, 1/\sqrt3, 1/\sqrt3, 1/\sqrt3)$ represent planes that coincide in space, just the former has normal pointing away from the origin and the latter pointing toward it. | | Point P with Euclidean coordinates (x,y,z) | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$. Because of this we might choose some other scaling of the inversive coordinates, say ... | | ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by the above case. | -| P lies on sphere or plane given by I | $Q(I_P, I) = 0$ | | +| P lies on sphere or plane given by I | $Q(I_P, I) = 0$ | Actually also works if $I$ is the coordinates of a point, in which case "lies on" simply means "coincides with". | | Sphere/planes represented by I and J are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1)$. Accordingly, their $Q$-product is −1. | | Sphere/planes represented by I and J intersect (respectively, don't intersect) | $\|Q(I,J)\| < (\text{resp. }>)\; 1$ | Follows from the angle formula, at least conceptually. | | P is center of sphere represented by I | Well, $Q(I_P, I)$ comes out to be $(\|P\|^2/r - r + \|P\|^2/r)/2 - \|P\|^2/r$ or just $-r/2$ . | Is it if and only if ?   No this probably doesn't work because center is not conformal quantity. | -- 2.34.1 From 2c1a42e251e250e798054094588cd41060859d84 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 25 Sep 2024 23:07:52 -0700 Subject: [PATCH 115/129] doc: Clarify orientation convention --- notes/inversive.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/notes/inversive.md b/notes/inversive.md index 31eb898..6af980b 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -6,12 +6,12 @@ These coordinates are of form $I=(c, r, x, y, z)$ where we think of $c$ as the c | Entity or Relationship | Representation | Comments/questions | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Sphere s with radius r>0 centered on P = (x,y,z) | $I_s = (1/c, 1/r, x/r, y/r, z/r)$ satisfying $Q(I_s,I_s) = -1$, i.e., $c = r/(\|P\|^2 - r^2)$. | Can also write $I_s = (\|P\|^2/r - r, 1/r, x/r, y/r, z/r)$ -- so there is no trouble if $\|P\| = r$; we just get first coordinate to be 0. | -| Plane p with unit normal $(x,y,z)$ through the (Euclidean) point $(sx,sy,sz)$ | $I_p = (-2s, 0, -x, -y, -z)$ | Note $Q(I_p, I_p)$ is still −1. This plane is at distance $s$ from the origin. And note that these are _oriented_ planes. For example, $(-2, 0, -1/\sqrt3, -1/\sqrt3, -1/\sqrt3)$ and $(2, 0, 1/\sqrt3, 1/\sqrt3, 1/\sqrt3)$ represent planes that coincide in space, just the former has normal pointing away from the origin and the latter pointing toward it. | -| Point P with Euclidean coordinates (x,y,z) | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$. Because of this we might choose some other scaling of the inversive coordinates, say ... | +| Sphere s with radius r>0 centered on P = (x,y,z) | $I_s = (1/c, 1/r, x/r, y/r, z/r)$ satisfying $Q(I_s,I_s) = -1$, i.e., $c = r/(\|P\|^2 - r^2)$. | Note that $1/c = \|P\|^2/r - r$, so there is no trouble if $\|P\| = r$; we just get first coordinate to be 0. Using the point representation $I_P$ from below, let's orient the sphere so that its normals point into the "positive side," where $Q(I_P, I_s) > 0$. The vector $I_s$ then represents a sphere with outward normals, while $-I_s$ represents one with inward normals. | +| Plane p with unit normal $(x,y,z)$ through the (Euclidean) point $(sx,sy,sz)$ | $I_p = (-2s, 0, -x, -y, -z)$ | Note that $Q(I_p, I_p)$ is still $−1$. The parameter $s$ is the plane's distance from the origin. We orient planes using the same convention we use for spheres. For example, $(-2, 0, -1/\sqrt3, -1/\sqrt3, -1/\sqrt3)$ and $(2, 0, 1/\sqrt3, 1/\sqrt3, 1/\sqrt3)$ represent planes that coincide in space, which have their normals pointing away from and toward the origin, respectively. | +| Point P with Euclidean coordinates (x,y,z) | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$. This gives us the freedom to choose a different normalization. For example, we could scale the representation shown here by $(\|P\|^2+1)^{-1}$, putting it on the sphere where the light cone intersects the plane where the first two coordinates sum to $1$. | | ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by the above case. | | P lies on sphere or plane given by I | $Q(I_P, I) = 0$ | Actually also works if $I$ is the coordinates of a point, in which case "lies on" simply means "coincides with". | -| Sphere/planes represented by I and J are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1)$. Accordingly, their $Q$-product is −1. | +| Sphere/planes represented by I and J are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1)$. Accordingly, their $Q$-product is $−1$. | | Sphere/planes represented by I and J intersect (respectively, don't intersect) | $\|Q(I,J)\| < (\text{resp. }>)\; 1$ | Follows from the angle formula, at least conceptually. | | P is center of sphere represented by I | Well, $Q(I_P, I)$ comes out to be $(\|P\|^2/r - r + \|P\|^2/r)/2 - \|P\|^2/r$ or just $-r/2$ . | Is it if and only if ?   No this probably doesn't work because center is not conformal quantity. | | Distance between P and R is d | $Q(I_P, I_R) = d^2/2$ | | -- 2.34.1 From 3f0cedfaaba17a6dd4ece8678768a34617a806e2 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 26 Sep 2024 01:07:22 -0700 Subject: [PATCH 116/129] doc: Clarify characterization of center of sphere In the process, clarify the signed distance from a point to a sphere and add inversion across a sphere. --- notes/inversive.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/notes/inversive.md b/notes/inversive.md index 6af980b..70e7958 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -13,17 +13,18 @@ These coordinates are of form $I=(c, r, x, y, z)$ where we think of $c$ as the c | P lies on sphere or plane given by I | $Q(I_P, I) = 0$ | Actually also works if $I$ is the coordinates of a point, in which case "lies on" simply means "coincides with". | | Sphere/planes represented by I and J are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1)$. Accordingly, their $Q$-product is $−1$. | | Sphere/planes represented by I and J intersect (respectively, don't intersect) | $\|Q(I,J)\| < (\text{resp. }>)\; 1$ | Follows from the angle formula, at least conceptually. | -| P is center of sphere represented by I | Well, $Q(I_P, I)$ comes out to be $(\|P\|^2/r - r + \|P\|^2/r)/2 - \|P\|^2/r$ or just $-r/2$ . | Is it if and only if ?   No this probably doesn't work because center is not conformal quantity. | +| $P$ is center of sphere rep'd by $I$ | $Q(I, I_P) = -r/2$, where $1/r = 2Q(I_\infty, I)$ is the signed curvature of the sphere, and $I_P$ is normalized to have $Q(I_\infty, I_P) = 1/2$ | This relationship is equivalent to both of the following. (1) The point $P$ has signed distance $-r/2$ from the sphere. (2) Inversion across the sphere maps $\infty$ to $P$. | | Distance between P and R is d | $Q(I_P, I_R) = d^2/2$ | | -| Distance between P and sphere/plane rep by I | | In the very simple case of a plane $I$ rep'd by $(2s, 0, x, y, z)$ and a point $P$ that lies on its perpendicular through the origin, rep'd by $(r^2, 1, rx, ry, rz)$ we get $Q(I, I_p) = s-r$, which is indeed the signed distance between $I$ and $P$. Not sure if this generalizes to other combinations? | +| Signed distance between point rep'd by $V$ and sphere/plane rep'd by $I$ is $d$| $\frac{Q(I, V)}{2Q(I_\infty, V)} = Q(I_\infty, I)\,d^2 - d$ | We can use a Euclidean motion, represented linearly by a Lorentz transformation that fixes $I_\infty$, to put the point on the $z$ axis and put the nearest point on the sphere/plane at the origin with its normal pointing in the positive $z$ direction. Then the sphere/plane is represented by $I = (0, 1/r, 0, 0, -1)$, and the point can be represented by any multiple of $I_P = (d^2, 1, 0, 0, d)$, giving $Q(I, I_P) = d^2/2r - d$. We turn this into a general expression by writing it in terms of Lorentz-invariant quantities and making it independent of the normalization of $I_P$. | | Distance between sphere/planes rep by I and J | Note that for any two Euclidean-concentric spheres rep by $I$ and $J$ with radius $r$ and $s,$ $Q(I,J) = -\frac12\left(\frac rs  + \frac sr\right)$ depends only on the ratio of $r$ and $s$. So this can't give something that determines the Euclidean distance between the two spheres, which presumably grows as the two spheres are blown up proportionally. For another example, for any two parallel planes, $Q(I,J) = \pm1$. | Alex had said: $Q(I,J)=\cosh(d/2)^2$ maybe where d is distance in usual hyperbolic metric. Or maybe $\cosh(d)$. That may be right depending on what's meant by the hyperbolic metric there, but it seems like it won't determine a reasonable Euclidean distance between planes, which should differ between different pairs of parallel planes. | | Sphere centered on P through R | | Probably just calculate distance etc. | -| Plane rep'd by I goes through center of sphere rep'd by J | I think this is equivalent to the plane being perpendicular to the sphere, i.e. $Q(I,J) = 0$. | | +| Plane rep'd by I goes through center of sphere rep'd by J | This is equivalent to the plane being perpendicular to the sphere: that is, $Q(I, J) = 0$. | | | Dihedral angle between planes (or spheres?) rep by I and J | $\theta = \arccos(Q(I,J))$ | Aaron Fenyes points out: The angle between spheres in $S^3$ matches the angle between the planes they bound in $R^{(1,4)}$, which matches the angle between the spacelike vectors perpendicular to those planes. So we should have $Q(I,J) = \cos(\theta)$. Note that when the spheres do not intersect, we can interpret this as the "imaginary angle" between them, via $\cosh(t) = \cos(it)$. | | R, P, S are collinear | Maybe just cross product of two differences is 0. Or, $R,P,S,\infty$ lie on a circle, or equivalently, $I_R,I_P,I_S,I_\infty$ span a plane (rather than a three-space). | $R,P,S$ lying on a line isn't a conformal property, but $R,P,S,\infty$ lying on a circle is. | | Plane through noncollinear R, P, S | Should be, just solve $Q(I, I_R) = 0$ etc. | | | circle | Maybe concentric sphere and the containing plane? Note it is easy to constrain the relationship between those two: they must be perpendicular. | Defn: circle is intersection of two spheres. That does cover lines. But you lose the canonicalness | | line | Maybe two perpendicular containing planes? Maybe the plane perpendicular to the line and through origin, together with the point of the line on that plane? Or maybe just as a bag of collinear points? | The first is the limiting case of the possible circle rep, but it is not canonical. The second appears to be canonical, but I don't see a circle rep that corresponds to it. | +| Inversion across sphere $s$, rep'd by $I$ | $v \mapsto v + 2Q(I, v)\,I$ | This is just an educated guess, but its behavior is consistent with inversion in at least two ways. (1) It fixes points on $s$ and spheres perpendicular to $s$. (2) It preserves dihedral angles with $s$. | The unification of spheres/planes is indeed attractive for a project like Dyna3. The relationship between this representation and Geometric Algebras is a bit murky; likely it somehow fits under the Geometric Algebra umbrella. -- 2.34.1 From 0e929123d4fd2580fa792cb0b40a14843bc3f755 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Thu, 3 Oct 2024 20:39:15 -0700 Subject: [PATCH 117/129] doc: correct condition that two sphere/planes intersect --- notes/inversive.md | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/notes/inversive.md b/notes/inversive.md index 70e7958..32bed18 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -4,27 +4,27 @@ These coordinates are of form $I=(c, r, x, y, z)$ where we think of $c$ as the co-radius, $r$ as the radius, and $x, y, z$ as the "Euclidean" part, which we abbreviate $E_I$. There is an underlying basic quadratic form $Q(I_1,I_2) = (c_1r_2+c_2r_1)/2 - x_1x_2 -y_1y_2-z_1z_2$ which aids in calculation/verification of coordinates in this representation. We have: -| Entity or Relationship | Representation | Comments/questions | -| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Sphere s with radius r>0 centered on P = (x,y,z) | $I_s = (1/c, 1/r, x/r, y/r, z/r)$ satisfying $Q(I_s,I_s) = -1$, i.e., $c = r/(\|P\|^2 - r^2)$. | Note that $1/c = \|P\|^2/r - r$, so there is no trouble if $\|P\| = r$; we just get first coordinate to be 0. Using the point representation $I_P$ from below, let's orient the sphere so that its normals point into the "positive side," where $Q(I_P, I_s) > 0$. The vector $I_s$ then represents a sphere with outward normals, while $-I_s$ represents one with inward normals. | -| Plane p with unit normal $(x,y,z)$ through the (Euclidean) point $(sx,sy,sz)$ | $I_p = (-2s, 0, -x, -y, -z)$ | Note that $Q(I_p, I_p)$ is still $−1$. The parameter $s$ is the plane's distance from the origin. We orient planes using the same convention we use for spheres. For example, $(-2, 0, -1/\sqrt3, -1/\sqrt3, -1/\sqrt3)$ and $(2, 0, 1/\sqrt3, 1/\sqrt3, 1/\sqrt3)$ represent planes that coincide in space, which have their normals pointing away from and toward the origin, respectively. | -| Point P with Euclidean coordinates (x,y,z) | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$. This gives us the freedom to choose a different normalization. For example, we could scale the representation shown here by $(\|P\|^2+1)^{-1}$, putting it on the sphere where the light cone intersects the plane where the first two coordinates sum to $1$. | -| ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by the above case. | -| P lies on sphere or plane given by I | $Q(I_P, I) = 0$ | Actually also works if $I$ is the coordinates of a point, in which case "lies on" simply means "coincides with". | -| Sphere/planes represented by I and J are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1)$. Accordingly, their $Q$-product is $−1$. | -| Sphere/planes represented by I and J intersect (respectively, don't intersect) | $\|Q(I,J)\| < (\text{resp. }>)\; 1$ | Follows from the angle formula, at least conceptually. | -| $P$ is center of sphere rep'd by $I$ | $Q(I, I_P) = -r/2$, where $1/r = 2Q(I_\infty, I)$ is the signed curvature of the sphere, and $I_P$ is normalized to have $Q(I_\infty, I_P) = 1/2$ | This relationship is equivalent to both of the following. (1) The point $P$ has signed distance $-r/2$ from the sphere. (2) Inversion across the sphere maps $\infty$ to $P$. | -| Distance between P and R is d | $Q(I_P, I_R) = d^2/2$ | | -| Signed distance between point rep'd by $V$ and sphere/plane rep'd by $I$ is $d$| $\frac{Q(I, V)}{2Q(I_\infty, V)} = Q(I_\infty, I)\,d^2 - d$ | We can use a Euclidean motion, represented linearly by a Lorentz transformation that fixes $I_\infty$, to put the point on the $z$ axis and put the nearest point on the sphere/plane at the origin with its normal pointing in the positive $z$ direction. Then the sphere/plane is represented by $I = (0, 1/r, 0, 0, -1)$, and the point can be represented by any multiple of $I_P = (d^2, 1, 0, 0, d)$, giving $Q(I, I_P) = d^2/2r - d$. We turn this into a general expression by writing it in terms of Lorentz-invariant quantities and making it independent of the normalization of $I_P$. | -| Distance between sphere/planes rep by I and J | Note that for any two Euclidean-concentric spheres rep by $I$ and $J$ with radius $r$ and $s,$ $Q(I,J) = -\frac12\left(\frac rs  + \frac sr\right)$ depends only on the ratio of $r$ and $s$. So this can't give something that determines the Euclidean distance between the two spheres, which presumably grows as the two spheres are blown up proportionally. For another example, for any two parallel planes, $Q(I,J) = \pm1$. | Alex had said: $Q(I,J)=\cosh(d/2)^2$ maybe where d is distance in usual hyperbolic metric. Or maybe $\cosh(d)$. That may be right depending on what's meant by the hyperbolic metric there, but it seems like it won't determine a reasonable Euclidean distance between planes, which should differ between different pairs of parallel planes. | -| Sphere centered on P through R | | Probably just calculate distance etc. | -| Plane rep'd by I goes through center of sphere rep'd by J | This is equivalent to the plane being perpendicular to the sphere: that is, $Q(I, J) = 0$. | | -| Dihedral angle between planes (or spheres?) rep by I and J | $\theta = \arccos(Q(I,J))$ | Aaron Fenyes points out: The angle between spheres in $S^3$ matches the angle between the planes they bound in $R^{(1,4)}$, which matches the angle between the spacelike vectors perpendicular to those planes. So we should have $Q(I,J) = \cos(\theta)$. Note that when the spheres do not intersect, we can interpret this as the "imaginary angle" between them, via $\cosh(t) = \cos(it)$. | -| R, P, S are collinear | Maybe just cross product of two differences is 0. Or, $R,P,S,\infty$ lie on a circle, or equivalently, $I_R,I_P,I_S,I_\infty$ span a plane (rather than a three-space). | $R,P,S$ lying on a line isn't a conformal property, but $R,P,S,\infty$ lying on a circle is. | -| Plane through noncollinear R, P, S | Should be, just solve $Q(I, I_R) = 0$ etc. | | -| circle | Maybe concentric sphere and the containing plane? Note it is easy to constrain the relationship between those two: they must be perpendicular. | Defn: circle is intersection of two spheres. That does cover lines. But you lose the canonicalness | -| line | Maybe two perpendicular containing planes? Maybe the plane perpendicular to the line and through origin, together with the point of the line on that plane? Or maybe just as a bag of collinear points? | The first is the limiting case of the possible circle rep, but it is not canonical. The second appears to be canonical, but I don't see a circle rep that corresponds to it. | -| Inversion across sphere $s$, rep'd by $I$ | $v \mapsto v + 2Q(I, v)\,I$ | This is just an educated guess, but its behavior is consistent with inversion in at least two ways. (1) It fixes points on $s$ and spheres perpendicular to $s$. (2) It preserves dihedral angles with $s$. | +| Entity or Relationship | Representation | Comments/questions | +| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Sphere s with radius r>0 centered on P = (x,y,z) | $I_s = (1/c, 1/r, x/r, y/r, z/r)$ satisfying $Q(I_s,I_s) = -1$, i.e., $c = r/(\|P\|^2 - r^2)$. | Note that $1/c = \|P\|^2/r - r$, so there is no trouble if $\|P\| = r$; we just get first coordinate to be 0. Using the point representation $I_P$ from below, let's orient the sphere so that its normals point into the "positive side," where $Q(I_P, I_s) > 0$. The vector $I_s$ then represents a sphere with outward normals, while $-I_s$ represents one with inward normals. | +| Plane p with unit normal $(x,y,z)$ through the (Euclidean) point $(sx,sy,sz)$ | $I_p = (-2s, 0, -x, -y, -z)$ | Note that $Q(I_p, I_p)$ is still $−1$. The parameter $s$ is the plane's distance from the origin. We orient planes using the same convention we use for spheres. For example, $(-2, 0, -1/\sqrt3, -1/\sqrt3, -1/\sqrt3)$ and $(2, 0, 1/\sqrt3, 1/\sqrt3, 1/\sqrt3)$ represent planes that coincide in space, which have their normals pointing away from and toward the origin, respectively. | +| Point P with Euclidean coordinates (x,y,z) | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$. This gives us the freedom to choose a different normalization. For example, we could scale the representation shown here by $(\|P\|^2+1)^{-1}$, putting it on the sphere where the light cone intersects the plane where the first two coordinates sum to $1$. | +| ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by the above case. | +| P lies on sphere or plane given by I | $Q(I_P, I) = 0$ | Actually also works if $I$ is the coordinates of a point, in which case "lies on" simply means "coincides with". | +| Sphere/planes represented by I and J are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1)$. Accordingly, their $Q$-product is $−1$. | +| Sphere/planes represented by I and J intersect (respectively, don't intersect) | $\|Q(I,J)\| \leq (\text{resp. }>)\; 1$ | Follows from the angle formula, at least conceptually. | +| $P$ is center of sphere rep'd by $I$ | $Q(I, I_P) = -r/2$, where $1/r = 2Q(I_\infty, I)$ is the signed curvature of the sphere, and $I_P$ is normalized to have $Q(I_\infty, I_P) = 1/2$ | This relationship is equivalent to both of the following. (1) The point $P$ has signed distance $-r/2$ from the sphere. (2) Inversion across the sphere maps $\infty$ to $P$. | +| Distance between P and R is d | $Q(I_P, I_R) = d^2/2$ | | +| Signed distance between point rep'd by $V$ and sphere/plane rep'd by $I$ is $d$ | $\frac{Q(I, V)}{2Q(I_\infty, V)} = Q(I_\infty, I)\,d^2 - d$ | We can use a Euclidean motion, represented linearly by a Lorentz transformation that fixes $I_\infty$, to put the point on the $z$ axis and put the nearest point on the sphere/plane at the origin with its normal pointing in the positive $z$ direction. Then the sphere/plane is represented by $I = (0, 1/r, 0, 0, -1)$, and the point can be represented by any multiple of $I_P = (d^2, 1, 0, 0, d)$, giving $Q(I, I_P) = d^2/2r - d$. We turn this into a general expression by writing it in terms of Lorentz-invariant quantities and making it independent of the normalization of $I_P$. | +| Distance between sphere/planes rep by I and J | Note that for any two Euclidean-concentric spheres rep by $I$ and $J$ with radius $r$ and $s,$ $Q(I,J) = -\frac12\left(\frac rs  + \frac sr\right)$ depends only on the ratio of $r$ and $s$. So this can't give something that determines the Euclidean distance between the two spheres, which presumably grows as the two spheres are blown up proportionally. For another example, for any two parallel planes, $Q(I,J) = \pm1$. | Alex had said: $Q(I,J)=\cosh(d/2)^2$ maybe where d is distance in usual hyperbolic metric. Or maybe $\cosh(d)$. That may be right depending on what's meant by the hyperbolic metric there, but it seems like it won't determine a reasonable Euclidean distance between planes, which should differ between different pairs of parallel planes. | +| Sphere centered on P through R | | Probably just calculate distance etc. | +| Plane rep'd by I goes through center of sphere rep'd by J | This is equivalent to the plane being perpendicular to the sphere: that is, $Q(I, J) = 0$. | | +| Dihedral angle between planes (or spheres?) rep by I and J | $\theta = \arccos(Q(I,J))$ | Aaron Fenyes points out: The angle between spheres in $S^3$ matches the angle between the planes they bound in $R^{(1,4)}$, which matches the angle between the spacelike vectors perpendicular to those planes. So we should have $Q(I,J) = \cos(\theta)$. Note that when the spheres do not intersect, we can interpret this as the "imaginary angle" between them, via $\cosh(t) = \cos(it)$. | +| R, P, S are collinear | Maybe just cross product of two differences is 0. Or, $R,P,S,\infty$ lie on a circle, or equivalently, $I_R,I_P,I_S,I_\infty$ span a plane (rather than a three-space). | $R,P,S$ lying on a line isn't a conformal property, but $R,P,S,\infty$ lying on a circle is. | +| Plane through noncollinear R, P, S | Should be, just solve $Q(I, I_R) = 0$ etc. | | +| circle | Maybe concentric sphere and the containing plane? Note it is easy to constrain the relationship between those two: they must be perpendicular. | Defn: circle is intersection of two spheres. That does cover lines. But you lose the canonicalness | +| line | Maybe two perpendicular containing planes? Maybe the plane perpendicular to the line and through origin, together with the point of the line on that plane? Or maybe just as a bag of collinear points? | The first is the limiting case of the possible circle rep, but it is not canonical. The second appears to be canonical, but I don't see a circle rep that corresponds to it. | +| Inversion across sphere $s$, rep'd by $I$ | $v \mapsto v + 2Q(I, v)\,I$ | This is just an educated guess, but its behavior is consistent with inversion in at least two ways. (1) It fixes points on $s$ and spheres perpendicular to $s$. (2) It preserves dihedral angles with $s$. | The unification of spheres/planes is indeed attractive for a project like Dyna3. The relationship between this representation and Geometric Algebras is a bit murky; likely it somehow fits under the Geometric Algebra umbrella. -- 2.34.1 From ed88c7b9f9d7a41cc7c5daea461409c4b96361d3 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Thu, 3 Oct 2024 21:20:32 -0700 Subject: [PATCH 118/129] doc: fix latex conflict with markdown table format --- notes/inversive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/inversive.md b/notes/inversive.md index 32bed18..cb3f36c 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -12,7 +12,7 @@ These coordinates are of form $I=(c, r, x, y, z)$ where we think of $c$ as the c | ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by the above case. | | P lies on sphere or plane given by I | $Q(I_P, I) = 0$ | Actually also works if $I$ is the coordinates of a point, in which case "lies on" simply means "coincides with". | | Sphere/planes represented by I and J are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1)$. Accordingly, their $Q$-product is $−1$. | -| Sphere/planes represented by I and J intersect (respectively, don't intersect) | $\|Q(I,J)\| \leq (\text{resp. }>)\; 1$ | Follows from the angle formula, at least conceptually. | +| Sphere/planes represented by I and J intersect (respectively, don't intersect) | $\lvert Q(I,J)\rvert \leq (\text{resp. }>)\; 1$ | Follows from the angle formula, at least conceptually. | | $P$ is center of sphere rep'd by $I$ | $Q(I, I_P) = -r/2$, where $1/r = 2Q(I_\infty, I)$ is the signed curvature of the sphere, and $I_P$ is normalized to have $Q(I_\infty, I_P) = 1/2$ | This relationship is equivalent to both of the following. (1) The point $P$ has signed distance $-r/2$ from the sphere. (2) Inversion across the sphere maps $\infty$ to $P$. | | Distance between P and R is d | $Q(I_P, I_R) = d^2/2$ | | | Signed distance between point rep'd by $V$ and sphere/plane rep'd by $I$ is $d$ | $\frac{Q(I, V)}{2Q(I_\infty, V)} = Q(I_\infty, I)\,d^2 - d$ | We can use a Euclidean motion, represented linearly by a Lorentz transformation that fixes $I_\infty$, to put the point on the $z$ axis and put the nearest point on the sphere/plane at the origin with its normal pointing in the positive $z$ direction. Then the sphere/plane is represented by $I = (0, 1/r, 0, 0, -1)$, and the point can be represented by any multiple of $I_P = (d^2, 1, 0, 0, d)$, giving $Q(I, I_P) = d^2/2r - d$. We turn this into a general expression by writing it in terms of Lorentz-invariant quantities and making it independent of the normalization of $I_P$. | -- 2.34.1 From 609cd2f814490ffe5644f30f4a74355aebb7240d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 15 Oct 2024 14:24:36 -0700 Subject: [PATCH 119/129] doc: Correct and clarify various relations --- notes/inversive.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/notes/inversive.md b/notes/inversive.md index 70e7958..966eb3b 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -12,10 +12,10 @@ These coordinates are of form $I=(c, r, x, y, z)$ where we think of $c$ as the c | ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by the above case. | | P lies on sphere or plane given by I | $Q(I_P, I) = 0$ | Actually also works if $I$ is the coordinates of a point, in which case "lies on" simply means "coincides with". | | Sphere/planes represented by I and J are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1)$. Accordingly, their $Q$-product is $−1$. | -| Sphere/planes represented by I and J intersect (respectively, don't intersect) | $\|Q(I,J)\| < (\text{resp. }>)\; 1$ | Follows from the angle formula, at least conceptually. | -| $P$ is center of sphere rep'd by $I$ | $Q(I, I_P) = -r/2$, where $1/r = 2Q(I_\infty, I)$ is the signed curvature of the sphere, and $I_P$ is normalized to have $Q(I_\infty, I_P) = 1/2$ | This relationship is equivalent to both of the following. (1) The point $P$ has signed distance $-r/2$ from the sphere. (2) Inversion across the sphere maps $\infty$ to $P$. | -| Distance between P and R is d | $Q(I_P, I_R) = d^2/2$ | | -| Signed distance between point rep'd by $V$ and sphere/plane rep'd by $I$ is $d$| $\frac{Q(I, V)}{2Q(I_\infty, V)} = Q(I_\infty, I)\,d^2 - d$ | We can use a Euclidean motion, represented linearly by a Lorentz transformation that fixes $I_\infty$, to put the point on the $z$ axis and put the nearest point on the sphere/plane at the origin with its normal pointing in the positive $z$ direction. Then the sphere/plane is represented by $I = (0, 1/r, 0, 0, -1)$, and the point can be represented by any multiple of $I_P = (d^2, 1, 0, 0, d)$, giving $Q(I, I_P) = d^2/2r - d$. We turn this into a general expression by writing it in terms of Lorentz-invariant quantities and making it independent of the normalization of $I_P$. | +| Sphere/planes represented by I and J intersect (respectively, don't intersect) | $\|Q(I,J)\| \le (\text{resp. }>)\; 1$ | Follows from the angle formula and the tangency condition, at least conceptually. One subtlety: parallel planes have $Q$-product $\pm 1$, because they intersect at infinity! | +| $P$ is center of sphere rep'd by $I$ | $Q(I, I_P) = -r/2$, where $1/r = 2Q(I_\infty, I)$ is the signed curvature of the sphere, and $I_P$ is normalized in the standard way, which is to set $Q(I_\infty, I_P) = 1/2$ | This relationship is equivalent to both of the following. (1) The point $P$ has signed distance $-r$ from the sphere. (2) Inversion across the sphere maps $\infty$ to $P$. | +| Distance between P and R is d | $Q(I_P, I_R) = d^2/2$ | If $P$ and $R$ are represented by non-normalized vectors $V_P$ and $V_R$, the relation becomes $Q(V_P, V_R) = 2\,Q(V_P, I_\infty)\,Q(V_R, I_\infty)\,d^2$. This version of the relation makes it easier to see why $d$ goes to infinity as $P$ or $R$ approaches the point at infinity. | +| Signed distance between point rep'd by $V$ and sphere/plane rep'd by $I$ is $d$| In general, $\frac{Q(I, V)}{2Q(I_\infty, V)} = Q(I_\infty, I)\,d^2 + d$. When $V$ is normalized in the usual way, this simplifies to $Q(I, V) = d^2/r + d$ for a sphere of radius $r$, and to $Q(I, V) = d$ for a plane. | We can use a Euclidean motion, represented linearly by a Lorentz transformation that fixes $I_\infty$, to put the point on the $z$ axis and put the nearest point on the sphere/plane at the origin with its normal pointing in the positive $z$ direction. Then the sphere/plane is represented by $I = (0, 1/r, 0, 0, -1)$, and the point can be represented by any multiple of $I_P = (d^2, 1, 0, 0, d)$, giving $Q(I, I_P) = d^2/2r + d$. We turn this into a general expression by writing it in terms of Lorentz-invariant quantities and making it independent of the normalization of $I_P$. | | Distance between sphere/planes rep by I and J | Note that for any two Euclidean-concentric spheres rep by $I$ and $J$ with radius $r$ and $s,$ $Q(I,J) = -\frac12\left(\frac rs  + \frac sr\right)$ depends only on the ratio of $r$ and $s$. So this can't give something that determines the Euclidean distance between the two spheres, which presumably grows as the two spheres are blown up proportionally. For another example, for any two parallel planes, $Q(I,J) = \pm1$. | Alex had said: $Q(I,J)=\cosh(d/2)^2$ maybe where d is distance in usual hyperbolic metric. Or maybe $\cosh(d)$. That may be right depending on what's meant by the hyperbolic metric there, but it seems like it won't determine a reasonable Euclidean distance between planes, which should differ between different pairs of parallel planes. | | Sphere centered on P through R | | Probably just calculate distance etc. | | Plane rep'd by I goes through center of sphere rep'd by J | This is equivalent to the plane being perpendicular to the sphere: that is, $Q(I, J) = 0$. | | -- 2.34.1 From 48732093f788c48334f4f813e2c2ad37d8ff59d4 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 16 Oct 2024 16:00:36 -0700 Subject: [PATCH 120/129] doc: Clean up engine comments --- engine-proto/gram-test/Engine.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index ac5fe54..22f5914 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -59,11 +59,10 @@ nullmix = [Matrix{Int64}(I, 3, 3) zeros(Int64, 3, 2); zeros(Int64, 2, 3) [-1 1; unmix = [Matrix{Int64}(I, 3, 3) zeros(Int64, 3, 2); zeros(Int64, 2, 3) [-1 1; 1 1]] # the Lorentz form -## [old] Q = diagm([1, 1, 1, 1, -1]) Q = [Matrix{Int64}(I, 3, 3) zeros(Int64, 3, 2); zeros(Int64, 2, 3) [0 -2; -2 0]] -# project a matrix onto the subspace of matrices whose entries vanish at the -# given indices +# project a matrix onto the subspace of matrices whose entries vanish away from +# the given indices function proj_to_entries(mat, indices) result = zeros(size(mat)) for (j, k) in indices -- 2.34.1 From 160cb47f552f9743410250d446911e73313d7009 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 20 Oct 2024 16:06:11 -0700 Subject: [PATCH 121/129] doc: Fix typos and expand some comments a bit in notes/inversive.md --- notes/inversive.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/notes/inversive.md b/notes/inversive.md index fc8ce0d..31ac9e9 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -2,29 +2,29 @@ (proposed by Alex Kontorovich as a practical system for doing 3D geometric calculations) -These coordinates are of form $I=(c, r, x, y, z)$ where we think of $c$ as the co-radius, $r$ as the radius, and $x, y, z$ as the "Euclidean" part, which we abbreviate $E_I$. There is an underlying basic quadratic form $Q(I_1,I_2) = (c_1r_2+c_2r_1)/2 - x_1x_2 -y_1y_2-z_1z_2$ which aids in calculation/verification of coordinates in this representation. We have: +These coordinates are of form $I=(c, b, x, y, z)$ where we think of $c$ as the co-radius, $b$ as the "bend" (reciprocal radius), and $x, y, z$ as the "Euclidean" part, which we abbreviate $E_I$. There is an underlying basic quadratic form $Q(I_1,I_2) = (c_1b_2+c_2b_1)/2 - x_1x_2 -y_1y_2-z_1z_2$ which aids in calculation/verification of coordinates in this representation. We have: -| Entity or Relationship | Representation | Comments/questions | -| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Sphere s with radius r>0 centered on P = (x,y,z) | $I_s = (1/c, 1/r, x/r, y/r, z/r)$ satisfying $Q(I_s,I_s) = -1$, i.e., $c = r/(\|P\|^2 - r^2)$. | Note that $1/c = \|P\|^2/r - r$, so there is no trouble if $\|P\| = r$; we just get first coordinate to be 0. Using the point representation $I_P$ from below, let's orient the sphere so that its normals point into the "positive side," where $Q(I_P, I_s) > 0$. The vector $I_s$ then represents a sphere with outward normals, while $-I_s$ represents one with inward normals. | -| Plane p with unit normal $(x,y,z)$ through the (Euclidean) point $(sx,sy,sz)$ | $I_p = (-2s, 0, -x, -y, -z)$ | Note that $Q(I_p, I_p)$ is still $−1$. The parameter $s$ is the plane's distance from the origin. We orient planes using the same convention we use for spheres. For example, $(-2, 0, -1/\sqrt3, -1/\sqrt3, -1/\sqrt3)$ and $(2, 0, 1/\sqrt3, 1/\sqrt3, 1/\sqrt3)$ represent planes that coincide in space, which have their normals pointing away from and toward the origin, respectively. | -| Point P with Euclidean coordinates (x,y,z) | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$. This gives us the freedom to choose a different normalization. For example, we could scale the representation shown here by $(\|P\|^2+1)^{-1}$, putting it on the sphere where the light cone intersects the plane where the first two coordinates sum to $1$. | -| ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by the above case. | -| P lies on sphere or plane given by I | $Q(I_P, I) = 0$ | Actually also works if $I$ is the coordinates of a point, in which case "lies on" simply means "coincides with". | -| Sphere/planes represented by I and J are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1)$. Accordingly, their $Q$-product is $−1$. | -| Sphere/planes represented by I and J intersect (respectively, don't intersect) | $\lvert Q(I,J)\rvert \le (\text{resp. }>)\; 1$ | Follows from the angle formula and the tangency condition, at least conceptually. One subtlety: parallel planes have $Q$-product $\pm 1$, because they intersect at infinity! | -| $P$ is center of sphere rep'd by $I$ | $Q(I, I_P) = -r/2$, where $1/r = 2Q(I_\infty, I)$ is the signed curvature of the sphere, and $I_P$ is normalized in the standard way, which is to set $Q(I_\infty, I_P) = 1/2$ | This relationship is equivalent to both of the following. (1) The point $P$ has signed distance $-r$ from the sphere. (2) Inversion across the sphere maps $\infty$ to $P$. | -| Distance between P and R is d | $Q(I_P, I_R) = d^2/2$ | If $P$ and $R$ are represented by non-normalized vectors $V_P$ and $V_R$, the relation becomes $Q(V_P, V_R) = 2\,Q(V_P, I_\infty)\,Q(V_R, I_\infty)\,d^2$. This version of the relation makes it easier to see why $d$ goes to infinity as $P$ or $R$ approaches the point at infinity. | -| Signed distance between point rep'd by $V$ and sphere/plane rep'd by $I$ is $d$ | In general, $\frac{Q(I, V)}{2Q(I_\infty, V)} = Q(I_\infty, I)\,d^2 + d$. When $V$ is normalized in the usual way, this simplifies to $Q(I, V) = d^2/r + d$ for a sphere of radius $r$, and to $Q(I, V) = d$ for a plane. | We can use a Euclidean motion, represented linearly by a Lorentz transformation that fixes $I_\infty$, to put the point on the $z$ axis and put the nearest point on the sphere/plane at the origin with its normal pointing in the positive $z$ direction. Then the sphere/plane is represented by $I = (0, 1/r, 0, 0, -1)$, and the point can be represented by any multiple of $I_P = (d^2, 1, 0, 0, d)$, giving $Q(I, I_P) = d^2/2r + d$. We turn this into a general expression by writing it in terms of Lorentz-invariant quantities and making it independent of the normalization of $I_P$. | -| Distance between sphere/planes rep by I and J | Note that for any two Euclidean-concentric spheres rep by $I$ and $J$ with radius $r$ and $s,$ $Q(I,J) = -\frac12\left(\frac rs  + \frac sr\right)$ depends only on the ratio of $r$ and $s$. So this can't give something that determines the Euclidean distance between the two spheres, which presumably grows as the two spheres are blown up proportionally. For another example, for any two parallel planes, $Q(I,J) = \pm1$. | Alex had said: $Q(I,J)=\cosh(d/2)^2$ maybe where d is distance in usual hyperbolic metric. Or maybe $\cosh(d)$. That may be right depending on what's meant by the hyperbolic metric there, but it seems like it won't determine a reasonable Euclidean distance between planes, which should differ between different pairs of parallel planes. | -| Sphere centered on P through R | | Probably just calculate distance etc. | -| Plane rep'd by I goes through center of sphere rep'd by J | This is equivalent to the plane being perpendicular to the sphere: that is, $Q(I, J) = 0$. | | -| Dihedral angle between planes (or spheres?) rep by I and J | $\theta = \arccos(Q(I,J))$ | Aaron Fenyes points out: The angle between spheres in $S^3$ matches the angle between the planes they bound in $R^{(1,4)}$, which matches the angle between the spacelike vectors perpendicular to those planes. So we should have $Q(I,J) = \cos(\theta)$. Note that when the spheres do not intersect, we can interpret this as the "imaginary angle" between them, via $\cosh(t) = \cos(it)$. | -| R, P, S are collinear | Maybe just cross product of two differences is 0. Or, $R,P,S,\infty$ lie on a circle, or equivalently, $I_R,I_P,I_S,I_\infty$ span a plane (rather than a three-space). | $R,P,S$ lying on a line isn't a conformal property, but $R,P,S,\infty$ lying on a circle is. | -| Plane through noncollinear R, P, S | Should be, just solve $Q(I, I_R) = 0$ etc. | | -| circle | Maybe concentric sphere and the containing plane? Note it is easy to constrain the relationship between those two: they must be perpendicular. | Defn: circle is intersection of two spheres. That does cover lines. But you lose the canonicalness | -| line | Maybe two perpendicular containing planes? Maybe the plane perpendicular to the line and through origin, together with the point of the line on that plane? Or maybe just as a bag of collinear points? | The first is the limiting case of the possible circle rep, but it is not canonical. The second appears to be canonical, but I don't see a circle rep that corresponds to it. | -| Inversion across sphere $s$, rep'd by $I$ | $v \mapsto v + 2Q(I, v)\,I$ | This is just an educated guess, but its behavior is consistent with inversion in at least two ways. (1) It fixes points on $s$ and spheres perpendicular to $s$. (2) It preserves dihedral angles with $s$. | +| Entity or Relationship | Representation | Comments/questions | +| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Sphere $s$ with radius $r>0$ centered on $P = (x,y,z)$ | $I_s = (\frac1{c}, \frac1{r}, \frac{x}{r}, \frac{y}{r}, \frac{z}{r})$ satisfying $Q(I_s,I_s) = -1$, i.e., $c = r/(\|P\|^2 - r^2)$. | Note that $1/c = \|P\|^2/r - r$, so there is no trouble if $\|P\| = r$; we just get first coordinate to be 0. Using the point representation $I_P$ from below, let's orient the sphere so that its normals point into the "positive side," where $Q(I_P, I_s) > 0$. The vector $I_s$ then represents a sphere with outward normals, while $-I_s$ represents one with inward normals. | +| Plane $p$ with unit normal $(x,y,z)$ through the (Euclidean) point $(sx,sy,sz)$ | $I_p = (-2s, 0, -x, -y, -z)$ | Note that $Q(I_p, I_p)$ is still $−1$. We orient planes using the same convention we use for spheres. For example, $(-2, 0, -1/\sqrt3, -1/\sqrt3, -1/\sqrt3)$ and $(2, 0, 1/\sqrt3, 1/\sqrt3, 1/\sqrt3)$ represent planes that coincide in space, which have their normals pointing away from and toward the origin, respectively. Note that the ray from $(sx, sy, sz) \in p$ in direction $(-x, -y, -z)$ is the ray perpendicular to the plane through the origin; since $(-x, -y, -z)$ is a unit vector, $(sx, sy, sz)$ and hence $p$ is at distance $s$ from the origin. These coordinates are essentially the limit of a sphere's coordinates as its radius goes to infinity, or equivalently, as its bend goes to 0. | +| Point $P$ with Euclidean coordinates $(x,y,z)$ | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$. This gives us the freedom to choose a different normalization. For example, we could scale the representation shown here by $(\|P\|^2+1)^{-1}$, putting it on the sphere where the light cone intersects the plane where the first two coordinates sum to $1$. | +| ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by (some normalization of) the above case. | +| Point $P$ lies on sphere or plane given by $I$ | $Q(I_P, I) = 0$ | Actually also works if $I$ is the coordinates of a point, in which case "lies on" simply means "coincides with". | +| Sphere/planes represented by $I$ and $J$ are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1)$. Accordingly, their $Q$-product is $−1$. | +| Sphere/planes represented by $I$ and $J$ intersect (respectively, don't intersect) | $\lvert Q(I,J)\rvert \le (\text{resp. }>)\; 1$ | Follows from the angle formula and the tangency condition, at least conceptually. One subtlety: parallel planes have $Q$-product $\pm 1$, because they intersect at infinity (and in fact, are "tangent" there)! | +| $P$ is center of sphere rep'd by $I$ | $Q(I, I_P) = -r/2$, where $1/r = 2Q(I_\infty, I)$ is the signed bend of the sphere, and $I_P$ is normalized in the standard way, which is to set $Q(I_\infty, I_P) = 1/2$ | This relationship is equivalent to both of the following. (1) The point $P$ has signed distance $-r$ from the sphere. (2) Inversion across the sphere maps $\infty$ to $P$. | +| Distance between points $P$ and $R$ is $d$ | $Q(I_P, I_R) = d^2/2$ | If $P$ and $R$ are represented by non-normalized vectors $V_P$ and $V_R$, the relation becomes $Q(V_P, V_R) = 2\,Q(V_P, I_\infty)\,Q(V_R, I_\infty)\,d^2$. This version of the relation makes it easier to see why $d$ goes to infinity as $P$ or $R$ approaches the point at infinity. | +| Signed distance between point rep'd by $V$ and sphere/plane rep'd by $I$ is $d$ | In general, $\frac{Q(I, V)}{2Q(I_\infty, V)} = Q(I_\infty, I)\,d^2 + d$. When $V$ is normalized in the usual way, this simplifies to $Q(I, V) = d^2/r + d$ for a sphere of radius $r$, and to $Q(I, V) = d$ for a plane. | We can use a Euclidean motion, represented linearly by a Lorentz transformation that fixes $I_\infty$, to put the point on the $z$ axis and put the nearest point on the sphere/plane at the origin with its normal pointing in the positive $z$ direction. Then the sphere/plane is represented by $I = (0, 1/r, 0, 0, -1)$, and the point can be represented by any multiple of $I_P = (d^2, 1, 0, 0, d)$, giving $Q(I, I_P) = d^2/2r + d.$ We turn this into a general expression by writing it in terms of Lorentz-invariant quantities and making it independent of the normalization of $I_P$. | +| Distance between sphere/planes rep by $I$ and $J$ | Note that for any two Euclidean-concentric spheres rep by $I$ and $J$ with radius $r$ and $s,$ $Q(I,J) = -\frac12\left(\frac rs  + \frac sr\right)$ depends only on the ratio of $r$ and $s$. So this can't give something that determines the Euclidean distance between the two spheres, which presumably grows as the two spheres are blown up proportionally. For another example, for any two parallel planes, $Q(I,J) = \pm1$. | Alex had said: $Q(I,J)=\cosh(d/2)^2$ maybe where d is distance in usual hyperbolic metric. Or maybe $\cosh(d)$. That may be right depending on what's meant by the hyperbolic metric there, but it seems like it won't determine a reasonable Euclidean distance between planes, which should differ between different pairs of parallel planes. | +| Sphere centered on point $P$ through point $R$ | | Probably just calculate distance etc. | +| Plane rep'd by $I$ goes through center of sphere rep'd by $J$ | This is equivalent to the plane being perpendicular to the sphere: that is, $Q(I, J) = 0$. | | +| Dihedral angle between planes or spheres rep by $I$ and $J$ | $\theta = \arccos(Q(I,J))$ | Aaron Fenyes points out: The angle between spheres in $S^3$ matches the angle between the planes they bound in $R^{(1,4)}$, which matches the angle between the spacelike vectors perpendicular to those planes. So we should have $Q(I,J) = \cos(\theta)$. Note that when the spheres do not intersect, we can interpret this as the "imaginary angle" between them, via $\cosh(t) = \cos(it)$. | +| Points $R, P, S$ are collinear | Maybe just cross product of two differences is 0. Or, $R,P,S,\infty$ lie on a circle, or equivalently, $I_R,I_P,I_S,I_\infty$ span a plane (rather than a three-space). | $R,P,S$ lying on a line isn't a conformal property, but $R,P,S,\infty$ lying on a circle is. | +| Plane through noncollinear $R, P, S$ | Should be, just solve $Q(I, I_R) = 0$ etc. | | +| circle | Maybe concentric sphere and the containing plane? Note it is easy to constrain the relationship between those two: they must be perpendicular. | Defn: circle is intersection of two spheres. That does cover lines. But you lose the canonicalness | +| line | Maybe two perpendicular containing planes? Maybe the plane perpendicular to the line and through origin, together with the point of the line on that plane? Or maybe just as a bag of collinear points? | The first is the limiting case of the possible circle rep, but it is not canonical. The second appears to be canonical, but I don't see a circle rep that corresponds to it. | +| Inversion of entity represented by $v$ across sphere $s$, rep'd by $I_s$ | $v \mapsto v + 2Q(I_s, v)\,I_s$ | This is just an educated guess, but its behavior is consistent with inversion in at least two ways. (1) It fixes points on $s$ and spheres perpendicular to $s$. (2) It preserves dihedral angles with $s$. | The unification of spheres/planes is indeed attractive for a project like Dyna3. The relationship between this representation and Geometric Algebras is a bit murky; likely it somehow fits under the Geometric Algebra umbrella. -- 2.34.1 From ce62a94ddb14ce93582d13b39804c3e524947046 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 20 Oct 2024 16:10:01 -0700 Subject: [PATCH 122/129] doc: Two more small typos --- notes/inversive.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notes/inversive.md b/notes/inversive.md index 31ac9e9..35bb252 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -9,9 +9,9 @@ These coordinates are of form $I=(c, b, x, y, z)$ where we think of $c$ as the c | Sphere $s$ with radius $r>0$ centered on $P = (x,y,z)$ | $I_s = (\frac1{c}, \frac1{r}, \frac{x}{r}, \frac{y}{r}, \frac{z}{r})$ satisfying $Q(I_s,I_s) = -1$, i.e., $c = r/(\|P\|^2 - r^2)$. | Note that $1/c = \|P\|^2/r - r$, so there is no trouble if $\|P\| = r$; we just get first coordinate to be 0. Using the point representation $I_P$ from below, let's orient the sphere so that its normals point into the "positive side," where $Q(I_P, I_s) > 0$. The vector $I_s$ then represents a sphere with outward normals, while $-I_s$ represents one with inward normals. | | Plane $p$ with unit normal $(x,y,z)$ through the (Euclidean) point $(sx,sy,sz)$ | $I_p = (-2s, 0, -x, -y, -z)$ | Note that $Q(I_p, I_p)$ is still $−1$. We orient planes using the same convention we use for spheres. For example, $(-2, 0, -1/\sqrt3, -1/\sqrt3, -1/\sqrt3)$ and $(2, 0, 1/\sqrt3, 1/\sqrt3, 1/\sqrt3)$ represent planes that coincide in space, which have their normals pointing away from and toward the origin, respectively. Note that the ray from $(sx, sy, sz) \in p$ in direction $(-x, -y, -z)$ is the ray perpendicular to the plane through the origin; since $(-x, -y, -z)$ is a unit vector, $(sx, sy, sz)$ and hence $p$ is at distance $s$ from the origin. These coordinates are essentially the limit of a sphere's coordinates as its radius goes to infinity, or equivalently, as its bend goes to 0. | | Point $P$ with Euclidean coordinates $(x,y,z)$ | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$. This gives us the freedom to choose a different normalization. For example, we could scale the representation shown here by $(\|P\|^2+1)^{-1}$, putting it on the sphere where the light cone intersects the plane where the first two coordinates sum to $1$. | -| ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by (some normalization of) the above case. | +| ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by (some normalization of) the above case. | | Point $P$ lies on sphere or plane given by $I$ | $Q(I_P, I) = 0$ | Actually also works if $I$ is the coordinates of a point, in which case "lies on" simply means "coincides with". | -| Sphere/planes represented by $I$ and $J$ are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1)$. Accordingly, their $Q$-product is $−1$. | +| Sphere/planes represented by $I$ and $J$ are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1).$ Accordingly, their $Q$-product is $−1$. | | Sphere/planes represented by $I$ and $J$ intersect (respectively, don't intersect) | $\lvert Q(I,J)\rvert \le (\text{resp. }>)\; 1$ | Follows from the angle formula and the tangency condition, at least conceptually. One subtlety: parallel planes have $Q$-product $\pm 1$, because they intersect at infinity (and in fact, are "tangent" there)! | | $P$ is center of sphere rep'd by $I$ | $Q(I, I_P) = -r/2$, where $1/r = 2Q(I_\infty, I)$ is the signed bend of the sphere, and $I_P$ is normalized in the standard way, which is to set $Q(I_\infty, I_P) = 1/2$ | This relationship is equivalent to both of the following. (1) The point $P$ has signed distance $-r$ from the sphere. (2) Inversion across the sphere maps $\infty$ to $P$. | | Distance between points $P$ and $R$ is $d$ | $Q(I_P, I_R) = d^2/2$ | If $P$ and $R$ are represented by non-normalized vectors $V_P$ and $V_R$, the relation becomes $Q(V_P, V_R) = 2\,Q(V_P, I_\infty)\,Q(V_R, I_\infty)\,d^2$. This version of the relation makes it easier to see why $d$ goes to infinity as $P$ or $R$ approaches the point at infinity. | -- 2.34.1 From d3e5a0bc37defbd0b0c489bc2bc515d0ec4ebe28 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 20 Oct 2024 16:15:27 -0700 Subject: [PATCH 123/129] doc: Weird unicode minus --- notes/inversive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/inversive.md b/notes/inversive.md index 35bb252..0211f5e 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -7,7 +7,7 @@ These coordinates are of form $I=(c, b, x, y, z)$ where we think of $c$ as the c | Entity or Relationship | Representation | Comments/questions | | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Sphere $s$ with radius $r>0$ centered on $P = (x,y,z)$ | $I_s = (\frac1{c}, \frac1{r}, \frac{x}{r}, \frac{y}{r}, \frac{z}{r})$ satisfying $Q(I_s,I_s) = -1$, i.e., $c = r/(\|P\|^2 - r^2)$. | Note that $1/c = \|P\|^2/r - r$, so there is no trouble if $\|P\| = r$; we just get first coordinate to be 0. Using the point representation $I_P$ from below, let's orient the sphere so that its normals point into the "positive side," where $Q(I_P, I_s) > 0$. The vector $I_s$ then represents a sphere with outward normals, while $-I_s$ represents one with inward normals. | -| Plane $p$ with unit normal $(x,y,z)$ through the (Euclidean) point $(sx,sy,sz)$ | $I_p = (-2s, 0, -x, -y, -z)$ | Note that $Q(I_p, I_p)$ is still $−1$. We orient planes using the same convention we use for spheres. For example, $(-2, 0, -1/\sqrt3, -1/\sqrt3, -1/\sqrt3)$ and $(2, 0, 1/\sqrt3, 1/\sqrt3, 1/\sqrt3)$ represent planes that coincide in space, which have their normals pointing away from and toward the origin, respectively. Note that the ray from $(sx, sy, sz) \in p$ in direction $(-x, -y, -z)$ is the ray perpendicular to the plane through the origin; since $(-x, -y, -z)$ is a unit vector, $(sx, sy, sz)$ and hence $p$ is at distance $s$ from the origin. These coordinates are essentially the limit of a sphere's coordinates as its radius goes to infinity, or equivalently, as its bend goes to 0. | +| Plane $p$ with unit normal $(x,y,z)$ through the (Euclidean) point $(sx,sy,sz)$ | $I_p = (-2s, 0, -x, -y, -z)$ | Note that $Q(I_p, I_p)$ is still $-1$. We orient planes using the same convention we use for spheres. For example, $(-2, 0, -1/\sqrt3, -1/\sqrt3, -1/\sqrt3)$ and $(2, 0, 1/\sqrt3, 1/\sqrt3, 1/\sqrt3)$ represent planes that coincide in space, which have their normals pointing away from and toward the origin, respectively. Note that the ray from $(sx, sy, sz) \in p$ in direction $(-x, -y, -z)$ is the ray perpendicular to the plane through the origin; since $(-x, -y, -z)$ is a unit vector, $(sx, sy, sz)$ and hence $p$ is at distance $s$ from the origin. These coordinates are essentially the limit of a sphere's coordinates as its radius goes to infinity, or equivalently, as its bend goes to 0. | | Point $P$ with Euclidean coordinates $(x,y,z)$ | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$. This gives us the freedom to choose a different normalization. For example, we could scale the representation shown here by $(\|P\|^2+1)^{-1}$, putting it on the sphere where the light cone intersects the plane where the first two coordinates sum to $1$. | | ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by (some normalization of) the above case. | | Point $P$ lies on sphere or plane given by $I$ | $Q(I_P, I) = 0$ | Actually also works if $I$ is the coordinates of a point, in which case "lies on" simply means "coincides with". | -- 2.34.1 From bb881f806c121f41cfe7f24a77dd2edbd4c7ae28 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 20 Oct 2024 16:17:18 -0700 Subject: [PATCH 124/129] doc: Won't tex formula immediately followed by dash --- notes/inversive.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notes/inversive.md b/notes/inversive.md index 0211f5e..df77846 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -11,8 +11,8 @@ These coordinates are of form $I=(c, b, x, y, z)$ where we think of $c$ as the c | Point $P$ with Euclidean coordinates $(x,y,z)$ | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$. This gives us the freedom to choose a different normalization. For example, we could scale the representation shown here by $(\|P\|^2+1)^{-1}$, putting it on the sphere where the light cone intersects the plane where the first two coordinates sum to $1$. | | ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by (some normalization of) the above case. | | Point $P$ lies on sphere or plane given by $I$ | $Q(I_P, I) = 0$ | Actually also works if $I$ is the coordinates of a point, in which case "lies on" simply means "coincides with". | -| Sphere/planes represented by $I$ and $J$ are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1).$ Accordingly, their $Q$-product is $−1$. | -| Sphere/planes represented by $I$ and $J$ intersect (respectively, don't intersect) | $\lvert Q(I,J)\rvert \le (\text{resp. }>)\; 1$ | Follows from the angle formula and the tangency condition, at least conceptually. One subtlety: parallel planes have $Q$-product $\pm 1$, because they intersect at infinity (and in fact, are "tangent" there)! | +| Sphere/planes represented by $I$ and $J$ are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1).$ Accordingly, their $Q$ - product is $−1$. | +| Sphere/planes represented by $I$ and $J$ intersect (respectively, don't intersect) | $\lvert Q(I,J)\rvert \le (\text{resp. }>)\; 1$ | Follows from the angle formula and the tangency condition, at least conceptually. One subtlety: parallel planes have $Q$ - product $\pm 1$, because they intersect at infinity (and in fact, are "tangent" there)! | | $P$ is center of sphere rep'd by $I$ | $Q(I, I_P) = -r/2$, where $1/r = 2Q(I_\infty, I)$ is the signed bend of the sphere, and $I_P$ is normalized in the standard way, which is to set $Q(I_\infty, I_P) = 1/2$ | This relationship is equivalent to both of the following. (1) The point $P$ has signed distance $-r$ from the sphere. (2) Inversion across the sphere maps $\infty$ to $P$. | | Distance between points $P$ and $R$ is $d$ | $Q(I_P, I_R) = d^2/2$ | If $P$ and $R$ are represented by non-normalized vectors $V_P$ and $V_R$, the relation becomes $Q(V_P, V_R) = 2\,Q(V_P, I_\infty)\,Q(V_R, I_\infty)\,d^2$. This version of the relation makes it easier to see why $d$ goes to infinity as $P$ or $R$ approaches the point at infinity. | | Signed distance between point rep'd by $V$ and sphere/plane rep'd by $I$ is $d$ | In general, $\frac{Q(I, V)}{2Q(I_\infty, V)} = Q(I_\infty, I)\,d^2 + d$. When $V$ is normalized in the usual way, this simplifies to $Q(I, V) = d^2/r + d$ for a sphere of radius $r$, and to $Q(I, V) = d$ for a plane. | We can use a Euclidean motion, represented linearly by a Lorentz transformation that fixes $I_\infty$, to put the point on the $z$ axis and put the nearest point on the sphere/plane at the origin with its normal pointing in the positive $z$ direction. Then the sphere/plane is represented by $I = (0, 1/r, 0, 0, -1)$, and the point can be represented by any multiple of $I_P = (d^2, 1, 0, 0, d)$, giving $Q(I, I_P) = d^2/2r + d.$ We turn this into a general expression by writing it in terms of Lorentz-invariant quantities and making it independent of the normalization of $I_P$. | -- 2.34.1 From 47c2217bcf7f3a8f8ef57894cb18c3d7d0266090 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 20 Oct 2024 19:27:05 -0700 Subject: [PATCH 125/129] doc: fix more weird Unicode characters, probably from marktest editor --- notes/inversive.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/notes/inversive.md b/notes/inversive.md index df77846..7f84fd0 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -11,16 +11,16 @@ These coordinates are of form $I=(c, b, x, y, z)$ where we think of $c$ as the c | Point $P$ with Euclidean coordinates $(x,y,z)$ | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$. This gives us the freedom to choose a different normalization. For example, we could scale the representation shown here by $(\|P\|^2+1)^{-1}$, putting it on the sphere where the light cone intersects the plane where the first two coordinates sum to $1$. | | ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by (some normalization of) the above case. | | Point $P$ lies on sphere or plane given by $I$ | $Q(I_P, I) = 0$ | Actually also works if $I$ is the coordinates of a point, in which case "lies on" simply means "coincides with". | -| Sphere/planes represented by $I$ and $J$ are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1).$ Accordingly, their $Q$ - product is $−1$. | +| Sphere/planes represented by $I$ and $J$ are tangent | If $I$ and $J$ have the same orientation where they touch, $Q(I,J) = -1$. If they have opposing orientations, $Q(I,J) = 1$. | For example, the $xy$ plane with normal $-e_z$, represented by $(0,0,0,0,1)$, is tangent with matching orientation to the unit sphere centered at $(0,0,1)$ with outward normals, represented by $(0,1,0,0,1).$ Accordingly, their $Q$ - product is $-1$. | | Sphere/planes represented by $I$ and $J$ intersect (respectively, don't intersect) | $\lvert Q(I,J)\rvert \le (\text{resp. }>)\; 1$ | Follows from the angle formula and the tangency condition, at least conceptually. One subtlety: parallel planes have $Q$ - product $\pm 1$, because they intersect at infinity (and in fact, are "tangent" there)! | | $P$ is center of sphere rep'd by $I$ | $Q(I, I_P) = -r/2$, where $1/r = 2Q(I_\infty, I)$ is the signed bend of the sphere, and $I_P$ is normalized in the standard way, which is to set $Q(I_\infty, I_P) = 1/2$ | This relationship is equivalent to both of the following. (1) The point $P$ has signed distance $-r$ from the sphere. (2) Inversion across the sphere maps $\infty$ to $P$. | | Distance between points $P$ and $R$ is $d$ | $Q(I_P, I_R) = d^2/2$ | If $P$ and $R$ are represented by non-normalized vectors $V_P$ and $V_R$, the relation becomes $Q(V_P, V_R) = 2\,Q(V_P, I_\infty)\,Q(V_R, I_\infty)\,d^2$. This version of the relation makes it easier to see why $d$ goes to infinity as $P$ or $R$ approaches the point at infinity. | | Signed distance between point rep'd by $V$ and sphere/plane rep'd by $I$ is $d$ | In general, $\frac{Q(I, V)}{2Q(I_\infty, V)} = Q(I_\infty, I)\,d^2 + d$. When $V$ is normalized in the usual way, this simplifies to $Q(I, V) = d^2/r + d$ for a sphere of radius $r$, and to $Q(I, V) = d$ for a plane. | We can use a Euclidean motion, represented linearly by a Lorentz transformation that fixes $I_\infty$, to put the point on the $z$ axis and put the nearest point on the sphere/plane at the origin with its normal pointing in the positive $z$ direction. Then the sphere/plane is represented by $I = (0, 1/r, 0, 0, -1)$, and the point can be represented by any multiple of $I_P = (d^2, 1, 0, 0, d)$, giving $Q(I, I_P) = d^2/2r + d.$ We turn this into a general expression by writing it in terms of Lorentz-invariant quantities and making it independent of the normalization of $I_P$. | -| Distance between sphere/planes rep by $I$ and $J$ | Note that for any two Euclidean-concentric spheres rep by $I$ and $J$ with radius $r$ and $s,$ $Q(I,J) = -\frac12\left(\frac rs  + \frac sr\right)$ depends only on the ratio of $r$ and $s$. So this can't give something that determines the Euclidean distance between the two spheres, which presumably grows as the two spheres are blown up proportionally. For another example, for any two parallel planes, $Q(I,J) = \pm1$. | Alex had said: $Q(I,J)=\cosh(d/2)^2$ maybe where d is distance in usual hyperbolic metric. Or maybe $\cosh(d)$. That may be right depending on what's meant by the hyperbolic metric there, but it seems like it won't determine a reasonable Euclidean distance between planes, which should differ between different pairs of parallel planes. | +| Distance between sphere/planes rep by $I$ and $J$ | Note that for any two Euclidean-concentric spheres rep by $I$ and $J$ with radius $r$ and $s,$ $Q(I,J) = -\frac12\left(\frac rs + \frac sr\right)$ depends only on the ratio of $r$ and $s$. So this can't give something that determines the Euclidean distance between the two spheres, which presumably grows as the two spheres are blown up proportionally. For another example, for any two parallel planes, $Q(I,J) = \pm1$. | Alex had said: $Q(I,J)=\cosh(d/2)^2$ maybe where d is distance in usual hyperbolic metric. Or maybe $\cosh(d)$. That may be right depending on what's meant by the hyperbolic metric there, but it seems like it won't determine a reasonable Euclidean distance between planes, which should differ between different pairs of parallel planes. | | Sphere centered on point $P$ through point $R$ | | Probably just calculate distance etc. | | Plane rep'd by $I$ goes through center of sphere rep'd by $J$ | This is equivalent to the plane being perpendicular to the sphere: that is, $Q(I, J) = 0$. | | | Dihedral angle between planes or spheres rep by $I$ and $J$ | $\theta = \arccos(Q(I,J))$ | Aaron Fenyes points out: The angle between spheres in $S^3$ matches the angle between the planes they bound in $R^{(1,4)}$, which matches the angle between the spacelike vectors perpendicular to those planes. So we should have $Q(I,J) = \cos(\theta)$. Note that when the spheres do not intersect, we can interpret this as the "imaginary angle" between them, via $\cosh(t) = \cos(it)$. | -| Points $R, P, S$ are collinear | Maybe just cross product of two differences is 0. Or, $R,P,S,\infty$ lie on a circle, or equivalently, $I_R,I_P,I_S,I_\infty$ span a plane (rather than a three-space). | $R,P,S$ lying on a line isn't a conformal property, but $R,P,S,\infty$ lying on a circle is. | +| Points $R, P, S$ are collinear | Maybe just cross product of two differences is 0. Or, $R,P,S,\infty$ lie on a circle, or equivalently, $I_R,I_P,I_S,I_\infty$ span a plane (rather than a three-space). | $R,P,S$ lying on a line isn't a conformal property, but $R,P,S,\infty$ lying on a circle is. | | Plane through noncollinear $R, P, S$ | Should be, just solve $Q(I, I_R) = 0$ etc. | | | circle | Maybe concentric sphere and the containing plane? Note it is easy to constrain the relationship between those two: they must be perpendicular. | Defn: circle is intersection of two spheres. That does cover lines. But you lose the canonicalness | | line | Maybe two perpendicular containing planes? Maybe the plane perpendicular to the line and through origin, together with the point of the line on that plane? Or maybe just as a bag of collinear points? | The first is the limiting case of the possible circle rep, but it is not canonical. The second appears to be canonical, but I don't see a circle rep that corresponds to it. | -- 2.34.1 From cbad99d356c77fd69f17914c33153ed4898ef1d9 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 20 Oct 2024 19:33:33 -0700 Subject: [PATCH 126/129] doc: punctuation --- notes/inversive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/inversive.md b/notes/inversive.md index 7f84fd0..02a60e4 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -6,7 +6,7 @@ These coordinates are of form $I=(c, b, x, y, z)$ where we think of $c$ as the c | Entity or Relationship | Representation | Comments/questions | | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Sphere $s$ with radius $r>0$ centered on $P = (x,y,z)$ | $I_s = (\frac1{c}, \frac1{r}, \frac{x}{r}, \frac{y}{r}, \frac{z}{r})$ satisfying $Q(I_s,I_s) = -1$, i.e., $c = r/(\|P\|^2 - r^2)$. | Note that $1/c = \|P\|^2/r - r$, so there is no trouble if $\|P\| = r$; we just get first coordinate to be 0. Using the point representation $I_P$ from below, let's orient the sphere so that its normals point into the "positive side," where $Q(I_P, I_s) > 0$. The vector $I_s$ then represents a sphere with outward normals, while $-I_s$ represents one with inward normals. | +| Sphere $s$ with radius $r>0$ centered on $P = (x,y,z)$ | $I_s = (\frac1{c}, \frac1{r}, \frac{x}{r}, \frac{y}{r}, \frac{z}{r})$ satisfying $Q(I_s,I_s) = -1,$ i.e., $c = r/(\|P\|^2 - r^2)$. | Note that $1/c = \|P\|^2/r - r$, so there is no trouble if $\|P\| = r$; we just get first coordinate to be 0. Using the point representation $I_P$ from below, let's orient the sphere so that its normals point into the "positive side," where $Q(I_P, I_s) > 0$. The vector $I_s$ then represents a sphere with outward normals, while $-I_s$ represents one with inward normals. | | Plane $p$ with unit normal $(x,y,z)$ through the (Euclidean) point $(sx,sy,sz)$ | $I_p = (-2s, 0, -x, -y, -z)$ | Note that $Q(I_p, I_p)$ is still $-1$. We orient planes using the same convention we use for spheres. For example, $(-2, 0, -1/\sqrt3, -1/\sqrt3, -1/\sqrt3)$ and $(2, 0, 1/\sqrt3, 1/\sqrt3, 1/\sqrt3)$ represent planes that coincide in space, which have their normals pointing away from and toward the origin, respectively. Note that the ray from $(sx, sy, sz) \in p$ in direction $(-x, -y, -z)$ is the ray perpendicular to the plane through the origin; since $(-x, -y, -z)$ is a unit vector, $(sx, sy, sz)$ and hence $p$ is at distance $s$ from the origin. These coordinates are essentially the limit of a sphere's coordinates as its radius goes to infinity, or equivalently, as its bend goes to 0. | | Point $P$ with Euclidean coordinates $(x,y,z)$ | $I_P = (\|P\|^2, 1, x, y, z)$ | Note $Q(I_P,I_P) = 0$. This gives us the freedom to choose a different normalization. For example, we could scale the representation shown here by $(\|P\|^2+1)^{-1}$, putting it on the sphere where the light cone intersects the plane where the first two coordinates sum to $1$. | | ∞, the "point at infinity" | $I_\infty = (1,0,0,0,0)$ | The only solution to $Q(I,I) = 0$ not covered by (some normalization of) the above case. | -- 2.34.1 From 7fb4e3adee6726fa39d2f0c7d269ed82360634ef Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 20 Oct 2024 19:41:05 -0700 Subject: [PATCH 127/129] doc: another unicode pseudo-space --- notes/inversive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/inversive.md b/notes/inversive.md index 02a60e4..b20ced4 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -16,7 +16,7 @@ These coordinates are of form $I=(c, b, x, y, z)$ where we think of $c$ as the c | $P$ is center of sphere rep'd by $I$ | $Q(I, I_P) = -r/2$, where $1/r = 2Q(I_\infty, I)$ is the signed bend of the sphere, and $I_P$ is normalized in the standard way, which is to set $Q(I_\infty, I_P) = 1/2$ | This relationship is equivalent to both of the following. (1) The point $P$ has signed distance $-r$ from the sphere. (2) Inversion across the sphere maps $\infty$ to $P$. | | Distance between points $P$ and $R$ is $d$ | $Q(I_P, I_R) = d^2/2$ | If $P$ and $R$ are represented by non-normalized vectors $V_P$ and $V_R$, the relation becomes $Q(V_P, V_R) = 2\,Q(V_P, I_\infty)\,Q(V_R, I_\infty)\,d^2$. This version of the relation makes it easier to see why $d$ goes to infinity as $P$ or $R$ approaches the point at infinity. | | Signed distance between point rep'd by $V$ and sphere/plane rep'd by $I$ is $d$ | In general, $\frac{Q(I, V)}{2Q(I_\infty, V)} = Q(I_\infty, I)\,d^2 + d$. When $V$ is normalized in the usual way, this simplifies to $Q(I, V) = d^2/r + d$ for a sphere of radius $r$, and to $Q(I, V) = d$ for a plane. | We can use a Euclidean motion, represented linearly by a Lorentz transformation that fixes $I_\infty$, to put the point on the $z$ axis and put the nearest point on the sphere/plane at the origin with its normal pointing in the positive $z$ direction. Then the sphere/plane is represented by $I = (0, 1/r, 0, 0, -1)$, and the point can be represented by any multiple of $I_P = (d^2, 1, 0, 0, d)$, giving $Q(I, I_P) = d^2/2r + d.$ We turn this into a general expression by writing it in terms of Lorentz-invariant quantities and making it independent of the normalization of $I_P$. | -| Distance between sphere/planes rep by $I$ and $J$ | Note that for any two Euclidean-concentric spheres rep by $I$ and $J$ with radius $r$ and $s,$ $Q(I,J) = -\frac12\left(\frac rs + \frac sr\right)$ depends only on the ratio of $r$ and $s$. So this can't give something that determines the Euclidean distance between the two spheres, which presumably grows as the two spheres are blown up proportionally. For another example, for any two parallel planes, $Q(I,J) = \pm1$. | Alex had said: $Q(I,J)=\cosh(d/2)^2$ maybe where d is distance in usual hyperbolic metric. Or maybe $\cosh(d)$. That may be right depending on what's meant by the hyperbolic metric there, but it seems like it won't determine a reasonable Euclidean distance between planes, which should differ between different pairs of parallel planes. | +| Distance between sphere/planes rep by $I$ and $J$ | Note that for any two Euclidean-concentric spheres rep by $I$ and $J$ with radius $r$ and $s,$ $Q(I,J) = -\frac12\left(\frac rs + \frac sr\right)$ depends only on the ratio of $r$ and $s$. So this can't give something that determines the Euclidean distance between the two spheres, which presumably grows as the two spheres are blown up proportionally. For another example, for any two parallel planes, $Q(I,J) = \pm1$. | Alex had said: $Q(I,J)=\cosh(d/2)^2$ maybe where d is distance in usual hyperbolic metric. Or maybe $\cosh(d)$. That may be right depending on what's meant by the hyperbolic metric there, but it seems like it won't determine a reasonable Euclidean distance between planes, which should differ between different pairs of parallel planes. | | Sphere centered on point $P$ through point $R$ | | Probably just calculate distance etc. | | Plane rep'd by $I$ goes through center of sphere rep'd by $J$ | This is equivalent to the plane being perpendicular to the sphere: that is, $Q(I, J) = 0$. | | | Dihedral angle between planes or spheres rep by $I$ and $J$ | $\theta = \arccos(Q(I,J))$ | Aaron Fenyes points out: The angle between spheres in $S^3$ matches the angle between the planes they bound in $R^{(1,4)}$, which matches the angle between the spacelike vectors perpendicular to those planes. So we should have $Q(I,J) = \cos(\theta)$. Note that when the spheres do not intersect, we can interpret this as the "imaginary angle" between them, via $\cosh(t) = \cos(it)$. | -- 2.34.1 From 5918b30bb2eee72f1a4b0c9a557f7eff31d636a4 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 20 Oct 2024 19:53:36 -0700 Subject: [PATCH 128/129] doc: expand on representations of lines --- notes/inversive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/inversive.md b/notes/inversive.md index b20ced4..fc66d20 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -23,7 +23,7 @@ These coordinates are of form $I=(c, b, x, y, z)$ where we think of $c$ as the c | Points $R, P, S$ are collinear | Maybe just cross product of two differences is 0. Or, $R,P,S,\infty$ lie on a circle, or equivalently, $I_R,I_P,I_S,I_\infty$ span a plane (rather than a three-space). | $R,P,S$ lying on a line isn't a conformal property, but $R,P,S,\infty$ lying on a circle is. | | Plane through noncollinear $R, P, S$ | Should be, just solve $Q(I, I_R) = 0$ etc. | | | circle | Maybe concentric sphere and the containing plane? Note it is easy to constrain the relationship between those two: they must be perpendicular. | Defn: circle is intersection of two spheres. That does cover lines. But you lose the canonicalness | -| line | Maybe two perpendicular containing planes? Maybe the plane perpendicular to the line and through origin, together with the point of the line on that plane? Or maybe just as a bag of collinear points? | The first is the limiting case of the possible circle rep, but it is not canonical. The second appears to be canonical, but I don't see a circle rep that corresponds to it. | +| line | Maybe two perpendicular containing planes? Maybe the plane perpendicular to the line and through origin, together with the point of the line on that plane? Or maybe just as a bag of collinear points? | The first is the limiting case of the possible circle rep, but it is not canonical. However, there is a distinguished "standard" choice we could make: always choose one plane to contain the origin and the line, and the other to be the perpendicular plane containing the line. That choice or Plücker coordinates might be the best we can do. If we use the standardized perpendicular planes choice, then adding a line would be equivalent to adding two planes and the two constraints that one contains the origin and the other is perpendicular to it. That doesn't seem so bad. The second convention (perpendicular plane through the origin and a point on it) appears to be canonical, but there doesn't seem to be a circle representation that tends to it in the limit. | | Inversion of entity represented by $v$ across sphere $s$, rep'd by $I_s$ | $v \mapsto v + 2Q(I_s, v)\,I_s$ | This is just an educated guess, but its behavior is consistent with inversion in at least two ways. (1) It fixes points on $s$ and spheres perpendicular to $s$. (2) It preserves dihedral angles with $s$. | The unification of spheres/planes is indeed attractive for a project like Dyna3. The relationship between this representation and Geometric Algebras is a bit murky; likely it somehow fits under the Geometric Algebra umbrella. -- 2.34.1 From cbab33fb398e70f117e48c5f961f1257727fe5ec Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 20 Oct 2024 20:02:51 -0700 Subject: [PATCH 129/129] doc: expand on representation of collinearity --- notes/inversive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/inversive.md b/notes/inversive.md index fc66d20..5ee6329 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -20,7 +20,7 @@ These coordinates are of form $I=(c, b, x, y, z)$ where we think of $c$ as the c | Sphere centered on point $P$ through point $R$ | | Probably just calculate distance etc. | | Plane rep'd by $I$ goes through center of sphere rep'd by $J$ | This is equivalent to the plane being perpendicular to the sphere: that is, $Q(I, J) = 0$. | | | Dihedral angle between planes or spheres rep by $I$ and $J$ | $\theta = \arccos(Q(I,J))$ | Aaron Fenyes points out: The angle between spheres in $S^3$ matches the angle between the planes they bound in $R^{(1,4)}$, which matches the angle between the spacelike vectors perpendicular to those planes. So we should have $Q(I,J) = \cos(\theta)$. Note that when the spheres do not intersect, we can interpret this as the "imaginary angle" between them, via $\cosh(t) = \cos(it)$. | -| Points $R, P, S$ are collinear | Maybe just cross product of two differences is 0. Or, $R,P,S,\infty$ lie on a circle, or equivalently, $I_R,I_P,I_S,I_\infty$ span a plane (rather than a three-space). | $R,P,S$ lying on a line isn't a conformal property, but $R,P,S,\infty$ lying on a circle is. | +| Points $R, P, S$ are collinear | Maybe just cross product of two differences is 0. Or, $R,P,S,\infty$ lie on a circle, or equivalently, $I_R,I_P,I_S,I_\infty$ span a plane (rather than a three-space). Or we can add two planes constrained to be perpendicular with one constrained to contain the origin, and all three points constrained to lie on both. But that's a lot of auxiliary entities and constraints... | $R,P,S$ lying on a line isn't a conformal property, but $R,P,S,\infty$ lying on a circle is. | | Plane through noncollinear $R, P, S$ | Should be, just solve $Q(I, I_R) = 0$ etc. | | | circle | Maybe concentric sphere and the containing plane? Note it is easy to constrain the relationship between those two: they must be perpendicular. | Defn: circle is intersection of two spheres. That does cover lines. But you lose the canonicalness | | line | Maybe two perpendicular containing planes? Maybe the plane perpendicular to the line and through origin, together with the point of the line on that plane? Or maybe just as a bag of collinear points? | The first is the limiting case of the possible circle rep, but it is not canonical. However, there is a distinguished "standard" choice we could make: always choose one plane to contain the origin and the line, and the other to be the perpendicular plane containing the line. That choice or Plücker coordinates might be the best we can do. If we use the standardized perpendicular planes choice, then adding a line would be equivalent to adding two planes and the two constraints that one contains the origin and the other is perpendicular to it. That doesn't seem so bad. The second convention (perpendicular plane through the origin and a point on it) appears to be canonical, but there doesn't seem to be a circle representation that tends to it in the limit. | -- 2.34.1