From b864cf7866e3399da3fcf861d752849a2c8dc5b5 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 24 Jan 2024 11:16:24 -0500 Subject: [PATCH 001/132] 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 From 4d5aa3b327f680cb7d1e2478bd499faefdab1004 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 26 Jan 2024 11:14:32 -0500 Subject: [PATCH 002/132] 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 From 463a3b21e1fa1c1a531645b4f9cfafbb2fbd2034 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 27 Jan 2024 12:28:29 -0500 Subject: [PATCH 003/132] 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 From 86dbd9ea45ce9908c6895355c936a29ffe00629c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 27 Jan 2024 14:21:03 -0500 Subject: [PATCH 004/132] 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 From c29000d912a15896f98c33af5aefd691899721ad Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 28 Jan 2024 01:34:13 -0500 Subject: [PATCH 005/132] 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 From 59a527af43b869da239a4c3789a52dea52a0da09 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 29 Jan 2024 12:28:45 -0500 Subject: [PATCH 006/132] 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 From 0731c7aac18fee58daa0675e84eb065c029984d7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 29 Jan 2024 12:41:07 -0500 Subject: [PATCH 007/132] 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 --- From 6349f298ae723c21361f7450207b64f8e7f5e19c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 29 Jan 2024 19:11:21 -0500 Subject: [PATCH 008/132] 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 From 4e02ee16fc32519d0d08773c58e59c44afa79ec8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 30 Jan 2024 02:45:14 -0500 Subject: [PATCH 009/132] 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 From 65d23fb6676c73aad5817941c94d75573b6e4d10 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 30 Jan 2024 02:49:33 -0500 Subject: [PATCH 010/132] 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 From a3f3f6a31bde0f28c01cb35c9ad18f7719232d90 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 1 Feb 2024 16:13:22 -0500 Subject: [PATCH 011/132] 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 From 21f09c4a4dfbea36ed28088030480bd7b6be22cb Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 4 Feb 2024 16:08:13 -0500 Subject: [PATCH 012/132] 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 From 43cbf8a3a0c3e2152306876e8481fbcab797f7f8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 5 Feb 2024 00:10:13 -0500 Subject: [PATCH 013/132] 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 From 45aaaafc8f44a33cf6fb74c1cbc3a09771785015 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 8 Feb 2024 01:53:55 -0500 Subject: [PATCH 014/132] 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 From f97090c9974cb7d3b33b06396f5aeb84dcf620de Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 8 Feb 2024 01:58:12 -0500 Subject: [PATCH 015/132] 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) From 95c0ff14b249f4033864859fde61336f2ced5962 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 9 Feb 2024 17:09:43 -0500 Subject: [PATCH 016/132] 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]))) ] From 34358a872800810975e23f8499efd201982d4641 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 9 Feb 2024 23:44:10 -0500 Subject: [PATCH 017/132] 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 From becefe0c47253f0d7017f8e045a8dbadb761a52f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 10 Feb 2024 00:59:50 -0500 Subject: [PATCH 018/132] 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 From 06872a04afb2b211550b471012d2ade5b92bd0d6 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 10 Feb 2024 01:06:06 -0500 Subject: [PATCH 019/132] 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]) From 8e33987f596ae9572b53885f5b4642105d45165c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 10 Feb 2024 13:46:01 -0500 Subject: [PATCH 020/132] 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 From af1d31f6e61552b0320c7127ef3c720f75e40dea Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 10 Feb 2024 14:21:52 -0500 Subject: [PATCH 021/132] 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 From b3b7c2026daa828bdc119cdb37e8ca7d0a948a61 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 10 Feb 2024 14:50:50 -0500 Subject: [PATCH 022/132] 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 From 621c4c577630b3dc7e351bdd0e521c7e2a31d8ba Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 10 Feb 2024 15:02:26 -0500 Subject: [PATCH 023/132] 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))) From 6f18d4efcc560290366e4083fb7c81a349fce5a8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 10 Feb 2024 15:10:48 -0500 Subject: [PATCH 024/132] 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 From 1f173708eb57fc2305b9a17ab88bdf1375cc26fd Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 10 Feb 2024 17:39:26 -0500 Subject: [PATCH 025/132] 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 From 6cf07dc6a10766026cf30365c3cd431d87f17967 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 12 Feb 2024 20:34:12 -0500 Subject: [PATCH 026/132] 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 From a450f701fbfa1818c7795d654e6f2a65d2b75b15 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 12 Feb 2024 21:14:07 -0500 Subject: [PATCH 027/132] 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] From 31d5e7e864704895dbf0273c46185476e53e40f1 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 12 Feb 2024 22:48:16 -0500 Subject: [PATCH 028/132] 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)) From e41bcc7e13d29217d51ed5e7810b2c8f4e904fed Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 13 Feb 2024 04:02:14 -0500 Subject: [PATCH 029/132] 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 --- From 291d5c8ff685b642fd3535b6c46179c9f134c469 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 15 Feb 2024 13:28:01 -0800 Subject: [PATCH 030/132] 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) From 8d8bc9162cd494798821b1d83c2f338a788e736d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 15 Feb 2024 14:27:41 -0800 Subject: [PATCH 031/132] 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 = [ From ae5db0f9eaa71a4f87b70e5d95efd43fa0fd9426 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 15 Feb 2024 16:00:46 -0800 Subject: [PATCH 032/132] 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) From ba365174d3a643c033a04178706fe04bfe2bc555 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 15 Feb 2024 16:16:06 -0800 Subject: [PATCH 033/132] 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 From f2000e5731f52e2583d15619738acbd86c7c7974 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 15 Feb 2024 16:25:09 -0800 Subject: [PATCH 034/132] 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)) From 3170a933e4594befb3395882dc453b3f96143ae7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 15 Feb 2024 17:16:37 -0800 Subject: [PATCH 035/132] 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) From 16826cf07c692390f965cd2fe9de5491810caa1d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 20 Feb 2024 22:35:24 -0500 Subject: [PATCH 036/132] 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 From 717e5a6200a61db02b3cc03ae6117cf394a4ad6f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 21 Feb 2024 02:50:22 -0500 Subject: [PATCH 037/132] 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 From ef33b8ee101b533894e7c04d8484e2531e96fd1a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 1 Mar 2024 13:26:20 -0500 Subject: [PATCH 038/132] 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 From 58a5c38e6229e6e30b9e468114c4f302140bb2eb Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 30 May 2024 00:36:03 -0700 Subject: [PATCH 039/132] 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) From d1ce91d2aa6f02706a11dd533daeb86ab8523f21 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 24 Jun 2024 19:37:57 -0700 Subject: [PATCH 040/132] 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)) From 3c344815196a63cec0e5887239db65facbf77426 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 25 Jun 2024 01:54:01 -0700 Subject: [PATCH 041/132] 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 === From 3b10c95d5ff92fb7f31475734c7a237f3d780780 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 25 Jun 2024 02:07:39 -0700 Subject: [PATCH 042/132] 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)) From 06a9dda5bb58079db955675a8ef7a4ab9fed4945 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 25 Jun 2024 13:40:40 -0700 Subject: [PATCH 044/132] 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 === From b7b5b9386b4cf9ca4f4f213bdc7f4066a868eadf Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 25 Jun 2024 16:30:19 -0700 Subject: [PATCH 045/132] 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 From 182b5bb9f69f7e7bbbc524da85eb43fa62549510 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 25 Jun 2024 17:57:16 -0700 Subject: [PATCH 046/132] 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 From 665cb30ce0bf4bc442f5fcfbca24b51d73c1d116 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 25 Jun 2024 23:31:00 -0700 Subject: [PATCH 047/132] 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 From a3b1f4920c24c401c240308582009f28580bc9fe Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 26 Jun 2024 00:41:21 -0700 Subject: [PATCH 048/132] 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) From 4a28a47520f1bd6a6291052a1db39e5f9a04e647 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 26 Jun 2024 01:06:27 -0700 Subject: [PATCH 049/132] 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] From 2b6c4f4720212da7449e0b03f1b6566cebef6028 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 26 Jun 2024 11:28:47 -0700 Subject: [PATCH 050/132] 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 # From c933e07312c0dc3b6c8523eeb811d14b92e7a6bc Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 26 Jun 2024 11:39:34 -0700 Subject: [PATCH 051/132] 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 From 7aaf134a3672ac712744ce475b7ea5810a4655c3 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 26 Jun 2024 13:15:54 -0700 Subject: [PATCH 052/132] 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 ~~~ From 3eb4fc6c91a58bc7c6a43a311847d4e89b1d857a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 26 Jun 2024 15:24:31 -0700 Subject: [PATCH 053/132] 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 From 5ea32ac53cfac68e1267d8863338c32695214c05 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 26 Jun 2024 15:51:57 -0700 Subject: [PATCH 054/132] 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 From 05a824834d84a48018556678f412ef7a73318abe Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 26 Jun 2024 15:56:51 -0700 Subject: [PATCH 055/132] 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; } From 242d630cc6f43ebe032b09a20615251c4fa9bbe2 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 27 Jun 2024 21:45:41 -0700 Subject: [PATCH 056/132] 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; } } From e7dde5800c79988aa0225d84822d55339b83978f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 2 Jul 2024 12:35:12 -0700 Subject: [PATCH 057/132] 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 From 133519cacbc276f891fedd21663a7e64c6c8f58f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 2 Jul 2024 14:57:57 -0700 Subject: [PATCH 058/132] 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 === From 17fefff61ed74c03ad529a2ed7d4955a410f6037 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 2 Jul 2024 17:16:19 -0700 Subject: [PATCH 059/132] 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 From abc53b4705610cd06ed352954cd185bd121d771b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 2 Jul 2024 17:16:31 -0700 Subject: [PATCH 060/132] 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]) From 7e94fef19e61d185ab405c32642a0598bc3179f2 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 6 Jul 2024 21:32:43 -0700 Subject: [PATCH 061/132] 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 === From d39244d308513230ba2dd51e9c2b00bf4553a088 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 6 Jul 2024 21:35:09 -0700 Subject: [PATCH 062/132] 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/132] 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) From 736ac50b075de1d3f4ffef3566a88152c0761e80 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 7 Jul 2024 17:58:55 -0700 Subject: [PATCH 064/132] 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 From 828498b3de397467dbea0d82ffbdf985fb05da2d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 8 Jul 2024 12:56:14 -0700 Subject: [PATCH 065/132] 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 From 9efa99e8be12265991cc78077e6d55e691a0f7ee Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 8 Jul 2024 12:56:28 -0700 Subject: [PATCH 066/132] 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 From 93dd05c3172426685c3bd49f95f4be7554bc1fda Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 8 Jul 2024 14:19:05 -0700 Subject: [PATCH 067/132] 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 From 610fc451f08d194835297dc72b04f00013ea4675 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 8 Jul 2024 14:19:25 -0700 Subject: [PATCH 068/132] 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) From 023759a26715e07c2dd00ad274af55ec0f369350 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 8 Jul 2024 14:21:10 -0700 Subject: [PATCH 069/132] 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) From 77bc124170e2ee72fe94ec55d92af7f689aa24f7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 9 Jul 2024 14:00:24 -0700 Subject: [PATCH 070/132] 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 From f84d475580391e5ef9b5ef2d0eca02af9c121087 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 9 Jul 2024 14:01:30 -0700 Subject: [PATCH 071/132] 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 From 5652719642b236d36205b167a49510c9d21ae87b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 9 Jul 2024 14:10:23 -0700 Subject: [PATCH 072/132] 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 From 4d5ea062a3dc47ae92472dda23e7f8bdccc6c614 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 9 Jul 2024 15:00:13 -0700 Subject: [PATCH 073/132] 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 From d538cbf716607f9b53a348c8dfa202363575abf0 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 10 Jul 2024 23:31:44 -0700 Subject: [PATCH 074/132] 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 From 3910b9f740b7a775b5642892ac2b0575c1c73b22 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 11 Jul 2024 13:43:52 -0700 Subject: [PATCH 075/132] 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 From 25b09ebf925bc9a03f3f52014b7d0a8a3693b12e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 15 Jul 2024 11:32:04 -0700 Subject: [PATCH 076/132] 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) From 7b3efbc385c5561ee19dfc6b3322d7e79748d65e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 15 Jul 2024 13:15:15 -0700 Subject: [PATCH 077/132] 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 From 53d8c380479546ed2b0cf2735502ba4afb671e1c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 15 Jul 2024 14:08:57 -0700 Subject: [PATCH 078/132] 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) From 94e0d321d5175f15e6a0a1f356ec8eff0c9b4b4a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 15 Jul 2024 14:31:30 -0700 Subject: [PATCH 079/132] 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) From b185fd4b83ff494e36967008ae2f207a190eb610 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 15 Jul 2024 15:52:38 -0700 Subject: [PATCH 080/132] 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 From 1ce609836bf45c16131bdec879c6c3fb613385af Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 15 Jul 2024 22:11:54 -0700 Subject: [PATCH 081/132] 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) From 7c77481f5e9990bc5a9723b64a7bd7532943ce4d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 15 Jul 2024 23:39:05 -0700 Subject: [PATCH 082/132] 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 From e6cf08a9b3535bbb2c2b364f669e76534a123dce Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 15 Jul 2024 23:54:59 -0700 Subject: [PATCH 083/132] 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) From bde42ebac0548d1665de75af7722301229f95d24 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 14:30:43 -0700 Subject: [PATCH 084/132] 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 From 2038103d805dce9e39f61bac3b2e3cfbf1b11c2c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 15:37:14 -0700 Subject: [PATCH 085/132] 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] From 4728959ae049f728c204e206236865427bad762c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 17:22:33 -0700 Subject: [PATCH 086/132] 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] ) From ea640f48617b3423edb70344cf504fbdb2b16c1e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 17:33:32 -0700 Subject: [PATCH 087/132] 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 From 5abd4ca6e16b1efc2c1ca8ad3d1610c920c41c5a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 17:49:43 -0700 Subject: [PATCH 088/132] 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] ) From 6d233b5ee90c32cbc83f8e242ddbdc0288abec9f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 18:08:36 -0700 Subject: [PATCH 089/132] 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]), From d51d43f481e9819b2a312e82da13235a6dd638ad Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 18:27:22 -0700 Subject: [PATCH 090/132] 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] From 6e719f9943754b1b120e728738390b26752c54c7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 18:27:58 -0700 Subject: [PATCH 091/132] 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( From a02b76544a9930d09a86d36e0ea4560c4f339c45 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 18:55:36 -0700 Subject: [PATCH 092/132] 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 From 96ffc59642ca656193193ba6ec9d87e12cb0036f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 19:01:34 -0700 Subject: [PATCH 093/132] 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( From 01f44324c12108fd04ff33577a32eb3d770dc4a5 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 22:45:17 -0700 Subject: [PATCH 094/132] 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 From 69a704d4145d581ec0c3599a05a1ae88b42698af Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 23:07:34 -0700 Subject: [PATCH 095/132] 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)), From d0340c0b658b428eeb6293edf1006c7ce9d7f093 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 17 Jul 2024 23:37:28 -0700 Subject: [PATCH 096/132] 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( From 74c7f64b0c708b013db0175f0e729c8f3fc2bbc8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 00:03:12 -0700 Subject: [PATCH 097/132] 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$ | | From 24dae6807bf9799d39a94088d02f7980d826d057 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 00:16:23 -0700 Subject: [PATCH 098/132] 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$ | | From 3764fde2f6ca75cd56686030d019b11c44182e12 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 00:27:10 -0700 Subject: [PATCH 099/132] 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. | From a7f9545a3704002c328e71a5ac1b601a14e74ca5 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 00:43:00 -0700 Subject: [PATCH 100/132] 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)), From 9007c8bc7c906067e03f040180a2bc48b179053a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 00:43:44 -0700 Subject: [PATCH 101/132] 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] From b040bbb7feff00f3399eb1a85f938d32c814608c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 00:50:48 -0700 Subject: [PATCH 102/132] 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") From b24dcc9af8c26241825093762a8e8b984ece5f23 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 01:04:40 -0700 Subject: [PATCH 103/132] 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 From 33c09917d0da99ad8d973254b37272ae0b4ef47e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 01:05:13 -0700 Subject: [PATCH 104/132] 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 From 71c10adbdd2f82d9979b3fae32be3da2720e1efe Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 01:12:49 -0700 Subject: [PATCH 105/132] 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] From 19a4d49497061b4c0523f32e0c5d7ed5af045af3 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 01:48:05 -0700 Subject: [PATCH 106/132] 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 From a26f1e3927472cdd1b2453509ba0b29a1d4ae702 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 03:16:57 -0700 Subject: [PATCH 107/132] 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 From 8a77cd74846f972cd1721fbefa130618ac03e048 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 03:21:46 -0700 Subject: [PATCH 108/132] 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 From 9d69a900e28e0694b984ed326d3c9b6d6edca668 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 18 Jul 2024 03:39:41 -0700 Subject: [PATCH 109/132] 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]) From b92be312e8966b9ecc04e51566e5635a592fa538 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 21 Oct 2024 03:18:47 +0000 Subject: [PATCH 110/132] Engine prototype (#13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds code for a Julia-language prototype of a configuration solver, in the `engine-proto` folder. It uses Julia version 1.10.0. ### Approaches Development of this PR tried two broad approaches to the constraint geometry problem. Each one suggested various solution techniques. The Gram matrix approach, with the low-rank factorization technique, seems the most promising. - **Algebraic** *(In the `alg-test` subfolder).* Write the constraints as polynomials in the inversive coordinates of the elements, and use computational algebraic geometry techniques to solve the resulting system. We tried the following techniques. - **Gröbner bases** *(`Engine.Algebraic.jl`).* Symbolic. Find a Gröbner basis for the ideal generated by the constraint equations. Information about the solution variety, like its codimension, is then relatively easy to extract. - **Homotopy continuation** *(`Engine.Numerical.jl`).* Numerical. Cut the solution set along a random hyperplane to get a generic zero-dimensional slice, and then use a fancy homotopy technique to approximate the points in that slice. A few notes about our experiences can be found on the [engine prototype](wiki/Engine-prototype) wiki page. - **Gram matrix** *(in the `gram-test` subfolder).* A construction is described completely, up to conformal transformations, by the Gram matrix of the vectors representing its elements. Express the constraints as fixed entries of the Gram matrix, and use numerical linear algebra techniques to find a list of vectors whose Gram matrix fits the bill. We tried the following techniques. - **LDL decomposition** *(`gram-test.sage`, `gram-test.jl`, `overlap-test.jl`).* Find a cluster of up to five elements whose Gram matrix is completely filled in by the constraints. Use LDL decomposition to find a list of vectors with that Gram matrix. This technique can be made algebraic, as seen in `overlap-test.jl`. - **Low-rank factorization** *(source files listed in findings section).* Write down a quadratic loss function that says how far a set of vectors is from meeting the Gram matrix constraints. Use a smooth optimization technique like Newton's method or gradient descent to find a zero of the loss function. In addition to the polished prototype described in the results section, we have an early prototype using an off-the-shelf factorization package (`low-rank-test.jl`) and an visualization of the loss function landscape near global minima (`basin-shapes.jl`). The [Gram matrix parameterization](wiki/Gram-matrix-parameterization) wiki page contains detailed notes on this approach. ### Findings With the algebraic approach, we hit a performance wall pretty quickly as our constructions grew. It was often hard to find real solutions of the polynomial system, since the techniques we use work most naturally in the complex world. With the Gram matrix approach, on the other hand, we could solve interesting problems in acceptably short times using the low-rank factorization technique. We put the optimization routine in its own module (`Engine.jl`) and used it to solve five example problems: - `overlapping-pyramids.jl` - `circles-in-triangle.jl` - `sphere-in-tetrahedron.jl` - `tetrahedron-radius-ratio.jl` - `irisawa-hexlet.jl` We plan to use low-rank factorization of the Gram matrix in our first app prototype. ### Visualizations We used the visualizer in the `ganja-test` folder to visually check our low-rank factorization results. The visualizer runs [Ganja.js](https://enkimute.github.io/ganja.js/) in an Electron app, made with [Blink](https://github.com/JuliaGizmos/Blink.jl). Although Ganja.js makes beautiful pictures under most circumstances, we found two obstacles to using it in production. - It seems to have precision problems with low-curvature spheres. - We couldn't figure out how to customize its clipping and transparency settings, and the default settings often obscure construction details. Co-authored-by: Aaron Fenyes Co-authored-by: Glen Whitney Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/13 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- engine-proto/alg-test/ConstructionViewer.jl | 223 ++ engine-proto/alg-test/Engine.Algebraic.jl | 203 ++ engine-proto/alg-test/Engine.Numerical.jl | 53 + engine-proto/alg-test/Engine.jl | 76 + engine-proto/alg-test/HittingSet.jl | 111 + engine-proto/ganja-test/ganja-test.html | 96 + engine-proto/ganja-test/ganja-test.jl | 127 ++ engine-proto/gram-test/Engine.jl | 450 ++++ engine-proto/gram-test/basin-shapes.jl | 99 + engine-proto/gram-test/circles-in-triangle.jl | 76 + engine-proto/gram-test/ganja-1.0.204.js | 1913 +++++++++++++++++ engine-proto/gram-test/gram-test.jl | 85 + engine-proto/gram-test/gram-test.sage | 27 + engine-proto/gram-test/irisawa-hexlet.jl | 77 + engine-proto/gram-test/low-rank-test.jl | 49 + engine-proto/gram-test/overlap-test.jl | 37 + .../gram-test/overlapping-pyramids.jl | 90 + .../gram-test/sphere-in-tetrahedron.jl | 67 + .../gram-test/tetrahedron-radius-ratio.jl | 96 + notes/inversive.md | 43 +- 20 files changed, 3977 insertions(+), 21 deletions(-) create mode 100644 engine-proto/alg-test/ConstructionViewer.jl create mode 100644 engine-proto/alg-test/Engine.Algebraic.jl create mode 100644 engine-proto/alg-test/Engine.Numerical.jl create mode 100644 engine-proto/alg-test/Engine.jl create mode 100644 engine-proto/alg-test/HittingSet.jl create mode 100644 engine-proto/ganja-test/ganja-test.html create mode 100644 engine-proto/ganja-test/ganja-test.jl create mode 100644 engine-proto/gram-test/Engine.jl create mode 100644 engine-proto/gram-test/basin-shapes.jl create mode 100644 engine-proto/gram-test/circles-in-triangle.jl create mode 100644 engine-proto/gram-test/ganja-1.0.204.js create mode 100644 engine-proto/gram-test/gram-test.jl create mode 100644 engine-proto/gram-test/gram-test.sage create mode 100644 engine-proto/gram-test/irisawa-hexlet.jl create mode 100644 engine-proto/gram-test/low-rank-test.jl create mode 100644 engine-proto/gram-test/overlap-test.jl create mode 100644 engine-proto/gram-test/overlapping-pyramids.jl create mode 100644 engine-proto/gram-test/sphere-in-tetrahedron.jl create mode 100644 engine-proto/gram-test/tetrahedron-radius-ratio.jl diff --git a/engine-proto/alg-test/ConstructionViewer.jl b/engine-proto/alg-test/ConstructionViewer.jl new file mode 100644 index 0000000..b9c8ffb --- /dev/null +++ b/engine-proto/alg-test/ConstructionViewer.jl @@ -0,0 +1,223 @@ +module Viewer + +using Blink +using Colors +using Printf + +using Main.Engine + +export ConstructionViewer, display!, opentools!, closetools! + +# === 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(Blink.Dict(:width => 620, :height => 830)) + + # set stylesheet + style!(win, """ + body { + background-color: #ccc; + } + + /* 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; + overflow-y: scroll; + border-radius: 10px; + background-color: #f0f0f0; + } + + #control-panel > div { + margin-top: 5px; + padding: 4px; + border-radius: 5px; + border: solid; + font-family: monospace; + } + """) + + # 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, """ + // create algebra + var CGA3 = Algebra(4, 1); + + // initialize element list and palette + var elements = []; + var palette = []; + + // declare handles for the view and its options + var view; + var viewOpt; + + // declare handles for the controls + var controlPanel; + var visToggles; + + // create scene function + function scene() { + commands = []; + for (let n = 0; n < elements.length; ++n) { + if (visToggles[n].checked) { + commands.push(palette[n], elements[n]); + } + } + return commands; + } + + function updateView() { + requestAnimationFrame(view.update.bind(view, scene)); + } + """) + + @js win begin + # create view + viewOpt = Dict( + :conformal => true, + :gl => true, + :devicePixelRatio => window.devicePixelRatio + ) + 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) + 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 = [] + for elt in eachcol(Engine.unmix * 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, + # 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 + + # create visibility toggles + @js viewer.win begin + controlPanel.replaceChildren() + visToggles = [] + end + 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 + @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 + + # update view + @js viewer.win updateView() +end + +function opentools!(viewer::ConstructionViewer) + size(viewer.win, 1240, 830) + opentools(viewer.win) +end + +function closetools!(viewer::ConstructionViewer) + closetools(viewer.win) + size(viewer.win, 620, 830) +end + +end + +# ~~~ sandbox setup ~~~ + +elements = let + 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 1+a + 0.5 0.5 0.5 0.5 1-a + ] +end + +# show construction +viewer = Viewer.ConstructionViewer() +Viewer.display!(viewer, elements) \ No newline at end of file diff --git a/engine-proto/alg-test/Engine.Algebraic.jl b/engine-proto/alg-test/Engine.Algebraic.jl new file mode 100644 index 0000000..a9b6667 --- /dev/null +++ b/engine-proto/alg-test/Engine.Algebraic.jl @@ -0,0 +1,203 @@ +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::Vector{Point{T}} + spheres::Vector{Sphere{T}} + relations::Vector{Relation{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), + 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 + # [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 + + (Generic.Ideal(coordring, eqns), eqns) +end + +end \ No newline at end of file diff --git a/engine-proto/alg-test/Engine.Numerical.jl b/engine-proto/alg-test/Engine.Numerical.jl new file mode 100644 index 0000000..d1e14bd --- /dev/null +++ b/engine-proto/alg-test/Engine.Numerical.jl @@ -0,0 +1,53 @@ +module Numerical + +using Random: default_rng +using LinearAlgebra +using AbstractAlgebra +using HomotopyContinuation: + Variable, Expression, AbstractSystem, System, LinearSubspace, + nvariables, isreal, witness_set, results +import GLMakie +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 + +# --- sampling --- + +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(rng, nvariables(F))) for _ in 1:dim)... + )) + cut = LinearSubspace(normals, fill(0., dim)) + filter(isreal, results(witness_set(F, cut, seed = 0x1974abba))) +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/alg-test/Engine.jl b/engine-proto/alg-test/Engine.jl new file mode 100644 index 0000000..f6f92c5 --- /dev/null +++ b/engine-proto/alg-test/Engine.jl @@ -0,0 +1,76 @@ +include("HittingSet.jl") + +module Engine + +include("Engine.Algebraic.jl") +include("Engine.Numerical.jl") + +using .Algebraic +using .Numerical + +export Construction, mprod, codimension, dimension + +end + +# ~~~ sandbox setup ~~~ + +using Random +using Distributions +using LinearAlgebra +using AbstractAlgebra +using HomotopyContinuation +using GLMakie + +CoeffType = Rational{Int64} + +spheres = [Engine.Sphere{CoeffType}() for _ in 1:3] +tangencies = [ + Engine.AlignsWithBy{CoeffType}( + spheres[n], + spheres[mod1(n+1, length(spheres))], + CoeffType(1) + ) + for n in 1:3 +] +ctx_tan_sph = Engine.Construction{CoeffType}(elements = spheres, relations = 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") + +# --- test rational cut --- + +coordring = base_ring(ideal_tan_sph) +vbls = Variable.(symbols(coordring)) + +# test a random witness set +system = CompiledSystem(System(eqns_tan_sph, variables = vbls)) +norm2 = vec -> real(dot(conj.(vec), vec)) +rng = MersenneTwister(6071) +n_planes = 6 +samples = [] +for _ in 1:n_planes + 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) + end + end +end +println("Found $(length(samples)) sample solutions") + +# show a sample solution +function show_solution(ctx, 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 diff --git a/engine-proto/alg-test/HittingSet.jl b/engine-proto/alg-test/HittingSet.jl new file mode 100644 index 0000000..347c4d2 --- /dev/null +++ b/engine-proto/alg-test/HittingSet.jl @@ -0,0 +1,111 @@ +module HittingSet + +export HittingSetProblem, solve + +HittingSetProblem{T} = Pair{Set{T}, Vector{Pair{T, Set{Set{T}}}}} + +# `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, targets)) + 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) + 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] + targets = 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(targets) + if isa(problem, HittingSetProblem{T}) + println("Correct type") + else + println("Wrong type: ", typeof(problem)) + end + problem +end + +end \ No newline at end of file diff --git a/engine-proto/ganja-test/ganja-test.html b/engine-proto/ganja-test/ganja-test.html new file mode 100644 index 0000000..0207dcc --- /dev/null +++ b/engine-proto/ganja-test/ganja-test.html @@ -0,0 +1,96 @@ + + + + + + + +

+ + + diff --git a/engine-proto/ganja-test/ganja-test.jl b/engine-proto/ganja-test/ganja-test.jl new file mode 100644 index 0000000..6c55061 --- /dev/null +++ b/engine-proto/ganja-test/ganja-test.jl @@ -0,0 +1,127 @@ +using Blink +using Colors + +# === 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) + +function add_element!(vec) + # add element + full_vec = [0; vec; fill(0, 26)] + 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 === + +# create window and open developer console +win = Window() +opentools(win) + +# set stylesheet +style!(win, """ + 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; + } +""") + +# 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; + } + + function flip() { + let last = elements.length - 1; + for (let n = 0; n < last; ++n) { + // reflect + elements[n] = CGA3.Mul(CGA3.Mul(elements[last], elements[n]), elements[last]); + + // 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 diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl new file mode 100644 index 0000000..22f5914 --- /dev/null +++ b/engine-proto/gram-test/Engine.jl @@ -0,0 +1,450 @@ +module Engine + +using LinearAlgebra +using GenericLinearAlgebra +using SparseArrays +using Random +using Optim + +export + rand_on_shell, Q, DescentHistory, + realize_gram_gradient, realize_gram_newton, realize_gram_optim, realize_gram + +# === guessing === + +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) + nullmix * [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) + +# === elements === + +point(pos) = [pos; 0.5; 0.5 * dot(pos, pos)] + +plane(normal, offset) = [-normal; 0; -offset] + +function sphere(center, radius) + dist_sq = dot(center, center) + [ + center / radius; + 0.5 / radius; + 0.5 * (dist_sq / radius - radius) + ] +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 = [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 away from +# 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)) + 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} + neg_grad::Array{Matrix{T}} + base_step::Array{Matrix{T}} + hess::Array{Hermitian{T, Matrix{T}}} + slope::Array{T} + stepsize::Array{T} + positive::Array{Bool} + 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), + 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), + 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, positive, backoff_steps, last_line_L, last_line_loss) + 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_gradient( + gram::SparseMatrixCSC{T, <:Any}, + guess::Matrix{T}; + scaled_tol = 1e-30, + min_efficiency = 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 = dot(Δ_proj, Δ_proj) + for _ 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) + dir = neg_grad / slope + + # store 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, 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 * dir + Δ_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 * 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 + push!(history.scaled_loss, loss / scale_adjustment) + 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 + hess = Hermitian(hess) + push!(history.hess, hess) + + # 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 + +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} +) 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), + Newton() + ) +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}, + frozen = nothing; + 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 + + # 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) + + # 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) + + # regularize the Hessian + min_eigval = minimum(eigvals(hess)) + push!(history.positive, min_eigval > 0) + if min_eigval <= 0 + hess -= reg_scale * min_eigval * I + end + + # 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 + 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) + step_success = false + for backoff_steps in 0:max_backoff_steps + history.stepsize[end] = rate + 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 + step_success = true + break + end + rate *= backoff + end + + # 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, loss < tol, history +end + +end \ No newline at end of file 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 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..1bd22a7 --- /dev/null +++ b/engine-proto/gram-test/circles-in-triangle.jl @@ -0,0 +1,76 @@ +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:9 + for k in 1:9 + filled = false + if 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 == 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)) +#= 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.seed!(58271) +guess = hcat( + Engine.plane(BigFloat[0, 0, 1], BigFloat(0)), + 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] + +# 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") \ No newline at end of file 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 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(1-n_pos); -ones(4-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(big_gram) +if isone(p) + printgood("Found a solution") +else + printbad("Didn't find a solution") +end +solution = transpose(L) +mform = U * inv(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; + 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_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) + +# 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 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 diff --git a/engine-proto/gram-test/irisawa-hexlet.jl b/engine-proto/gram-test/irisawa-hexlet.jl new file mode 100644 index 0000000..67def8c --- /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 "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 chain sphere + 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/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..e75531a --- /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(AbstractAlgebra.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) diff --git a/engine-proto/gram-test/overlapping-pyramids.jl b/engine-proto/gram-test/overlapping-pyramids.jl new file mode 100644 index 0000000..a4ae01a --- /dev/null +++ b/engine-proto/gram-test/overlapping-pyramids.jl @@ -0,0 +1,90 @@ +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 +# also mutually tangent +J = Int64[] +K = Int64[] +values = BigFloat[] +for j in 1:7 + for k in 1:7 + if (j <= 5 && k <= 5) || (j >= 3 && k >= 3) + push!(J, j) + push!(K, k) + push!(values, j == k ? 1 : -1) + end + end +end +gram = sparse(J, K, values) + +# set the independent variable +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 +Random.seed!(50793) +guess = let + 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 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)) + ) +end + +# 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) +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 === + +#= +R, gens = polynomial_ring(AbstractAlgebra.Rationals{BigInt}(), ["x", "t₁", "t₂", "t₃"]) +x = gens[1] +t = gens[2:4] + +S, u = polynomial_ring(AbstractAlgebra.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) +=# \ 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 new file mode 100644 index 0000000..97f0720 --- /dev/null +++ b/engine-proto/gram-test/sphere-in-tetrahedron.jl @@ -0,0 +1,67 @@ +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: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 +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)), + BigFloat[0, 0, 0, 0, 1] +) +frozen = [CartesianIndex(j, 6) for j in 1:5] + +# 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") \ No newline at end of file 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..7ceb794 --- /dev/null +++ b/engine-proto/gram-test/tetrahedron-radius-ratio.jl @@ -0,0 +1,96 @@ +include("Engine.jl") + +using LinearAlgebra +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:11 + for k in 1:11 + filled = false + if j == 11 + if k <= 4 + push!(values, 0) + filled = true + end + elseif k == 11 + if j <= 4 + push!(values, 0) + filled = true + end + elseif j == k + push!(values, j <= 6 ? 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 7 <= k <= 10 && k - j != 6 + push!(values, 0) + filled = true + end + elseif k <= 4 + if j == 5 + push!(values, -1) + filled = true + 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) + 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 0 + 1 -1 1 -1 0 0 + 1 -1 -1 1 0 0 + 0 0 0 0 1.5 0.5 + 1 1 1 1 -0.5 -1.5 + ] + 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( + [CartesianIndex(4, k) for k in 7:10], + [CartesianIndex(j, 11) for j in 1:5] +) + +# 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]) +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 diff --git a/notes/inversive.md b/notes/inversive.md index 6de7ef2..5ee6329 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -2,28 +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)$. | 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)$ | -| 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 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. | -| 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. | | -| 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 = (\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). 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. | +| 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. From 86fa682b311733a0b7bb0b538c6aaef53937d768 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 21 Oct 2024 23:38:27 +0000 Subject: [PATCH 111/132] feat: Application prototype (#14) Creates a prototype user interface for dyna3 in the `app-proto` folder. The interface is dynamically constructed using [Sycamore](https://sycamore.dev). The prototype includes: * An application state model (the `AppState` type) * A constraint problem model (the `Assembly` type), used in the application state * Two views * A 3D rendering of the assembly (the `Display` component) * A list of elements and constraints (the `Outline` component) The following features confirm that the views can reflect and send input to the model: * You can select elements by clicking and shift-clicking them in the outline. The selected elements are highlighted in the display. * You can add elements using a button above the outline. The new elements appear in the display. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/14 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/.gitignore | 4 + app-proto/Cargo.toml | 42 ++++ app-proto/index.html | 9 + app-proto/main.css | 124 ++++++++++ app-proto/src/add_remove.rs | 242 +++++++++++++++++++ app-proto/src/assembly.rs | 93 ++++++++ app-proto/src/display.rs | 443 +++++++++++++++++++++++++++++++++++ app-proto/src/engine.rs | 27 +++ app-proto/src/identity.vert | 7 + app-proto/src/inversive.frag | 234 ++++++++++++++++++ app-proto/src/main.rs | 42 ++++ app-proto/src/outline.rs | 161 +++++++++++++ 12 files changed, 1428 insertions(+) create mode 100644 app-proto/.gitignore create mode 100644 app-proto/Cargo.toml create mode 100644 app-proto/index.html create mode 100644 app-proto/main.css create mode 100644 app-proto/src/add_remove.rs create mode 100644 app-proto/src/assembly.rs create mode 100644 app-proto/src/display.rs create mode 100644 app-proto/src/engine.rs create mode 100644 app-proto/src/identity.vert create mode 100644 app-proto/src/inversive.frag create mode 100644 app-proto/src/main.rs create mode 100644 app-proto/src/outline.rs diff --git a/app-proto/.gitignore b/app-proto/.gitignore new file mode 100644 index 0000000..19aa86b --- /dev/null +++ b/app-proto/.gitignore @@ -0,0 +1,4 @@ +target +dist +profiling +Cargo.lock \ No newline at end of file diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml new file mode 100644 index 0000000..920469a --- /dev/null +++ b/app-proto/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "sketch-outline" +version = "0.1.0" +authors = ["Aaron"] +edition = "2021" + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +itertools = "0.13.0" +js-sys = "0.3.70" +nalgebra = "0.33.0" +rustc-hash = "2.0.0" +slab = "0.4.9" +sycamore = "0.9.0-beta.3" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.7", optional = true } + +[dependencies.web-sys] +version = "0.3.69" +features = [ + 'HtmlCanvasElement', + 'Performance', + 'WebGl2RenderingContext', + 'WebGlBuffer', + 'WebGlProgram', + 'WebGlShader', + 'WebGlUniformLocation', + 'WebGlVertexArrayObject' +] + +[dev-dependencies] +wasm-bindgen-test = "0.3.34" + +[profile.release] +opt-level = "s" # optimize for small code size +debug = true # include debug symbols diff --git a/app-proto/index.html b/app-proto/index.html new file mode 100644 index 0000000..5474fe9 --- /dev/null +++ b/app-proto/index.html @@ -0,0 +1,9 @@ + + + + + Sketch outline + + + + diff --git a/app-proto/main.css b/app-proto/main.css new file mode 100644 index 0000000..bdbacfb --- /dev/null +++ b/app-proto/main.css @@ -0,0 +1,124 @@ +body { + margin: 0px; + color: #fcfcfc; + background-color: #222; +} + +/* sidebar */ + +#sidebar { + display: flex; + flex-direction: column; + float: left; + width: 450px; + height: 100vh; + margin: 0px; + padding: 0px; + border-width: 0px 1px 0px 0px; + border-style: solid; + border-color: #555; +} + +/* add-remove */ + +#add-remove { + display: flex; + gap: 8px; + margin: 8px; +} + +#add-remove > button { + width: 32px; + height: 32px; + font-size: large; +} + +/* outline */ + +#outline { + flex-grow: 1; + margin: 0px; + padding: 0px; + overflow-y: scroll; +} + +li { + user-select: none; +} + +summary { + display: flex; +} + +summary.selected { + color: #fff; + background-color: #444; +} + +summary > div, .cst { + padding-top: 4px; + padding-bottom: 4px; +} + +.elt, .cst { + display: flex; + flex-grow: 1; + padding-left: 8px; + padding-right: 8px; +} + +.elt-switch { + width: 18px; + padding-left: 2px; + text-align: center; +} + +details:has(li) .elt-switch::after { + content: '▸'; +} + +details[open]:has(li) .elt-switch::after { + content: '▾'; +} + +.elt-label { + flex-grow: 1; +} + +.cst-label { + flex-grow: 1; +} + +.elt-rep { + display: flex; +} + +.elt-rep > div, .cst-rep { + padding: 2px 0px 0px 0px; + font-size: 10pt; + text-align: center; + width: 56px; +} + +.cst { + font-style: italic; +} + +.cst > input { + margin: 0px 8px 0px 0px; +} + +/* display */ + +canvas { + float: left; + margin-left: 20px; + margin-top: 20px; + background-color: #020202; + border: 1px solid #555; + border-radius: 16px; +} + +canvas:focus { + border-color: #aaa; +} \ No newline at end of file diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs new file mode 100644 index 0000000..ab5db70 --- /dev/null +++ b/app-proto/src/add_remove.rs @@ -0,0 +1,242 @@ +use std::collections::BTreeSet; /* DEBUG */ +use sycamore::prelude::*; +use web_sys::{console, wasm_bindgen::JsValue}; + +use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}}; + +/* DEBUG */ +fn load_gen_assemb(assembly: &Assembly) { + let _ = assembly.try_insert_element( + Element { + id: String::from("gemini_a"), + label: String::from("Castor"), + color: [1.00_f32, 0.25_f32, 0.00_f32], + rep: engine::sphere(0.5, 0.5, 0.0, 1.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: String::from("gemini_b"), + label: String::from("Pollux"), + color: [0.00_f32, 0.25_f32, 1.00_f32], + rep: engine::sphere(-0.5, -0.5, 0.0, 1.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: String::from("ursa_major"), + label: String::from("Ursa major"), + color: [0.25_f32, 0.00_f32, 1.00_f32], + rep: engine::sphere(-0.5, 0.5, 0.0, 0.75), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: String::from("ursa_minor"), + label: String::from("Ursa minor"), + color: [0.25_f32, 1.00_f32, 0.00_f32], + rep: engine::sphere(0.5, -0.5, 0.0, 0.5), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: String::from("moon_deimos"), + label: String::from("Deimos"), + color: [0.75_f32, 0.75_f32, 0.00_f32], + rep: engine::sphere(0.0, 0.15, 1.0, 0.25), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: String::from("moon_phobos"), + label: String::from("Phobos"), + color: [0.00_f32, 0.75_f32, 0.50_f32], + rep: engine::sphere(0.0, -0.15, -1.0, 0.25), + constraints: BTreeSet::default() + } + ); + assembly.insert_constraint( + Constraint { + args: ( + assembly.elements_by_id.with_untracked(|elts_by_id| elts_by_id["gemini_a"]), + assembly.elements_by_id.with_untracked(|elts_by_id| elts_by_id["gemini_b"]) + ), + rep: 0.5, + active: create_signal(true) + } + ); +} + +/* DEBUG */ +fn load_low_curv_assemb(assembly: &Assembly) { + let a = 0.75_f64.sqrt(); + let _ = assembly.try_insert_element( + Element { + id: "central".to_string(), + label: "Central".to_string(), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: engine::sphere(0.0, 0.0, 0.0, 1.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: "assemb_plane".to_string(), + label: "Assembly plane".to_string(), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: "side1".to_string(), + label: "Side 1".to_string(), + color: [1.00_f32, 0.00_f32, 0.25_f32], + rep: engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: "side2".to_string(), + label: "Side 2".to_string(), + color: [0.25_f32, 1.00_f32, 0.00_f32], + rep: engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: "side3".to_string(), + label: "Side 3".to_string(), + color: [0.00_f32, 0.25_f32, 1.00_f32], + rep: engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: "corner1".to_string(), + label: "Corner 1".to_string(), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: "corner2".to_string(), + label: "Corner 2".to_string(), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: String::from("corner3"), + label: String::from("Corner 3"), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0), + constraints: BTreeSet::default() + } + ); +} + +#[component] +pub fn AddRemove() -> View { + /* DEBUG */ + let assembly_name = create_signal("general".to_string()); + create_effect(move || { + // get name of chosen assembly + let name = assembly_name.get_clone(); + console::log_1( + &JsValue::from(format!("Showing assembly \"{}\"", name.clone())) + ); + + batch(|| { + let state = use_context::(); + let assembly = &state.assembly; + + // clear state + assembly.elements.update(|elts| elts.clear()); + assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); + state.selection.update(|sel| sel.clear()); + + // load assembly + match name.as_str() { + "general" => load_gen_assemb(assembly), + "low-curv" => load_low_curv_assemb(assembly), + _ => () + }; + }); + }); + + view! { + div(id="add-remove") { + button( + on:click=|_| { + let state = use_context::(); + state.assembly.insert_new_element(); + + /* DEBUG */ + // print updated list of elements by identifier + console::log_1(&JsValue::from("elements by identifier:")); + for (id, key) in state.assembly.elements_by_id.get_clone().iter() { + console::log_3( + &JsValue::from(" "), + &JsValue::from(id), + &JsValue::from(*key) + ); + } + } + ) { "+" } + button( + disabled={ + let state = use_context::(); + state.selection.with(|sel| sel.len() != 2) + }, + on:click=|_| { + let state = use_context::(); + let args = state.selection.with( + |sel| { + let arg_vec: Vec<_> = sel.into_iter().collect(); + (arg_vec[0].clone(), arg_vec[1].clone()) + } + ); + state.assembly.insert_constraint(Constraint { + args: args, + rep: 0.0, + active: create_signal(true) + }); + state.selection.update(|sel| sel.clear()); + + /* DEBUG */ + // print updated constraint list + console::log_1(&JsValue::from("constraints:")); + state.assembly.constraints.with(|csts| { + for (_, cst) in csts.into_iter() { + console::log_5( + &JsValue::from(" "), + &JsValue::from(cst.args.0), + &JsValue::from(cst.args.1), + &JsValue::from(":"), + &JsValue::from(cst.rep) + ); + } + }); + } + ) { "🔗" } + select(bind:value=assembly_name) { /* DEBUG */ + option(value="general") { "General" } + option(value="low-curv") { "Low-curvature" } + } + } + } +} \ No newline at end of file diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs new file mode 100644 index 0000000..e8dab79 --- /dev/null +++ b/app-proto/src/assembly.rs @@ -0,0 +1,93 @@ +use nalgebra::DVector; +use rustc_hash::FxHashMap; +use slab::Slab; +use std::collections::BTreeSet; +use sycamore::prelude::*; + +#[derive(Clone, PartialEq)] +pub struct Element { + pub id: String, + pub label: String, + pub color: [f32; 3], + pub rep: DVector, + pub constraints: BTreeSet +} + +#[derive(Clone)] +pub struct Constraint { + pub args: (usize, usize), + pub rep: f64, + pub active: Signal +} + +// a complete, view-independent description of an assembly +#[derive(Clone)] +pub struct Assembly { + // elements and constraints + pub elements: Signal>, + pub constraints: Signal>, + + // indexing + pub elements_by_id: Signal> +} + +impl Assembly { + pub fn new() -> Assembly { + Assembly { + elements: create_signal(Slab::new()), + constraints: create_signal(Slab::new()), + elements_by_id: create_signal(FxHashMap::default()) + } + } + + // insert an element into the assembly without checking whether we already + // have an element with the same identifier. any element that does have the + // same identifier will get kicked out of the `elements_by_id` index + fn insert_element_unchecked(&self, elt: Element) { + let id = elt.id.clone(); + let key = self.elements.update(|elts| elts.insert(elt)); + self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); + } + + pub fn try_insert_element(&self, elt: Element) -> bool { + let can_insert = self.elements_by_id.with_untracked( + |elts_by_id| !elts_by_id.contains_key(&elt.id) + ); + if can_insert { + self.insert_element_unchecked(elt); + } + can_insert + } + + pub fn insert_new_element(&self) { + // find the next unused identifier in the default sequence + let mut id_num = 1; + let mut id = format!("sphere{}", id_num); + while self.elements_by_id.with_untracked( + |elts_by_id| elts_by_id.contains_key(&id) + ) { + id_num += 1; + id = format!("sphere{}", id_num); + } + + // create and insert a new element + self.insert_element_unchecked( + Element { + id: id, + label: format!("Sphere {}", id_num), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]), + constraints: BTreeSet::default() + } + ); + } + + pub fn insert_constraint(&self, constraint: Constraint) { + let args = constraint.args; + let key = self.constraints.update(|csts| csts.insert(constraint)); + self.elements.update(|elts| { + elts[args.0].constraints.insert(key); + elts[args.1].constraints.insert(key); + }) + } +} \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs new file mode 100644 index 0000000..c32b470 --- /dev/null +++ b/app-proto/src/display.rs @@ -0,0 +1,443 @@ +use core::array; +use nalgebra::{DMatrix, Rotation3, Vector3}; +use sycamore::{prelude::*, motion::create_raf}; +use web_sys::{ + console, + window, + KeyboardEvent, + WebGl2RenderingContext, + WebGlProgram, + WebGlShader, + WebGlUniformLocation, + wasm_bindgen::{JsCast, JsValue} +}; + +use crate::AppState; + +fn compile_shader( + context: &WebGl2RenderingContext, + shader_type: u32, + source: &str, +) -> WebGlShader { + let shader = context.create_shader(shader_type).unwrap(); + context.shader_source(&shader, source); + context.compile_shader(&shader); + shader +} + +fn get_uniform_array_locations( + context: &WebGl2RenderingContext, + program: &WebGlProgram, + var_name: &str, + member_name_opt: Option<&str> +) -> [Option; N] { + array::from_fn(|n| { + let name = match member_name_opt { + Some(member_name) => format!("{var_name}[{n}].{member_name}"), + None => format!("{var_name}[{n}]") + }; + context.get_uniform_location(&program, name.as_str()) + }) +} + +// load the given data into the vertex input of the given name +fn bind_vertex_attrib( + context: &WebGl2RenderingContext, + index: u32, + size: i32, + data: &[f32] +) { + // create a data buffer and bind it to ARRAY_BUFFER + let buffer = context.create_buffer().unwrap(); + context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer)); + + // load the given data into the buffer. the function `Float32Array::view` + // creates a raw view into our module's `WebAssembly.Memory` buffer. + // allocating more memory will change the buffer, invalidating the view. + // that means we have to make sure we don't allocate any memory until the + // view is dropped + unsafe { + context.buffer_data_with_array_buffer_view( + WebGl2RenderingContext::ARRAY_BUFFER, + &js_sys::Float32Array::view(&data), + WebGl2RenderingContext::STATIC_DRAW, + ); + } + + // allow the target attribute to be used + context.enable_vertex_attrib_array(index); + + // take whatever's bound to ARRAY_BUFFER---here, the data buffer created + // above---and bind it to the target attribute + // + // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer + // + context.vertex_attrib_pointer_with_i32( + index, + size, + WebGl2RenderingContext::FLOAT, + false, // don't normalize + 0, // zero stride + 0, // zero offset + ); +} + +#[component] +pub fn Display() -> View { + let state = use_context::(); + + // canvas + let display = create_node_ref(); + + // navigation + let pitch_up = create_signal(0.0); + let pitch_down = create_signal(0.0); + let yaw_right = create_signal(0.0); + let yaw_left = create_signal(0.0); + let roll_ccw = create_signal(0.0); + let roll_cw = create_signal(0.0); + let zoom_in = create_signal(0.0); + let zoom_out = create_signal(0.0); + let turntable = create_signal(false); /* BENCHMARKING */ + + // change listener + let scene_changed = create_signal(true); + create_effect(move || { + state.assembly.elements.track(); + state.selection.track(); + scene_changed.set(true); + }); + + /* INSTRUMENTS */ + const SAMPLE_PERIOD: i32 = 60; + let mut last_sample_time = 0.0; + let mut frames_since_last_sample = 0; + let mean_frame_interval = create_signal(0.0); + + on_mount(move || { + // timing + let mut last_time = 0.0; + + // viewpoint + const ROT_SPEED: f64 = 0.4; // in radians per second + const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second + const TURNTABLE_SPEED: f64 = 0.1; /* BENCHMARKING */ + let mut orientation = DMatrix::::identity(5, 5); + let mut rotation = DMatrix::::identity(5, 5); + let mut location_z: f64 = 5.0; + + // display parameters + const OPACITY: f32 = 0.5; /* SCAFFOLDING */ + const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */ + const LAYER_THRESHOLD: i32 = 0; /* DEBUG */ + const DEBUG_MODE: i32 = 0; /* DEBUG */ + + /* INSTRUMENTS */ + let performance = window().unwrap().performance().unwrap(); + + // get the display canvas + let canvas = display.get().unchecked_into::(); + let ctx = canvas + .get_context("webgl2") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap(); + + // compile and attach the vertex and fragment shaders + let vertex_shader = compile_shader( + &ctx, + WebGl2RenderingContext::VERTEX_SHADER, + include_str!("identity.vert"), + ); + let fragment_shader = compile_shader( + &ctx, + WebGl2RenderingContext::FRAGMENT_SHADER, + include_str!("inversive.frag"), + ); + let program = ctx.create_program().unwrap(); + ctx.attach_shader(&program, &vertex_shader); + ctx.attach_shader(&program, &fragment_shader); + ctx.link_program(&program); + let link_status = ctx + .get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS) + .as_bool() + .unwrap(); + let link_msg = if link_status { + "Linked successfully" + } else { + "Linking failed" + }; + console::log_1(&JsValue::from(link_msg)); + ctx.use_program(Some(&program)); + + /* DEBUG */ + // print the maximum number of vectors that can be passed as + // uniforms to a fragment shader. the OpenGL ES 3.0 standard + // requires this maximum to be at least 224, as discussed in the + // documentation of the GL_MAX_FRAGMENT_UNIFORM_VECTORS parameter + // here: + // + // https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml + // + // there are also other size limits. for example, on Aaron's + // machine, the the length of a float or genType array seems to be + // capped at 1024 elements + console::log_2( + &ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(), + &JsValue::from("uniform vectors available") + ); + + // find indices of vertex attributes and uniforms + const SPHERE_MAX: usize = 200; + let position_index = ctx.get_attrib_location(&program, "position") as u32; + let sphere_cnt_loc = ctx.get_uniform_location(&program, "sphere_cnt"); + let sphere_sp_locs = get_uniform_array_locations::( + &ctx, &program, "sphere_list", Some("sp") + ); + let sphere_lt_locs = get_uniform_array_locations::( + &ctx, &program, "sphere_list", Some("lt") + ); + let color_locs = get_uniform_array_locations::( + &ctx, &program, "color_list", None + ); + let highlight_locs = get_uniform_array_locations::( + &ctx, &program, "highlight_list", None + ); + let resolution_loc = ctx.get_uniform_location(&program, "resolution"); + let shortdim_loc = ctx.get_uniform_location(&program, "shortdim"); + let opacity_loc = ctx.get_uniform_location(&program, "opacity"); + let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold"); + let debug_mode_loc = ctx.get_uniform_location(&program, "debug_mode"); + + // create a vertex array and bind it to the graphics context + let vertex_array = ctx.create_vertex_array().unwrap(); + ctx.bind_vertex_array(Some(&vertex_array)); + + // set the vertex positions + const VERTEX_CNT: usize = 6; + let positions: [f32; 3*VERTEX_CNT] = [ + // northwest triangle + -1.0, -1.0, 0.0, + -1.0, 1.0, 0.0, + 1.0, 1.0, 0.0, + // southeast triangle + -1.0, -1.0, 0.0, + 1.0, 1.0, 0.0, + 1.0, -1.0, 0.0 + ]; + bind_vertex_attrib(&ctx, position_index, 3, &positions); + + // set up a repainting routine + let (_, start_animation_loop, _) = create_raf(move || { + // get the time step + let time = performance.now(); + let time_step = 0.001*(time - last_time); + last_time = time; + + // get the navigation state + let pitch_up_val = pitch_up.get(); + let pitch_down_val = pitch_down.get(); + let yaw_right_val = yaw_right.get(); + let yaw_left_val = yaw_left.get(); + let roll_ccw_val = roll_ccw.get(); + let roll_cw_val = roll_cw.get(); + let zoom_in_val = zoom_in.get(); + let zoom_out_val = zoom_out.get(); + let turntable_val = turntable.get(); /* BENCHMARKING */ + + // update the assembly's orientation + let ang_vel = { + let pitch = pitch_up_val - pitch_down_val; + let yaw = yaw_right_val - yaw_left_val; + let roll = roll_ccw_val - roll_cw_val; + if pitch != 0.0 || yaw != 0.0 || roll != 0.0 { + ROT_SPEED * Vector3::new(-pitch, yaw, roll).normalize() + } else { + Vector3::zeros() + } + } /* BENCHMARKING */ + if turntable_val { + Vector3::new(0.0, TURNTABLE_SPEED, 0.0) + } else { + Vector3::zeros() + }; + let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0); + rotation_sp.copy_from( + Rotation3::from_scaled_axis(time_step * ang_vel).matrix() + ); + orientation = &rotation * &orientation; + + // update the assembly's location + let zoom = zoom_out_val - zoom_in_val; + location_z *= (time_step * ZOOM_SPEED * zoom).exp(); + + if scene_changed.get() { + /* INSTRUMENTS */ + // measure mean frame interval + frames_since_last_sample += 1; + if frames_since_last_sample >= SAMPLE_PERIOD { + mean_frame_interval.set((time - last_sample_time) / (SAMPLE_PERIOD as f64)); + last_sample_time = time; + frames_since_last_sample = 0; + } + + // find the map from assembly space to world space + let location = { + let u = -location_z; + DMatrix::from_column_slice(5, 5, &[ + 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, u, + 0.0, 0.0, 2.0*u, 1.0, u*u, + 0.0, 0.0, 0.0, 0.0, 1.0 + ]) + }; + let assembly_to_world = &location * &orientation; + + // get the assembly + let elements = state.assembly.elements.get_clone(); + let element_iter = (&elements).into_iter(); + let reps_world: Vec<_> = element_iter.clone().map(|(_, elt)| &assembly_to_world * &elt.rep).collect(); + let colors: Vec<_> = element_iter.clone().map(|(key, elt)| + if state.selection.with(|sel| sel.contains(&key)) { + elt.color.map(|ch| 0.2 + 0.8*ch) + } else { + elt.color + } + ).collect(); + let highlights: Vec<_> = element_iter.map(|(key, _)| + if state.selection.with(|sel| sel.contains(&key)) { + 1.0_f32 + } else { + HIGHLIGHT + } + ).collect(); + + // set the resolution + let width = canvas.width() as f32; + let height = canvas.height() as f32; + ctx.uniform2f(resolution_loc.as_ref(), width, height); + ctx.uniform1f(shortdim_loc.as_ref(), width.min(height)); + + // pass the assembly + ctx.uniform1i(sphere_cnt_loc.as_ref(), elements.len() as i32); + for n in 0..reps_world.len() { + let v = &reps_world[n]; + ctx.uniform3f( + sphere_sp_locs[n].as_ref(), + v[0] as f32, v[1] as f32, v[2] as f32 + ); + ctx.uniform2f( + sphere_lt_locs[n].as_ref(), + v[3] as f32, v[4] as f32 + ); + ctx.uniform3fv_with_f32_array( + color_locs[n].as_ref(), + &colors[n] + ); + ctx.uniform1f( + highlight_locs[n].as_ref(), + highlights[n] + ); + } + + // pass the display parameters + ctx.uniform1f(opacity_loc.as_ref(), OPACITY); + ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); + ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); + + // draw the scene + ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); + + // clear the scene change flag + scene_changed.set( + pitch_up_val != 0.0 + || pitch_down_val != 0.0 + || yaw_left_val != 0.0 + || yaw_right_val != 0.0 + || roll_cw_val != 0.0 + || roll_ccw_val != 0.0 + || zoom_in_val != 0.0 + || zoom_out_val != 0.0 + || turntable_val /* BENCHMARKING */ + ); + } else { + frames_since_last_sample = 0; + mean_frame_interval.set(-1.0); + } + }); + start_animation_loop(); + }); + + let set_nav_signal = move |event: KeyboardEvent, value: f64| { + let mut navigating = true; + let shift = event.shift_key(); + match event.key().as_str() { + "ArrowUp" if shift => zoom_in.set(value), + "ArrowDown" if shift => zoom_out.set(value), + "ArrowUp" => pitch_up.set(value), + "ArrowDown" => pitch_down.set(value), + "ArrowRight" if shift => roll_cw.set(value), + "ArrowLeft" if shift => roll_ccw.set(value), + "ArrowRight" => yaw_right.set(value), + "ArrowLeft" => yaw_left.set(value), + _ => navigating = false + }; + if navigating { + scene_changed.set(true); + event.prevent_default(); + } + }; + + view! { + /* TO DO */ + // switch back to integer-valued parameters when that becomes possible + // again + canvas( + ref=display, + width="600", + height="600", + tabindex="0", + on:keydown=move |event: KeyboardEvent| { + if event.key() == "Shift" { + roll_cw.set(yaw_right.get()); + roll_ccw.set(yaw_left.get()); + zoom_in.set(pitch_up.get()); + zoom_out.set(pitch_down.get()); + yaw_right.set(0.0); + yaw_left.set(0.0); + pitch_up.set(0.0); + pitch_down.set(0.0); + } else { + if event.key() == "Enter" { /* BENCHMARKING */ + turntable.set_fn(|turn| !turn); + scene_changed.set(true); + } + set_nav_signal(event, 1.0); + } + }, + on:keyup=move |event: KeyboardEvent| { + if event.key() == "Shift" { + yaw_right.set(roll_cw.get()); + yaw_left.set(roll_ccw.get()); + pitch_up.set(zoom_in.get()); + pitch_down.set(zoom_out.get()); + roll_cw.set(0.0); + roll_ccw.set(0.0); + zoom_in.set(0.0); + zoom_out.set(0.0); + } else { + set_nav_signal(event, 0.0); + } + }, + on:blur=move |_| { + pitch_up.set(0.0); + pitch_down.set(0.0); + yaw_right.set(0.0); + yaw_left.set(0.0); + roll_ccw.set(0.0); + roll_cw.set(0.0); + } + ) + } +} \ No newline at end of file diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs new file mode 100644 index 0000000..79668bb --- /dev/null +++ b/app-proto/src/engine.rs @@ -0,0 +1,27 @@ +use nalgebra::DVector; + +// the sphere with the given center and radius, with inward-pointing normals +pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVector { + let center_norm_sq = center_x * center_x + center_y * center_y + center_z * center_z; + DVector::from_column_slice(&[ + center_x / radius, + center_y / radius, + center_z / radius, + 0.5 / radius, + 0.5 * (center_norm_sq / radius - radius) + ]) +} + +// the sphere of curvature `curv` whose closest point to the origin has position +// `off * dir` and normal `dir`, where `dir` is a unit vector. setting the +// curvature to zero gives a plane +pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f64) -> DVector { + let norm_sp = 1.0 + off * curv; + DVector::from_column_slice(&[ + norm_sp * dir_x, + norm_sp * dir_y, + norm_sp * dir_z, + 0.5 * curv, + off * (1.0 + 0.5 * off * curv) + ]) +} \ No newline at end of file diff --git a/app-proto/src/identity.vert b/app-proto/src/identity.vert new file mode 100644 index 0000000..183a65f --- /dev/null +++ b/app-proto/src/identity.vert @@ -0,0 +1,7 @@ +#version 300 es + +in vec4 position; + +void main() { + gl_Position = position; +} \ No newline at end of file diff --git a/app-proto/src/inversive.frag b/app-proto/src/inversive.frag new file mode 100644 index 0000000..d50cb1e --- /dev/null +++ b/app-proto/src/inversive.frag @@ -0,0 +1,234 @@ +#version 300 es + +precision highp float; + +out vec4 outColor; + +// --- inversive geometry --- + +struct vecInv { + vec3 sp; + vec2 lt; +}; + +// --- uniforms --- + +// assembly +const int SPHERE_MAX = 200; +uniform int sphere_cnt; +uniform vecInv sphere_list[SPHERE_MAX]; +uniform vec3 color_list[SPHERE_MAX]; +uniform float highlight_list[SPHERE_MAX]; + +// view +uniform vec2 resolution; +uniform float shortdim; + +// controls +uniform float opacity; +uniform int layer_threshold; +uniform bool debug_mode; + +// light and camera +const float focal_slope = 0.3; +const vec3 light_dir = normalize(vec3(2., 2., 1.)); +const float ixn_threshold = 0.005; +const float INTERIOR_DIMMING = 0.7; + +// --- sRGB --- + +// map colors from RGB space to sRGB space, as specified in the sRGB standard +// (IEC 61966-2-1:1999) +// +// https://www.color.org/sRGB.pdf +// https://www.color.org/chardata/rgb/srgb.xalter +// +// in RGB space, color value is proportional to light intensity, so linear +// color-vector interpolation corresponds to physical light mixing. in sRGB +// space, the color encoding used by many monitors, we use more of the value +// interval to represent low intensities, and less of the interval to represent +// high intensities. this improves color quantization + +float sRGB(float t) { + if (t <= 0.0031308) { + return 12.92*t; + } else { + return 1.055*pow(t, 5./12.) - 0.055; + } +} + +vec3 sRGB(vec3 color) { + return vec3(sRGB(color.r), sRGB(color.g), sRGB(color.b)); +} + +// --- shading --- + +struct Fragment { + vec3 pt; + vec3 normal; + vec4 color; +}; + +Fragment sphere_shading(vecInv v, vec3 pt, vec3 base_color) { + // the expression for normal needs to be checked. it's supposed to give the + // negative gradient of the lorentz product between the impact point vector + // and the sphere vector with respect to the coordinates of the impact + // point. i calculated it in my head and decided that the result looked good + // enough for now + vec3 normal = normalize(-v.sp + 2.*v.lt.s*pt); + + float incidence = dot(normal, light_dir); + float illum = mix(0.4, 1.0, max(incidence, 0.0)); + return Fragment(pt, normal, vec4(illum * base_color, opacity)); +} + +float intersection_dist(Fragment a, Fragment b) { + float intersection_sin = length(cross(a.normal, b.normal)); + vec3 disp = a.pt - b.pt; + return max( + abs(dot(a.normal, disp)), + abs(dot(b.normal, disp)) + ) / intersection_sin; +} + +// --- ray-casting --- + +struct TaggedDepth { + float depth; + float dimming; + int id; +}; + +// if `a/b` is less than this threshold, we approximate `a*u^2 + b*u + c` by +// the linear function `b*u + c` +const float DEG_THRESHOLD = 1e-9; + +// the depths, represented as multiples of `dir`, where the line generated by +// `dir` hits the sphere represented by `v`. if both depths are positive, the +// smaller one is returned in the first component. if only one depth is +// positive, it could be returned in either component +vec2 sphere_cast(vecInv v, vec3 dir) { + float a = -v.lt.s * dot(dir, dir); + float b = dot(v.sp, dir); + float c = -v.lt.t; + + float adjust = 4.*a*c/(b*b); + if (adjust < 1.) { + // as long as `b` is non-zero, the linear approximation of + // + // a*u^2 + b*u + c + // + // at `u = 0` will reach zero at a finite depth `u_lin`. the root of the + // quadratic adjacent to `u_lin` is stored in `lin_root`. if both roots + // have the same sign, `lin_root` will be the one closer to `u = 0` + float square_rect_ratio = 1. + sqrt(1. - adjust); + float lin_root = -(2.*c)/b / square_rect_ratio; + if (abs(a) > DEG_THRESHOLD * abs(b)) { + return vec2(lin_root, -b/(2.*a) * square_rect_ratio); + } else { + return vec2(lin_root, -1.); + } + } else { + // the line through `dir` misses the sphere completely + return vec2(-1., -1.); + } +} + +void main() { + vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; + vec3 dir = vec3(focal_slope * scr, -1.); + + // cast rays through the spheres + const int LAYER_MAX = 12; + TaggedDepth top_hits [LAYER_MAX]; + int layer_cnt = 0; + for (int id = 0; id < sphere_cnt; ++id) { + // find out where the ray hits the sphere + vec2 hit_depths = sphere_cast(sphere_list[id], dir); + + // insertion-sort the points we hit into the hit list + float dimming = 1.; + for (int side = 0; side < 2; ++side) { + float depth = hit_depths[side]; + if (depth > 0.) { + for (int layer = layer_cnt; layer >= 0; --layer) { + if (layer < 1 || top_hits[layer-1].depth <= depth) { + // we're not as close to the screen as the hit before + // the empty slot, so insert here + if (layer < LAYER_MAX) { + top_hits[layer] = TaggedDepth(depth, dimming, id); + } + break; + } else { + // we're closer to the screen than the hit before the + // empty slot, so move that hit into the empty slot + top_hits[layer] = top_hits[layer-1]; + } + } + layer_cnt = min(layer_cnt + 1, LAYER_MAX); + dimming = INTERIOR_DIMMING; + } + } + } + + /* DEBUG */ + // in debug mode, show the layer count instead of the shaded image + if (debug_mode) { + // at the bottom of the screen, show the color scale instead of the + // layer count + if (gl_FragCoord.y < 10.) layer_cnt = int(16. * gl_FragCoord.x / resolution.x); + + // convert number to color + ivec3 bits = layer_cnt / ivec3(1, 2, 4); + vec3 color = mod(vec3(bits), 2.); + if (layer_cnt % 16 >= 8) { + color = mix(color, vec3(0.5), 0.5); + } + outColor = vec4(color, 1.); + return; + } + + // composite the sphere fragments + vec3 color = vec3(0.); + int layer = layer_cnt - 1; + TaggedDepth hit = top_hits[layer]; + Fragment frag_next = sphere_shading( + sphere_list[hit.id], + hit.depth * dir, + hit.dimming * color_list[hit.id] + ); + float highlight_next = highlight_list[hit.id]; + --layer; + for (; layer >= layer_threshold; --layer) { + // load the current fragment + Fragment frag = frag_next; + float highlight = highlight_next; + + // shade the next fragment + hit = top_hits[layer]; + frag_next = sphere_shading( + sphere_list[hit.id], + hit.depth * dir, + hit.dimming * color_list[hit.id] + ); + highlight_next = highlight_list[hit.id]; + + // highlight intersections + float ixn_dist = intersection_dist(frag, frag_next); + float max_highlight = max(highlight, highlight_next); + float ixn_highlight = 0.5 * max_highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist)); + frag.color = mix(frag.color, vec4(1.), ixn_highlight); + frag_next.color = mix(frag_next.color, vec4(1.), ixn_highlight); + + // highlight cusps + float cusp_cos = abs(dot(dir, frag.normal)); + float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list[hit.id].lt.s); + float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos)); + frag.color = mix(frag.color, vec4(1.), cusp_highlight); + + // composite the current fragment + color = mix(color, frag.color.rgb, frag.color.a); + } + color = mix(color, frag_next.color.rgb, frag_next.color.a); + outColor = vec4(sRGB(color), 1.); +} \ No newline at end of file diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs new file mode 100644 index 0000000..2c71a83 --- /dev/null +++ b/app-proto/src/main.rs @@ -0,0 +1,42 @@ +mod add_remove; +mod assembly; +mod display; +mod engine; +mod outline; + +use rustc_hash::FxHashSet; +use sycamore::prelude::*; + +use add_remove::AddRemove; +use assembly::Assembly; +use display::Display; +use outline::Outline; + +#[derive(Clone)] +struct AppState { + assembly: Assembly, + selection: Signal> +} + +impl AppState { + fn new() -> AppState { + AppState { + assembly: Assembly::new(), + selection: create_signal(FxHashSet::default()) + } + } +} + +fn main() { + sycamore::render(|| { + provide_context(AppState::new()); + + view! { + div(id="sidebar") { + AddRemove {} + Outline {} + } + Display {} + } + }); +} \ No newline at end of file diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs new file mode 100644 index 0000000..4e4de9c --- /dev/null +++ b/app-proto/src/outline.rs @@ -0,0 +1,161 @@ +use itertools::Itertools; +use sycamore::{prelude::*, web::tags::div}; +use web_sys::{Element, KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; + +use crate::AppState; + +// this component lists the elements of the assembly, showing the constraints +// on each element as a collapsible sub-list. its implementation is based on +// Kate Morley's HTML + CSS tree views: +// +// https://iamkate.com/code/tree-views/ +// +#[component] +pub fn Outline() -> View { + // sort the elements alphabetically by ID + let elements_sorted = create_memo(|| { + let state = use_context::(); + state.assembly.elements + .get_clone() + .into_iter() + .sorted_by_key(|(_, elt)| elt.id.clone()) + .collect() + }); + + view! { + ul( + id="outline", + on:click={ + let state = use_context::(); + move |_| state.selection.update(|sel| sel.clear()) + } + ) { + Keyed( + list=elements_sorted, + view=|(key, elt)| { + let state = use_context::(); + let class = create_memo({ + move || { + if state.selection.with(|sel| sel.contains(&key)) { + "selected" + } else { + "" + } + } + }); + let label = elt.label.clone(); + let rep_components = elt.rep.iter().map(|u| { + let u_coord = u.to_string().replace("-", "\u{2212}"); + View::from(div().children(u_coord)) + }).collect::>(); + let constrained = elt.constraints.len() > 0; + let details_node = create_node_ref(); + view! { + /* [TO DO] switch to integer-valued parameters whenever + that becomes possible again */ + li { + details(ref=details_node) { + summary( + class=class.get(), + on:keydown={ + move |event: KeyboardEvent| { + match event.key().as_str() { + "Enter" => { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&key) { + sel.insert(key); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); + sel.insert(key); + }); + } + event.prevent_default(); + }, + "ArrowRight" if constrained => { + let _ = details_node + .get() + .unchecked_into::() + .set_attribute("open", ""); + }, + "ArrowLeft" => { + let _ = details_node + .get() + .unchecked_into::() + .remove_attribute("open"); + }, + _ => () + } + } + } + ) { + div( + class="elt-switch", + on:click=|event: MouseEvent| event.stop_propagation() + ) + div( + class="elt", + on:click={ + move |event: MouseEvent| { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&key) { + sel.insert(key); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); + sel.insert(key); + }); + } + event.stop_propagation(); + event.prevent_default(); + } + } + ) { + div(class="elt-label") { (label) } + div(class="elt-rep") { (rep_components) } + } + } + ul(class="constraints") { + Keyed( + list=elt.constraints.into_iter().collect::>(), + view=move |c_key: usize| { + let c_state = use_context::(); + let assembly = &c_state.assembly; + let cst = assembly.constraints.with(|csts| csts[c_key].clone()); + let other_arg = if cst.args.0 == key { + cst.args.1 + } else { + cst.args.0 + }; + let other_arg_label = assembly.elements.with(|elts| elts[other_arg].label.clone()); + view! { + li(class="cst") { + input(r#type="checkbox", bind:checked=cst.active) + div(class="cst-label") { (other_arg_label) } + div(class="cst-rep") { (cst.rep) } + } + } + }, + key=|c_key| c_key.clone() + ) + } + } + } + } + }, + key=|(key, elt)| ( + key.clone(), + elt.id.clone(), + elt.label.clone(), + elt.constraints.clone() + ) + ) + } + } +} \ No newline at end of file From 707618cdd3adeb488c2b040a4e48f8789e20c7e9 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Tue, 12 Nov 2024 00:46:16 +0000 Subject: [PATCH 112/132] Integrate engine into application prototype (#15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the engine prototype to Rust, integrate it into the application prototype, and use it to enforce the constraints. ### Features To see the engine in action: 1. Add a constraint by shift-clicking to select two spheres in the outline view and then hitting the 🔗 button 2. Click a summary arrow to see the outline item for the new constraint 2. Set the constraint's Lorentz product by entering a value in the text field at the right end of the outline item * *The display should update as soon as you press* Enter *or focus away from the text field* The checkbox at the left end of a constraint outline item controls whether the constraint is active. Activating a constraint triggers a solution update. (Deactivating a constraint doesn't, since the remaining active constraints are still satisfied.) ### Precision The Julia prototype of the engine uses a generic scalar type, so you can pass in any type the linear algebra functions are implemented for. The examples use the [adjustable-precision](https://docs.julialang.org/en/v1/base/numbers/#Base.MPFR.setprecision) `BigFloat` type. In the Rust port of the engine, the scalar type is currently fixed at `f64`. Switching to generic scalars shouldn't be too hard, but I haven't looked into [which other types](https://www.nalgebra.org/docs/user_guide/generic_programming) the linear algebra functions are implemented for. ### Testing To confirm quantitatively that the Rust port of the engine is working, you can go to the `app-proto` folder and: * Run some automated tests by calling `cargo test`. * Inspect the optimization process in a few examples calling the `run-examples` script. The first example that prints is the same as the Irisawa hexlet example from the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then ``` include("irisawa-hexlet.jl") for (step, scaled_loss) in enumerate(history_alt.scaled_loss) println(rpad(step-1, 4), " | ", scaled_loss) end ``` you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show. ### A small engine revision The Rust port of the engine improves on the Julia prototype in one part of the constraint-solving routine: projecting the Hessian onto the subspace where the frozen entries stay constant. The Julia prototype does this by removing the rows and columns of the Hessian that correspond to the frozen entries, finding the Newton step from the resulting "compressed" Hessian, and then adding zero entries to the Newton step in the appropriate places. The Rust port instead replaces each frozen row and column with its corresponding standard unit vector, avoiding the finicky compressing and decompressing steps. To confirm that this version of the constraint-solving routine works the same as the original, I implemented it in Julia as `realize_gram_alt_proj`. The solutions we get from this routine match the ones we get from the original `realize_gram` to very high precision, and in the simplest examples (`sphere-in-tetrahedron.jl` and `tetrahedron-radius-ratio.jl`), the descent paths also match to very high precision. In a more complicated example (`irisawa-hexlet.jl`), the descent paths diverge about a quarter of the way into the search, even though they end up in the same place. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/15 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.toml | 2 + app-proto/main.css | 11 +- app-proto/run-examples | 8 + app-proto/src/add_remove.rs | 118 ++-- app-proto/src/assembly.rs | 128 ++++- app-proto/src/display.rs | 4 +- app-proto/src/engine.rs | 515 +++++++++++++++++- app-proto/src/main.rs | 4 +- app-proto/src/outline.rs | 52 +- engine-proto/gram-test/Engine.jl | 143 ++++- engine-proto/gram-test/irisawa-hexlet.jl | 11 +- .../gram-test/sphere-in-tetrahedron.jl | 11 +- .../gram-test/tetrahedron-radius-ratio.jl | 11 +- notes/inversive.md | 22 + 14 files changed, 947 insertions(+), 93 deletions(-) create mode 100755 app-proto/run-examples diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 920469a..e5bc05e 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -10,6 +10,7 @@ default = ["console_error_panic_hook"] [dependencies] itertools = "0.13.0" js-sys = "0.3.70" +lazy_static = "1.5.0" nalgebra = "0.33.0" rustc-hash = "2.0.0" slab = "0.4.9" @@ -25,6 +26,7 @@ console_error_panic_hook = { version = "0.1.7", optional = true } version = "0.3.69" features = [ 'HtmlCanvasElement', + 'HtmlInputElement', 'Performance', 'WebGl2RenderingContext', 'WebGlBuffer', diff --git a/app-proto/main.css b/app-proto/main.css index bdbacfb..32ae5bf 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -93,7 +93,7 @@ details[open]:has(li) .elt-switch::after { display: flex; } -.elt-rep > div, .cst-rep { +.elt-rep > div { padding: 2px 0px 0px 0px; font-size: 10pt; text-align: center; @@ -104,10 +104,17 @@ details[open]:has(li) .elt-switch::after { font-style: italic; } -.cst > input { +.cst > input[type=checkbox] { margin: 0px 8px 0px 0px; } +.cst > input[type=text] { + color: #fcfcfc; + background-color: inherit; + border: 1px solid #555; + border-radius: 2px; +} + /* display */ canvas { diff --git a/app-proto/run-examples b/app-proto/run-examples new file mode 100755 index 0000000..6a5e3ae --- /dev/null +++ b/app-proto/run-examples @@ -0,0 +1,8 @@ +# based on "Enabling print statements in Cargo tests", by Jon Almeida +# +# https://jonalmeida.com/posts/2015/01/23/print-cargo/ +# + +cargo test -- --nocapture engine::tests::irisawa_hexlet_test +cargo test -- --nocapture engine::tests::three_spheres_example +cargo test -- --nocapture engine::tests::point_on_sphere_example diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index ab5db70..19b4b8d 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -11,8 +11,9 @@ fn load_gen_assemb(assembly: &Assembly) { id: String::from("gemini_a"), label: String::from("Castor"), color: [1.00_f32, 0.25_f32, 0.00_f32], - rep: engine::sphere(0.5, 0.5, 0.0, 1.0), - constraints: BTreeSet::default() + representation: engine::sphere(0.5, 0.5, 0.0, 1.0), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -20,8 +21,9 @@ fn load_gen_assemb(assembly: &Assembly) { id: String::from("gemini_b"), label: String::from("Pollux"), color: [0.00_f32, 0.25_f32, 1.00_f32], - rep: engine::sphere(-0.5, -0.5, 0.0, 1.0), - constraints: BTreeSet::default() + representation: engine::sphere(-0.5, -0.5, 0.0, 1.0), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -29,8 +31,9 @@ fn load_gen_assemb(assembly: &Assembly) { id: String::from("ursa_major"), label: String::from("Ursa major"), color: [0.25_f32, 0.00_f32, 1.00_f32], - rep: engine::sphere(-0.5, 0.5, 0.0, 0.75), - constraints: BTreeSet::default() + representation: engine::sphere(-0.5, 0.5, 0.0, 0.75), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -38,8 +41,9 @@ fn load_gen_assemb(assembly: &Assembly) { id: String::from("ursa_minor"), label: String::from("Ursa minor"), color: [0.25_f32, 1.00_f32, 0.00_f32], - rep: engine::sphere(0.5, -0.5, 0.0, 0.5), - constraints: BTreeSet::default() + representation: engine::sphere(0.5, -0.5, 0.0, 0.5), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -47,8 +51,9 @@ fn load_gen_assemb(assembly: &Assembly) { id: String::from("moon_deimos"), label: String::from("Deimos"), color: [0.75_f32, 0.75_f32, 0.00_f32], - rep: engine::sphere(0.0, 0.15, 1.0, 0.25), - constraints: BTreeSet::default() + representation: engine::sphere(0.0, 0.15, 1.0, 0.25), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -56,18 +61,9 @@ fn load_gen_assemb(assembly: &Assembly) { id: String::from("moon_phobos"), label: String::from("Phobos"), color: [0.00_f32, 0.75_f32, 0.50_f32], - rep: engine::sphere(0.0, -0.15, -1.0, 0.25), - constraints: BTreeSet::default() - } - ); - assembly.insert_constraint( - Constraint { - args: ( - assembly.elements_by_id.with_untracked(|elts_by_id| elts_by_id["gemini_a"]), - assembly.elements_by_id.with_untracked(|elts_by_id| elts_by_id["gemini_b"]) - ), - rep: 0.5, - active: create_signal(true) + representation: engine::sphere(0.0, -0.15, -1.0, 0.25), + constraints: BTreeSet::default(), + index: 0 } ); } @@ -80,8 +76,9 @@ fn load_low_curv_assemb(assembly: &Assembly) { id: "central".to_string(), label: "Central".to_string(), color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: engine::sphere(0.0, 0.0, 0.0, 1.0), - constraints: BTreeSet::default() + representation: engine::sphere(0.0, 0.0, 0.0, 1.0), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -89,8 +86,9 @@ fn load_low_curv_assemb(assembly: &Assembly) { id: "assemb_plane".to_string(), label: "Assembly plane".to_string(), color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0), - constraints: BTreeSet::default() + representation: engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -98,8 +96,9 @@ fn load_low_curv_assemb(assembly: &Assembly) { id: "side1".to_string(), label: "Side 1".to_string(), color: [1.00_f32, 0.00_f32, 0.25_f32], - rep: engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0), - constraints: BTreeSet::default() + representation: engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -107,8 +106,9 @@ fn load_low_curv_assemb(assembly: &Assembly) { id: "side2".to_string(), label: "Side 2".to_string(), color: [0.25_f32, 1.00_f32, 0.00_f32], - rep: engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0), - constraints: BTreeSet::default() + representation: engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -116,8 +116,9 @@ fn load_low_curv_assemb(assembly: &Assembly) { id: "side3".to_string(), label: "Side 3".to_string(), color: [0.00_f32, 0.25_f32, 1.00_f32], - rep: engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0), - constraints: BTreeSet::default() + representation: engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -125,8 +126,9 @@ fn load_low_curv_assemb(assembly: &Assembly) { id: "corner1".to_string(), label: "Corner 1".to_string(), color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0), - constraints: BTreeSet::default() + representation: engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -134,8 +136,9 @@ fn load_low_curv_assemb(assembly: &Assembly) { id: "corner2".to_string(), label: "Corner 2".to_string(), color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0), - constraints: BTreeSet::default() + representation: engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -143,8 +146,9 @@ fn load_low_curv_assemb(assembly: &Assembly) { id: String::from("corner3"), label: String::from("Corner 3"), color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0), - constraints: BTreeSet::default() + representation: engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0), + constraints: BTreeSet::default(), + index: 0 } ); } @@ -204,33 +208,51 @@ pub fn AddRemove() -> View { }, on:click=|_| { let state = use_context::(); - let args = state.selection.with( + let subjects = state.selection.with( |sel| { - let arg_vec: Vec<_> = sel.into_iter().collect(); - (arg_vec[0].clone(), arg_vec[1].clone()) + let subject_vec: Vec<_> = sel.into_iter().collect(); + (subject_vec[0].clone(), subject_vec[1].clone()) } ); + let lorentz_prod = create_signal(0.0); + let active = create_signal(true); state.assembly.insert_constraint(Constraint { - args: args, - rep: 0.0, - active: create_signal(true) + subjects: subjects, + lorentz_prod: lorentz_prod, + lorentz_prod_text: create_signal(String::new()), + lorentz_prod_valid: create_signal(false), + active: active, }); + state.assembly.realize(); state.selection.update(|sel| sel.clear()); /* DEBUG */ // print updated constraint list - console::log_1(&JsValue::from("constraints:")); + console::log_1(&JsValue::from("Constraints:")); state.assembly.constraints.with(|csts| { for (_, cst) in csts.into_iter() { console::log_5( &JsValue::from(" "), - &JsValue::from(cst.args.0), - &JsValue::from(cst.args.1), + &JsValue::from(cst.subjects.0), + &JsValue::from(cst.subjects.1), &JsValue::from(":"), - &JsValue::from(cst.rep) + &JsValue::from(cst.lorentz_prod.get_untracked()) ); } }); + + // update the realization when the constraint activated, or + // edited while active + create_effect(move || { + lorentz_prod.track(); + console::log_2( + &JsValue::from("Lorentz product updated to"), + &JsValue::from(lorentz_prod.get_untracked()) + ); + if active.get() { + state.assembly.realize(); + } + }); } ) { "🔗" } select(bind:value=assembly_name) { /* DEBUG */ diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index e8dab79..0cdf61b 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,22 +1,40 @@ -use nalgebra::DVector; +use nalgebra::{DMatrix, DVector}; use rustc_hash::FxHashMap; use slab::Slab; use std::collections::BTreeSet; use sycamore::prelude::*; +use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ + +use crate::engine::{realize_gram, PartialMatrix}; + +// the types of the keys we use to access an assembly's elements and constraints +pub type ElementKey = usize; +pub type ConstraintKey = usize; + +pub type ElementColor = [f32; 3]; #[derive(Clone, PartialEq)] pub struct Element { pub id: String, pub label: String, - pub color: [f32; 3], - pub rep: DVector, - pub constraints: BTreeSet + pub color: ElementColor, + pub representation: DVector, + pub constraints: BTreeSet, + + // the configuration matrix column index that was assigned to this element + // last time the assembly was realized + /* TO DO */ + // this is public, as a kludge, because `Element` doesn't have a constructor + // yet. it should be made private as soon as the constructor is written + pub index: usize } #[derive(Clone)] pub struct Constraint { - pub args: (usize, usize), - pub rep: f64, + pub subjects: (ElementKey, ElementKey), + pub lorentz_prod: Signal, + pub lorentz_prod_text: Signal, + pub lorentz_prod_valid: Signal, pub active: Signal } @@ -28,7 +46,7 @@ pub struct Assembly { pub constraints: Signal>, // indexing - pub elements_by_id: Signal> + pub elements_by_id: Signal> } impl Assembly { @@ -40,6 +58,8 @@ impl Assembly { } } + // --- inserting elements and constraints --- + // insert an element into the assembly without checking whether we already // have an element with the same identifier. any element that does have the // same identifier will get kicked out of the `elements_by_id` index @@ -76,18 +96,100 @@ impl Assembly { id: id, label: format!("Sphere {}", id_num), color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]), - constraints: BTreeSet::default() + representation: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]), + constraints: BTreeSet::default(), + index: 0 } ); } pub fn insert_constraint(&self, constraint: Constraint) { - let args = constraint.args; + let subjects = constraint.subjects; let key = self.constraints.update(|csts| csts.insert(constraint)); self.elements.update(|elts| { - elts[args.0].constraints.insert(key); - elts[args.1].constraints.insert(key); - }) + elts[subjects.0].constraints.insert(key); + elts[subjects.1].constraints.insert(key); + }); + } + + // --- realization --- + + pub fn realize(&self) { + // index the elements + self.elements.update_silent(|elts| { + for (index, (_, elt)) in elts.into_iter().enumerate() { + elt.index = index; + } + }); + + // set up the Gram matrix and the initial configuration matrix + let (gram, guess) = self.elements.with_untracked(|elts| { + // set up the off-diagonal part of the Gram matrix + let mut gram_to_be = PartialMatrix::new(); + self.constraints.with_untracked(|csts| { + for (_, cst) in csts { + if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() { + let subjects = cst.subjects; + let row = elts[subjects.0].index; + let col = elts[subjects.1].index; + gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked()); + } + } + }); + + // set up the initial configuration matrix and the diagonal of the + // Gram matrix + let mut guess_to_be = DMatrix::::zeros(5, elts.len()); + for (_, elt) in elts { + let index = elt.index; + gram_to_be.push_sym(index, index, 1.0); + guess_to_be.set_column(index, &elt.representation); + } + + (gram_to_be, guess_to_be) + }); + + /* DEBUG */ + // log the Gram matrix + console::log_1(&JsValue::from("Gram matrix:")); + gram.log_to_console(); + + /* DEBUG */ + // log the initial configuration matrix + console::log_1(&JsValue::from("Old configuration:")); + for j in 0..guess.nrows() { + let mut row_str = String::new(); + for k in 0..guess.ncols() { + row_str.push_str(format!(" {:>8.3}", guess[(j, k)]).as_str()); + } + console::log_1(&JsValue::from(row_str)); + } + + // look for a configuration with the given Gram matrix + let (config, success, history) = realize_gram( + &gram, guess, &[], + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + + /* DEBUG */ + // report the outcome of the search + console::log_1(&JsValue::from( + if success { + "Target accuracy achieved!" + } else { + "Failed to reach target accuracy" + } + )); + console::log_2(&JsValue::from("Steps:"), &JsValue::from(history.scaled_loss.len() - 1)); + console::log_2(&JsValue::from("Loss:"), &JsValue::from(*history.scaled_loss.last().unwrap())); + + if success { + // read out the solution + self.elements.update(|elts| { + for (_, elt) in elts.iter_mut() { + elt.representation.set_column(0, &config.column(elt.index)); + } + }); + } } } \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index c32b470..79199ec 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -297,7 +297,9 @@ pub fn Display() -> View { // get the assembly let elements = state.assembly.elements.get_clone(); let element_iter = (&elements).into_iter(); - let reps_world: Vec<_> = element_iter.clone().map(|(_, elt)| &assembly_to_world * &elt.rep).collect(); + let reps_world: Vec<_> = element_iter.clone().map( + |(_, elt)| &assembly_to_world * &elt.representation + ).collect(); let colors: Vec<_> = element_iter.clone().map(|(key, elt)| if state.selection.with(|sel| sel.contains(&key)) { elt.color.map(|ch| 0.2 + 0.8*ch) diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 79668bb..343b96e 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -1,4 +1,13 @@ -use nalgebra::DVector; +use lazy_static::lazy_static; +use nalgebra::{Const, DMatrix, DVector, Dyn}; +use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ + +// --- elements --- + +#[cfg(test)] +pub fn point(x: f64, y: f64, z: f64) -> DVector { + DVector::from_column_slice(&[x, y, z, 0.5, 0.5*(x*x + y*y + z*z)]) +} // the sphere with the given center and radius, with inward-pointing normals pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVector { @@ -24,4 +33,508 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 0.5 * curv, off * (1.0 + 0.5 * off * curv) ]) +} + +// --- partial matrices --- + +struct MatrixEntry { + index: (usize, usize), + value: f64 +} + +pub struct PartialMatrix(Vec); + +impl PartialMatrix { + pub fn new() -> PartialMatrix { + PartialMatrix(Vec::::new()) + } + + pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { + let PartialMatrix(entries) = self; + entries.push(MatrixEntry { index: (row, col), value: value }); + if row != col { + entries.push(MatrixEntry { index: (col, row), value: value }); + } + } + + /* DEBUG */ + pub fn log_to_console(&self) { + let PartialMatrix(entries) = self; + for ent in entries { + let ent_str = format!(" {} {} {}", ent.index.0, ent.index.1, ent.value); + console::log_1(&JsValue::from(ent_str.as_str())); + } + } + + fn proj(&self, a: &DMatrix) -> DMatrix { + let mut result = DMatrix::::zeros(a.nrows(), a.ncols()); + let PartialMatrix(entries) = self; + for ent in entries { + result[ent.index] = a[ent.index]; + } + result + } + + fn sub_proj(&self, rhs: &DMatrix) -> DMatrix { + let mut result = DMatrix::::zeros(rhs.nrows(), rhs.ncols()); + let PartialMatrix(entries) = self; + for ent in entries { + result[ent.index] = ent.value - rhs[ent.index]; + } + result + } +} + +// --- descent history --- + +pub struct DescentHistory { + pub config: Vec>, + pub scaled_loss: Vec, + pub neg_grad: Vec>, + pub min_eigval: Vec, + pub base_step: Vec>, + pub backoff_steps: Vec +} + +impl DescentHistory { + fn new() -> DescentHistory { + DescentHistory { + config: Vec::>::new(), + scaled_loss: Vec::::new(), + neg_grad: Vec::>::new(), + min_eigval: Vec::::new(), + base_step: Vec::>::new(), + backoff_steps: Vec::::new(), + } + } +} + +// --- gram matrix realization --- + +// the Lorentz form +lazy_static! { + static ref Q: DMatrix = DMatrix::from_row_slice(5, 5, &[ + 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, -2.0, + 0.0, 0.0, 0.0, -2.0, 0.0 + ]); +} + +struct SearchState { + config: DMatrix, + err_proj: DMatrix, + loss: f64 +} + +impl SearchState { + fn from_config(gram: &PartialMatrix, config: DMatrix) -> SearchState { + let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config)); + let loss = err_proj.norm_squared(); + SearchState { + config: config, + err_proj: err_proj, + loss: loss + } + } +} + +fn basis_matrix(index: (usize, usize), nrows: usize, ncols: usize) -> DMatrix { + let mut result = DMatrix::::zeros(nrows, ncols); + result[index] = 1.0; + result +} + +// use backtracking line search to find a better configuration +fn seek_better_config( + gram: &PartialMatrix, + state: &SearchState, + base_step: &DMatrix, + base_target_improvement: f64, + min_efficiency: f64, + backoff: f64, + max_backoff_steps: i32 +) -> Option<(SearchState, i32)> { + let mut rate = 1.0; + for backoff_steps in 0..max_backoff_steps { + let trial_config = &state.config + rate * base_step; + let trial_state = SearchState::from_config(gram, trial_config); + let improvement = state.loss - trial_state.loss; + if improvement >= min_efficiency * rate * base_target_improvement { + return Some((trial_state, backoff_steps)); + } + rate *= backoff; + } + None +} + +// seek a matrix `config` for which `config' * Q * config` matches the partial +// matrix `gram`. use gradient descent starting from `guess` +pub fn realize_gram( + gram: &PartialMatrix, + guess: DMatrix, + frozen: &[(usize, usize)], + scaled_tol: f64, + min_efficiency: f64, + backoff: f64, + reg_scale: f64, + max_descent_steps: i32, + max_backoff_steps: i32 +) -> (DMatrix, bool, DescentHistory) { + // start the descent history + let mut history = DescentHistory::new(); + + // find the dimension of the search space + let element_dim = guess.nrows(); + let assembly_dim = guess.ncols(); + let total_dim = element_dim * assembly_dim; + + // scale the tolerance + let scale_adjustment = (gram.0.len() as f64).sqrt(); + let tol = scale_adjustment * scaled_tol; + + // convert the frozen indices to stacked format + let frozen_stacked: Vec = frozen.into_iter().map( + |index| index.1*element_dim + index.0 + ).collect(); + + // use Newton's method with backtracking and gradient descent backup + let mut state = SearchState::from_config(gram, guess); + for _ in 0..max_descent_steps { + // stop if the loss is tolerably low + history.config.push(state.config.clone()); + history.scaled_loss.push(state.loss / scale_adjustment); + if state.loss < tol { break; } + + // find the negative gradient of the loss function + let neg_grad = 4.0 * &*Q * &state.config * &state.err_proj; + let mut neg_grad_stacked = neg_grad.clone().reshape_generic(Dyn(total_dim), Const::<1>); + history.neg_grad.push(neg_grad.clone()); + + // find the negative Hessian of the loss function + let mut hess_cols = Vec::>::with_capacity(total_dim); + for col in 0..assembly_dim { + for row in 0..element_dim { + let index = (row, col); + let basis_mat = basis_matrix(index, element_dim, assembly_dim); + let neg_d_err = + basis_mat.tr_mul(&*Q) * &state.config + + state.config.tr_mul(&*Q) * &basis_mat; + let neg_d_err_proj = gram.proj(&neg_d_err); + let deriv_grad = 4.0 * &*Q * ( + -&basis_mat * &state.err_proj + + &state.config * &neg_d_err_proj + ); + hess_cols.push(deriv_grad.reshape_generic(Dyn(total_dim), Const::<1>)); + } + } + let mut hess = DMatrix::from_columns(hess_cols.as_slice()); + + // regularize the Hessian + let min_eigval = hess.symmetric_eigenvalues().min(); + if min_eigval <= 0.0 { + hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim); + } + history.min_eigval.push(min_eigval); + + // project the negative gradient and negative Hessian onto the + // orthogonal complement of the frozen subspace + let zero_col = DVector::zeros(total_dim); + let zero_row = zero_col.transpose(); + for &k in &frozen_stacked { + neg_grad_stacked[k] = 0.0; + hess.set_row(k, &zero_row); + hess.set_column(k, &zero_col); + hess[(k, k)] = 1.0; + } + + // compute the Newton step + /* + we need to either handle or eliminate the case where the minimum + eigenvalue of the Hessian is zero, so the regularized Hessian is + singular. right now, this causes the Cholesky decomposition to return + `None`, leading to a panic when we unrap + */ + let base_step_stacked = hess.cholesky().unwrap().solve(&neg_grad_stacked); + let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim)); + history.base_step.push(base_step.clone()); + + // use backtracking line search to find a better configuration + match seek_better_config( + gram, &state, &base_step, neg_grad.dot(&base_step), + min_efficiency, backoff, max_backoff_steps + ) { + Some((better_state, backoff_steps)) => { + state = better_state; + history.backoff_steps.push(backoff_steps); + }, + None => return (state.config, false, history) + }; + } + (state.config, state.loss < tol, history) +} + +// --- tests --- + +#[cfg(test)] +mod tests { + use std::{array, f64::consts::PI}; + + use super::*; + + #[test] + fn sub_proj_test() { + let target = PartialMatrix(vec![ + MatrixEntry { index: (0, 0), value: 19.0 }, + MatrixEntry { index: (0, 2), value: 39.0 }, + MatrixEntry { index: (1, 1), value: 59.0 }, + MatrixEntry { index: (1, 2), value: 69.0 } + ]); + let attempt = DMatrix::::from_row_slice(2, 3, &[ + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0 + ]); + let expected_result = DMatrix::::from_row_slice(2, 3, &[ + 18.0, 0.0, 36.0, + 0.0, 54.0, 63.0 + ]); + assert_eq!(target.sub_proj(&attempt), expected_result); + } + + #[test] + fn zero_loss_test() { + let gram = PartialMatrix({ + let mut entries = Vec::::new(); + for j in 0..3 { + for k in 0..3 { + entries.push(MatrixEntry { + index: (j, k), + value: if j == k { 1.0 } else { -1.0 } + }); + } + } + entries + }); + let config = { + let a: f64 = 0.75_f64.sqrt(); + DMatrix::from_columns(&[ + sphere(1.0, 0.0, 0.0, a), + sphere(-0.5, a, 0.0, a), + sphere(-0.5, -a, 0.0, a) + ]) + }; + let state = SearchState::from_config(&gram, config); + assert!(state.loss.abs() < f64::EPSILON); + } + + // 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/ + // + #[test] + fn irisawa_hexlet_test() { + let gram = PartialMatrix({ + let mut entries = Vec::::new(); + for s in 0..9 { + // each sphere is represented by a spacelike vector + entries.push(MatrixEntry { index: (s, s), value: 1.0 }); + + // the circumscribing sphere is tangent to all of the other + // spheres, with matching orientation + if s > 0 { + entries.push(MatrixEntry { index: (0, s), value: 1.0 }); + entries.push(MatrixEntry { index: (s, 0), value: 1.0 }); + } + + if s > 2 { + // each chain sphere is tangent to the "sun" and "moon" + // spheres, with opposing orientation + for n in 1..3 { + entries.push(MatrixEntry { index: (s, n), value: -1.0 }); + entries.push(MatrixEntry { index: (n, s), value: -1.0 }); + } + + // each chain sphere is tangent to the next chain sphere, + // with opposing orientation + let s_next = 3 + (s-2) % 6; + entries.push(MatrixEntry { index: (s, s_next), value: -1.0 }); + entries.push(MatrixEntry { index: (s_next, s), value: -1.0 }); + } + } + entries + }); + let guess = DMatrix::from_columns( + [ + sphere(0.0, 0.0, 0.0, 15.0), + sphere(0.0, 0.0, -9.0, 5.0), + sphere(0.0, 0.0, 11.0, 3.0) + ].into_iter().chain( + (1..=6).map( + |k| { + let ang = (k as f64) * PI/3.0; + sphere(9.0 * ang.cos(), 9.0 * ang.sin(), 0.0, 2.5) + } + ) + ).collect::>().as_slice() + ); + let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k)); + const SCALED_TOL: f64 = 1.0e-12; + let (config, success, history) = realize_gram( + &gram, guess, &frozen, + SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + ); + let entry_tol = SCALED_TOL.sqrt(); + let solution_diams = [30.0, 10.0, 6.0, 5.0, 15.0, 10.0, 3.75, 2.5, 2.0 + 8.0/11.0]; + for (k, diam) in solution_diams.into_iter().enumerate() { + assert!((config[(3, k)] - 1.0 / diam).abs() < entry_tol); + } + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + if success { + println!("\nChain diameters:"); + println!(" {} sun (given)", 1.0 / config[(3, 3)]); + for k in 4..9 { + println!(" {} sun", 1.0 / config[(3, k)]); + } + } + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } + } + + // --- process inspection examples --- + + // these tests are meant for human inspection, not automated use. run them + // one at a time in `--nocapture` mode and read through the results and + // optimization histories that they print out. the `run-examples` script + // will run all of them + + #[test] + fn three_spheres_example() { + let gram = PartialMatrix({ + let mut entries = Vec::::new(); + for j in 0..3 { + for k in 0..3 { + entries.push(MatrixEntry { + index: (j, k), + value: if j == k { 1.0 } else { -1.0 } + }); + } + } + entries + }); + let guess = { + let a: f64 = 0.75_f64.sqrt(); + DMatrix::from_columns(&[ + sphere(1.0, 0.0, 0.0, 1.0), + sphere(-0.5, a, 0.0, 1.0), + sphere(-0.5, -a, 0.0, 1.0) + ]) + }; + println!(); + let (config, success, history) = realize_gram( + &gram, guess, &[], + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } + } + + #[test] + fn point_on_sphere_example() { + let gram = PartialMatrix({ + let mut entries = Vec::::new(); + for j in 0..2 { + for k in 0..2 { + entries.push(MatrixEntry { + index: (j, k), + value: if (j, k) == (1, 1) { 1.0 } else { 0.0 } + }); + } + } + entries + }); + let guess = DMatrix::from_columns(&[ + point(0.0, 0.0, 2.0), + sphere(0.0, 0.0, 0.0, 1.0) + ]); + let frozen = [(3, 0)]; + println!(); + let (config, success, history) = realize_gram( + &gram, guess, &frozen, + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + print!("Configuration:{}", config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } + } + + /* TO DO */ + // --- new test placed here to avoid merge conflict --- + + // at the frozen indices, the optimization steps should have exact zeros, + // and the realized configuration should match the initial guess + #[test] + fn frozen_entry_test() { + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for j in 0..2 { + for k in j..2 { + gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); + } + } + gram_to_be + }; + let guess = DMatrix::from_columns(&[ + point(0.0, 0.0, 2.0), + sphere(0.0, 0.0, 0.0, 1.0) + ]); + let frozen = [(3, 0), (3, 1)]; + println!(); + let (config, success, history) = realize_gram( + &gram, guess.clone(), &frozen, + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + assert_eq!(success, true); + for base_step in history.base_step.into_iter() { + for index in frozen { + assert_eq!(base_step[index], 0.0); + } + } + for index in frozen { + assert_eq!(config[index], guess[index]); + } + } } \ No newline at end of file diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index 2c71a83..897f9d4 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -8,14 +8,14 @@ use rustc_hash::FxHashSet; use sycamore::prelude::*; use add_remove::AddRemove; -use assembly::Assembly; +use assembly::{Assembly, ElementKey}; use display::Display; use outline::Outline; #[derive(Clone)] struct AppState { assembly: Assembly, - selection: Signal> + selection: Signal> } impl AppState { diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 4e4de9c..f7c975c 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -1,12 +1,40 @@ use itertools::Itertools; use sycamore::{prelude::*, web::tags::div}; -use web_sys::{Element, KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; +use web_sys::{ + Element, + Event, + HtmlInputElement, + KeyboardEvent, + MouseEvent, + wasm_bindgen::JsCast +}; -use crate::AppState; +use crate::{AppState, assembly::Constraint}; -// this component lists the elements of the assembly, showing the constraints -// on each element as a collapsible sub-list. its implementation is based on -// Kate Morley's HTML + CSS tree views: +// an editable view of the Lorentz product representing a constraint +#[component(inline_props)] +fn LorentzProductInput(constraint: Constraint) -> View { + view! { + input( + r#type="text", + bind:value=constraint.lorentz_prod_text, + on:change=move |event: Event| { + let target: HtmlInputElement = event.target().unwrap().unchecked_into(); + match target.value().parse::() { + Ok(lorentz_prod) => batch(|| { + constraint.lorentz_prod.set(lorentz_prod); + constraint.lorentz_prod_valid.set(true); + }), + Err(_) => constraint.lorentz_prod_valid.set(false) + }; + } + ) + } +} + +// a component that lists the elements of the current assembly, showing the +// constraints on each element as a collapsible sub-list. its implementation +// is based on Kate Morley's HTML + CSS tree views: // // https://iamkate.com/code/tree-views/ // @@ -44,15 +72,13 @@ pub fn Outline() -> View { } }); let label = elt.label.clone(); - let rep_components = elt.rep.iter().map(|u| { + let rep_components = elt.representation.iter().map(|u| { let u_coord = u.to_string().replace("-", "\u{2212}"); View::from(div().children(u_coord)) }).collect::>(); let constrained = elt.constraints.len() > 0; let details_node = create_node_ref(); view! { - /* [TO DO] switch to integer-valued parameters whenever - that becomes possible again */ li { details(ref=details_node) { summary( @@ -124,21 +150,21 @@ pub fn Outline() -> View { ul(class="constraints") { Keyed( list=elt.constraints.into_iter().collect::>(), - view=move |c_key: usize| { + view=move |c_key| { let c_state = use_context::(); let assembly = &c_state.assembly; let cst = assembly.constraints.with(|csts| csts[c_key].clone()); - let other_arg = if cst.args.0 == key { - cst.args.1 + let other_arg = if cst.subjects.0 == key { + cst.subjects.1 } else { - cst.args.0 + cst.subjects.0 }; let other_arg_label = assembly.elements.with(|elts| elts[other_arg].label.clone()); view! { li(class="cst") { input(r#type="checkbox", bind:checked=cst.active) div(class="cst-label") { (other_arg_label) } - div(class="cst-rep") { (cst.rep) } + LorentzProductInput(constraint=cst) } } }, diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index 22f5914..6dfb6e9 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -8,7 +8,8 @@ using Optim export rand_on_shell, Q, DescentHistory, - realize_gram_gradient, realize_gram_newton, realize_gram_optim, realize_gram + realize_gram_gradient, realize_gram_newton, realize_gram_optim, + realize_gram_alt_proj, realize_gram # === guessing === @@ -143,7 +144,7 @@ function realize_gram_gradient( break end - # find negative gradient of loss function + # find the negative gradient of the loss function neg_grad = 4*Q*L*Δ_proj slope = norm(neg_grad) dir = neg_grad / slope @@ -232,7 +233,7 @@ function realize_gram_newton( break end - # find the negative gradient of loss function + # find the negative gradient of the loss function neg_grad = 4*Q*L*Δ_proj # find the negative Hessian of the loss function @@ -313,6 +314,129 @@ function realize_gram_optim( ) 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`, with an +# alternate technique for finding the projected base step from the unprojected +# Hessian +function realize_gram_alt_proj( + gram::SparseMatrixCSC{T, <:Any}, + guess::Matrix{T}, + frozen = CartesianIndex[]; + scaled_tol = 1e-30, + min_efficiency = 0.5, + 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 + + # convert the frozen indices to stacked format + frozen_stacked = [(index[2]-1)*element_dim + index[1] for index in frozen] + + # initialize search state + L = copy(guess) + Δ_proj = proj_diff(gram, L'*Q*L) + loss = dot(Δ_proj, Δ_proj) + + # use Newton's method with backtracking and gradient descent backup + for step in 1:max_descent_steps + # stop if the loss is tolerably low + if loss < tol + break + end + + # find the negative gradient of the 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_sym = Hermitian(hess) + push!(history.hess, hess_sym) + + # regularize the Hessian + min_eigval = minimum(eigvals(hess_sym)) + push!(history.positive, min_eigval > 0) + if min_eigval <= 0 + hess -= reg_scale * min_eigval * I + end + + # compute the Newton step + neg_grad_stacked = reshape(neg_grad, total_dim) + for k in frozen_stacked + neg_grad_stacked[k] = 0 + hess[k, :] .= 0 + hess[:, k] .= 0 + hess[k, k] = 1 + end + base_step_stacked = Hermitian(hess) \ neg_grad_stacked + base_step = reshape(base_step_stacked, dims) + push!(history.base_step, base_step) + + # 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) + step_success = false + base_target_improvement = dot(neg_grad, base_step) + for backoff_steps in 0:max_backoff_steps + history.stepsize[end] = rate + 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 * base_target_improvement + history.backoff_steps[end] = backoff_steps + step_success = true + break + end + rate *= backoff + end + + # 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, loss < tol, history +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( @@ -321,7 +445,6 @@ function realize_gram( frozen = nothing; scaled_tol = 1e-30, min_efficiency = 0.5, - init_rate = 1.0, backoff = 0.9, reg_scale = 1.1, max_descent_steps = 200, @@ -352,20 +475,19 @@ function realize_gram( unfrozen_stacked = reshape(is_unfrozen, total_dim) end - # initialize variables - grad_rate = init_rate + # initialize search state L = copy(guess) - - # use Newton's method with backtracking and gradient descent backup Δ_proj = proj_diff(gram, L'*Q*L) loss = dot(Δ_proj, Δ_proj) + + # use Newton's method with backtracking and gradient descent backup 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 + # find the negative gradient of the loss function neg_grad = 4*Q*L*Δ_proj # find the negative Hessian of the loss function @@ -420,6 +542,7 @@ function realize_gram( empty!(history.last_line_loss) rate = one(T) step_success = false + base_target_improvement = dot(neg_grad, base_step) for backoff_steps in 0:max_backoff_steps history.stepsize[end] = rate L = L_last + rate * base_step @@ -428,7 +551,7 @@ function realize_gram( 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) + if improvement >= min_efficiency * rate * base_target_improvement history.backoff_steps[end] = backoff_steps step_success = true break diff --git a/engine-proto/gram-test/irisawa-hexlet.jl b/engine-proto/gram-test/irisawa-hexlet.jl index 67def8c..607db61 100644 --- a/engine-proto/gram-test/irisawa-hexlet.jl +++ b/engine-proto/gram-test/irisawa-hexlet.jl @@ -74,4 +74,13 @@ if success for k in 5:9 println(" ", 1 / L[4,k], " sun") end -end \ No newline at end of file +end + +# test an alternate technique for finding the projected base step from the +# unprojected Hessian +L_alt, success_alt, history_alt = Engine.realize_gram_alt_proj(gram, guess, frozen) +completed_gram_alt = L_alt'*Engine.Q*L_alt +println("\nDifference in result using alternate projection:\n") +display(completed_gram_alt - completed_gram) +println("\nDifference in steps: ", size(history_alt.scaled_loss, 1) - size(history.scaled_loss, 1)) +println("Difference in loss: ", history_alt.scaled_loss[end] - history.scaled_loss[end], "\n") \ 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 97f0720..5d479cf 100644 --- a/engine-proto/gram-test/sphere-in-tetrahedron.jl +++ b/engine-proto/gram-test/sphere-in-tetrahedron.jl @@ -64,4 +64,13 @@ 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], "\n") + +# test an alternate technique for finding the projected base step from the +# unprojected Hessian +L_alt, success_alt, history_alt = Engine.realize_gram_alt_proj(gram, guess, frozen) +completed_gram_alt = L_alt'*Engine.Q*L_alt +println("\nDifference in result using alternate projection:\n") +display(completed_gram_alt - completed_gram) +println("\nDifference in steps: ", size(history_alt.scaled_loss, 1) - size(history.scaled_loss, 1)) +println("Difference in loss: ", history_alt.scaled_loss[end] - history.scaled_loss[end], "\n") \ No newline at end of file diff --git a/engine-proto/gram-test/tetrahedron-radius-ratio.jl b/engine-proto/gram-test/tetrahedron-radius-ratio.jl index 7ceb794..9fec28e 100644 --- a/engine-proto/gram-test/tetrahedron-radius-ratio.jl +++ b/engine-proto/gram-test/tetrahedron-radius-ratio.jl @@ -93,4 +93,13 @@ 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 +end + +# test an alternate technique for finding the projected base step from the +# unprojected Hessian +L_alt, success_alt, history_alt = Engine.realize_gram_alt_proj(gram, guess, frozen) +completed_gram_alt = L_alt'*Engine.Q*L_alt +println("\nDifference in result using alternate projection:\n") +display(completed_gram_alt - completed_gram) +println("\nDifference in steps: ", size(history_alt.scaled_loss, 1) - size(history.scaled_loss, 1)) +println("Difference in loss: ", history_alt.scaled_loss[end] - history.scaled_loss[end], "\n") \ No newline at end of file diff --git a/notes/inversive.md b/notes/inversive.md index 5ee6329..933eb35 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -41,3 +41,25 @@ I will have to work out formulas for the Euclidean distance between two entities In this vein, it seems as though if J1 and J2 are the reps of two points, then Q(J1,J2) = d^2/2. So then the sphere centered at J1 through J2 is (J1-(2Q(J1,J2),0,0,0,0))/sqrt(2Q(J1,J2)). Ugh has a sqrt in it. Similarly for sphere centered at J3 through J2, (J3-(2Q(J3,J2),0000))/sqrt(2Q(J3,J2)). J1,J2,J3 are collinear if these spheres are tangent, i.e. if those vectors have Q-inner-product 1, which is to say Q(J1,J3) - Q(J1,J2) - Q(J3,J2) = 2sqrt(Q(J1,J2)Q(J2,J3)). But maybe that's not the simplest way of putting it. After all, we can just say that the cross-product of the two differences is 0; that has no square roots in it. One conceivable way to canonicalize lines is to use the *perpendicular* plane that goes through the origin, that's uniquely defined, and anyway just amounts to I = (0,0,d) where d is the ordinary direction vector of the line; and a point J in that plane that the line goes through, which just amounts to J=(r^2,1,E) with Q(I,J) = 0, i.e. E\dot d = 0. It's also the point on the line closest to the origin. The reason that we don't usually use that point as the companion to the direction vector is that the resulting set of six coordinates is not homogeneous. But here that's not an issue, since we have our standard point coordinates and plane coordinates; and for a plane through the origin, only two of the direction coordinates are really free, and then we have the one dot-product relation, so only two of the point coordinates are really free, giving us the correct dimensionality of 4 for the set of lines. So in some sense this says that we could take naively as coordinates for a line the projection of the unit direction vector to the xy plane and the projection of the line's closest point to the origin to the xy plane. That doesn't seem to have any weird gimbal locks or discontinuities or anything. And with these coordinates, you can test if the point E=x,y,z is on the line (dx,dy,cx,cy) by extending (dx,dy) to d via dz = sqrt(1-dx^2 - dy^2), extending (cx,cy) to c by determining cz via d\dot c = 0, and then checking if d\cross(E-c) = 0. And you can see if two lines are parallel just by checking if they have the same direction vector, and if not, you can see if they are coplanar by projecting both of their closest points perpendicularly onto the line in the direction of the cross product of their directions, and if the projections match they are coplanar. + +#### Engine Conventions + +The coordinate conventions used in the engine are different from the ones used in these notes. Marking the engine vectors and coordinates with $'$, we have +$$I' = (x', y', z', b', c'),$$ +where +$$ +\begin{align*} +x' & = x & b' & = b/2 \\ +y' & = y & c' & = c/2. \\ +z' & = z +\end{align*} +$$ +The engine uses the quadratic form $Q' = -Q$, which is expressed in engine coordinates as +$$Q'(I'_1, I'_2) = x'_1 x'_2 + y'_1 y'_2 + z'_1 z'_2 - 2(b'_1c'_2 + c'_1 b'_2).$$ +In the `engine` module, the matrix of $Q'$ is encoded in the lazy static variable `Q`. + +In the engine's coordinate conventions, a sphere with radius $r > 0$ centered on $P = (P_x, P_y, P_z)$ is represented by the vector +$$I'_s = \left(\frac{P_x}{r}, \frac{P_y}{r}, \frac{P_z}{r}, \frac1{2r}, \frac{\|P\|^2 - r^2}{2r}\right),$$ +which has the normalization $Q'(I'_s, I'_s) = 1$. The point $P$ is represented by the vector +$$I'_P = \left(P_x, P_y, P_z, \frac{1}{2}, \frac{\|P\|^2}{2}\right).$$ +In the `engine` module, these formulas are encoded in the `sphere` and `point` functions. \ No newline at end of file From 65cee1ecc236402ef4b5a462778cd8df51c58908 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Fri, 15 Nov 2024 03:32:47 +0000 Subject: [PATCH 113/132] Clean up the outline view (#19) Clean up the source code and interface of the outline view. In addition, [fix a bug](commit/6e42681b719d7ec97c4225ca321225979bf87b56) that could cause `Assembly::realize` to react to itself under certain circumstances. Those circumstances arose, making the bug noticeable, while this branch was being written. #### Source code - Modularize the `Outline` component into smaller components. - Switch from static iteration to dynamic Sycamore lists. This reduces the amount of re-rendering that happens when an element or constraint changes. It also allows constraint details to stay open or closed during constraint updates, rather than resetting to closed. - Make `Element::index` private, as discussed [here](pulls/15#issuecomment-1816). #### Interface - Make constraints editable, updating the assembly realization on input. Flag constraints where the Lorentz product value doesn't parse. - Round element vector coordinates to prevent the displayed strings from overlapping. Note that issue #20 was created by this PR, but it will be addressed shortly. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/19 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.toml | 4 +- app-proto/index.html | 4 +- app-proto/main.css | 90 +++++++++--- app-proto/src/add_remove.rs | 222 +++++++++++++--------------- app-proto/src/assembly.rs | 71 +++++---- app-proto/src/display.rs | 61 +++++--- app-proto/src/outline.rs | 280 +++++++++++++++++++----------------- 7 files changed, 404 insertions(+), 328 deletions(-) diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index e5bc05e..e623b26 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "sketch-outline" +name = "dyna3" version = "0.1.0" -authors = ["Aaron"] +authors = ["Aaron Fenyes", "Glen Whitney"] edition = "2021" [features] diff --git a/app-proto/index.html b/app-proto/index.html index 5474fe9..92238f4 100644 --- a/app-proto/index.html +++ b/app-proto/index.html @@ -2,8 +2,10 @@ - Sketch outline + dyna3 + + diff --git a/app-proto/main.css b/app-proto/main.css index 32ae5bf..b9fc0a1 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -1,7 +1,20 @@ +:root { + --text: #fcfcfc; /* almost white */ + --text-bright: white; + --text-invalid: #f58fc2; /* bright pink */ + --border: #555; /* light gray */ + --border-focus: #aaa; /* bright gray */ + --border-invalid: #70495c; /* dusky pink */ + --selection-highlight: #444; /* medium gray */ + --page-background: #222; /* dark gray */ + --display-background: #020202; /* almost black */ +} + body { margin: 0px; - color: #fcfcfc; - background-color: #222; + color: var(--text); + background-color: var(--page-background); + font-family: 'Fira Sans', sans-serif; } /* sidebar */ @@ -16,7 +29,7 @@ body { padding: 0px; border-width: 0px 1px 0px 0px; border-style: solid; - border-color: #555; + border-color: var(--border); } /* add-remove */ @@ -33,6 +46,15 @@ body { font-size: large; } +/* KLUDGE */ +/* + for convenience, we're using emoji as temporary icons for some buttons. these + buttons need to be displayed in an emoji font +*/ +#add-remove > button.emoji { + font-family: 'Noto Emoji', sans-serif; +} + /* outline */ #outline { @@ -51,81 +73,103 @@ summary { } summary.selected { - color: #fff; - background-color: #444; + color: var(--text-bright); + background-color: var(--selection-highlight); } -summary > div, .cst { +summary > div, .constraint { padding-top: 4px; padding-bottom: 4px; } -.elt, .cst { +.element, .constraint { display: flex; flex-grow: 1; padding-left: 8px; padding-right: 8px; } -.elt-switch { +.element-switch { width: 18px; padding-left: 2px; text-align: center; } -details:has(li) .elt-switch::after { +details:has(li) .element-switch::after { content: '▸'; } -details[open]:has(li) .elt-switch::after { +details[open]:has(li) .element-switch::after { content: '▾'; } -.elt-label { +.element-label { flex-grow: 1; } -.cst-label { +.constraint-label { flex-grow: 1; } -.elt-rep { +.element-representation { display: flex; } -.elt-rep > div { +.element-representation > div { padding: 2px 0px 0px 0px; font-size: 10pt; - text-align: center; + font-variant-numeric: tabular-nums; + text-align: right; width: 56px; } -.cst { +.constraint { font-style: italic; } -.cst > input[type=checkbox] { +.constraint.invalid { + color: var(--text-invalid); +} + +.constraint > input[type=checkbox] { margin: 0px 8px 0px 0px; } -.cst > input[type=text] { - color: #fcfcfc; +.constraint > input[type=text] { + color: inherit; background-color: inherit; - border: 1px solid #555; + border: 1px solid var(--border); border-radius: 2px; } +.constraint.invalid > input[type=text] { + border-color: var(--border-invalid); +} + +.status { + width: 20px; + padding-left: 4px; + text-align: center; + font-family: 'Noto Emoji'; + font-style: normal; +} + +.invalid > .status::after, details:has(.invalid):not([open]) .status::after { + content: '⚠'; + color: var(--text-invalid); +} + /* display */ canvas { float: left; margin-left: 20px; margin-top: 20px; - background-color: #020202; - border: 1px solid #555; + background-color: var(--display-background); + border: 1px solid var(--border); border-radius: 16px; } canvas:focus { - border-color: #aaa; + border-color: var(--border-focus); } \ No newline at end of file diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 19b4b8d..ba02e65 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -1,155 +1,130 @@ -use std::collections::BTreeSet; /* DEBUG */ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}}; /* DEBUG */ +// load an example assembly for testing. this code will be removed once we've +// built a more formal test assembly system fn load_gen_assemb(assembly: &Assembly) { let _ = assembly.try_insert_element( - Element { - id: String::from("gemini_a"), - label: String::from("Castor"), - color: [1.00_f32, 0.25_f32, 0.00_f32], - representation: engine::sphere(0.5, 0.5, 0.0, 1.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("gemini_a"), + String::from("Castor"), + [1.00_f32, 0.25_f32, 0.00_f32], + engine::sphere(0.5, 0.5, 0.0, 1.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("gemini_b"), - label: String::from("Pollux"), - color: [0.00_f32, 0.25_f32, 1.00_f32], - representation: engine::sphere(-0.5, -0.5, 0.0, 1.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("gemini_b"), + String::from("Pollux"), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere(-0.5, -0.5, 0.0, 1.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("ursa_major"), - label: String::from("Ursa major"), - color: [0.25_f32, 0.00_f32, 1.00_f32], - representation: engine::sphere(-0.5, 0.5, 0.0, 0.75), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("ursa_major"), + String::from("Ursa major"), + [0.25_f32, 0.00_f32, 1.00_f32], + engine::sphere(-0.5, 0.5, 0.0, 0.75) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("ursa_minor"), - label: String::from("Ursa minor"), - color: [0.25_f32, 1.00_f32, 0.00_f32], - representation: engine::sphere(0.5, -0.5, 0.0, 0.5), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("ursa_minor"), + String::from("Ursa minor"), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere(0.5, -0.5, 0.0, 0.5) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("moon_deimos"), - label: String::from("Deimos"), - color: [0.75_f32, 0.75_f32, 0.00_f32], - representation: engine::sphere(0.0, 0.15, 1.0, 0.25), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("moon_deimos"), + String::from("Deimos"), + [0.75_f32, 0.75_f32, 0.00_f32], + engine::sphere(0.0, 0.15, 1.0, 0.25) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("moon_phobos"), - label: String::from("Phobos"), - color: [0.00_f32, 0.75_f32, 0.50_f32], - representation: engine::sphere(0.0, -0.15, -1.0, 0.25), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("moon_phobos"), + String::from("Phobos"), + [0.00_f32, 0.75_f32, 0.50_f32], + engine::sphere(0.0, -0.15, -1.0, 0.25) + ) ); } /* DEBUG */ +// load an example assembly for testing. this code will be removed once we've +// built a more formal test assembly system fn load_low_curv_assemb(assembly: &Assembly) { let a = 0.75_f64.sqrt(); let _ = assembly.try_insert_element( - Element { - id: "central".to_string(), - label: "Central".to_string(), - color: [0.75_f32, 0.75_f32, 0.75_f32], - representation: engine::sphere(0.0, 0.0, 0.0, 1.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "central".to_string(), + "Central".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "assemb_plane".to_string(), - label: "Assembly plane".to_string(), - color: [0.75_f32, 0.75_f32, 0.75_f32], - representation: engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "assemb_plane".to_string(), + "Assembly plane".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "side1".to_string(), - label: "Side 1".to_string(), - color: [1.00_f32, 0.00_f32, 0.25_f32], - representation: engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "side1".to_string(), + "Side 1".to_string(), + [1.00_f32, 0.00_f32, 0.25_f32], + engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "side2".to_string(), - label: "Side 2".to_string(), - color: [0.25_f32, 1.00_f32, 0.00_f32], - representation: engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "side2".to_string(), + "Side 2".to_string(), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "side3".to_string(), - label: "Side 3".to_string(), - color: [0.00_f32, 0.25_f32, 1.00_f32], - representation: engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "side3".to_string(), + "Side 3".to_string(), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "corner1".to_string(), - label: "Corner 1".to_string(), - color: [0.75_f32, 0.75_f32, 0.75_f32], - representation: engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "corner1".to_string(), + "Corner 1".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "corner2".to_string(), - label: "Corner 2".to_string(), - color: [0.75_f32, 0.75_f32, 0.75_f32], - representation: engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "corner2".to_string(), + "Corner 2".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("corner3"), - label: String::from("Corner 3"), - color: [0.75_f32, 0.75_f32, 0.75_f32], - representation: engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("corner3"), + String::from("Corner 3"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0) + ) ); } @@ -202,6 +177,7 @@ pub fn AddRemove() -> View { } ) { "+" } button( + class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button disabled={ let state = use_context::(); state.selection.with(|sel| sel.len() != 2) @@ -215,15 +191,15 @@ pub fn AddRemove() -> View { } ); let lorentz_prod = create_signal(0.0); + let lorentz_prod_valid = create_signal(false); let active = create_signal(true); state.assembly.insert_constraint(Constraint { subjects: subjects, lorentz_prod: lorentz_prod, lorentz_prod_text: create_signal(String::new()), - lorentz_prod_valid: create_signal(false), + lorentz_prod_valid: lorentz_prod_valid, active: active, }); - state.assembly.realize(); state.selection.update(|sel| sel.clear()); /* DEBUG */ @@ -241,23 +217,23 @@ pub fn AddRemove() -> View { } }); - // update the realization when the constraint activated, or - // edited while active + // update the realization when the constraint becomes active + // and valid, or is edited while active and valid create_effect(move || { + console::log_1(&JsValue::from( + format!("Constraint ({}, {}) updated", subjects.0, subjects.1) + )); lorentz_prod.track(); - console::log_2( - &JsValue::from("Lorentz product updated to"), - &JsValue::from(lorentz_prod.get_untracked()) - ); - if active.get() { + if active.get() && lorentz_prod_valid.get() { state.assembly.realize(); } }); } ) { "🔗" } - select(bind:value=assembly_name) { /* DEBUG */ + select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser option(value="general") { "General" } option(value="low-curv") { "Low-curvature" } + option(value="empty") { "Empty" } } } } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 0cdf61b..35b4417 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -18,17 +18,33 @@ pub struct Element { pub id: String, pub label: String, pub color: ElementColor, - pub representation: DVector, - pub constraints: BTreeSet, + pub representation: Signal>, + pub constraints: Signal>, // the configuration matrix column index that was assigned to this element // last time the assembly was realized - /* TO DO */ - // this is public, as a kludge, because `Element` doesn't have a constructor - // yet. it should be made private as soon as the constructor is written - pub index: usize + column_index: usize } +impl Element { + pub fn new( + id: String, + label: String, + color: ElementColor, + representation: DVector + ) -> Element { + Element { + id: id, + label: label, + color: color, + representation: create_signal(representation), + constraints: create_signal(BTreeSet::default()), + column_index: 0 + } + } +} + + #[derive(Clone)] pub struct Constraint { pub subjects: (ElementKey, ElementKey), @@ -92,24 +108,23 @@ impl Assembly { // create and insert a new element self.insert_element_unchecked( - Element { - id: id, - label: format!("Sphere {}", id_num), - color: [0.75_f32, 0.75_f32, 0.75_f32], - representation: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + id, + format!("Sphere {}", id_num), + [0.75_f32, 0.75_f32, 0.75_f32], + DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]) + ) ); } pub fn insert_constraint(&self, constraint: Constraint) { let subjects = constraint.subjects; let key = self.constraints.update(|csts| csts.insert(constraint)); - self.elements.update(|elts| { - elts[subjects.0].constraints.insert(key); - elts[subjects.1].constraints.insert(key); - }); + let subject_constraints = self.elements.with( + |elts| (elts[subjects.0].constraints, elts[subjects.1].constraints) + ); + subject_constraints.0.update(|csts| csts.insert(key)); + subject_constraints.1.update(|csts| csts.insert(key)); } // --- realization --- @@ -118,7 +133,7 @@ impl Assembly { // index the elements self.elements.update_silent(|elts| { for (index, (_, elt)) in elts.into_iter().enumerate() { - elt.index = index; + elt.column_index = index; } }); @@ -130,8 +145,8 @@ impl Assembly { for (_, cst) in csts { if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() { let subjects = cst.subjects; - let row = elts[subjects.0].index; - let col = elts[subjects.1].index; + let row = elts[subjects.0].column_index; + let col = elts[subjects.1].column_index; gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked()); } } @@ -141,9 +156,9 @@ impl Assembly { // Gram matrix let mut guess_to_be = DMatrix::::zeros(5, elts.len()); for (_, elt) in elts { - let index = elt.index; + let index = elt.column_index; gram_to_be.push_sym(index, index, 1.0); - guess_to_be.set_column(index, &elt.representation); + guess_to_be.set_column(index, &elt.representation.get_clone_untracked()); } (gram_to_be, guess_to_be) @@ -185,11 +200,11 @@ impl Assembly { if success { // read out the solution - self.elements.update(|elts| { - for (_, elt) in elts.iter_mut() { - elt.representation.set_column(0, &config.column(elt.index)); - } - }); + for (_, elt) in self.elements.get_clone_untracked() { + elt.representation.update( + |rep| rep.set_column(0, &config.column(elt.column_index)) + ); + } } } } \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 79199ec..ee0af47 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -103,7 +103,11 @@ pub fn Display() -> View { // change listener let scene_changed = create_signal(true); create_effect(move || { - state.assembly.elements.track(); + state.assembly.elements.with(|elts| { + for (_, elt) in elts { + elt.representation.track(); + } + }); state.selection.track(); scene_changed.set(true); }); @@ -295,25 +299,40 @@ pub fn Display() -> View { let assembly_to_world = &location * &orientation; // get the assembly - let elements = state.assembly.elements.get_clone(); - let element_iter = (&elements).into_iter(); - let reps_world: Vec<_> = element_iter.clone().map( - |(_, elt)| &assembly_to_world * &elt.representation - ).collect(); - let colors: Vec<_> = element_iter.clone().map(|(key, elt)| - if state.selection.with(|sel| sel.contains(&key)) { - elt.color.map(|ch| 0.2 + 0.8*ch) - } else { - elt.color - } - ).collect(); - let highlights: Vec<_> = element_iter.map(|(key, _)| - if state.selection.with(|sel| sel.contains(&key)) { - 1.0_f32 - } else { - HIGHLIGHT - } - ).collect(); + let ( + elt_cnt, + reps_world, + colors, + highlights + ) = state.assembly.elements.with(|elts| { + ( + // number of elements + elts.len() as i32, + + // representation vectors in world coordinates + elts.iter().map( + |(_, elt)| elt.representation.with(|rep| &assembly_to_world * rep) + ).collect::>(), + + // colors + elts.iter().map(|(key, elt)| { + if state.selection.with(|sel| sel.contains(&key)) { + elt.color.map(|ch| 0.2 + 0.8*ch) + } else { + elt.color + } + }).collect::>(), + + // highlight levels + elts.iter().map(|(key, _)| { + if state.selection.with(|sel| sel.contains(&key)) { + 1.0_f32 + } else { + HIGHLIGHT + } + }).collect::>() + ) + }); // set the resolution let width = canvas.width() as f32; @@ -322,7 +341,7 @@ pub fn Display() -> View { ctx.uniform1f(shortdim_loc.as_ref(), width.min(height)); // pass the assembly - ctx.uniform1i(sphere_cnt_loc.as_ref(), elements.len() as i32); + ctx.uniform1i(sphere_cnt_loc.as_ref(), elt_cnt); for n in 0..reps_world.len() { let v = &reps_world[n]; ctx.uniform3f( diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index f7c975c..ee1603f 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -1,7 +1,6 @@ use itertools::Itertools; -use sycamore::{prelude::*, web::tags::div}; +use sycamore::prelude::*; use web_sys::{ - Element, Event, HtmlInputElement, KeyboardEvent, @@ -9,7 +8,7 @@ use web_sys::{ wasm_bindgen::JsCast }; -use crate::{AppState, assembly::Constraint}; +use crate::{AppState, assembly, assembly::{Constraint, ConstraintKey, ElementKey}}; // an editable view of the Lorentz product representing a constraint #[component(inline_props)] @@ -32,6 +31,143 @@ fn LorentzProductInput(constraint: Constraint) -> View { } } +// a list item that shows a constraint in an outline view of an element +#[component(inline_props)] +fn ConstraintOutlineItem(constraint_key: ConstraintKey, element_key: ElementKey) -> View { + let state = use_context::(); + let assembly = &state.assembly; + let constraint = assembly.constraints.with(|csts| csts[constraint_key].clone()); + let other_subject = if constraint.subjects.0 == element_key { + constraint.subjects.1 + } else { + constraint.subjects.0 + }; + let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); + let class = constraint.lorentz_prod_valid.map( + |&lorentz_prod_valid| if lorentz_prod_valid { "constraint" } else { "constraint invalid" } + ); + view! { + li(class=class.get()) { + input(r#type="checkbox", bind:checked=constraint.active) + div(class="constraint-label") { (other_subject_label) } + LorentzProductInput(constraint=constraint) + div(class="status") + } + } +} + +// a list item that shows an element in an outline view of an assembly +#[component(inline_props)] +fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { + let state = use_context::(); + let class = state.selection.map( + move |sel| if sel.contains(&key) { "selected" } else { "" } + ); + let label = element.label.clone(); + let rep_components = element.representation.map( + |rep| rep.iter().map( + |u| format!("{:.3}", u).replace("-", "\u{2212}") + ).collect() + ); + let constrained = element.constraints.map(|csts| csts.len() > 0); + let constraint_list = element.constraints.map( + |csts| csts.clone().into_iter().collect() + ); + let details_node = create_node_ref(); + view! { + li { + details(ref=details_node) { + summary( + class=class.get(), + on:keydown={ + move |event: KeyboardEvent| { + match event.key().as_str() { + "Enter" => { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&key) { + sel.insert(key); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); + sel.insert(key); + }); + } + event.prevent_default(); + }, + "ArrowRight" if constrained.get() => { + let _ = details_node + .get() + .unchecked_into::() + .set_attribute("open", ""); + }, + "ArrowLeft" => { + let _ = details_node + .get() + .unchecked_into::() + .remove_attribute("open"); + }, + _ => () + } + } + } + ) { + div( + class="element-switch", + on:click=|event: MouseEvent| event.stop_propagation() + ) + div( + class="element", + on:click={ + move |event: MouseEvent| { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&key) { + sel.insert(key); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); + sel.insert(key); + }); + } + event.stop_propagation(); + event.prevent_default(); + } + } + ) { + div(class="element-label") { (label) } + div(class="element-representation") { + Indexed( + list=rep_components, + view=|coord_str| view! { + div { (coord_str) } + } + ) + } + div(class="status") + } + } + ul(class="constraints") { + Keyed( + list=constraint_list, + view=move |cst_key| view! { + ConstraintOutlineItem( + constraint_key=cst_key, + element_key=key + ) + }, + key=|cst_key| cst_key.clone() + ) + } + } + } + } +} + // a component that lists the elements of the current assembly, showing the // constraints on each element as a collapsible sub-list. its implementation // is based on Kate Morley's HTML + CSS tree views: @@ -40,15 +176,16 @@ fn LorentzProductInput(constraint: Constraint) -> View { // #[component] pub fn Outline() -> View { - // sort the elements alphabetically by ID - let elements_sorted = create_memo(|| { - let state = use_context::(); - state.assembly.elements - .get_clone() + let state = use_context::(); + + // list the elements alphabetically by ID + let element_list = state.assembly.elements.map( + |elts| elts + .clone() .into_iter() .sorted_by_key(|(_, elt)| elt.id.clone()) .collect() - }); + ); view! { ul( @@ -59,128 +196,11 @@ pub fn Outline() -> View { } ) { Keyed( - list=elements_sorted, - view=|(key, elt)| { - let state = use_context::(); - let class = create_memo({ - move || { - if state.selection.with(|sel| sel.contains(&key)) { - "selected" - } else { - "" - } - } - }); - let label = elt.label.clone(); - let rep_components = elt.representation.iter().map(|u| { - let u_coord = u.to_string().replace("-", "\u{2212}"); - View::from(div().children(u_coord)) - }).collect::>(); - let constrained = elt.constraints.len() > 0; - let details_node = create_node_ref(); - view! { - li { - details(ref=details_node) { - summary( - class=class.get(), - on:keydown={ - move |event: KeyboardEvent| { - match event.key().as_str() { - "Enter" => { - if event.shift_key() { - state.selection.update(|sel| { - if !sel.remove(&key) { - sel.insert(key); - } - }); - } else { - state.selection.update(|sel| { - sel.clear(); - sel.insert(key); - }); - } - event.prevent_default(); - }, - "ArrowRight" if constrained => { - let _ = details_node - .get() - .unchecked_into::() - .set_attribute("open", ""); - }, - "ArrowLeft" => { - let _ = details_node - .get() - .unchecked_into::() - .remove_attribute("open"); - }, - _ => () - } - } - } - ) { - div( - class="elt-switch", - on:click=|event: MouseEvent| event.stop_propagation() - ) - div( - class="elt", - on:click={ - move |event: MouseEvent| { - if event.shift_key() { - state.selection.update(|sel| { - if !sel.remove(&key) { - sel.insert(key); - } - }); - } else { - state.selection.update(|sel| { - sel.clear(); - sel.insert(key); - }); - } - event.stop_propagation(); - event.prevent_default(); - } - } - ) { - div(class="elt-label") { (label) } - div(class="elt-rep") { (rep_components) } - } - } - ul(class="constraints") { - Keyed( - list=elt.constraints.into_iter().collect::>(), - view=move |c_key| { - let c_state = use_context::(); - let assembly = &c_state.assembly; - let cst = assembly.constraints.with(|csts| csts[c_key].clone()); - let other_arg = if cst.subjects.0 == key { - cst.subjects.1 - } else { - cst.subjects.0 - }; - let other_arg_label = assembly.elements.with(|elts| elts[other_arg].label.clone()); - view! { - li(class="cst") { - input(r#type="checkbox", bind:checked=cst.active) - div(class="cst-label") { (other_arg_label) } - LorentzProductInput(constraint=cst) - } - } - }, - key=|c_key| c_key.clone() - ) - } - } - } - } + list=element_list, + view=|(key, elt)| view! { + ElementOutlineItem(key=key, element=elt) }, - key=|(key, elt)| ( - key.clone(), - elt.id.clone(), - elt.label.clone(), - elt.constraints.clone() - ) + key=|(key, _)| key.clone() ) } } From e917272c60f2046567c0da48543f721b20d913e2 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Fri, 22 Nov 2024 02:25:10 +0000 Subject: [PATCH 114/132] Give each element a serial number (#22) Give each `Element` a serial number, which identifies it uniquely. The serial number is assigned by the `Element::new` constructor. Because disallows potentially unsafe global state (at least without explicit `unsafe` blocks), the next serial number is stored in a thread-safe static atomic variable (`assembly::NEXT_ELEMENT_SERIAL`), as suggested in [this StackOverflow answer](https://stackoverflow.com/a/32936288). Since the overhead for keeping track of memory ordering should be minimal, we're using the strongest available ordering: [sequentially consistent](https://marabos.nl/atomics/memory-ordering.html#seqcst). Resolves #20. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/22 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/src/assembly.rs | 25 ++++++++++++++++++++++++- app-proto/src/outline.rs | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 35b4417..59cba41 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,7 +1,7 @@ use nalgebra::{DMatrix, DVector}; use rustc_hash::FxHashMap; use slab::Slab; -use std::collections::BTreeSet; +use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ @@ -13,6 +13,13 @@ pub type ConstraintKey = usize; pub type ElementColor = [f32; 3]; +/* KLUDGE */ +// we should reconsider this design when we build a system for switching between +// assemblies. at that point, we might want to switch to hierarchical keys, +// where each each element has a key that identifies it within its assembly and +// each assembly has a key that identifies it within the sesssion +static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0); + #[derive(Clone, PartialEq)] pub struct Element { pub id: String, @@ -20,6 +27,10 @@ pub struct Element { pub color: ElementColor, pub representation: Signal>, pub constraints: Signal>, + + // a serial number, assigned by `Element::new`, that uniquely identifies + // each element + pub serial: u64, // the configuration matrix column index that was assigned to this element // last time the assembly was realized @@ -33,12 +44,24 @@ impl Element { color: ElementColor, representation: DVector ) -> Element { + // take the next serial number, panicking if that was the last number we + // had left. the technique we use to panic on overflow is taken from + // _Rust Atomics and Locks_, by Mara Bos + // + // https://marabos.nl/atomics/atomics.html#example-handle-overflow + // + let serial = NEXT_ELEMENT_SERIAL.fetch_update( + Ordering::SeqCst, Ordering::SeqCst, + |serial| serial.checked_add(1) + ).expect("Out of serial numbers for elements"); + Element { id: id, label: label, color: color, representation: create_signal(representation), constraints: create_signal(BTreeSet::default()), + serial: serial, column_index: 0 } } diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index ee1603f..e2cf49c 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -200,7 +200,7 @@ pub fn Outline() -> View { view=|(key, elt)| view! { ElementOutlineItem(key=key, element=elt) }, - key=|(key, _)| key.clone() + key=|(_, elt)| elt.serial ) } } From a8e13b81103acce319c14ec7a25e354fb12693bc Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Tue, 26 Nov 2024 00:32:50 +0000 Subject: [PATCH 115/132] Turn non-automated tests into Cargo examples (#24) Some of the Cargo tests on the main branch are designed to print output for human inspection, not to verify computations automatically. The incoming branch turns these tests into Cargo examples. It also makes two organizational changes in pursuit of this goal: - It introduces a dyna3 library target, which the examples use as a dependency. In the future, this target could grow into an officially maintained dyna3 library. - It puts the code for realizing the Irisawa hexlet into a new conditionally compiled `engine::irisawa` module. This code is shared by a test and an example. Compilation is controlled by the `dev` feature, which is turned on by default in development mode. I've verified that printed output of the examples hasn't changed between the head (848f7d6) and base (e917272) of the incoming branch. Co-authored-by: Aaron Fenyes Co-authored-by: Glen Whitney Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/24 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- README.md | 48 +++++ app-proto/Cargo.toml | 6 + app-proto/examples/irisawa-hexlet.rs | 25 +++ app-proto/examples/point-on-sphere.rs | 38 ++++ app-proto/examples/three-spheres.rs | 40 +++++ app-proto/run-examples | 13 +- app-proto/src/engine.rs | 245 ++++++++------------------ app-proto/src/lib.rs | 1 + 8 files changed, 241 insertions(+), 175 deletions(-) create mode 100644 app-proto/examples/irisawa-hexlet.rs create mode 100644 app-proto/examples/point-on-sphere.rs create mode 100644 app-proto/examples/three-spheres.rs create mode 100644 app-proto/src/lib.rs diff --git a/README.md b/README.md index 9ea9cbf..1ea3302 100644 --- a/README.md +++ b/README.md @@ -17,3 +17,51 @@ Note that currently this is just the barest beginnings of the project, more of a * Able to run in browser (so implemented in WASM-compatible language) * Produce scalable graphics of 3D diagrams, and maybe STL files (or other fabricatable file format) as well. + +## Prototype + +The latest prototype is in the folder `app-proto`. It includes both a user interface and a numerical constraint-solving engine. + +### Install the prerequisites + +1. Install [`rustup`](https://rust-lang.github.io/rustup/): the officially recommended Rust toolchain manager + * It's available on Ubuntu as a [Snap](https://snapcraft.io/rustup) +2. Call `rustup default stable` to "download the latest stable release of Rust and set it as your default toolchain" + * If you forget, the `rustup` [help system](https://github.com/rust-lang/rustup/blob/d9b3601c3feb2e88cf3f8ca4f7ab4fdad71441fd/src/errors.rs#L109-L112) will remind you +3. Call `rustup target add wasm32-unknown-unknown` to add the [most generic 32-bit WebAssembly target](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-unknown-unknown.html) +4. Call `cargo install wasm-pack` to install the [WebAssembly toolchain](https://rustwasm.github.io/docs/wasm-pack/) +5. Call `cargo install trunk` to install the [Trunk](https://trunkrs.dev/) web-build tool +6. Add the `.cargo/bin` folder in your home directory to your executable search path + * This lets you call Trunk, and other tools installed by Cargo, without specifying their paths + * On POSIX systems, the search path is stored in the `PATH` environment variable + +### Play with the prototype + +1. Go into the `app-proto` folder +2. Call `trunk serve --release` to build and serve the prototype + * *The crates the prototype depends on will be downloaded and served automatically* + * *For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag* +3. In a web browser, visit one of the URLs listed under the message `INFO 📡 server listening at:` + * *Touching any file in the `app-proto` folder will make Trunk rebuild and live-reload the prototype* +4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype + +### Run the engine on some example problems + +1. Go into the `app-proto` folder +2. Call `./run-examples` + * *For each example problem, the engine will print the value of the loss function at each optimization step* + * *The first example that prints is the same as the Irisawa hexlet example from the Julia version of the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then* + + ```julia + include("irisawa-hexlet.jl") + for (step, scaled_loss) in enumerate(history_alt.scaled_loss) + println(rpad(step-1, 4), " | ", scaled_loss) + end + ``` + + *you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show* + +### Run the automated tests + +1. Go into the `app-proto` folder +2. Call `cargo test` diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index e623b26..38205a7 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [features] default = ["console_error_panic_hook"] +dev = [] [dependencies] itertools = "0.13.0" @@ -36,7 +37,12 @@ features = [ 'WebGlVertexArrayObject' ] +# the self-dependency specifies features to use for tests and examples +# +# https://github.com/rust-lang/cargo/issues/2911#issuecomment-1483256987 +# [dev-dependencies] +dyna3 = { path = ".", default-features = false, features = ["dev"] } wasm-bindgen-test = "0.3.34" [profile.release] diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs new file mode 100644 index 0000000..fc14f91 --- /dev/null +++ b/app-proto/examples/irisawa-hexlet.rs @@ -0,0 +1,25 @@ +use dyna3::engine::{Q, irisawa::realize_irisawa_hexlet}; + +fn main() { + const SCALED_TOL: f64 = 1.0e-12; + let (config, success, history) = realize_irisawa_hexlet(SCALED_TOL); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + if success { + println!("\nChain diameters:"); + println!(" {} sun (given)", 1.0 / config[(3, 3)]); + for k in 4..9 { + println!(" {} sun", 1.0 / config[(3, k)]); + } + } + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } +} \ No newline at end of file diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs new file mode 100644 index 0000000..0e4765a --- /dev/null +++ b/app-proto/examples/point-on-sphere.rs @@ -0,0 +1,38 @@ +use nalgebra::DMatrix; + +use dyna3::engine::{Q, point, realize_gram, sphere, PartialMatrix}; + +fn main() { + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for j in 0..2 { + for k in j..2 { + gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); + } + } + gram_to_be + }; + let guess = DMatrix::from_columns(&[ + point(0.0, 0.0, 2.0), + sphere(0.0, 0.0, 0.0, 1.0) + ]); + let frozen = [(3, 0)]; + println!(); + let (config, success, history) = realize_gram( + &gram, guess, &frozen, + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + print!("Configuration:{}", config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } +} \ No newline at end of file diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs new file mode 100644 index 0000000..d348b18 --- /dev/null +++ b/app-proto/examples/three-spheres.rs @@ -0,0 +1,40 @@ +use nalgebra::DMatrix; + +use dyna3::engine::{Q, realize_gram, sphere, PartialMatrix}; + +fn main() { + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for j in 0..3 { + for k in j..3 { + gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); + } + } + gram_to_be + }; + let guess = { + let a: f64 = 0.75_f64.sqrt(); + DMatrix::from_columns(&[ + sphere(1.0, 0.0, 0.0, 1.0), + sphere(-0.5, a, 0.0, 1.0), + sphere(-0.5, -a, 0.0, 1.0) + ]) + }; + println!(); + let (config, success, history) = realize_gram( + &gram, guess, &[], + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } +} \ No newline at end of file diff --git a/app-proto/run-examples b/app-proto/run-examples index 6a5e3ae..bc7e933 100755 --- a/app-proto/run-examples +++ b/app-proto/run-examples @@ -1,8 +1,11 @@ -# based on "Enabling print statements in Cargo tests", by Jon Almeida +#!/bin/sh + +# run all Cargo examples, as described here: # -# https://jonalmeida.com/posts/2015/01/23/print-cargo/ +# Karol Kuczmarski. "Add examples to your Rust libraries" +# http://xion.io/post/code/rust-examples.html # -cargo test -- --nocapture engine::tests::irisawa_hexlet_test -cargo test -- --nocapture engine::tests::three_spheres_example -cargo test -- --nocapture engine::tests::point_on_sphere_example +cargo run --example irisawa-hexlet +cargo run --example three-spheres +cargo run --example point-on-sphere diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 343b96e..285a9c6 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -4,7 +4,7 @@ use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ // --- elements --- -#[cfg(test)] +#[cfg(feature = "dev")] pub fn point(x: f64, y: f64, z: f64) -> DVector { DVector::from_column_slice(&[x, y, z, 0.5, 0.5*(x*x + y*y + z*z)]) } @@ -113,7 +113,7 @@ impl DescentHistory { // the Lorentz form lazy_static! { - static ref Q: DMatrix = DMatrix::from_row_slice(5, 5, &[ + pub static ref Q: DMatrix = DMatrix::from_row_slice(5, 5, &[ 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, @@ -277,12 +277,79 @@ pub fn realize_gram( // --- tests --- -#[cfg(test)] -mod tests { +// 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/ +// +#[cfg(feature = "dev")] +pub mod irisawa { use std::{array, f64::consts::PI}; use super::*; + pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, bool, DescentHistory) { + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for s in 0..9 { + // each sphere is represented by a spacelike vector + gram_to_be.push_sym(s, s, 1.0); + + // the circumscribing sphere is tangent to all of the other + // spheres, with matching orientation + if s > 0 { + gram_to_be.push_sym(0, s, 1.0); + } + + if s > 2 { + // each chain sphere is tangent to the "sun" and "moon" + // spheres, with opposing orientation + for n in 1..3 { + gram_to_be.push_sym(s, n, -1.0); + } + + // each chain sphere is tangent to the next chain sphere, + // with opposing orientation + let s_next = 3 + (s-2) % 6; + gram_to_be.push_sym(s, s_next, -1.0); + } + } + gram_to_be + }; + + let guess = DMatrix::from_columns( + [ + sphere(0.0, 0.0, 0.0, 15.0), + sphere(0.0, 0.0, -9.0, 5.0), + sphere(0.0, 0.0, 11.0, 3.0) + ].into_iter().chain( + (1..=6).map( + |k| { + let ang = (k as f64) * PI/3.0; + sphere(9.0 * ang.cos(), 9.0 * ang.sin(), 0.0, 2.5) + } + ) + ).collect::>().as_slice() + ); + + // the frozen entries fix the radii of the circumscribing sphere, the + // "sun" and "moon" spheres, and one of the chain spheres + let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k)); + + realize_gram( + &gram, guess, &frozen, + scaled_tol, 0.5, 0.9, 1.1, 200, 110 + ) + } +} + +#[cfg(test)] +mod tests { + use super::{*, irisawa::realize_irisawa_hexlet}; + #[test] fn sub_proj_test() { let target = PartialMatrix(vec![ @@ -328,182 +395,20 @@ mod tests { assert!(state.loss.abs() < f64::EPSILON); } - // 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/ - // #[test] fn irisawa_hexlet_test() { - let gram = PartialMatrix({ - let mut entries = Vec::::new(); - for s in 0..9 { - // each sphere is represented by a spacelike vector - entries.push(MatrixEntry { index: (s, s), value: 1.0 }); - - // the circumscribing sphere is tangent to all of the other - // spheres, with matching orientation - if s > 0 { - entries.push(MatrixEntry { index: (0, s), value: 1.0 }); - entries.push(MatrixEntry { index: (s, 0), value: 1.0 }); - } - - if s > 2 { - // each chain sphere is tangent to the "sun" and "moon" - // spheres, with opposing orientation - for n in 1..3 { - entries.push(MatrixEntry { index: (s, n), value: -1.0 }); - entries.push(MatrixEntry { index: (n, s), value: -1.0 }); - } - - // each chain sphere is tangent to the next chain sphere, - // with opposing orientation - let s_next = 3 + (s-2) % 6; - entries.push(MatrixEntry { index: (s, s_next), value: -1.0 }); - entries.push(MatrixEntry { index: (s_next, s), value: -1.0 }); - } - } - entries - }); - let guess = DMatrix::from_columns( - [ - sphere(0.0, 0.0, 0.0, 15.0), - sphere(0.0, 0.0, -9.0, 5.0), - sphere(0.0, 0.0, 11.0, 3.0) - ].into_iter().chain( - (1..=6).map( - |k| { - let ang = (k as f64) * PI/3.0; - sphere(9.0 * ang.cos(), 9.0 * ang.sin(), 0.0, 2.5) - } - ) - ).collect::>().as_slice() - ); - let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k)); + // solve Irisawa's problem const SCALED_TOL: f64 = 1.0e-12; - let (config, success, history) = realize_gram( - &gram, guess, &frozen, - SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 - ); + let (config, _, _) = realize_irisawa_hexlet(SCALED_TOL); + + // check against Irisawa's solution let entry_tol = SCALED_TOL.sqrt(); let solution_diams = [30.0, 10.0, 6.0, 5.0, 15.0, 10.0, 3.75, 2.5, 2.0 + 8.0/11.0]; for (k, diam) in solution_diams.into_iter().enumerate() { assert!((config[(3, k)] - 1.0 / diam).abs() < entry_tol); } - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - if success { - println!("\nChain diameters:"); - println!(" {} sun (given)", 1.0 / config[(3, 3)]); - for k in 4..9 { - println!(" {} sun", 1.0 / config[(3, k)]); - } - } - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); - } } - // --- process inspection examples --- - - // these tests are meant for human inspection, not automated use. run them - // one at a time in `--nocapture` mode and read through the results and - // optimization histories that they print out. the `run-examples` script - // will run all of them - - #[test] - fn three_spheres_example() { - let gram = PartialMatrix({ - let mut entries = Vec::::new(); - for j in 0..3 { - for k in 0..3 { - entries.push(MatrixEntry { - index: (j, k), - value: if j == k { 1.0 } else { -1.0 } - }); - } - } - entries - }); - let guess = { - let a: f64 = 0.75_f64.sqrt(); - DMatrix::from_columns(&[ - sphere(1.0, 0.0, 0.0, 1.0), - sphere(-0.5, a, 0.0, 1.0), - sphere(-0.5, -a, 0.0, 1.0) - ]) - }; - println!(); - let (config, success, history) = realize_gram( - &gram, guess, &[], - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 - ); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); - } - } - - #[test] - fn point_on_sphere_example() { - let gram = PartialMatrix({ - let mut entries = Vec::::new(); - for j in 0..2 { - for k in 0..2 { - entries.push(MatrixEntry { - index: (j, k), - value: if (j, k) == (1, 1) { 1.0 } else { 0.0 } - }); - } - } - entries - }); - let guess = DMatrix::from_columns(&[ - point(0.0, 0.0, 2.0), - sphere(0.0, 0.0, 0.0, 1.0) - ]); - let frozen = [(3, 0)]; - println!(); - let (config, success, history) = realize_gram( - &gram, guess, &frozen, - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 - ); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - print!("Configuration:{}", config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); - } - } - - /* TO DO */ - // --- new test placed here to avoid merge conflict --- - // at the frozen indices, the optimization steps should have exact zeros, // and the realized configuration should match the initial guess #[test] diff --git a/app-proto/src/lib.rs b/app-proto/src/lib.rs new file mode 100644 index 0000000..0d9bc4a --- /dev/null +++ b/app-proto/src/lib.rs @@ -0,0 +1 @@ +pub mod engine; \ No newline at end of file From b490c8707fd506c5d819692976bcbccd17127a9c Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Wed, 27 Nov 2024 05:02:06 +0000 Subject: [PATCH 116/132] Click the display to select spheres (#25) On the incoming branch, you can select a sphere by clicking it in the display. Holding *shift* while clicking enables multiple selection. These controls match the ones already implemented in the outline view. Since the selection routine is now used in multiple places, the incoming branch factors it out into the `AppState::select` method. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/25 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.toml | 1 + app-proto/src/assembly.rs | 45 ++++++++++++++++++++++++++++++- app-proto/src/display.rs | 57 ++++++++++++++++++++++++++++++++++++--- app-proto/src/main.rs | 18 +++++++++++++ app-proto/src/outline.rs | 13 +-------- 5 files changed, 118 insertions(+), 16 deletions(-) diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 38205a7..c11fef4 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -26,6 +26,7 @@ console_error_panic_hook = { version = "0.1.7", optional = true } [dependencies.web-sys] version = "0.3.69" features = [ + 'DomRect', 'HtmlCanvasElement', 'HtmlInputElement', 'Performance', diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 59cba41..fb5bbf7 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,4 +1,4 @@ -use nalgebra::{DMatrix, DVector}; +use nalgebra::{DMatrix, DVector, Vector3}; use rustc_hash::FxHashMap; use slab::Slab; use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; @@ -65,6 +65,49 @@ impl Element { column_index: 0 } } + + // the smallest positive depth, represented as a multiple of `dir`, where + // the line generated by `dir` hits the element (which is assumed to be a + // sphere). returns `None` if the line misses the sphere. this function + // should be kept synchronized with `sphere_cast` in `inversive.frag`, which + // does essentially the same thing on the GPU side + pub fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix) -> Option { + // if `a/b` is less than this threshold, we approximate + // `a*u^2 + b*u + c` by the linear function `b*u + c` + const DEG_THRESHOLD: f64 = 1e-9; + + let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); + let a = -rep[3] * dir.norm_squared(); + let b = rep.rows_range(..3).dot(&dir); + let c = -rep[4]; + + let adjust = 4.0*a*c/(b*b); + if adjust < 1.0 { + // as long as `b` is non-zero, the linear approximation of + // + // a*u^2 + b*u + c + // + // at `u = 0` will reach zero at a finite depth `u_lin`. the root of + // the quadratic adjacent to `u_lin` is stored in `lin_root`. if + // both roots have the same sign, `lin_root` will be the one closer + // to `u = 0` + let square_rect_ratio = 1.0 + (1.0 - adjust).sqrt(); + let lin_root = -(2.0*c)/b / square_rect_ratio; + if a.abs() > DEG_THRESHOLD * b.abs() { + if lin_root > 0.0 { + Some(lin_root) + } else { + let other_root = -b/(2.*a) * square_rect_ratio; + (other_root > 0.0).then_some(other_root) + } + } else { + (lin_root > 0.0).then_some(lin_root) + } + } else { + // the line through `dir` misses the sphere completely + None + } + } } diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index ee0af47..c39e575 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -4,7 +4,9 @@ use sycamore::{prelude::*, motion::create_raf}; use web_sys::{ console, window, + Element, KeyboardEvent, + MouseEvent, WebGl2RenderingContext, WebGlProgram, WebGlShader, @@ -12,7 +14,7 @@ use web_sys::{ wasm_bindgen::{JsCast, JsValue} }; -use crate::AppState; +use crate::{AppState, assembly::ElementKey}; fn compile_shader( context: &WebGl2RenderingContext, @@ -82,6 +84,24 @@ fn bind_vertex_attrib( ); } +// the direction in camera space that a mouse event is pointing along +fn event_dir(event: &MouseEvent) -> Vector3 { + let target: Element = event.target().unwrap().unchecked_into(); + let rect = target.get_bounding_client_rect(); + let width = rect.width(); + let height = rect.height(); + let shortdim = width.min(height); + + // this constant should be kept synchronized with `inversive.frag` + const FOCAL_SLOPE: f64 = 0.3; + + Vector3::new( + FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim, + FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim, + -1.0 + ) +} + #[component] pub fn Display() -> View { let state = use_context::(); @@ -89,6 +109,9 @@ pub fn Display() -> View { // canvas let display = create_node_ref(); + // viewpoint + let assembly_to_world = create_signal(DMatrix::::identity(5, 5)); + // navigation let pitch_up = create_signal(0.0); let pitch_down = create_signal(0.0); @@ -296,7 +319,7 @@ pub fn Display() -> View { 0.0, 0.0, 0.0, 0.0, 1.0 ]) }; - let assembly_to_world = &location * &orientation; + let asm_to_world = &location * &orientation; // get the assembly let ( @@ -311,7 +334,7 @@ pub fn Display() -> View { // representation vectors in world coordinates elts.iter().map( - |(_, elt)| elt.representation.with(|rep| &assembly_to_world * rep) + |(_, elt)| elt.representation.with(|rep| &asm_to_world * rep) ).collect::>(), // colors @@ -370,6 +393,9 @@ pub fn Display() -> View { // draw the scene ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); + // update the viewpoint + assembly_to_world.set(asm_to_world); + // clear the scene change flag scene_changed.set( pitch_up_val != 0.0 @@ -458,6 +484,31 @@ pub fn Display() -> View { yaw_left.set(0.0); roll_ccw.set(0.0); roll_cw.set(0.0); + }, + on:click=move |event: MouseEvent| { + // find the nearest element along the pointer direction + let dir = event_dir(&event); + console::log_1(&JsValue::from(dir.to_string())); + let mut clicked: Option<(ElementKey, f64)> = None; + for (key, elt) in state.assembly.elements.get_clone_untracked() { + match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world)) { + Some(depth) => match clicked { + Some((_, best_depth)) => { + if depth < best_depth { + clicked = Some((key, depth)) + } + }, + None => clicked = Some((key, depth)) + } + None => () + }; + } + + // if we clicked something, select it + match clicked { + Some((key, _)) => state.select(key, event.shift_key()), + None => state.selection.update(|sel| sel.clear()) + }; } ) } diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index 897f9d4..8a012d3 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -25,6 +25,24 @@ impl AppState { selection: create_signal(FxHashSet::default()) } } + + // in single-selection mode, select the element with the given key. in + // multiple-selection mode, toggle whether the element with the given key + // is selected + fn select(&self, key: ElementKey, multi: bool) { + if multi { + self.selection.update(|sel| { + if !sel.remove(&key) { + sel.insert(key); + } + }); + } else { + self.selection.update(|sel| { + sel.clear(); + sel.insert(key); + }); + } + } } fn main() { diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index e2cf49c..148f870 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -83,18 +83,7 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { move |event: KeyboardEvent| { match event.key().as_str() { "Enter" => { - if event.shift_key() { - state.selection.update(|sel| { - if !sel.remove(&key) { - sel.insert(key); - } - }); - } else { - state.selection.update(|sel| { - sel.clear(); - sel.insert(key); - }); - } + state.select(key, event.shift_key()); event.prevent_default(); }, "ArrowRight" if constrained.get() => { From 22870342f3370197fce91aa47ece13558a1b5dee Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 30 Dec 2024 22:53:07 +0000 Subject: [PATCH 117/132] Manipulate the assembly (#29) feat: Find tangent space of solution variety, use for perturbations ### Tangent space #### Implementation The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`. At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution. After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option` and enforce the following invariants: 1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices. 2. If an element is affected by a constraint, it has a column index. The comments in `assembly.rs` state the invariants and describe how they're enforced. #### Automated testing The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis. #### Limitations The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually. ### Deformation #### Implementation The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk. For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop. The function `Assembly::deform` works like this: 1. Take a list of element motions 2. Project them onto the tangent space of the solution variety 3. Sum them to get a deformation $v$ of the whole assembly 4. Step the assembly along the "mass shell" geodesic tangent to $v$ * This step stays on the solution variety to first order 5. Call `realize` to bring the assembly back onto the solution variety #### Manual testing To manipulate the assembly: 1. Select a sphere 2. Make sure the display has focus 3. Hold the following keys: * **A**/**D** for $x$ translation * **W**/**S** for $y$ translation * **shift**+**W**/**S** for $z$ translation #### Limitations Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus. Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere. When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console. During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]* Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/examples/irisawa-hexlet.rs | 2 +- app-proto/examples/point-on-sphere.rs | 2 +- app-proto/examples/three-spheres.rs | 2 +- app-proto/src/assembly.rs | 141 +++++++++++++++++++++-- app-proto/src/display.rs | 98 +++++++++++++++- app-proto/src/engine.rs | 159 +++++++++++++++++++++++--- app-proto/src/main.rs | 4 + 7 files changed, 374 insertions(+), 34 deletions(-) diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs index fc14f91..2bc94c0 100644 --- a/app-proto/examples/irisawa-hexlet.rs +++ b/app-proto/examples/irisawa-hexlet.rs @@ -2,7 +2,7 @@ use dyna3::engine::{Q, irisawa::realize_irisawa_hexlet}; fn main() { const SCALED_TOL: f64 = 1.0e-12; - let (config, success, history) = realize_irisawa_hexlet(SCALED_TOL); + let (config, _, success, history) = realize_irisawa_hexlet(SCALED_TOL); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); if success { println!("Target accuracy achieved!"); diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs index 0e4765a..13040e5 100644 --- a/app-proto/examples/point-on-sphere.rs +++ b/app-proto/examples/point-on-sphere.rs @@ -18,7 +18,7 @@ fn main() { ]); let frozen = [(3, 0)]; println!(); - let (config, success, history) = realize_gram( + let (config, _, success, history) = realize_gram( &gram, guess, &frozen, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs index d348b18..19acfd1 100644 --- a/app-proto/examples/three-spheres.rs +++ b/app-proto/examples/three-spheres.rs @@ -21,7 +21,7 @@ fn main() { ]) }; println!(); - let (config, success, history) = realize_gram( + let (config, _, success, history) = realize_gram( &gram, guess, &[], 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index fb5bbf7..278a8f9 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,11 +1,11 @@ -use nalgebra::{DMatrix, DVector, Vector3}; +use nalgebra::{DMatrix, DVector, DVectorView, Vector3}; use rustc_hash::FxHashMap; use slab::Slab; use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ -use crate::engine::{realize_gram, PartialMatrix}; +use crate::engine::{realize_gram, ConfigSubspace, PartialMatrix, Q}; // the types of the keys we use to access an assembly's elements and constraints pub type ElementKey = usize; @@ -33,8 +33,9 @@ pub struct Element { pub serial: u64, // the configuration matrix column index that was assigned to this element - // last time the assembly was realized - column_index: usize + // last time the assembly was realized, or `None` if the element has never + // been through a realization + column_index: Option } impl Element { @@ -62,7 +63,7 @@ impl Element { representation: create_signal(representation), constraints: create_signal(BTreeSet::default()), serial: serial, - column_index: 0 + column_index: None } } @@ -109,7 +110,6 @@ impl Element { } } } - #[derive(Clone)] pub struct Constraint { @@ -120,6 +120,13 @@ pub struct Constraint { pub active: Signal } +pub struct ElementMotion<'a> { + pub key: ElementKey, + pub velocity: DVectorView<'a, f64> +} + +type AssemblyMotion<'a> = Vec>; + // a complete, view-independent description of an assembly #[derive(Clone)] pub struct Assembly { @@ -127,6 +134,18 @@ pub struct Assembly { pub elements: Signal>, pub constraints: Signal>, + // solution variety tangent space. the basis vectors are stored in + // configuration matrix format, ordered according to the elements' column + // indices. when you realize the assembly, every element that's present + // during realization gets a column index and is reflected in the tangent + // space. since the methods in this module never assign column indices + // without later realizing the assembly, we get the following invariant: + // + // (1) if an element has a column index, its tangent motions can be found + // in that column of the tangent space basis matrices + // + pub tangent: Signal, + // indexing pub elements_by_id: Signal> } @@ -136,6 +155,7 @@ impl Assembly { Assembly { elements: create_signal(Slab::new()), constraints: create_signal(Slab::new()), + tangent: create_signal(ConfigSubspace::zero(0)), elements_by_id: create_signal(FxHashMap::default()) } } @@ -199,7 +219,7 @@ impl Assembly { // index the elements self.elements.update_silent(|elts| { for (index, (_, elt)) in elts.into_iter().enumerate() { - elt.column_index = index; + elt.column_index = Some(index); } }); @@ -211,8 +231,8 @@ impl Assembly { for (_, cst) in csts { if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() { let subjects = cst.subjects; - let row = elts[subjects.0].column_index; - let col = elts[subjects.1].column_index; + let row = elts[subjects.0].column_index.unwrap(); + let col = elts[subjects.1].column_index.unwrap(); gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked()); } } @@ -222,7 +242,7 @@ impl Assembly { // Gram matrix let mut guess_to_be = DMatrix::::zeros(5, elts.len()); for (_, elt) in elts { - let index = elt.column_index; + let index = elt.column_index.unwrap(); gram_to_be.push_sym(index, index, 1.0); guess_to_be.set_column(index, &elt.representation.get_clone_untracked()); } @@ -247,7 +267,7 @@ impl Assembly { } // look for a configuration with the given Gram matrix - let (config, success, history) = realize_gram( + let (config, tangent, success, history) = realize_gram( &gram, guess, &[], 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); @@ -263,14 +283,111 @@ impl Assembly { )); console::log_2(&JsValue::from("Steps:"), &JsValue::from(history.scaled_loss.len() - 1)); console::log_2(&JsValue::from("Loss:"), &JsValue::from(*history.scaled_loss.last().unwrap())); + console::log_2(&JsValue::from("Tangent dimension:"), &JsValue::from(tangent.dim())); if success { // read out the solution for (_, elt) in self.elements.get_clone_untracked() { elt.representation.update( - |rep| rep.set_column(0, &config.column(elt.column_index)) + |rep| rep.set_column(0, &config.column(elt.column_index.unwrap())) ); } + + // save the tangent space + self.tangent.set_silent(tangent); } } + + // --- deformation --- + + // project the given motion to the tangent space of the solution variety and + // move the assembly along it. the implementation is based on invariant (1) + // from above and the following additional invariant: + // + // (2) if an element is affected by a constraint, it has a column index + // + // we have this invariant because the assembly gets realized each time you + // add a constraint + pub fn deform(&self, motion: AssemblyMotion) { + /* KLUDGE */ + // when the tangent space is zero, deformation won't do anything, but + // the attempt to deform should be registered in the UI. this console + // message will do for now + if self.tangent.with(|tan| tan.dim() <= 0 && tan.assembly_dim() > 0) { + console::log_1(&JsValue::from("The assembly is rigid")); + } + + // give a column index to each moving element that doesn't have one yet. + // this temporarily breaks invariant (1), but the invariant will be + // restored when we realize the assembly at the end of the deformation. + // in the process, we find out how many matrix columns we'll need to + // hold the deformation + let realized_dim = self.tangent.with(|tan| tan.assembly_dim()); + let motion_dim = self.elements.update_silent(|elts| { + let mut next_column_index = realized_dim; + for elt_motion in motion.iter() { + let moving_elt = &mut elts[elt_motion.key]; + if moving_elt.column_index.is_none() { + moving_elt.column_index = Some(next_column_index); + next_column_index += 1; + } + } + next_column_index + }); + + // project the element motions onto the tangent space of the solution + // variety and sum them to get a deformation of the whole assembly. the + // matrix `motion_proj` that holds the deformation has extra columns for + // any moving elements that aren't reflected in the saved tangent space + const ELEMENT_DIM: usize = 5; + let mut motion_proj = DMatrix::zeros(ELEMENT_DIM, motion_dim); + for elt_motion in motion { + // we can unwrap the column index because we know that every moving + // element has one at this point + let column_index = self.elements.with_untracked( + |elts| elts[elt_motion.key].column_index.unwrap() + ); + + if column_index < realized_dim { + // this element had a column index when we started, so by + // invariant (1), it's reflected in the tangent space + let mut target_columns = motion_proj.columns_mut(0, realized_dim); + target_columns += self.tangent.with( + |tan| tan.proj(&elt_motion.velocity, column_index) + ); + } else { + // this element didn't have a column index when we started, so + // by invariant (2), it's unconstrained + let mut target_column = motion_proj.column_mut(column_index); + target_column += elt_motion.velocity; + } + } + + // step each element along the mass shell geodesic that matches its + // velocity in the deformation found above + /* KLUDGE */ + // since our test assemblies only include spheres, we assume that every + // element is on the 1 mass shell + for (_, elt) in self.elements.get_clone_untracked() { + elt.representation.update_silent(|rep| { + match elt.column_index { + Some(column_index) => { + let rep_next = &*rep + motion_proj.column(column_index); + let normalizer = rep_next.dot(&(&*Q * &rep_next)); + rep.set_column(0, &(rep_next / normalizer)); + }, + None => { + console::log_1(&JsValue::from( + format!("No velocity to unpack for fresh element \"{}\"", elt.id) + )) + } + }; + }); + } + + // bring the configuration back onto the solution variety. this also + // gets the elements' column indices and the saved tangent space back in + // sync + self.realize(); + } } \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index c39e575..d8ee23c 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -1,5 +1,5 @@ use core::array; -use nalgebra::{DMatrix, Rotation3, Vector3}; +use nalgebra::{DMatrix, DVector, Rotation3, Vector3}; use sycamore::{prelude::*, motion::create_raf}; use web_sys::{ console, @@ -14,7 +14,7 @@ use web_sys::{ wasm_bindgen::{JsCast, JsValue} }; -use crate::{AppState, assembly::ElementKey}; +use crate::{AppState, assembly::{ElementKey, ElementMotion}}; fn compile_shader( context: &WebGl2RenderingContext, @@ -123,6 +123,14 @@ pub fn Display() -> View { let zoom_out = create_signal(0.0); let turntable = create_signal(false); /* BENCHMARKING */ + // manipulation + let translate_neg_x = create_signal(0.0); + let translate_pos_x = create_signal(0.0); + let translate_neg_y = create_signal(0.0); + let translate_pos_y = create_signal(0.0); + let translate_neg_z = create_signal(0.0); + let translate_pos_z = create_signal(0.0); + // change listener let scene_changed = create_signal(true); create_effect(move || { @@ -141,6 +149,7 @@ pub fn Display() -> View { let mut frames_since_last_sample = 0; let mean_frame_interval = create_signal(0.0); + let assembly_for_raf = state.assembly.clone(); on_mount(move || { // timing let mut last_time = 0.0; @@ -153,6 +162,9 @@ pub fn Display() -> View { let mut rotation = DMatrix::::identity(5, 5); let mut location_z: f64 = 5.0; + // manipulation + const TRANSLATION_SPEED: f64 = 0.15; // in length units per second + // display parameters const OPACITY: f32 = 0.5; /* SCAFFOLDING */ const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */ @@ -273,6 +285,14 @@ pub fn Display() -> View { let zoom_out_val = zoom_out.get(); let turntable_val = turntable.get(); /* BENCHMARKING */ + // get the manipulation state + let translate_neg_x_val = translate_neg_x.get(); + let translate_pos_x_val = translate_pos_x.get(); + let translate_neg_y_val = translate_neg_y.get(); + let translate_pos_y_val = translate_pos_y.get(); + let translate_neg_z_val = translate_neg_z.get(); + let translate_pos_z_val = translate_pos_z.get(); + // update the assembly's orientation let ang_vel = { let pitch = pitch_up_val - pitch_down_val; @@ -298,6 +318,41 @@ pub fn Display() -> View { let zoom = zoom_out_val - zoom_in_val; location_z *= (time_step * ZOOM_SPEED * zoom).exp(); + // manipulate the assembly + if state.selection.with(|sel| sel.len() == 1) { + let sel = state.selection.with( + |sel| *sel.into_iter().next().unwrap() + ); + let rep = state.assembly.elements.with_untracked( + |elts| elts[sel].representation.get_clone_untracked() + ); + let translate_x = translate_pos_x_val - translate_neg_x_val; + let translate_y = translate_pos_y_val - translate_neg_y_val; + let translate_z = translate_pos_z_val - translate_neg_z_val; + if translate_x != 0.0 || translate_y != 0.0 || translate_z != 0.0 { + let vel_field = { + let u = Vector3::new(translate_x, translate_y, translate_z).normalize(); + DMatrix::from_column_slice(5, 5, &[ + 0.0, 0.0, 0.0, 0.0, u[0], + 0.0, 0.0, 0.0, 0.0, u[1], + 0.0, 0.0, 0.0, 0.0, u[2], + 2.0*u[0], 2.0*u[1], 2.0*u[2], 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0 + ]) + }; + let elt_motion: DVector = time_step * TRANSLATION_SPEED * vel_field * rep; + assembly_for_raf.deform( + vec![ + ElementMotion { + key: sel, + velocity: elt_motion.as_view() + } + ] + ); + scene_changed.set(true); + } + } + if scene_changed.get() { /* INSTRUMENTS */ // measure mean frame interval @@ -416,7 +471,7 @@ pub fn Display() -> View { start_animation_loop(); }); - let set_nav_signal = move |event: KeyboardEvent, value: f64| { + let set_nav_signal = move |event: &KeyboardEvent, value: f64| { let mut navigating = true; let shift = event.shift_key(); match event.key().as_str() { @@ -436,6 +491,23 @@ pub fn Display() -> View { } }; + let set_manip_signal = move |event: &KeyboardEvent, value: f64| { + let mut manipulating = true; + let shift = event.shift_key(); + match event.key().as_str() { + "d" | "D" => translate_pos_x.set(value), + "a" | "A" => translate_neg_x.set(value), + "w" | "W" if shift => translate_neg_z.set(value), + "s" | "S" if shift => translate_pos_z.set(value), + "w" | "W" => translate_pos_y.set(value), + "s" | "S" => translate_neg_y.set(value), + _ => manipulating = false + }; + if manipulating { + event.prevent_default(); + } + }; + view! { /* TO DO */ // switch back to integer-valued parameters when that becomes possible @@ -447,6 +519,7 @@ pub fn Display() -> View { tabindex="0", on:keydown=move |event: KeyboardEvent| { if event.key() == "Shift" { + // swap navigation inputs roll_cw.set(yaw_right.get()); roll_ccw.set(yaw_left.get()); zoom_in.set(pitch_up.get()); @@ -455,16 +528,24 @@ pub fn Display() -> View { yaw_left.set(0.0); pitch_up.set(0.0); pitch_down.set(0.0); + + // swap manipulation inputs + translate_pos_z.set(translate_neg_y.get()); + translate_neg_z.set(translate_pos_y.get()); + translate_pos_y.set(0.0); + translate_neg_y.set(0.0); } else { if event.key() == "Enter" { /* BENCHMARKING */ turntable.set_fn(|turn| !turn); scene_changed.set(true); } - set_nav_signal(event, 1.0); + set_nav_signal(&event, 1.0); + set_manip_signal(&event, 1.0); } }, on:keyup=move |event: KeyboardEvent| { if event.key() == "Shift" { + // swap navigation inputs yaw_right.set(roll_cw.get()); yaw_left.set(roll_ccw.get()); pitch_up.set(zoom_in.get()); @@ -473,8 +554,15 @@ pub fn Display() -> View { roll_ccw.set(0.0); zoom_in.set(0.0); zoom_out.set(0.0); + + // swap manipulation inputs + translate_pos_y.set(translate_neg_z.get()); + translate_neg_y.set(translate_pos_z.get()); + translate_pos_z.set(0.0); + translate_neg_z.set(0.0); } else { - set_nav_signal(event, 0.0); + set_nav_signal(&event, 0.0); + set_manip_signal(&event, 0.0); } }, on:blur=move |_| { diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 285a9c6..6a0202e 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -1,5 +1,5 @@ use lazy_static::lazy_static; -use nalgebra::{Const, DMatrix, DVector, Dyn}; +use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen}; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ // --- elements --- @@ -85,6 +85,75 @@ impl PartialMatrix { } } +// --- configuration subspaces --- + +#[derive(Clone)] +pub struct ConfigSubspace { + assembly_dim: usize, + basis: Vec> +} + +impl ConfigSubspace { + pub fn zero(assembly_dim: usize) -> ConfigSubspace { + ConfigSubspace { + assembly_dim: assembly_dim, + basis: Vec::new() + } + } + + // approximate the kernel of a symmetric endomorphism of the configuration + // space for `assembly_dim` elements. we consider an eigenvector to be part + // of the kernel if its eigenvalue is smaller than the constant `THRESHOLD` + fn symmetric_kernel(a: DMatrix, assembly_dim: usize) -> ConfigSubspace { + const ELEMENT_DIM: usize = 5; + const THRESHOLD: f64 = 1.0e-4; + let eig = SymmetricEigen::new(a); + let eig_vecs = eig.eigenvectors.column_iter(); + let eig_pairs = eig.eigenvalues.iter().zip(eig_vecs); + let basis = eig_pairs.filter_map( + |(λ, v)| (λ.abs() < THRESHOLD).then_some( + Into::>::into( + v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim)) + ) + ) + ); + + /* DEBUG */ + // print the eigenvalues + #[cfg(all(target_family = "wasm", target_os = "unknown"))] + console::log_1(&JsValue::from( + format!("Eigenvalues used to find kernel:{}", eig.eigenvalues) + )); + + ConfigSubspace { + assembly_dim: assembly_dim, + basis: basis.collect() + } + } + + pub fn dim(&self) -> usize { + self.basis.len() + } + + pub fn assembly_dim(&self) -> usize { + self.assembly_dim + } + + // find the projection onto this subspace, with respect to the Euclidean + // inner product, of the motion where the element with the given column + // index has velocity `v` + pub fn proj(&self, v: &DVectorView, column_index: usize) -> DMatrix { + if self.dim() == 0 { + const ELEMENT_DIM: usize = 5; + DMatrix::zeros(ELEMENT_DIM, self.assembly_dim) + } else { + self.basis.iter().map( + |b| b.column(column_index).dot(&v) * b + ).sum() + } + } +} + // --- descent history --- pub struct DescentHistory { @@ -181,7 +250,7 @@ pub fn realize_gram( reg_scale: f64, max_descent_steps: i32, max_backoff_steps: i32 -) -> (DMatrix, bool, DescentHistory) { +) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { // start the descent history let mut history = DescentHistory::new(); @@ -201,12 +270,8 @@ pub fn realize_gram( // use Newton's method with backtracking and gradient descent backup let mut state = SearchState::from_config(gram, guess); + let mut hess = DMatrix::zeros(element_dim, assembly_dim); for _ in 0..max_descent_steps { - // stop if the loss is tolerably low - history.config.push(state.config.clone()); - history.scaled_loss.push(state.loss / scale_adjustment); - if state.loss < tol { break; } - // find the negative gradient of the loss function let neg_grad = 4.0 * &*Q * &state.config * &state.err_proj; let mut neg_grad_stacked = neg_grad.clone().reshape_generic(Dyn(total_dim), Const::<1>); @@ -229,7 +294,7 @@ pub fn realize_gram( hess_cols.push(deriv_grad.reshape_generic(Dyn(total_dim), Const::<1>)); } } - let mut hess = DMatrix::from_columns(hess_cols.as_slice()); + hess = DMatrix::from_columns(hess_cols.as_slice()); // regularize the Hessian let min_eigval = hess.symmetric_eigenvalues().min(); @@ -249,6 +314,11 @@ pub fn realize_gram( hess[(k, k)] = 1.0; } + // stop if the loss is tolerably low + history.config.push(state.config.clone()); + history.scaled_loss.push(state.loss / scale_adjustment); + if state.loss < tol { break; } + // compute the Newton step /* we need to either handle or eliminate the case where the minimum @@ -256,7 +326,7 @@ pub fn realize_gram( singular. right now, this causes the Cholesky decomposition to return `None`, leading to a panic when we unrap */ - let base_step_stacked = hess.cholesky().unwrap().solve(&neg_grad_stacked); + let base_step_stacked = hess.clone().cholesky().unwrap().solve(&neg_grad_stacked); let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim)); history.base_step.push(base_step.clone()); @@ -269,10 +339,16 @@ pub fn realize_gram( state = better_state; history.backoff_steps.push(backoff_steps); }, - None => return (state.config, false, history) + None => return (state.config, ConfigSubspace::zero(assembly_dim), false, history) }; } - (state.config, state.loss < tol, history) + let success = state.loss < tol; + let tangent = if success { + ConfigSubspace::symmetric_kernel(hess, assembly_dim) + } else { + ConfigSubspace::zero(assembly_dim) + }; + (state.config, tangent, success, history) } // --- tests --- @@ -291,7 +367,7 @@ pub mod irisawa { use super::*; - pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, bool, DescentHistory) { + pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { let gram = { let mut gram_to_be = PartialMatrix::new(); for s in 0..9 { @@ -399,7 +475,7 @@ mod tests { fn irisawa_hexlet_test() { // solve Irisawa's problem const SCALED_TOL: f64 = 1.0e-12; - let (config, _, _) = realize_irisawa_hexlet(SCALED_TOL); + let (config, _, _, _) = realize_irisawa_hexlet(SCALED_TOL); // check against Irisawa's solution let entry_tol = SCALED_TOL.sqrt(); @@ -409,6 +485,61 @@ mod tests { } } + #[test] + fn tangent_test() { + const SCALED_TOL: f64 = 1.0e-12; + const ELEMENT_DIM: usize = 5; + const ASSEMBLY_DIM: usize = 3; + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for j in 0..3 { + for k in j..3 { + gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); + } + } + gram_to_be + }; + let guess = DMatrix::from_columns(&[ + sphere(0.0, 0.0, 0.0, -2.0), + sphere(0.0, 0.0, 1.0, 1.0), + sphere(0.0, 0.0, -1.0, 1.0) + ]); + let frozen: [_; 5] = std::array::from_fn(|k| (k, 0)); + let (config, tangent, success, history) = realize_gram( + &gram, guess.clone(), &frozen, + SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + ); + assert_eq!(config, guess); + assert_eq!(success, true); + assert_eq!(history.scaled_loss.len(), 1); + + // confirm that the tangent space has dimension five or less + let ConfigSubspace(ref tangent_basis) = tangent; + assert_eq!(tangent_basis.len(), 5); + + // confirm that the tangent space contains all the motions we expect it + // to. since we've already bounded the dimension of the tangent space, + // this confirms that the tangent space is what we expect it to be + let tangent_motions = vec![ + basis_matrix((0, 1), ELEMENT_DIM, ASSEMBLY_DIM), + basis_matrix((1, 1), ELEMENT_DIM, ASSEMBLY_DIM), + basis_matrix((0, 2), ELEMENT_DIM, ASSEMBLY_DIM), + basis_matrix((1, 2), ELEMENT_DIM, ASSEMBLY_DIM), + DMatrix::::from_column_slice(ELEMENT_DIM, 3, &[ + 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, -1.0, -0.25, -1.0, + 0.0, 0.0, -1.0, 0.25, 1.0 + ]) + ]; + let tol_sq = ((ELEMENT_DIM * ASSEMBLY_DIM) as f64) * SCALED_TOL * SCALED_TOL; + for motion in tangent_motions { + let motion_proj: DMatrix<_> = motion.column_iter().enumerate().map( + |(k, v)| tangent.proj(&v, k) + ).sum(); + assert!((motion - motion_proj).norm_squared() < tol_sq); + } + } + // at the frozen indices, the optimization steps should have exact zeros, // and the realized configuration should match the initial guess #[test] @@ -428,7 +559,7 @@ mod tests { ]); let frozen = [(3, 0), (3, 1)]; println!(); - let (config, success, history) = realize_gram( + let (config, _, success, history) = realize_gram( &gram, guess.clone(), &frozen, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index 8a012d3..f961504 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -46,6 +46,10 @@ impl AppState { } fn main() { + // set the console error panic hook + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); + sycamore::render(|| { provide_context(AppState::new()); From 817a446fad0c7b04c9a43c5bafd09ba2a3e68483 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Fri, 31 Jan 2025 19:34:33 +0000 Subject: [PATCH 118/132] Switch to Euclidean-invariant projection onto tangent space of solution variety (#34) This pull request addresses issues #32 and #33 by projecting nudges onto the tangent space of the solution variety using a Euclidean-invariant inner product, which I'm calling the *uniform* inner product. ### Definition of the uniform inner product For spheres and planes, the uniform inner product is defined on the tangent space of the hyperboloid $\langle v, v \rangle = 1$. For points, it's defined on the tangent space of the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$. The tangent space of an assembly can be expressed as the direct sum of the tangent spaces of the elements. We extend the uniform inner product to assemblies by declaring the tangent spaces of different elements to be orthogonal. #### For spheres and planes If $v = [x, y, z, b, c]^\top$ is on the hyperboloid $\langle v, v \rangle = 1$, the vectors $$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right],\;\left[ \begin{array}{l} 2bx \\ 2by \\ 2bz \\ 2b^2 \\ 2bc + 1 \end{array} \right]$$ form a basis for the tangent space of hyperboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product. The first three vectors in the basis are unit-speed translations along the coordinate axes. The last vector moves the surface at unit speed along its normal field. For spheres, this increases the radius at unit rate. For planes, this translates the plane parallel to itself at unit speed. This description makes it clear that the uniform inner product is invariant under Euclidean motions. #### For points If $v = [x, y, z, b, c]^\top$ is on the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$, the vectors $$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right]$$ form a basis for the tangent space of paraboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product. The meanings of the basis vectors, and the argument that the uniform inner product is Euclidean-invariant, are the same as for spheres and planes. In the engine, we pad the basis with $[0, 0, 0, 0, 1]^\top$ to keep the number of uniform coordinates consistent across element types. ### Confirmation of intended behavior Two new tests confirm that we've corrected the misbehaviors described in issues #32 and #33. Issue | Test ---|--- #32 | `proj_equivar_test` #33 | `tangent_test_kaleidocycle` Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/34 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/examples/kaleidocycle.rs | 72 ++++++ app-proto/run-examples | 1 + app-proto/src/assembly.rs | 12 +- app-proto/src/display.rs | 38 +-- app-proto/src/engine.rs | 363 ++++++++++++++++++++++++++--- 5 files changed, 432 insertions(+), 54 deletions(-) create mode 100644 app-proto/examples/kaleidocycle.rs diff --git a/app-proto/examples/kaleidocycle.rs b/app-proto/examples/kaleidocycle.rs new file mode 100644 index 0000000..88116d3 --- /dev/null +++ b/app-proto/examples/kaleidocycle.rs @@ -0,0 +1,72 @@ +use nalgebra::{DMatrix, DVector}; +use std::{array, f64::consts::PI}; + +use dyna3::engine::{Q, point, realize_gram, PartialMatrix}; + +fn main() { + // set up a kaleidocycle, made of points with fixed distances between them, + // and find its tangent space + const N_POINTS: usize = 12; + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for block in (0..N_POINTS).step_by(2) { + let block_next = (block + 2) % N_POINTS; + for j in 0..2 { + // diagonal and hinge edges + for k in j..2 { + gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); + } + + // non-hinge edges + for k in 0..2 { + gram_to_be.push_sym(block + j, block_next + k, -0.625); + } + } + } + gram_to_be + }; + let guess = { + const N_HINGES: usize = 6; + let guess_elts = (0..N_HINGES).step_by(2).flat_map( + |n| { + let ang_hor = (n as f64) * PI/3.0; + let ang_vert = ((n + 1) as f64) * PI/3.0; + let x_vert = ang_vert.cos(); + let y_vert = ang_vert.sin(); + [ + point(0.0, 0.0, 0.0), + point(ang_hor.cos(), ang_hor.sin(), 0.0), + point(x_vert, y_vert, -0.5), + point(x_vert, y_vert, 0.5) + ] + } + ).collect::>(); + DMatrix::from_columns(&guess_elts) + }; + let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k)); + let (config, tangent, success, history) = realize_gram( + &gram, guess, &frozen, + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + print!("Completed Gram matrix:{}", config.tr_mul(&*Q) * &config); + print!("Configuration:{}", config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}\n", history.scaled_loss.last().unwrap()); + + // find the kaleidocycle's twist motion + let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); + let down = -&up; + let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( + |n| [ + tangent.proj(&up.as_view(), n), + tangent.proj(&down.as_view(), n+1) + ] + ).sum(); + let normalization = 5.0 / twist_motion[(2, 0)]; + print!("Twist motion:{}", normalization * twist_motion); +} \ No newline at end of file diff --git a/app-proto/run-examples b/app-proto/run-examples index bc7e933..52173b0 100755 --- a/app-proto/run-examples +++ b/app-proto/run-examples @@ -9,3 +9,4 @@ cargo run --example irisawa-hexlet cargo run --example three-spheres cargo run --example point-on-sphere +cargo run --example kaleidocycle \ No newline at end of file diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 278a8f9..9b0e065 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -5,7 +5,7 @@ use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ -use crate::engine::{realize_gram, ConfigSubspace, PartialMatrix, Q}; +use crate::engine::{realize_gram, local_unif_to_std, ConfigSubspace, PartialMatrix, Q}; // the types of the keys we use to access an assembly's elements and constraints pub type ElementKey = usize; @@ -120,6 +120,7 @@ pub struct Constraint { pub active: Signal } +// the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { pub key: ElementKey, pub velocity: DVectorView<'a, f64> @@ -359,7 +360,14 @@ impl Assembly { // this element didn't have a column index when we started, so // by invariant (2), it's unconstrained let mut target_column = motion_proj.column_mut(column_index); - target_column += elt_motion.velocity; + let unif_to_std = self.elements.with_untracked( + |elts| { + elts[elt_motion.key].representation.with_untracked( + |rep| local_unif_to_std(rep.as_view()) + ) + } + ); + target_column += unif_to_std * elt_motion.velocity; } } diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index d8ee23c..4e0c7e4 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -130,6 +130,8 @@ pub fn Display() -> View { let translate_pos_y = create_signal(0.0); let translate_neg_z = create_signal(0.0); let translate_pos_z = create_signal(0.0); + let shrink_neg = create_signal(0.0); + let shrink_pos = create_signal(0.0); // change listener let scene_changed = create_signal(true); @@ -164,6 +166,7 @@ pub fn Display() -> View { // manipulation const TRANSLATION_SPEED: f64 = 0.15; // in length units per second + const SHRINKING_SPEED: f64 = 0.15; // in length units per second // display parameters const OPACITY: f32 = 0.5; /* SCAFFOLDING */ @@ -292,6 +295,8 @@ pub fn Display() -> View { let translate_pos_y_val = translate_pos_y.get(); let translate_neg_z_val = translate_neg_z.get(); let translate_pos_z_val = translate_pos_z.get(); + let shrink_neg_val = shrink_neg.get(); + let shrink_pos_val = shrink_pos.get(); // update the assembly's orientation let ang_vel = { @@ -323,24 +328,27 @@ pub fn Display() -> View { let sel = state.selection.with( |sel| *sel.into_iter().next().unwrap() ); - let rep = state.assembly.elements.with_untracked( - |elts| elts[sel].representation.get_clone_untracked() - ); let translate_x = translate_pos_x_val - translate_neg_x_val; let translate_y = translate_pos_y_val - translate_neg_y_val; let translate_z = translate_pos_z_val - translate_neg_z_val; - if translate_x != 0.0 || translate_y != 0.0 || translate_z != 0.0 { - let vel_field = { - let u = Vector3::new(translate_x, translate_y, translate_z).normalize(); - DMatrix::from_column_slice(5, 5, &[ - 0.0, 0.0, 0.0, 0.0, u[0], - 0.0, 0.0, 0.0, 0.0, u[1], - 0.0, 0.0, 0.0, 0.0, u[2], - 2.0*u[0], 2.0*u[1], 2.0*u[2], 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0 - ]) + let shrink = shrink_pos_val - shrink_neg_val; + let translating = + translate_x != 0.0 + || translate_y != 0.0 + || translate_z != 0.0; + if translating || shrink != 0.0 { + let elt_motion = { + let u = if translating { + TRANSLATION_SPEED * Vector3::new( + translate_x, translate_y, translate_z + ).normalize() + } else { + Vector3::zeros() + }; + time_step * DVector::from_column_slice( + &[u[0], u[1], u[2], SHRINKING_SPEED * shrink] + ) }; - let elt_motion: DVector = time_step * TRANSLATION_SPEED * vel_field * rep; assembly_for_raf.deform( vec![ ElementMotion { @@ -501,6 +509,8 @@ pub fn Display() -> View { "s" | "S" if shift => translate_pos_z.set(value), "w" | "W" => translate_pos_y.set(value), "s" | "S" => translate_neg_y.set(value), + "]" | "}" => shrink_neg.set(value), + "[" | "{" => shrink_pos.set(value), _ => manipulating = false }; if manipulating { diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 6a0202e..eee19ac 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -90,32 +90,34 @@ impl PartialMatrix { #[derive(Clone)] pub struct ConfigSubspace { assembly_dim: usize, - basis: Vec> + basis_std: Vec>, + basis_proj: Vec> } impl ConfigSubspace { pub fn zero(assembly_dim: usize) -> ConfigSubspace { ConfigSubspace { assembly_dim: assembly_dim, - basis: Vec::new() + basis_proj: Vec::new(), + basis_std: Vec::new() } } // approximate the kernel of a symmetric endomorphism of the configuration // space for `assembly_dim` elements. we consider an eigenvector to be part // of the kernel if its eigenvalue is smaller than the constant `THRESHOLD` - fn symmetric_kernel(a: DMatrix, assembly_dim: usize) -> ConfigSubspace { - const ELEMENT_DIM: usize = 5; - const THRESHOLD: f64 = 1.0e-4; - let eig = SymmetricEigen::new(a); + fn symmetric_kernel(a: DMatrix, proj_to_std: DMatrix, assembly_dim: usize) -> ConfigSubspace { + // find a basis for the kernel. the basis is expressed in the projection + // coordinates, and it's orthonormal with respect to the projection + // inner product + const THRESHOLD: f64 = 0.1; + let eig = SymmetricEigen::new(proj_to_std.tr_mul(&a) * &proj_to_std); let eig_vecs = eig.eigenvectors.column_iter(); let eig_pairs = eig.eigenvalues.iter().zip(eig_vecs); - let basis = eig_pairs.filter_map( - |(λ, v)| (λ.abs() < THRESHOLD).then_some( - Into::>::into( - v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim)) - ) - ) + let basis_proj = DMatrix::from_columns( + eig_pairs.filter_map( + |(λ, v)| (λ.abs() < THRESHOLD).then_some(v) + ).collect::>().as_slice() ); /* DEBUG */ @@ -125,30 +127,45 @@ impl ConfigSubspace { format!("Eigenvalues used to find kernel:{}", eig.eigenvalues) )); + // express the basis in the standard coordinates + let basis_std = proj_to_std * &basis_proj; + + const ELEMENT_DIM: usize = 5; + const UNIFORM_DIM: usize = 4; ConfigSubspace { assembly_dim: assembly_dim, - basis: basis.collect() + basis_std: basis_std.column_iter().map( + |v| Into::>::into( + v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim)) + ) + ).collect(), + basis_proj: basis_proj.column_iter().map( + |v| Into::>::into( + v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim)) + ) + ).collect() } } pub fn dim(&self) -> usize { - self.basis.len() + self.basis_std.len() } pub fn assembly_dim(&self) -> usize { self.assembly_dim } - // find the projection onto this subspace, with respect to the Euclidean - // inner product, of the motion where the element with the given column - // index has velocity `v` + // find the projection onto this subspace of the motion where the element + // with the given column index has velocity `v`. the velocity is given in + // projection coordinates, and the projection is done with respect to the + // projection inner product pub fn proj(&self, v: &DVectorView, column_index: usize) -> DMatrix { if self.dim() == 0 { const ELEMENT_DIM: usize = 5; DMatrix::zeros(ELEMENT_DIM, self.assembly_dim) } else { - self.basis.iter().map( - |b| b.column(column_index).dot(&v) * b + self.basis_proj.iter().zip(self.basis_std.iter()).map( + |(b_proj, b_std)| b_proj.column(column_index).dot(&v) * b_std ).sum() } } @@ -215,6 +232,37 @@ fn basis_matrix(index: (usize, usize), nrows: usize, ncols: usize) -> DMatrix) -> DMatrix { + const ELEMENT_DIM: usize = 5; + const UNIFORM_DIM: usize = 4; + let curv = 2.0*v[3]; + if v.dot(&(&*Q * v)) < 0.5 { + // `v` represents a point. the normalization condition says that the + // curvature component of `v` is 1/2 + DMatrix::from_column_slice(ELEMENT_DIM, UNIFORM_DIM, &[ + curv, 0.0, 0.0, 0.0, v[0], + 0.0, curv, 0.0, 0.0, v[1], + 0.0, 0.0, curv, 0.0, v[2], + 0.0, 0.0, 0.0, 0.0, 1.0 + ]) + } else { + // `v` represents a sphere. the normalization condition says that the + // Lorentz product of `v` with itself is 1 + DMatrix::from_column_slice(ELEMENT_DIM, UNIFORM_DIM, &[ + curv, 0.0, 0.0, 0.0, v[0], + 0.0, curv, 0.0, 0.0, v[1], + 0.0, 0.0, curv, 0.0, v[2], + curv*v[0], curv*v[1], curv*v[2], curv*v[3], curv*v[4] + 1.0 + ]) + } +} + // use backtracking line search to find a better configuration fn seek_better_config( gram: &PartialMatrix, @@ -344,7 +392,19 @@ pub fn realize_gram( } let success = state.loss < tol; let tangent = if success { - ConfigSubspace::symmetric_kernel(hess, assembly_dim) + // express the uniform basis in the standard basis + const UNIFORM_DIM: usize = 4; + let total_dim_unif = UNIFORM_DIM * assembly_dim; + let mut unif_to_std = DMatrix::::zeros(total_dim, total_dim_unif); + for n in 0..assembly_dim { + let block_start = (element_dim * n, UNIFORM_DIM * n); + unif_to_std + .view_mut(block_start, (element_dim, UNIFORM_DIM)) + .copy_from(&local_unif_to_std(state.config.column(n))); + } + + // find the kernel of the Hessian. give it the uniform inner product + ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim) } else { ConfigSubspace::zero(assembly_dim) }; @@ -424,6 +484,9 @@ pub mod irisawa { #[cfg(test)] mod tests { + use nalgebra::Vector3; + use std::{array, f64::consts::{FRAC_1_SQRT_2, PI}, iter}; + use super::{*, irisawa::realize_irisawa_hexlet}; #[test] @@ -486,10 +549,8 @@ mod tests { } #[test] - fn tangent_test() { + fn tangent_test_three_spheres() { const SCALED_TOL: f64 = 1.0e-12; - const ELEMENT_DIM: usize = 5; - const ASSEMBLY_DIM: usize = 3; let gram = { let mut gram_to_be = PartialMatrix::new(); for j in 0..3 { @@ -514,32 +575,258 @@ mod tests { assert_eq!(history.scaled_loss.len(), 1); // confirm that the tangent space has dimension five or less - let ConfigSubspace(ref tangent_basis) = tangent; - assert_eq!(tangent_basis.len(), 5); + assert_eq!(tangent.basis_std.len(), 5); // confirm that the tangent space contains all the motions we expect it // to. since we've already bounded the dimension of the tangent space, // this confirms that the tangent space is what we expect it to be - let tangent_motions = vec![ - basis_matrix((0, 1), ELEMENT_DIM, ASSEMBLY_DIM), - basis_matrix((1, 1), ELEMENT_DIM, ASSEMBLY_DIM), - basis_matrix((0, 2), ELEMENT_DIM, ASSEMBLY_DIM), - basis_matrix((1, 2), ELEMENT_DIM, ASSEMBLY_DIM), - DMatrix::::from_column_slice(ELEMENT_DIM, 3, &[ - 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, -1.0, -0.25, -1.0, - 0.0, 0.0, -1.0, 0.25, 1.0 + const UNIFORM_DIM: usize = 4; + let element_dim = guess.nrows(); + let assembly_dim = guess.ncols(); + let tangent_motions_unif = vec![ + basis_matrix((0, 1), UNIFORM_DIM, assembly_dim), + basis_matrix((1, 1), UNIFORM_DIM, assembly_dim), + basis_matrix((0, 2), UNIFORM_DIM, assembly_dim), + basis_matrix((1, 2), UNIFORM_DIM, assembly_dim), + DMatrix::::from_column_slice(UNIFORM_DIM, assembly_dim, &[ + 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, -0.5, -0.5, + 0.0, 0.0, -0.5, 0.5 ]) ]; - let tol_sq = ((ELEMENT_DIM * ASSEMBLY_DIM) as f64) * SCALED_TOL * SCALED_TOL; - for motion in tangent_motions { - let motion_proj: DMatrix<_> = motion.column_iter().enumerate().map( + let tangent_motions_std = vec![ + basis_matrix((0, 1), element_dim, assembly_dim), + basis_matrix((1, 1), element_dim, assembly_dim), + basis_matrix((0, 2), element_dim, assembly_dim), + basis_matrix((1, 2), element_dim, assembly_dim), + DMatrix::::from_column_slice(element_dim, assembly_dim, &[ + 0.0, 0.0, 0.0, 0.00, 0.0, + 0.0, 0.0, -1.0, -0.25, -1.0, + 0.0, 0.0, -1.0, 0.25, 1.0 + ]) + ]; + let tol_sq = ((element_dim * assembly_dim) as f64) * SCALED_TOL * SCALED_TOL; + for (motion_unif, motion_std) in tangent_motions_unif.into_iter().zip(tangent_motions_std) { + let motion_proj: DMatrix<_> = motion_unif.column_iter().enumerate().map( |(k, v)| tangent.proj(&v, k) ).sum(); - assert!((motion - motion_proj).norm_squared() < tol_sq); + assert!((motion_std - motion_proj).norm_squared() < tol_sq); } } + fn translation_motion_unif(vel: &Vector3, assembly_dim: usize) -> Vec> { + let mut elt_motion = DVector::zeros(4); + elt_motion.fixed_rows_mut::<3>(0).copy_from(vel); + iter::repeat(elt_motion).take(assembly_dim).collect() + } + + fn rotation_motion_unif(ang_vel: &Vector3, points: Vec>) -> Vec> { + points.into_iter().map( + |pt| { + let vel = ang_vel.cross(&pt.fixed_rows::<3>(0)); + let mut elt_motion = DVector::zeros(4); + elt_motion.fixed_rows_mut::<3>(0).copy_from(&vel); + elt_motion + } + ).collect() + } + + #[test] + fn tangent_test_kaleidocycle() { + // set up a kaleidocycle, made of points with fixed distances between + // them, and find its tangent space + const N_POINTS: usize = 12; + const N_HINGES: usize = 6; + const SCALED_TOL: f64 = 1.0e-12; + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for block in (0..N_POINTS).step_by(2) { + let block_next = (block + 2) % N_POINTS; + for j in 0..2 { + // diagonal and hinge edges + for k in j..2 { + gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); + } + + // non-hinge edges + for k in 0..2 { + gram_to_be.push_sym(block + j, block_next + k, -0.625); + } + } + } + gram_to_be + }; + let guess = { + let guess_elts = (0..N_HINGES).step_by(2).flat_map( + |n| { + let ang_hor = (n as f64) * PI/3.0; + let ang_vert = ((n + 1) as f64) * PI/3.0; + let x_vert = ang_vert.cos(); + let y_vert = ang_vert.sin(); + [ + point(0.0, 0.0, 0.0), + point(ang_hor.cos(), ang_hor.sin(), 0.0), + point(x_vert, y_vert, -0.5), + point(x_vert, y_vert, 0.5) + ] + } + ).collect::>(); + DMatrix::from_columns(&guess_elts) + }; + let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k)); + let (config, tangent, success, history) = realize_gram( + &gram, guess.clone(), &frozen, + SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + ); + assert_eq!(config, guess); + assert_eq!(success, true); + assert_eq!(history.scaled_loss.len(), 1); + + // list some motions that should form a basis for the tangent space of + // the solution variety + let element_dim = guess.nrows(); + let assembly_dim = guess.ncols(); + let tangent_motions_unif = vec![ + // the translations along the coordinate axes + translation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), assembly_dim), + translation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), assembly_dim), + translation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), assembly_dim), + + // the rotations about the coordinate axes + rotation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), guess.column_iter().collect()), + rotation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), guess.column_iter().collect()), + rotation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), guess.column_iter().collect()), + + // the twist motion. more precisely: a motion that keeps the center + // of mass stationary and preserves the distances between the + // vertices to first order. this has to be the twist as long as: + // - twisting is the kaleidocycle's only internal degree of + // freedom + // - every first-order motion of the kaleidocycle comes from an + // actual motion + (0..N_HINGES).step_by(2).flat_map( + |n| { + let ang_vert = ((n + 1) as f64) * PI/3.0; + let vel_vert_x = 4.0 * ang_vert.cos(); + let vel_vert_y = 4.0 * ang_vert.sin(); + [ + DVector::from_column_slice(&[0.0, 0.0, 5.0, 0.0]), + DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]), + DVector::from_column_slice(&[-vel_vert_x, -vel_vert_y, -3.0, 0.0]), + DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0]) + ] + } + ).collect::>() + ]; + let tangent_motions_std = tangent_motions_unif.iter().map( + |motion| DMatrix::from_columns( + &guess.column_iter().zip(motion).map( + |(v, elt_motion)| local_unif_to_std(v) * elt_motion + ).collect::>() + ) + ).collect::>(); + + // confirm that the dimension of the tangent space is no greater than + // expected + assert_eq!(tangent.basis_std.len(), tangent_motions_unif.len()); + + // confirm that the tangent space contains all the motions we expect it + // to. since we've already bounded the dimension of the tangent space, + // this confirms that the tangent space is what we expect it to be + let tol_sq = ((element_dim * assembly_dim) as f64) * SCALED_TOL * SCALED_TOL; + for (motion_unif, motion_std) in tangent_motions_unif.into_iter().zip(tangent_motions_std) { + let motion_proj: DMatrix<_> = motion_unif.into_iter().enumerate().map( + |(k, v)| tangent.proj(&v.as_view(), k) + ).sum(); + assert!((motion_std - motion_proj).norm_squared() < tol_sq); + } + } + + fn translation(dis: Vector3) -> DMatrix { + const ELEMENT_DIM: usize = 5; + DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[ + 1.0, 0.0, 0.0, 0.0, dis[0], + 0.0, 1.0, 0.0, 0.0, dis[1], + 0.0, 0.0, 1.0, 0.0, dis[2], + 2.0*dis[0], 2.0*dis[1], 2.0*dis[2], 1.0, dis.norm_squared(), + 0.0, 0.0, 0.0, 0.0, 1.0 + ]) + } + + // confirm that projection onto a configuration subspace is equivariant with + // respect to Euclidean motions + #[test] + fn proj_equivar_test() { + // find a pair of spheres that meet at 120° + const SCALED_TOL: f64 = 1.0e-12; + let gram = { + let mut gram_to_be = PartialMatrix::new(); + gram_to_be.push_sym(0, 0, 1.0); + gram_to_be.push_sym(1, 1, 1.0); + gram_to_be.push_sym(0, 1, 0.5); + gram_to_be + }; + let guess_orig = DMatrix::from_columns(&[ + sphere(0.0, 0.0, 0.5, 1.0), + sphere(0.0, 0.0, -0.5, 1.0) + ]); + let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram( + &gram, guess_orig.clone(), &[], + SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + ); + assert_eq!(config_orig, guess_orig); + assert_eq!(success_orig, true); + assert_eq!(history_orig.scaled_loss.len(), 1); + + // find another pair of spheres that meet at 120°. we'll think of this + // solution as a transformed version of the original one + let guess_tfm = { + let a = 0.5 * FRAC_1_SQRT_2; + DMatrix::from_columns(&[ + sphere(a, 0.0, 7.0 + a, 1.0), + sphere(-a, 0.0, 7.0 - a, 1.0) + ]) + }; + let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram( + &gram, guess_tfm.clone(), &[], + SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + ); + assert_eq!(config_tfm, guess_tfm); + assert_eq!(success_tfm, true); + assert_eq!(history_tfm.scaled_loss.len(), 1); + + // project a nudge to the tangent space of the solution variety at the + // original solution + let motion_orig = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); + let motion_orig_proj = tangent_orig.proj(&motion_orig.as_view(), 0); + + // project the equivalent nudge to the tangent space of the solution + // variety at the transformed solution + let motion_tfm = DVector::from_column_slice(&[FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0]); + let motion_tfm_proj = tangent_tfm.proj(&motion_tfm.as_view(), 0); + + // take the transformation that sends the original solution to the + // transformed solution and apply it to the motion that the original + // solution makes in response to the nudge + const ELEMENT_DIM: usize = 5; + let rot = DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[ + FRAC_1_SQRT_2, 0.0, -FRAC_1_SQRT_2, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, + FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0, 0.0, + 0.0, 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 1.0 + ]); + let transl = translation(Vector3::new(0.0, 0.0, 7.0)); + let motion_proj_tfm = transl * rot * motion_orig_proj; + + // confirm that the projection of the nudge is equivariant. we loosen + // the comparison tolerance because the transformation seems to + // introduce some numerical error + const SCALED_TOL_TFM: f64 = 1.0e-9; + let tol_sq = ((guess_orig.nrows() * guess_orig.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM; + assert!((motion_proj_tfm - motion_tfm_proj).norm_squared() < tol_sq); + } + // at the frozen indices, the optimization steps should have exact zeros, // and the realized configuration should match the initial guess #[test] From 25017176fd8ab03c875ba53b95803e72c9cca91a Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Thu, 6 Feb 2025 22:53:41 +0000 Subject: [PATCH 119/132] Adjust normalization step of nudge routine (#43) The brach to be merged partially addresses issue #42 by changing the way we normalize element representations after stepping them in a straight line through configuration space during a nudge. On the main branch, we rescale the whole representation vector. On the branch to be merged, we instead contract the representation vector toward the last coordinate axis by rescaling the spatial and curvature components. ### Improvement in leakage This change reduces the directional leakage described in #42. For a quantitative comparison, I used the [reproduction prodcedure](issues/42#user-content-leakage) from that issue, holding **W** until the second coordinate of Deimos had increased by 4 units (from 0.6 to 4.6). During this motion, the third coordinate changed by 0.158 units on the main branch, but only 0.007 units on the branch to be merged. In other words, this pull request decreased drift by roughly a factor of 20. ### Neutral changes in oscillation and jitter This change makes oscillation and jitter happen differently during the reproduction procedures from #42, but I wouldn't describe them as being better or worse. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/43 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/src/assembly.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 9b0e065..7073c9e 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -5,7 +5,7 @@ use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ -use crate::engine::{realize_gram, local_unif_to_std, ConfigSubspace, PartialMatrix, Q}; +use crate::engine::{realize_gram, local_unif_to_std, ConfigSubspace, PartialMatrix}; // the types of the keys we use to access an assembly's elements and constraints pub type ElementKey = usize; @@ -371,8 +371,8 @@ impl Assembly { } } - // step each element along the mass shell geodesic that matches its - // velocity in the deformation found above + // step the assembly along the deformation. this changes the elements' + // normalizations, so we restore those afterward /* KLUDGE */ // since our test assemblies only include spheres, we assume that every // element is on the 1 mass shell @@ -380,9 +380,16 @@ impl Assembly { elt.representation.update_silent(|rep| { match elt.column_index { Some(column_index) => { - let rep_next = &*rep + motion_proj.column(column_index); - let normalizer = rep_next.dot(&(&*Q * &rep_next)); - rep.set_column(0, &(rep_next / normalizer)); + // step the assembly along the deformation + *rep += motion_proj.column(column_index); + + // restore normalization by contracting toward the last + // coordinate axis + let q_sp = rep.fixed_rows::<3>(0).norm_squared(); + let half_q_lt = -2.0 * rep[3] * rep[4]; + let half_q_lt_sq = half_q_lt * half_q_lt; + let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); + rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling); }, None => { console::log_1(&JsValue::from( From 46324fecc692d21aa3648c168b8df89c7031cbb6 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Sat, 8 Feb 2025 06:08:36 +0000 Subject: [PATCH 120/132] Use workaround to keep representation coordinates in order (#46) This fixes #41 by rendering representation vectors with a static list view rather than an `Indexed` view. The Sycamore maintainer has confirmed that `Indexed` is always supposed to display list items in order, so I think #41 is likely caused by a bug in `Indexed`. We should consider reverting this pull request when the bug is fixed. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/46 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/src/outline.rs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 148f870..a6e968d 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -64,11 +64,16 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { move |sel| if sel.contains(&key) { "selected" } else { "" } ); let label = element.label.clone(); - let rep_components = element.representation.map( - |rep| rep.iter().map( - |u| format!("{:.3}", u).replace("-", "\u{2212}") - ).collect() - ); + let rep_components = move || { + element.representation.with( + |rep| rep.iter().map( + |u| { + let u_str = format!("{:.3}", u).replace("-", "\u{2212}"); + view! { div { (u_str) } } + } + ).collect::>() + ) + }; let constrained = element.constraints.map(|csts| csts.len() > 0); let constraint_list = element.constraints.map( |csts| csts.clone().into_iter().collect() @@ -129,14 +134,7 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { } ) { div(class="element-label") { (label) } - div(class="element-representation") { - Indexed( - list=rep_components, - view=|coord_str| view! { - div { (coord_str) } - } - ) - } + div(class="element-representation") { (rep_components) } div(class="status") } } From da28bc99d2f46a2cc3141d05c8fe1a57e336fc71 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 10 Mar 2025 23:43:24 +0000 Subject: [PATCH 121/132] Generalize constraints to observables (#48) Unifies the interface elements for measuring and constraining real-valued observables, as proposed in issue #47. The resulting combination is called a "Regulator," at least in the code. They are presented as text inputs in the table view. When a Regulatore is in measurement mode (has no "set point"), the text field displays its value. Entering a desired value into the text field creates a set point, and then the Regulator acts to (attempt to) constrain the value. Setting the desired value to the empty string switches the observable back to measurement mode. If you enter a desired value that can't be parsed as a floating point number, the regulator input is flagged as invalid and it has no effect on the state of the regulator. The set point can in this case be restored to its previous value (or to no set point if that was its prior state) by pressing the "Esc" key. Co-authored-by: Aaron Fenyes Co-authored-by: glen Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/48 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.toml | 1 + app-proto/main.css | 47 +++++++++----- app-proto/src/add_remove.rs | 44 ++----------- app-proto/src/assembly.rs | 115 ++++++++++++++++++++++++--------- app-proto/src/main.rs | 1 + app-proto/src/outline.rs | 123 ++++++++++++++++++++++++------------ app-proto/src/specified.rs | 44 +++++++++++++ 7 files changed, 249 insertions(+), 126 deletions(-) create mode 100644 app-proto/src/specified.rs diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index c11fef4..8000327 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -13,6 +13,7 @@ itertools = "0.13.0" js-sys = "0.3.70" lazy_static = "1.5.0" nalgebra = "0.33.0" +readonly = "0.2.12" rustc-hash = "2.0.0" slab = "0.4.9" sycamore = "0.9.0-beta.3" diff --git a/app-proto/main.css b/app-proto/main.css index b9fc0a1..4726a27 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -3,7 +3,8 @@ --text-bright: white; --text-invalid: #f58fc2; /* bright pink */ --border: #555; /* light gray */ - --border-focus: #aaa; /* bright gray */ + --border-focus-dark: #aaa; /* bright gray */ + --border-focus-light: white; --border-invalid: #70495c; /* dusky pink */ --selection-highlight: #444; /* medium gray */ --page-background: #222; /* dark gray */ @@ -23,7 +24,7 @@ body { display: flex; flex-direction: column; float: left; - width: 450px; + width: 500px; height: 100vh; margin: 0px; padding: 0px; @@ -77,12 +78,12 @@ summary.selected { background-color: var(--selection-highlight); } -summary > div, .constraint { +summary > div, .regulator { padding-top: 4px; padding-bottom: 4px; } -.element, .constraint { +.element, .regulator { display: flex; flex-grow: 1; padding-left: 8px; @@ -107,7 +108,7 @@ details[open]:has(li) .element-switch::after { flex-grow: 1; } -.constraint-label { +.regulator-label { flex-grow: 1; } @@ -123,26 +124,34 @@ details[open]:has(li) .element-switch::after { width: 56px; } -.constraint { +.regulator { font-style: italic; } -.constraint.invalid { - color: var(--text-invalid); +.regulator-type { + padding: 2px 8px 0px 8px; + font-size: 10pt; } -.constraint > input[type=checkbox] { - margin: 0px 8px 0px 0px; -} - -.constraint > input[type=text] { +.regulator-input { color: inherit; background-color: inherit; border: 1px solid var(--border); border-radius: 2px; } -.constraint.invalid > input[type=text] { +.regulator-input::placeholder { + color: inherit; + opacity: 54%; + font-style: italic; +} + +.regulator-input.constraint { + background-color: var(--display-background); +} + +.regulator-input.invalid { + color: var(--text-invalid); border-color: var(--border-invalid); } @@ -154,7 +163,7 @@ details[open]:has(li) .element-switch::after { font-style: normal; } -.invalid > .status::after, details:has(.invalid):not([open]) .status::after { +.regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after { content: '⚠'; color: var(--text-invalid); } @@ -171,5 +180,11 @@ canvas { } canvas:focus { - border-color: var(--border-focus); + border-color: var(--border-focus-dark); + outline: none; +} + +input:focus { + border-color: var(--border-focus-light); + outline: none; } \ No newline at end of file diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index ba02e65..5fed411 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -1,7 +1,11 @@ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; -use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}}; +use crate::{ + engine, + AppState, + assembly::{Assembly, Element} +}; /* DEBUG */ // load an example assembly for testing. this code will be removed once we've @@ -190,44 +194,8 @@ pub fn AddRemove() -> View { (subject_vec[0].clone(), subject_vec[1].clone()) } ); - let lorentz_prod = create_signal(0.0); - let lorentz_prod_valid = create_signal(false); - let active = create_signal(true); - state.assembly.insert_constraint(Constraint { - subjects: subjects, - lorentz_prod: lorentz_prod, - lorentz_prod_text: create_signal(String::new()), - lorentz_prod_valid: lorentz_prod_valid, - active: active, - }); + state.assembly.insert_new_regulator(subjects); state.selection.update(|sel| sel.clear()); - - /* DEBUG */ - // print updated constraint list - console::log_1(&JsValue::from("Constraints:")); - state.assembly.constraints.with(|csts| { - for (_, cst) in csts.into_iter() { - console::log_5( - &JsValue::from(" "), - &JsValue::from(cst.subjects.0), - &JsValue::from(cst.subjects.1), - &JsValue::from(":"), - &JsValue::from(cst.lorentz_prod.get_untracked()) - ); - } - }); - - // update the realization when the constraint becomes active - // and valid, or is edited while active and valid - create_effect(move || { - console::log_1(&JsValue::from( - format!("Constraint ({}, {}) updated", subjects.0, subjects.1) - )); - lorentz_prod.track(); - if active.get() && lorentz_prod_valid.get() { - state.assembly.realize(); - } - }); } ) { "🔗" } select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 7073c9e..18176df 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -5,11 +5,14 @@ use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ -use crate::engine::{realize_gram, local_unif_to_std, ConfigSubspace, PartialMatrix}; +use crate::{ + engine::{Q, local_unif_to_std, realize_gram, ConfigSubspace, PartialMatrix}, + specified::SpecifiedValue +}; -// the types of the keys we use to access an assembly's elements and constraints +// the types of the keys we use to access an assembly's elements and regulators pub type ElementKey = usize; -pub type ConstraintKey = usize; +pub type RegulatorKey = usize; pub type ElementColor = [f32; 3]; @@ -26,8 +29,11 @@ pub struct Element { pub label: String, pub color: ElementColor, pub representation: Signal>, - pub constraints: Signal>, - + + // All regulators with this element as a subject. The assembly owning + // this element is responsible for keeping this set up to date. + pub regulators: Signal>, + // a serial number, assigned by `Element::new`, that uniquely identifies // each element pub serial: u64, @@ -61,7 +67,7 @@ impl Element { label: label, color: color, representation: create_signal(representation), - constraints: create_signal(BTreeSet::default()), + regulators: create_signal(BTreeSet::default()), serial: serial, column_index: None } @@ -111,13 +117,11 @@ impl Element { } } -#[derive(Clone)] -pub struct Constraint { +#[derive(Clone, Copy)] +pub struct Regulator { pub subjects: (ElementKey, ElementKey), - pub lorentz_prod: Signal, - pub lorentz_prod_text: Signal, - pub lorentz_prod_valid: Signal, - pub active: Signal + pub measurement: ReadSignal, + pub set_point: Signal } // the velocity is expressed in uniform coordinates @@ -131,9 +135,9 @@ type AssemblyMotion<'a> = Vec>; // a complete, view-independent description of an assembly #[derive(Clone)] pub struct Assembly { - // elements and constraints + // elements and regulators pub elements: Signal>, - pub constraints: Signal>, + pub regulators: Signal>, // solution variety tangent space. the basis vectors are stored in // configuration matrix format, ordered according to the elements' column @@ -155,13 +159,13 @@ impl Assembly { pub fn new() -> Assembly { Assembly { elements: create_signal(Slab::new()), - constraints: create_signal(Slab::new()), + regulators: create_signal(Slab::new()), tangent: create_signal(ConfigSubspace::zero(0)), elements_by_id: create_signal(FxHashMap::default()) } } - // --- inserting elements and constraints --- + // --- inserting elements and regulators --- // insert an element into the assembly without checking whether we already // have an element with the same identifier. any element that does have the @@ -204,14 +208,61 @@ impl Assembly { ); } - pub fn insert_constraint(&self, constraint: Constraint) { - let subjects = constraint.subjects; - let key = self.constraints.update(|csts| csts.insert(constraint)); - let subject_constraints = self.elements.with( - |elts| (elts[subjects.0].constraints, elts[subjects.1].constraints) + fn insert_regulator(&self, regulator: Regulator) { + let subjects = regulator.subjects; + let key = self.regulators.update(|regs| regs.insert(regulator)); + let subject_regulators = self.elements.with( + |elts| (elts[subjects.0].regulators, elts[subjects.1].regulators) ); - subject_constraints.0.update(|csts| csts.insert(key)); - subject_constraints.1.update(|csts| csts.insert(key)); + subject_regulators.0.update(|regs| regs.insert(key)); + subject_regulators.1.update(|regs| regs.insert(key)); + } + + pub fn insert_new_regulator(self, subjects: (ElementKey, ElementKey)) { + // create and insert a new regulator + let measurement = self.elements.map( + move |elts| { + let reps = ( + elts[subjects.0].representation.get_clone(), + elts[subjects.1].representation.get_clone() + ); + reps.0.dot(&(&*Q * reps.1)) + } + ); + let set_point = create_signal(SpecifiedValue::from_empty_spec()); + self.insert_regulator(Regulator { + subjects: subjects, + measurement: measurement, + set_point: set_point + }); + + /* DEBUG */ + // print an updated list of regulators + console::log_1(&JsValue::from("Regulators:")); + self.regulators.with(|regs| { + for (_, reg) in regs.into_iter() { + console::log_5( + &JsValue::from(" "), + &JsValue::from(reg.subjects.0), + &JsValue::from(reg.subjects.1), + &JsValue::from(":"), + ®.set_point.with_untracked( + |set_pt| JsValue::from(set_pt.spec.as_str()) + ) + ); + } + }); + + // update the realization when the regulator becomes a constraint, or is + // edited while acting as a constraint + create_effect(move || { + console::log_1(&JsValue::from( + format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) + )); + if set_point.with(|set_pt| set_pt.is_present()) { + self.realize(); + } + }); } // --- realization --- @@ -228,14 +279,16 @@ impl Assembly { let (gram, guess) = self.elements.with_untracked(|elts| { // set up the off-diagonal part of the Gram matrix let mut gram_to_be = PartialMatrix::new(); - self.constraints.with_untracked(|csts| { - for (_, cst) in csts { - if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() { - let subjects = cst.subjects; - let row = elts[subjects.0].column_index.unwrap(); - let col = elts[subjects.1].column_index.unwrap(); - gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked()); - } + self.regulators.with_untracked(|regs| { + for (_, reg) in regs { + reg.set_point.with_untracked(|set_pt| { + if let Some(val) = set_pt.value { + let subjects = reg.subjects; + let row = elts[subjects.0].column_index.unwrap(); + let col = elts[subjects.1].column_index.unwrap(); + gram_to_be.push_sym(row, col, val); + } + }); } }); diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index f961504..6ab3e49 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -3,6 +3,7 @@ mod assembly; mod display; mod engine; mod outline; +mod specified; use rustc_hash::FxHashSet; use sycamore::prelude::*; diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index a6e968d..002baea 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -1,56 +1,97 @@ use itertools::Itertools; use sycamore::prelude::*; use web_sys::{ - Event, - HtmlInputElement, KeyboardEvent, MouseEvent, wasm_bindgen::JsCast }; -use crate::{AppState, assembly, assembly::{Constraint, ConstraintKey, ElementKey}}; +use crate::{ + AppState, + assembly, + assembly::{ElementKey, Regulator, RegulatorKey}, + specified::SpecifiedValue +}; -// an editable view of the Lorentz product representing a constraint +// an editable view of a regulator #[component(inline_props)] -fn LorentzProductInput(constraint: Constraint) -> View { +fn RegulatorInput(regulator: Regulator) -> View { + let valid = create_signal(true); + let value = create_signal( + regulator.set_point.with_untracked(|set_pt| set_pt.spec.clone()) + ); + + // this closure resets the input value to the regulator's set point + // specification + let reset_value = move || { + batch(|| { + valid.set(true); + value.set(regulator.set_point.with(|set_pt| set_pt.spec.clone())); + }) + }; + + // reset the input value whenever the regulator's set point specification + // is updated + create_effect(reset_value); + view! { input( r#type="text", - bind:value=constraint.lorentz_prod_text, - on:change=move |event: Event| { - let target: HtmlInputElement = event.target().unwrap().unchecked_into(); - match target.value().parse::() { - Ok(lorentz_prod) => batch(|| { - constraint.lorentz_prod.set(lorentz_prod); - constraint.lorentz_prod_valid.set(true); - }), - Err(_) => constraint.lorentz_prod_valid.set(false) - }; + class=move || { + if valid.get() { + regulator.set_point.with(|set_pt| { + if set_pt.is_present() { + "regulator-input constraint" + } else { + "regulator-input" + } + }) + } else { + "regulator-input invalid" + } + }, + placeholder=regulator.measurement.with(|result| result.to_string()), + bind:value=value, + on:change=move |_| { + valid.set( + match SpecifiedValue::try_from(value.get_clone_untracked()) { + Ok(set_pt) => { + regulator.set_point.set(set_pt); + true + } + Err(_) => false + } + ) + }, + on:keydown={ + move |event: KeyboardEvent| { + match event.key().as_str() { + "Escape" => reset_value(), + _ => () + } + } } ) } } -// a list item that shows a constraint in an outline view of an element +// a list item that shows a regulator in an outline view of an element #[component(inline_props)] -fn ConstraintOutlineItem(constraint_key: ConstraintKey, element_key: ElementKey) -> View { +fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View { let state = use_context::(); let assembly = &state.assembly; - let constraint = assembly.constraints.with(|csts| csts[constraint_key].clone()); - let other_subject = if constraint.subjects.0 == element_key { - constraint.subjects.1 + let regulator = assembly.regulators.with(|regs| regs[regulator_key]); + let other_subject = if regulator.subjects.0 == element_key { + regulator.subjects.1 } else { - constraint.subjects.0 + regulator.subjects.0 }; let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); - let class = constraint.lorentz_prod_valid.map( - |&lorentz_prod_valid| if lorentz_prod_valid { "constraint" } else { "constraint invalid" } - ); view! { - li(class=class.get()) { - input(r#type="checkbox", bind:checked=constraint.active) - div(class="constraint-label") { (other_subject_label) } - LorentzProductInput(constraint=constraint) + li(class="regulator") { + div(class="regulator-label") { (other_subject_label) } + div(class="regulator-type") { "Inversive distance" } + RegulatorInput(regulator=regulator) div(class="status") } } @@ -74,9 +115,9 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { ).collect::>() ) }; - let constrained = element.constraints.map(|csts| csts.len() > 0); - let constraint_list = element.constraints.map( - |csts| csts.clone().into_iter().collect() + let regulated = element.regulators.map(|regs| regs.len() > 0); + let regulator_list = element.regulators.map( + |regs| regs.clone().into_iter().collect() ); let details_node = create_node_ref(); view! { @@ -91,7 +132,7 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { state.select(key, event.shift_key()); event.prevent_default(); }, - "ArrowRight" if constrained.get() => { + "ArrowRight" if regulated.get() => { let _ = details_node .get() .unchecked_into::() @@ -138,16 +179,16 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { div(class="status") } } - ul(class="constraints") { + ul(class="regulators") { Keyed( - list=constraint_list, - view=move |cst_key| view! { - ConstraintOutlineItem( - constraint_key=cst_key, + list=regulator_list, + view=move |reg_key| view! { + RegulatorOutlineItem( + regulator_key=reg_key, element_key=key ) }, - key=|cst_key| cst_key.clone() + key=|reg_key| reg_key.clone() ) } } @@ -155,9 +196,9 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { } } -// a component that lists the elements of the current assembly, showing the -// constraints on each element as a collapsible sub-list. its implementation -// is based on Kate Morley's HTML + CSS tree views: +// a component that lists the elements of the current assembly, showing each +// element's regulators in a collapsible sub-list. its implementation is based +// on Kate Morley's HTML + CSS tree views: // // https://iamkate.com/code/tree-views/ // diff --git a/app-proto/src/specified.rs b/app-proto/src/specified.rs new file mode 100644 index 0000000..cfe7fc3 --- /dev/null +++ b/app-proto/src/specified.rs @@ -0,0 +1,44 @@ +use std::num::ParseFloatError; + +// a real number described by a specification string. since the structure is +// read-only, we can guarantee that `spec` always specifies `value` in the +// following format +// ┌──────────────────────────────────────────────────────┬───────────┐ +// │ `spec` │ `value` │ +// ┝━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━┥ +// │ a string that parses to the floating-point value `x` │ `Some(x)` │ +// ├──────────────────────────────────────────────────────┼───────────┤ +// │ the empty string │ `None` │ +// └──────────────────────────────────────────────────────┴───────────┘ +#[readonly::make] +pub struct SpecifiedValue { + pub spec: String, + pub value: Option +} + +impl SpecifiedValue { + pub fn from_empty_spec() -> SpecifiedValue { + SpecifiedValue { spec: String::new(), value: None } + } + + pub fn is_present(&self) -> bool { + matches!(self.value, Some(_)) + } +} + +// a `SpecifiedValue` can be constructed from a specification string, formatted +// as described in the comment on the structure definition. the result is `Ok` +// if the specification is properly formatted, and `Error` if not +impl TryFrom for SpecifiedValue { + type Error = ParseFloatError; + + fn try_from(spec: String) -> Result { + if spec.is_empty() { + Ok(SpecifiedValue::from_empty_spec()) + } else { + spec.parse::().map( + |value| SpecifiedValue { spec: spec, value: Some(value) } + ) + } + } +} \ No newline at end of file From 2c4fd39c1fcb36c61f7828487185aa907912f591 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Wed, 12 Mar 2025 21:54:56 +0000 Subject: [PATCH 122/132] refactor: Tidy up engine tests (#72) ### `zero_loss_test` - Drop the redundant type hint in the definition of `a`. ### `tangent_test_three_spheres` - Get the dimension from the expected basis, rather than putting it in by hand. ### `tangent_test_kaleidocycle` - Factor out the realization code, in the same style as `realize_irisawa_hexlet`. - Rename the `irisawa` submodule to `examples`. ### `frozen_entry_test` - Move up into the section for simpler tests, between `zero_loss_test` and `irisawa_hexlet_test`. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/72 Reviewed-by: Glen Whitney Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/examples/irisawa-hexlet.rs | 2 +- app-proto/examples/kaleidocycle.rs | 52 +------ app-proto/src/engine.rs | 218 ++++++++++++++------------- 3 files changed, 121 insertions(+), 151 deletions(-) diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs index 2bc94c0..639a494 100644 --- a/app-proto/examples/irisawa-hexlet.rs +++ b/app-proto/examples/irisawa-hexlet.rs @@ -1,4 +1,4 @@ -use dyna3::engine::{Q, irisawa::realize_irisawa_hexlet}; +use dyna3::engine::{Q, examples::realize_irisawa_hexlet}; fn main() { const SCALED_TOL: f64 = 1.0e-12; diff --git a/app-proto/examples/kaleidocycle.rs b/app-proto/examples/kaleidocycle.rs index 88116d3..2779ab1 100644 --- a/app-proto/examples/kaleidocycle.rs +++ b/app-proto/examples/kaleidocycle.rs @@ -1,53 +1,10 @@ use nalgebra::{DMatrix, DVector}; -use std::{array, f64::consts::PI}; -use dyna3::engine::{Q, point, realize_gram, PartialMatrix}; +use dyna3::engine::{Q, examples::realize_kaleidocycle}; fn main() { - // set up a kaleidocycle, made of points with fixed distances between them, - // and find its tangent space - const N_POINTS: usize = 12; - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for block in (0..N_POINTS).step_by(2) { - let block_next = (block + 2) % N_POINTS; - for j in 0..2 { - // diagonal and hinge edges - for k in j..2 { - gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); - } - - // non-hinge edges - for k in 0..2 { - gram_to_be.push_sym(block + j, block_next + k, -0.625); - } - } - } - gram_to_be - }; - let guess = { - const N_HINGES: usize = 6; - let guess_elts = (0..N_HINGES).step_by(2).flat_map( - |n| { - let ang_hor = (n as f64) * PI/3.0; - let ang_vert = ((n + 1) as f64) * PI/3.0; - let x_vert = ang_vert.cos(); - let y_vert = ang_vert.sin(); - [ - point(0.0, 0.0, 0.0), - point(ang_hor.cos(), ang_hor.sin(), 0.0), - point(x_vert, y_vert, -0.5), - point(x_vert, y_vert, 0.5) - ] - } - ).collect::>(); - DMatrix::from_columns(&guess_elts) - }; - let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k)); - let (config, tangent, success, history) = realize_gram( - &gram, guess, &frozen, - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 - ); + const SCALED_TOL: f64 = 1.0e-12; + let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); print!("Completed Gram matrix:{}", config.tr_mul(&*Q) * &config); print!("Configuration:{}", config); if success { @@ -58,7 +15,8 @@ fn main() { println!("Steps: {}", history.scaled_loss.len() - 1); println!("Loss: {}\n", history.scaled_loss.last().unwrap()); - // find the kaleidocycle's twist motion + // find the kaleidocycle's twist motion by projecting onto the tangent space + const N_POINTS: usize = 12; let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); let down = -&up; let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index eee19ac..35f898c 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -413,20 +413,20 @@ pub fn realize_gram( // --- tests --- -// 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/ -// #[cfg(feature = "dev")] -pub mod irisawa { +pub mod examples { use std::{array, f64::consts::PI}; use super::*; + // 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/ + // pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { let gram = { let mut gram_to_be = PartialMatrix::new(); @@ -480,14 +480,64 @@ pub mod irisawa { scaled_tol, 0.5, 0.9, 1.1, 200, 110 ) } + + // set up a kaleidocycle, made of points with fixed distances between them, + // and find its tangent space + pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + const N_POINTS: usize = 12; + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for block in (0..N_POINTS).step_by(2) { + let block_next = (block + 2) % N_POINTS; + for j in 0..2 { + // diagonal and hinge edges + for k in j..2 { + gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); + } + + // non-hinge edges + for k in 0..2 { + gram_to_be.push_sym(block + j, block_next + k, -0.625); + } + } + } + gram_to_be + }; + + let guess = { + const N_HINGES: usize = 6; + let guess_elts = (0..N_HINGES).step_by(2).flat_map( + |n| { + let ang_hor = (n as f64) * PI/3.0; + let ang_vert = ((n + 1) as f64) * PI/3.0; + let x_vert = ang_vert.cos(); + let y_vert = ang_vert.sin(); + [ + point(0.0, 0.0, 0.0), + point(ang_hor.cos(), ang_hor.sin(), 0.0), + point(x_vert, y_vert, -0.5), + point(x_vert, y_vert, 0.5) + ] + } + ).collect::>(); + DMatrix::from_columns(&guess_elts) + }; + + let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k)); + + realize_gram( + &gram, guess, &frozen, + scaled_tol, 0.5, 0.9, 1.1, 200, 110 + ) + } } #[cfg(test)] mod tests { use nalgebra::Vector3; - use std::{array, f64::consts::{FRAC_1_SQRT_2, PI}, iter}; + use std::{f64::consts::{FRAC_1_SQRT_2, PI}, iter}; - use super::{*, irisawa::realize_irisawa_hexlet}; + use super::{*, examples::*}; #[test] fn sub_proj_test() { @@ -523,7 +573,7 @@ mod tests { entries }); let config = { - let a: f64 = 0.75_f64.sqrt(); + let a = 0.75_f64.sqrt(); DMatrix::from_columns(&[ sphere(1.0, 0.0, 0.0, a), sphere(-0.5, a, 0.0, a), @@ -534,6 +584,40 @@ mod tests { assert!(state.loss.abs() < f64::EPSILON); } + // at the frozen indices, the optimization steps should have exact zeros, + // and the realized configuration should match the initial guess + #[test] + fn frozen_entry_test() { + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for j in 0..2 { + for k in j..2 { + gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); + } + } + gram_to_be + }; + let guess = DMatrix::from_columns(&[ + point(0.0, 0.0, 2.0), + sphere(0.0, 0.0, 0.0, 1.0) + ]); + let frozen = [(3, 0), (3, 1)]; + println!(); + let (config, _, success, history) = realize_gram( + &gram, guess.clone(), &frozen, + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + assert_eq!(success, true); + for base_step in history.base_step.into_iter() { + for index in frozen { + assert_eq!(base_step[index], 0.0); + } + } + for index in frozen { + assert_eq!(config[index], guess[index]); + } + } + #[test] fn irisawa_hexlet_test() { // solve Irisawa's problem @@ -574,12 +658,8 @@ mod tests { assert_eq!(success, true); assert_eq!(history.scaled_loss.len(), 1); - // confirm that the tangent space has dimension five or less - assert_eq!(tangent.basis_std.len(), 5); - - // confirm that the tangent space contains all the motions we expect it - // to. since we've already bounded the dimension of the tangent space, - // this confirms that the tangent space is what we expect it to be + // list some motions that should form a basis for the tangent space of + // the solution variety const UNIFORM_DIM: usize = 4; let element_dim = guess.nrows(); let assembly_dim = guess.ncols(); @@ -605,6 +685,14 @@ mod tests { 0.0, 0.0, -1.0, 0.25, 1.0 ]) ]; + + // confirm that the dimension of the tangent space is no greater than + // expected + assert_eq!(tangent.basis_std.len(), tangent_motions_std.len()); + + // confirm that the tangent space contains all the motions we expect it + // to. since we've already bounded the dimension of the tangent space, + // this confirms that the tangent space is what we expect it to be let tol_sq = ((element_dim * assembly_dim) as f64) * SCALED_TOL * SCALED_TOL; for (motion_unif, motion_std) in tangent_motions_unif.into_iter().zip(tangent_motions_std) { let motion_proj: DMatrix<_> = motion_unif.column_iter().enumerate().map( @@ -633,59 +721,17 @@ mod tests { #[test] fn tangent_test_kaleidocycle() { - // set up a kaleidocycle, made of points with fixed distances between - // them, and find its tangent space - const N_POINTS: usize = 12; - const N_HINGES: usize = 6; + // set up a kaleidocycle and find its tangent space const SCALED_TOL: f64 = 1.0e-12; - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for block in (0..N_POINTS).step_by(2) { - let block_next = (block + 2) % N_POINTS; - for j in 0..2 { - // diagonal and hinge edges - for k in j..2 { - gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); - } - - // non-hinge edges - for k in 0..2 { - gram_to_be.push_sym(block + j, block_next + k, -0.625); - } - } - } - gram_to_be - }; - let guess = { - let guess_elts = (0..N_HINGES).step_by(2).flat_map( - |n| { - let ang_hor = (n as f64) * PI/3.0; - let ang_vert = ((n + 1) as f64) * PI/3.0; - let x_vert = ang_vert.cos(); - let y_vert = ang_vert.sin(); - [ - point(0.0, 0.0, 0.0), - point(ang_hor.cos(), ang_hor.sin(), 0.0), - point(x_vert, y_vert, -0.5), - point(x_vert, y_vert, 0.5) - ] - } - ).collect::>(); - DMatrix::from_columns(&guess_elts) - }; - let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k)); - let (config, tangent, success, history) = realize_gram( - &gram, guess.clone(), &frozen, - SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 - ); - assert_eq!(config, guess); + let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); assert_eq!(success, true); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of // the solution variety - let element_dim = guess.nrows(); - let assembly_dim = guess.ncols(); + const N_HINGES: usize = 6; + let element_dim = config.nrows(); + let assembly_dim = config.ncols(); let tangent_motions_unif = vec![ // the translations along the coordinate axes translation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), assembly_dim), @@ -693,9 +739,9 @@ mod tests { translation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), assembly_dim), // the rotations about the coordinate axes - rotation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), guess.column_iter().collect()), - rotation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), guess.column_iter().collect()), - rotation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), guess.column_iter().collect()), + rotation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), config.column_iter().collect()), + rotation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), config.column_iter().collect()), + rotation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), config.column_iter().collect()), // the twist motion. more precisely: a motion that keeps the center // of mass stationary and preserves the distances between the @@ -720,7 +766,7 @@ mod tests { ]; let tangent_motions_std = tangent_motions_unif.iter().map( |motion| DMatrix::from_columns( - &guess.column_iter().zip(motion).map( + &config.column_iter().zip(motion).map( |(v, elt_motion)| local_unif_to_std(v) * elt_motion ).collect::>() ) @@ -826,38 +872,4 @@ mod tests { let tol_sq = ((guess_orig.nrows() * guess_orig.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM; assert!((motion_proj_tfm - motion_tfm_proj).norm_squared() < tol_sq); } - - // at the frozen indices, the optimization steps should have exact zeros, - // and the realized configuration should match the initial guess - #[test] - fn frozen_entry_test() { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..2 { - for k in j..2 { - gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); - } - } - gram_to_be - }; - let guess = DMatrix::from_columns(&[ - point(0.0, 0.0, 2.0), - sphere(0.0, 0.0, 0.0, 1.0) - ]); - let frozen = [(3, 0), (3, 1)]; - println!(); - let (config, _, success, history) = realize_gram( - &gram, guess.clone(), &frozen, - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 - ); - assert_eq!(success, true); - for base_step in history.base_step.into_iter() { - for index in frozen { - assert_eq!(base_step[index], 0.0); - } - } - for index in frozen { - assert_eq!(config[index], guess[index]); - } - } } \ No newline at end of file From b86f1761517c2b65a2ebabbd1d0d779ae59a1449 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Wed, 2 Apr 2025 20:31:42 +0000 Subject: [PATCH 123/132] feat: Continuous integration via Forgejo Actions/runners (#75) Adds a continuous integration workflow to the repository, using the [Forgejo Actions](https://forgejo.org/docs/next/user/actions/) framework. Concurrently, Aaron added a [wiki page](https://code.studioinfinity.org/glen/dyna3/wiki/Continuous-integration) to document the continuous integration system. In particular, this page explains how to [run continuous integration checks on a development machine](wiki/Continuous-integration#execution), either directly or in a container. Co-authored-by: Aaron Fenyes Co-authored-by: Glen Whitney Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/75 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- .forgejo/setup-trunk/action.yaml | 22 + .../workflows/continuous-integration.yaml | 29 + .gitignore | 8 +- app-proto/Cargo.lock | 788 ++++++++++++++++++ app-proto/Cargo.toml | 20 + app-proto/src/main.rs | 3 + app-proto/src/tests.rs | 14 + 7 files changed, 877 insertions(+), 7 deletions(-) create mode 100644 .forgejo/setup-trunk/action.yaml create mode 100644 .forgejo/workflows/continuous-integration.yaml create mode 100644 app-proto/Cargo.lock create mode 100644 app-proto/src/tests.rs diff --git a/.forgejo/setup-trunk/action.yaml b/.forgejo/setup-trunk/action.yaml new file mode 100644 index 0000000..6007527 --- /dev/null +++ b/.forgejo/setup-trunk/action.yaml @@ -0,0 +1,22 @@ +# set up the Trunk web build system +# +# https://trunkrs.dev +# +# the `curl` call is based on David Tolnay's `rust-toolchain` action +# +# https://github.com/dtolnay/rust-toolchain +# +runs: + using: "composite" + steps: + - run: rustup target add wasm32-unknown-unknown + + # install the Trunk binary to `ci-bin` within the workspace directory, which + # is determined by the `github.workspace` label and reflected in the + # `GITHUB_WORKSPACE` environment variable. then, make the `trunk` command + # available by placing the fully qualified path to `ci-bin` on the + # workflow's search path + - run: mkdir -p ci-bin + - run: curl --output - --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail 'https://github.com/trunk-rs/trunk/releases/download/v0.21.12/trunk-x86_64-unknown-linux-gnu.tar.gz' | tar --gunzip --extract --file - + working-directory: ci-bin + - run: echo "${{ github.workspace }}/ci-bin" >> $GITHUB_PATH diff --git a/.forgejo/workflows/continuous-integration.yaml b/.forgejo/workflows/continuous-integration.yaml new file mode 100644 index 0000000..daf8923 --- /dev/null +++ b/.forgejo/workflows/continuous-integration.yaml @@ -0,0 +1,29 @@ +on: + pull_request: + push: + branches: [main] +jobs: + # run the automated tests, reporting success if the tests pass and were built + # without warnings. the examples are run as tests, because we've configured + # each example target with `test = true` and `harness = false` in Cargo.toml. + # Trunk build failures caused by problems outside the Rust source code, like + # missing assets, should be caught by `trunk_build_test` + test: + runs-on: docker + container: + image: cimg/rust:1.85-node + defaults: + run: + # set the default working directory for each `run` step, relative to the + # workspace directory. this default only affects `run` steps (and if we + # tried to set the `working-directory` label for any other kind of step, + # it wouldn't be recognized anyway) + working-directory: app-proto + steps: + # Check out the repository so that its top-level directory is the + # workspace directory (action variable `github.workspace`, environment + # variable `$GITHUB_WORKSPACE`): + - uses: https://code.forgejo.org/actions/checkout@v4 + + - uses: ./.forgejo/setup-trunk + - run: RUSTFLAGS='-D warnings' cargo test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3e95fba..ba2944f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,2 @@ -node_modules -site -docbuild -__tests__ -coverage -dyna3.zip -tmpproj +ci-bin *~ diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock new file mode 100644 index 0000000..9738589 --- /dev/null +++ b/app-proto/Cargo.lock @@ -0,0 +1,788 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytemuck" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "dyna3" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "dyna3", + "itertools", + "js-sys", + "lazy_static", + "nalgebra", + "readonly", + "rustc-hash", + "slab", + "sycamore", + "wasm-bindgen-test", + "web-sys", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matrixmultiply" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "minicov" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71e683cd655513b99affab7d317deb690528255a0d5f717f1024093c12b169" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "nalgebra" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c4b5f057b303842cf3262c27e465f4c303572e7f6b0648f60e16248ac3397f4" +dependencies = [ + "approx", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "readonly" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2a62d85ed81ca5305dc544bd42c8804c5060b78ffa5ad3c64b0fb6a8c13d062" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + +[[package]] +name = "safe_arch" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3460605018fdc9612bce72735cba0d27efbcd9904780d44c7e3a9948f96148a" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simba" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a386a501cd104797982c15ae17aafe8b9261315b5d07e3ec803f2ea26be0fa" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "sycamore" +version = "0.9.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dedaf7237c05913604a5b0b2536b613f6c8510c6b213d2583b1294869755cabd" +dependencies = [ + "hashbrown", + "indexmap", + "paste", + "sycamore-core", + "sycamore-macro", + "sycamore-reactive", + "sycamore-web", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "sycamore-core" +version = "0.9.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ddddc3d1bcb38c04ad55d2d1ab4f6a358e4daaeae0a0436892f1fade9fb31a" +dependencies = [ + "hashbrown", + "paste", + "sycamore-reactive", +] + +[[package]] +name = "sycamore-macro" +version = "0.9.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77181c27cb753e86065308901871ccc7456fb19527b6a4ffacad3b63175ed014" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "rand", + "sycamore-view-parser", + "syn", +] + +[[package]] +name = "sycamore-reactive" +version = "0.9.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa6870203507c07e850687c0ccf528eb0f04240e3596bac9137007ffb6c50b1" +dependencies = [ + "paste", + "slotmap", + "smallvec", +] + +[[package]] +name = "sycamore-view-parser" +version = "0.9.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6144640af2eafffc68a92f3aacbbfaa21f7fd31906e2336fe304fd100fe226b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sycamore-web" +version = "0.9.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca93dcf1b1830bf1aac93508ed51babcda92c1d32d96067ab416d94e4b7c475" +dependencies = [ + "html-escape", + "js-sys", + "once_cell", + "paste", + "smallvec", + "sycamore-core", + "sycamore-macro", + "sycamore-reactive", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68497a05fb21143a08a7d24fc81763384a3072ee43c44e86aad1744d6adef9d9" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "minicov", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wide" +version = "0.7.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b828f995bf1e9622031f8009f8481a85406ce1f4d4588ff746d872043e855690" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 8000327..5ab7299 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -50,3 +50,23 @@ wasm-bindgen-test = "0.3.34" [profile.release] opt-level = "s" # optimize for small code size debug = true # include debug symbols + +[[example]] +name = "irisawa-hexlet" +test = true +harness = false + +[[example]] +name = "kaleidocycle" +test = true +harness = false + +[[example]] +name = "point-on-sphere" +test = true +harness = false + +[[example]] +name = "three-spheres" +test = true +harness = false diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index 6ab3e49..e581997 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -5,6 +5,9 @@ mod engine; mod outline; mod specified; +#[cfg(test)] +mod tests; + use rustc_hash::FxHashSet; use sycamore::prelude::*; diff --git a/app-proto/src/tests.rs b/app-proto/src/tests.rs new file mode 100644 index 0000000..2c5436b --- /dev/null +++ b/app-proto/src/tests.rs @@ -0,0 +1,14 @@ +use std::process::Command; + +// build and bundle the application, reporting success if there are no errors or +// warnings. to see this test fail while others succeed, try moving `index.html` +// or one of the assets that it links to +#[test] +fn trunk_build_test() { + let build_status = Command::new("trunk") + .arg("build") + .env("RUSTFLAGS", "-D warnings") + .status() + .expect("Call to Trunk failed"); + assert!(build_status.success()); +} \ No newline at end of file From 23ba5acad7e6fb3025af63e99d908dd29bde78f4 Mon Sep 17 00:00:00 2001 From: glen Date: Fri, 18 Apr 2025 04:34:30 +0000 Subject: [PATCH 124/132] Add a top-level run command to the "play with prototype" in README (#81) It's convenient to stay in the top-level directory of a project. This change to the README explains how to run the prototype from the top level. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/81 Co-authored-by: glen Co-committed-by: glen --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1ea3302..3a29eb0 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,10 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter ### Play with the prototype -1. Go into the `app-proto` folder -2. Call `trunk serve --release` to build and serve the prototype +1. From the `app-proto` folder, call `trunk serve --release` to build and serve the prototype * *The crates the prototype depends on will be downloaded and served automatically* * *For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag* + * *If you want to stay in the top-level folder, you can call `trunk serve --config app-proto [--release]`* from there instead. 3. In a web browser, visit one of the URLs listed under the message `INFO 📡 server listening at:` * *Touching any file in the `app-proto` folder will make Trunk rebuild and live-reload the prototype* 4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype From 360ce12d8baf7f7200b422df5c43894765bd3e03 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 21 Apr 2025 23:40:42 +0000 Subject: [PATCH 125/132] feat: Curvature regulators (#80) Prior to this commit, there's only one kind of regulator: the one that regulates the inversive distance between two spheres (or, more generally, the Lorentz product between two element representation vectors). Adds a new kind of regulator, which regulates the curvature of a sphere (issue #55). In the process, introduces a general framework based on new traits for organizing and sharing code between different kinds of regulators. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/80 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/examples/point-on-sphere.rs | 25 +- app-proto/examples/three-spheres.rs | 29 +- app-proto/src/add_remove.rs | 63 ++--- app-proto/src/assembly.rs | 364 ++++++++++++++++++------ app-proto/src/engine.rs | 388 ++++++++++++++++---------- app-proto/src/outline.rs | 102 +++++-- 6 files changed, 640 insertions(+), 331 deletions(-) diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs index 13040e5..880d7b0 100644 --- a/app-proto/examples/point-on-sphere.rs +++ b/app-proto/examples/point-on-sphere.rs @@ -1,26 +1,19 @@ -use nalgebra::DMatrix; - -use dyna3::engine::{Q, point, realize_gram, sphere, PartialMatrix}; +use dyna3::engine::{Q, point, realize_gram, sphere, ConstraintProblem}; fn main() { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..2 { - for k in j..2 { - gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); - } - } - gram_to_be - }; - let guess = DMatrix::from_columns(&[ + let mut problem = ConstraintProblem::from_guess(&[ point(0.0, 0.0, 2.0), sphere(0.0, 0.0, 0.0, 1.0) ]); - let frozen = [(3, 0)]; + for j in 0..2 { + for k in j..2 { + problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); + } + } + problem.frozen.push(3, 0, problem.guess[(3, 0)]); println!(); let (config, _, success, history) = realize_gram( - &gram, guess, &frozen, - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); print!("Configuration:{}", config); diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs index 19acfd1..3f3cc44 100644 --- a/app-proto/examples/three-spheres.rs +++ b/app-proto/examples/three-spheres.rs @@ -1,29 +1,22 @@ -use nalgebra::DMatrix; - -use dyna3::engine::{Q, realize_gram, sphere, PartialMatrix}; +use dyna3::engine::{Q, realize_gram, sphere, ConstraintProblem}; fn main() { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..3 { - for k in j..3 { - gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); - } - } - gram_to_be - }; - let guess = { + let mut problem = ConstraintProblem::from_guess({ let a: f64 = 0.75_f64.sqrt(); - DMatrix::from_columns(&[ + &[ sphere(1.0, 0.0, 0.0, 1.0), sphere(-0.5, a, 0.0, 1.0), sphere(-0.5, -a, 0.0, 1.0) - ]) - }; + ] + }); + for j in 0..3 { + for k in j..3 { + problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); + } + } println!(); let (config, _, success, history) = realize_gram( - &gram, guess, &[], - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); if success { diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 5fed411..14fcd41 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -4,14 +4,14 @@ use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ engine, AppState, - assembly::{Assembly, Element} + assembly::{Assembly, Element, InversiveDistanceRegulator} }; /* DEBUG */ // load an example assembly for testing. this code will be removed once we've // built a more formal test assembly system fn load_gen_assemb(assembly: &Assembly) { - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("gemini_a"), String::from("Castor"), @@ -19,7 +19,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(0.5, 0.5, 0.0, 1.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("gemini_b"), String::from("Pollux"), @@ -27,7 +27,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(-0.5, -0.5, 0.0, 1.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("ursa_major"), String::from("Ursa major"), @@ -35,7 +35,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(-0.5, 0.5, 0.0, 0.75) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("ursa_minor"), String::from("Ursa minor"), @@ -43,7 +43,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(0.5, -0.5, 0.0, 0.5) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("moon_deimos"), String::from("Deimos"), @@ -51,7 +51,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(0.0, 0.15, 1.0, 0.25) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("moon_phobos"), String::from("Phobos"), @@ -66,7 +66,7 @@ fn load_gen_assemb(assembly: &Assembly) { // built a more formal test assembly system fn load_low_curv_assemb(assembly: &Assembly) { let a = 0.75_f64.sqrt(); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "central".to_string(), "Central".to_string(), @@ -74,7 +74,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere(0.0, 0.0, 0.0, 1.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "assemb_plane".to_string(), "Assembly plane".to_string(), @@ -82,7 +82,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "side1".to_string(), "Side 1".to_string(), @@ -90,7 +90,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "side2".to_string(), "Side 2".to_string(), @@ -98,7 +98,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "side3".to_string(), "Side 3".to_string(), @@ -106,7 +106,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "corner1".to_string(), "Corner 1".to_string(), @@ -114,7 +114,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "corner2".to_string(), "Corner 2".to_string(), @@ -122,7 +122,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("corner3"), String::from("Corner 3"), @@ -148,6 +148,7 @@ pub fn AddRemove() -> View { let assembly = &state.assembly; // clear state + assembly.regulators.update(|regs| regs.clear()); assembly.elements.update(|elts| elts.clear()); assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); state.selection.update(|sel| sel.clear()); @@ -166,18 +167,7 @@ pub fn AddRemove() -> View { button( on:click=|_| { let state = use_context::(); - state.assembly.insert_new_element(); - - /* DEBUG */ - // print updated list of elements by identifier - console::log_1(&JsValue::from("elements by identifier:")); - for (id, key) in state.assembly.elements_by_id.get_clone().iter() { - console::log_3( - &JsValue::from(" "), - &JsValue::from(id), - &JsValue::from(*key) - ); - } + state.assembly.insert_new_sphere(); } ) { "+" } button( @@ -188,13 +178,20 @@ pub fn AddRemove() -> View { }, on:click=|_| { let state = use_context::(); - let subjects = state.selection.with( - |sel| { - let subject_vec: Vec<_> = sel.into_iter().collect(); - (subject_vec[0].clone(), subject_vec[1].clone()) - } + let subjects: [_; 2] = state.selection.with( + // the button is only enabled when two elements are + // selected, so we know the cast to a two-element array + // will succeed + |sel| sel + .clone() + .into_iter() + .collect::>() + .try_into() + .unwrap() + ); + state.assembly.insert_regulator( + InversiveDistanceRegulator::new(subjects, &state.assembly) ); - state.assembly.insert_new_regulator(subjects); state.selection.update(|sel| sel.clear()); } ) { "🔗" } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 18176df..5c926ca 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,12 +1,21 @@ use nalgebra::{DMatrix, DVector, DVectorView, Vector3}; use rustc_hash::FxHashMap; use slab::Slab; -use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; +use std::{collections::BTreeSet, rc::Rc, sync::atomic::{AtomicU64, Ordering}}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use crate::{ - engine::{Q, local_unif_to_std, realize_gram, ConfigSubspace, PartialMatrix}, + engine::{ + Q, + change_half_curvature, + local_unif_to_std, + realize_gram, + sphere, + ConfigSubspace, + ConstraintProblem + }, + outline::OutlineItem, specified::SpecifiedValue }; @@ -23,6 +32,10 @@ pub type ElementColor = [f32; 3]; // each assembly has a key that identifies it within the sesssion static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0); +pub trait ProblemPoser { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab); +} + #[derive(Clone, PartialEq)] pub struct Element { pub id: String, @@ -30,8 +43,8 @@ pub struct Element { pub color: ElementColor, pub representation: Signal>, - // All regulators with this element as a subject. The assembly owning - // this element is responsible for keeping this set up to date. + // the regulators this element is subject to. the assembly that owns the + // element is responsible for keeping this set up to date pub regulators: Signal>, // a serial number, assigned by `Element::new`, that uniquely identifies @@ -45,6 +58,8 @@ pub struct Element { } impl Element { + const CURVATURE_COMPONENT: usize = 3; + pub fn new( id: String, label: String, @@ -117,13 +132,148 @@ impl Element { } } -#[derive(Clone, Copy)] -pub struct Regulator { - pub subjects: (ElementKey, ElementKey), +impl ProblemPoser for Element { + fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab) { + let index = self.column_index.expect( + format!("Element \"{}\" should be indexed before writing problem data", self.id).as_str() + ); + problem.gram.push_sym(index, index, 1.0); + problem.guess.set_column(index, &self.representation.get_clone_untracked()); + } +} + +pub trait Regulator: ProblemPoser + OutlineItem { + fn subjects(&self) -> Vec; + fn measurement(&self) -> ReadSignal; + fn set_point(&self) -> Signal; + + // this method is used to responsively precondition the assembly for + // realization when the regulator becomes a constraint, or is edited while + // acting as a constraint. it should track the set point, do any desired + // preconditioning when the set point is present, and use its return value + // to report whether the set is present. the default implementation does no + // preconditioning + fn try_activate(&self, _assembly: &Assembly) -> bool { + self.set_point().with(|set_pt| set_pt.is_present()) + } +} + +pub struct InversiveDistanceRegulator { + pub subjects: [ElementKey; 2], pub measurement: ReadSignal, pub set_point: Signal } +impl InversiveDistanceRegulator { + pub fn new(subjects: [ElementKey; 2], assembly: &Assembly) -> InversiveDistanceRegulator { + let measurement = assembly.elements.map( + move |elts| { + let representations = subjects.map(|subj| elts[subj].representation); + representations[0].with(|rep_0| + representations[1].with(|rep_1| + rep_0.dot(&(&*Q * rep_1)) + ) + ) + } + ); + + let set_point = create_signal(SpecifiedValue::from_empty_spec()); + + InversiveDistanceRegulator { subjects, measurement, set_point } + } +} + +impl Regulator for InversiveDistanceRegulator { + fn subjects(&self) -> Vec { + self.subjects.into() + } + + fn measurement(&self) -> ReadSignal { + self.measurement + } + + fn set_point(&self) -> Signal { + self.set_point + } +} + +impl ProblemPoser for InversiveDistanceRegulator { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab) { + self.set_point.with_untracked(|set_pt| { + if let Some(val) = set_pt.value { + let [row, col] = self.subjects.map( + |subj| elts[subj].column_index.expect( + "Subjects should be indexed before inversive distance regulator writes problem data" + ) + ); + problem.gram.push_sym(row, col, val); + } + }); + } +} + +pub struct HalfCurvatureRegulator { + pub subject: ElementKey, + pub measurement: ReadSignal, + pub set_point: Signal +} + +impl HalfCurvatureRegulator { + pub fn new(subject: ElementKey, assembly: &Assembly) -> HalfCurvatureRegulator { + let measurement = assembly.elements.map( + move |elts| elts[subject].representation.with( + |rep| rep[Element::CURVATURE_COMPONENT] + ) + ); + + let set_point = create_signal(SpecifiedValue::from_empty_spec()); + + HalfCurvatureRegulator { subject, measurement, set_point } + } +} + +impl Regulator for HalfCurvatureRegulator { + fn subjects(&self) -> Vec { + vec![self.subject] + } + + fn measurement(&self) -> ReadSignal { + self.measurement + } + + fn set_point(&self) -> Signal { + self.set_point + } + + fn try_activate(&self, assembly: &Assembly) -> bool { + match self.set_point.with(|set_pt| set_pt.value) { + Some(half_curv) => { + let representation = assembly.elements.with_untracked( + |elts| elts[self.subject].representation + ); + representation.update( + |rep| change_half_curvature(rep, half_curv) + ); + true + } + None => false + } + } +} + +impl ProblemPoser for HalfCurvatureRegulator { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab) { + self.set_point.with_untracked(|set_pt| { + if let Some(val) = set_pt.value { + let col = elts[self.subject].column_index.expect( + "Subject should be indexed before half-curvature regulator writes problem data" + ); + problem.frozen.push(Element::CURVATURE_COMPONENT, col, val); + } + }); + } +} + // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { pub key: ElementKey, @@ -137,7 +287,7 @@ type AssemblyMotion<'a> = Vec>; pub struct Assembly { // elements and regulators pub elements: Signal>, - pub regulators: Signal>, + pub regulators: Signal>>, // solution variety tangent space. the basis vectors are stored in // configuration matrix format, ordered according to the elements' column @@ -167,26 +317,33 @@ impl Assembly { // --- inserting elements and regulators --- - // insert an element into the assembly without checking whether we already + // insert a sphere into the assembly without checking whether we already // have an element with the same identifier. any element that does have the // same identifier will get kicked out of the `elements_by_id` index - fn insert_element_unchecked(&self, elt: Element) { + fn insert_sphere_unchecked(&self, elt: Element) -> ElementKey { + // insert the sphere let id = elt.id.clone(); let key = self.elements.update(|elts| elts.insert(elt)); self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); + + // regulate the sphere's curvature + self.insert_regulator(HalfCurvatureRegulator::new(key, &self)); + + key } - pub fn try_insert_element(&self, elt: Element) -> bool { + pub fn try_insert_sphere(&self, elt: Element) -> Option { let can_insert = self.elements_by_id.with_untracked( |elts_by_id| !elts_by_id.contains_key(&elt.id) ); if can_insert { - self.insert_element_unchecked(elt); + Some(self.insert_sphere_unchecked(elt)) + } else { + None } - can_insert } - pub fn insert_new_element(&self) { + pub fn insert_new_sphere(&self) { // find the next unused identifier in the default sequence let mut id_num = 1; let mut id = format!("sphere{}", id_num); @@ -197,70 +354,69 @@ impl Assembly { id = format!("sphere{}", id_num); } - // create and insert a new element - self.insert_element_unchecked( + // create and insert a sphere + let _ = self.insert_sphere_unchecked( Element::new( id, format!("Sphere {}", id_num), [0.75_f32, 0.75_f32, 0.75_f32], - DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]) + sphere(0.0, 0.0, 0.0, 1.0) ) ); } - fn insert_regulator(&self, regulator: Regulator) { - let subjects = regulator.subjects; - let key = self.regulators.update(|regs| regs.insert(regulator)); - let subject_regulators = self.elements.with( - |elts| (elts[subjects.0].regulators, elts[subjects.1].regulators) + pub fn insert_regulator(&self, regulator: T) { + // add the regulator to the assembly's regulator list + let regulator_rc = Rc::new(regulator); + let key = self.regulators.update( + |regs| regs.insert(regulator_rc.clone()) ); - subject_regulators.0.update(|regs| regs.insert(key)); - subject_regulators.1.update(|regs| regs.insert(key)); - } - - pub fn insert_new_regulator(self, subjects: (ElementKey, ElementKey)) { - // create and insert a new regulator - let measurement = self.elements.map( - move |elts| { - let reps = ( - elts[subjects.0].representation.get_clone(), - elts[subjects.1].representation.get_clone() - ); - reps.0.dot(&(&*Q * reps.1)) + + // add the regulator to each subject's regulator list + let subjects = regulator_rc.subjects(); + let subject_regulators: Vec<_> = self.elements.with_untracked( + |elts| subjects.into_iter().map( + |subj| elts[subj].regulators + ).collect() + ); + for regulators in subject_regulators { + regulators.update(|regs| regs.insert(key)); + } + + // update the realization when the regulator becomes a constraint, or is + // edited while acting as a constraint + let self_for_effect = self.clone(); + create_effect(move || { + /* DEBUG */ + // log the regulator update + console::log_1(&JsValue::from( + format!("Updated regulator with subjects {:?}", regulator_rc.subjects()) + )); + + if regulator_rc.try_activate(&self_for_effect) { + self_for_effect.realize(); } - ); - let set_point = create_signal(SpecifiedValue::from_empty_spec()); - self.insert_regulator(Regulator { - subjects: subjects, - measurement: measurement, - set_point: set_point }); /* DEBUG */ // print an updated list of regulators console::log_1(&JsValue::from("Regulators:")); - self.regulators.with(|regs| { + self.regulators.with_untracked(|regs| { for (_, reg) in regs.into_iter() { - console::log_5( - &JsValue::from(" "), - &JsValue::from(reg.subjects.0), - &JsValue::from(reg.subjects.1), - &JsValue::from(":"), - ®.set_point.with_untracked( - |set_pt| JsValue::from(set_pt.spec.as_str()) + console::log_1(&JsValue::from(format!( + " {:?}: {}", + reg.subjects(), + reg.set_point().with_untracked( + |set_pt| { + let spec = &set_pt.spec; + if spec.is_empty() { + "__".to_string() + } else { + spec.clone() + } + } ) - ); - } - }); - - // update the realization when the regulator becomes a constraint, or is - // edited while acting as a constraint - create_effect(move || { - console::log_1(&JsValue::from( - format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) - )); - if set_point.with(|set_pt| set_pt.is_present()) { - self.realize(); + ))); } }); } @@ -275,55 +431,39 @@ impl Assembly { } }); - // set up the Gram matrix and the initial configuration matrix - let (gram, guess) = self.elements.with_untracked(|elts| { - // set up the off-diagonal part of the Gram matrix - let mut gram_to_be = PartialMatrix::new(); + // set up the constraint problem + let problem = self.elements.with_untracked(|elts| { + let mut problem = ConstraintProblem::new(elts.len()); + for (_, elt) in elts { + elt.pose(&mut problem, elts); + } self.regulators.with_untracked(|regs| { for (_, reg) in regs { - reg.set_point.with_untracked(|set_pt| { - if let Some(val) = set_pt.value { - let subjects = reg.subjects; - let row = elts[subjects.0].column_index.unwrap(); - let col = elts[subjects.1].column_index.unwrap(); - gram_to_be.push_sym(row, col, val); - } - }); + reg.pose(&mut problem, elts); } }); - - // set up the initial configuration matrix and the diagonal of the - // Gram matrix - let mut guess_to_be = DMatrix::::zeros(5, elts.len()); - for (_, elt) in elts { - let index = elt.column_index.unwrap(); - gram_to_be.push_sym(index, index, 1.0); - guess_to_be.set_column(index, &elt.representation.get_clone_untracked()); - } - - (gram_to_be, guess_to_be) + problem }); /* DEBUG */ // log the Gram matrix console::log_1(&JsValue::from("Gram matrix:")); - gram.log_to_console(); + problem.gram.log_to_console(); /* DEBUG */ // log the initial configuration matrix console::log_1(&JsValue::from("Old configuration:")); - for j in 0..guess.nrows() { + for j in 0..problem.guess.nrows() { let mut row_str = String::new(); - for k in 0..guess.ncols() { - row_str.push_str(format!(" {:>8.3}", guess[(j, k)]).as_str()); + for k in 0..problem.guess.ncols() { + row_str.push_str(format!(" {:>8.3}", problem.guess[(j, k)]).as_str()); } console::log_1(&JsValue::from(row_str)); } // look for a configuration with the given Gram matrix let (config, tangent, success, history) = realize_gram( - &gram, guess, &[], - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); /* DEBUG */ @@ -458,4 +598,48 @@ impl Assembly { // sync self.realize(); } +} + +#[cfg(test)] +mod tests { + use crate::engine; + + use super::*; + + #[test] + #[should_panic(expected = "Element \"sphere\" should be indexed before writing problem data")] + fn unindexed_element_test() { + let _ = create_root(|| { + Element::new( + "sphere".to_string(), + "Sphere".to_string(), + [1.0_f32, 1.0_f32, 1.0_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ).pose(&mut ConstraintProblem::new(1), &Slab::new()); + }); + } + + #[test] + #[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")] + fn unindexed_subject_test_inversive_distance() { + let _ = create_root(|| { + let mut elts = Slab::new(); + let subjects = [0, 1].map(|k| { + elts.insert( + Element::new( + format!("sphere{k}"), + format!("Sphere {k}"), + [1.0_f32, 1.0_f32, 1.0_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ) + ) + }); + elts[subjects[0]].column_index = Some(0); + InversiveDistanceRegulator { + subjects: subjects, + measurement: create_memo(|| 0.0), + set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()) + }.pose(&mut ConstraintProblem::new(2), &elts); + }); + } } \ No newline at end of file diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 35f898c..869a7de 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -35,9 +35,43 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 ]) } +// given a sphere's representation vector, change the sphere's half-curvature to +// `half-curv` and then restore normalization by contracting the representation +// vector toward the curvature axis +pub fn change_half_curvature(rep: &mut DVector, half_curv: f64) { + // set the sphere's half-curvature to the desired value + rep[3] = half_curv; + + // restore normalization by contracting toward the curvature axis + const SIZE_THRESHOLD: f64 = 1e-9; + let half_q_lt = -2.0 * half_curv * rep[4]; + let half_q_lt_sq = half_q_lt * half_q_lt; + let mut spatial = rep.fixed_rows_mut::<3>(0); + let q_sp = spatial.norm_squared(); + if q_sp < SIZE_THRESHOLD && half_q_lt_sq < SIZE_THRESHOLD { + spatial.copy_from_slice( + &[0.0, 0.0, (1.0 - 2.0 * half_q_lt).sqrt()] + ); + } else { + let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); + spatial.scale_mut(1.0 / scaling); + rep[4] /= scaling; + } + + /* DEBUG */ + // verify normalization + let rep_for_debug = rep.clone(); + console::log_1(&JsValue::from( + format!( + "Sphere self-product after curvature change: {}", + rep_for_debug.dot(&(&*Q * &rep_for_debug)) + ) + )); +} + // --- partial matrices --- -struct MatrixEntry { +pub struct MatrixEntry { index: (usize, usize), value: f64 } @@ -49,42 +83,72 @@ impl PartialMatrix { PartialMatrix(Vec::::new()) } - pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { + pub fn push(&mut self, row: usize, col: usize, value: f64) { let PartialMatrix(entries) = self; entries.push(MatrixEntry { index: (row, col), value: value }); + } + + pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { + self.push(row, col, value); if row != col { - entries.push(MatrixEntry { index: (col, row), value: value }); + self.push(col, row, value); } } /* DEBUG */ pub fn log_to_console(&self) { - let PartialMatrix(entries) = self; - for ent in entries { - let ent_str = format!(" {} {} {}", ent.index.0, ent.index.1, ent.value); - console::log_1(&JsValue::from(ent_str.as_str())); + for &MatrixEntry { index: (row, col), value } in self { + console::log_1(&JsValue::from( + format!(" {} {} {}", row, col, value) + )); } } + fn freeze(&self, a: &DMatrix) -> DMatrix { + let mut result = a.clone(); + for &MatrixEntry { index, value } in self { + result[index] = value; + } + result + } + fn proj(&self, a: &DMatrix) -> DMatrix { let mut result = DMatrix::::zeros(a.nrows(), a.ncols()); - let PartialMatrix(entries) = self; - for ent in entries { - result[ent.index] = a[ent.index]; + for &MatrixEntry { index, .. } in self { + result[index] = a[index]; } result } fn sub_proj(&self, rhs: &DMatrix) -> DMatrix { let mut result = DMatrix::::zeros(rhs.nrows(), rhs.ncols()); - let PartialMatrix(entries) = self; - for ent in entries { - result[ent.index] = ent.value - rhs[ent.index]; + for &MatrixEntry { index, value } in self { + result[index] = value - rhs[index]; } result } } +impl IntoIterator for PartialMatrix { + type Item = MatrixEntry; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + let PartialMatrix(entries) = self; + entries.into_iter() + } +} + +impl<'a> IntoIterator for &'a PartialMatrix { + type Item = &'a MatrixEntry; + type IntoIter = std::slice::Iter<'a, MatrixEntry>; + + fn into_iter(self) -> Self::IntoIter { + let PartialMatrix(entries) = self; + entries.into_iter() + } +} + // --- configuration subspaces --- #[derive(Clone)] @@ -195,6 +259,34 @@ impl DescentHistory { } } +// --- constraint problems --- + +pub struct ConstraintProblem { + pub gram: PartialMatrix, + pub frozen: PartialMatrix, + pub guess: DMatrix, +} + +impl ConstraintProblem { + pub fn new(element_count: usize) -> ConstraintProblem { + const ELEMENT_DIM: usize = 5; + ConstraintProblem { + gram: PartialMatrix::new(), + frozen: PartialMatrix::new(), + guess: DMatrix::::zeros(ELEMENT_DIM, element_count) + } + } + + #[cfg(feature = "dev")] + pub fn from_guess(guess_columns: &[DVector]) -> ConstraintProblem { + ConstraintProblem { + gram: PartialMatrix::new(), + frozen: PartialMatrix::new(), + guess: DMatrix::from_columns(guess_columns) + } + } +} + // --- gram matrix realization --- // the Lorentz form @@ -286,12 +378,12 @@ fn seek_better_config( None } -// seek a matrix `config` for which `config' * Q * config` matches the partial -// matrix `gram`. use gradient descent starting from `guess` +// seek a matrix `config` that matches the partial matrix `problem.frozen` and +// has `config' * Q * config` matching the partial matrix `problem.gram`. start +// at `problem.guess`, set the frozen entries to their desired values, and then +// use a regularized Newton's method to seek the desired Gram matrix pub fn realize_gram( - gram: &PartialMatrix, - guess: DMatrix, - frozen: &[(usize, usize)], + problem: &ConstraintProblem, scaled_tol: f64, min_efficiency: f64, backoff: f64, @@ -299,6 +391,11 @@ pub fn realize_gram( max_descent_steps: i32, max_backoff_steps: i32 ) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + // destructure the problem data + let ConstraintProblem { + gram, guess, frozen + } = problem; + // start the descent history let mut history = DescentHistory::new(); @@ -313,11 +410,11 @@ pub fn realize_gram( // convert the frozen indices to stacked format let frozen_stacked: Vec = frozen.into_iter().map( - |index| index.1*element_dim + index.0 + |MatrixEntry { index: (row, col), .. }| col*element_dim + row ).collect(); - // use Newton's method with backtracking and gradient descent backup - let mut state = SearchState::from_config(gram, guess); + // use a regularized Newton's method with backtracking + let mut state = SearchState::from_config(gram, frozen.freeze(guess)); let mut hess = DMatrix::zeros(element_dim, assembly_dim); for _ in 0..max_descent_steps { // find the negative gradient of the loss function @@ -415,7 +512,7 @@ pub fn realize_gram( #[cfg(feature = "dev")] pub mod examples { - use std::{array, f64::consts::PI}; + use std::f64::consts::PI; use super::*; @@ -428,35 +525,7 @@ pub mod examples { // https://www.nippon.com/en/japan-topics/c12801/ // pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for s in 0..9 { - // each sphere is represented by a spacelike vector - gram_to_be.push_sym(s, s, 1.0); - - // the circumscribing sphere is tangent to all of the other - // spheres, with matching orientation - if s > 0 { - gram_to_be.push_sym(0, s, 1.0); - } - - if s > 2 { - // each chain sphere is tangent to the "sun" and "moon" - // spheres, with opposing orientation - for n in 1..3 { - gram_to_be.push_sym(s, n, -1.0); - } - - // each chain sphere is tangent to the next chain sphere, - // with opposing orientation - let s_next = 3 + (s-2) % 6; - gram_to_be.push_sym(s, s_next, -1.0); - } - } - gram_to_be - }; - - let guess = DMatrix::from_columns( + let mut problem = ConstraintProblem::from_guess( [ sphere(0.0, 0.0, 0.0, 15.0), sphere(0.0, 0.0, -9.0, 5.0), @@ -471,42 +540,45 @@ pub mod examples { ).collect::>().as_slice() ); + for s in 0..9 { + // each sphere is represented by a spacelike vector + problem.gram.push_sym(s, s, 1.0); + + // the circumscribing sphere is tangent to all of the other + // spheres, with matching orientation + if s > 0 { + problem.gram.push_sym(0, s, 1.0); + } + + if s > 2 { + // each chain sphere is tangent to the "sun" and "moon" + // spheres, with opposing orientation + for n in 1..3 { + problem.gram.push_sym(s, n, -1.0); + } + + // each chain sphere is tangent to the next chain sphere, + // with opposing orientation + let s_next = 3 + (s-2) % 6; + problem.gram.push_sym(s, s_next, -1.0); + } + } + // the frozen entries fix the radii of the circumscribing sphere, the // "sun" and "moon" spheres, and one of the chain spheres - let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k)); + for k in 0..4 { + problem.frozen.push(3, k, problem.guess[(3, k)]); + } - realize_gram( - &gram, guess, &frozen, - scaled_tol, 0.5, 0.9, 1.1, 200, 110 - ) + realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110) } // set up a kaleidocycle, made of points with fixed distances between them, // and find its tangent space pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { - const N_POINTS: usize = 12; - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for block in (0..N_POINTS).step_by(2) { - let block_next = (block + 2) % N_POINTS; - for j in 0..2 { - // diagonal and hinge edges - for k in j..2 { - gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); - } - - // non-hinge edges - for k in 0..2 { - gram_to_be.push_sym(block + j, block_next + k, -0.625); - } - } - } - gram_to_be - }; - - let guess = { - const N_HINGES: usize = 6; - let guess_elts = (0..N_HINGES).step_by(2).flat_map( + const N_HINGES: usize = 6; + let mut problem = ConstraintProblem::from_guess( + (0..N_HINGES).step_by(2).flat_map( |n| { let ang_hor = (n as f64) * PI/3.0; let ang_vert = ((n + 1) as f64) * PI/3.0; @@ -519,16 +591,30 @@ pub mod examples { point(x_vert, y_vert, 0.5) ] } - ).collect::>(); - DMatrix::from_columns(&guess_elts) - }; + ).collect::>().as_slice() + ); - let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k)); + const N_POINTS: usize = 2 * N_HINGES; + for block in (0..N_POINTS).step_by(2) { + let block_next = (block + 2) % N_POINTS; + for j in 0..2 { + // diagonal and hinge edges + for k in j..2 { + problem.gram.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); + } + + // non-hinge edges + for k in 0..2 { + problem.gram.push_sym(block + j, block_next + k, -0.625); + } + } + } - realize_gram( - &gram, guess, &frozen, - scaled_tol, 0.5, 0.9, 1.1, 200, 110 - ) + for k in 0..N_POINTS { + problem.frozen.push(3, k, problem.guess[(3, k)]) + } + + realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110) } } @@ -539,6 +625,25 @@ mod tests { use super::{*, examples::*}; + #[test] + fn freeze_test() { + let frozen = PartialMatrix(vec![ + MatrixEntry { index: (0, 0), value: 14.0 }, + MatrixEntry { index: (0, 2), value: 28.0 }, + MatrixEntry { index: (1, 1), value: 42.0 }, + MatrixEntry { index: (1, 2), value: 49.0 } + ]); + let config = DMatrix::::from_row_slice(2, 3, &[ + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0 + ]); + let expected_result = DMatrix::::from_row_slice(2, 3, &[ + 14.0, 2.0, 28.0, + 4.0, 42.0, 49.0 + ]); + assert_eq!(frozen.freeze(&config), expected_result); + } + #[test] fn sub_proj_test() { let target = PartialMatrix(vec![ @@ -560,18 +665,12 @@ mod tests { #[test] fn zero_loss_test() { - let gram = PartialMatrix({ - let mut entries = Vec::::new(); - for j in 0..3 { - for k in 0..3 { - entries.push(MatrixEntry { - index: (j, k), - value: if j == k { 1.0 } else { -1.0 } - }); - } + let mut gram = PartialMatrix::new(); + for j in 0..3 { + for k in 0..3 { + gram.push(j, k, if j == k { 1.0 } else { -1.0 }); } - entries - }); + } let config = { let a = 0.75_f64.sqrt(); DMatrix::from_columns(&[ @@ -584,37 +683,33 @@ mod tests { assert!(state.loss.abs() < f64::EPSILON); } + /* TO DO */ // at the frozen indices, the optimization steps should have exact zeros, - // and the realized configuration should match the initial guess + // and the realized configuration should have the desired values #[test] fn frozen_entry_test() { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..2 { - for k in j..2 { - gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); - } - } - gram_to_be - }; - let guess = DMatrix::from_columns(&[ + let mut problem = ConstraintProblem::from_guess(&[ point(0.0, 0.0, 2.0), - sphere(0.0, 0.0, 0.0, 1.0) + sphere(0.0, 0.0, 0.0, 0.95) ]); - let frozen = [(3, 0), (3, 1)]; - println!(); + for j in 0..2 { + for k in j..2 { + problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); + } + } + problem.frozen.push(3, 0, problem.guess[(3, 0)]); + problem.frozen.push(3, 1, 0.5); let (config, _, success, history) = realize_gram( - &gram, guess.clone(), &frozen, - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); assert_eq!(success, true); for base_step in history.base_step.into_iter() { - for index in frozen { + for &MatrixEntry { index, .. } in &problem.frozen { assert_eq!(base_step[index], 0.0); } } - for index in frozen { - assert_eq!(config[index], guess[index]); + for MatrixEntry { index, value } in problem.frozen { + assert_eq!(config[index], value); } } @@ -635,34 +730,32 @@ mod tests { #[test] fn tangent_test_three_spheres() { const SCALED_TOL: f64 = 1.0e-12; - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..3 { - for k in j..3 { - gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); - } - } - gram_to_be - }; - let guess = DMatrix::from_columns(&[ + const ELEMENT_DIM: usize = 5; + let mut problem = ConstraintProblem::from_guess(&[ sphere(0.0, 0.0, 0.0, -2.0), sphere(0.0, 0.0, 1.0, 1.0), sphere(0.0, 0.0, -1.0, 1.0) ]); - let frozen: [_; 5] = std::array::from_fn(|k| (k, 0)); + for j in 0..3 { + for k in j..3 { + problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); + } + } + for n in 0..ELEMENT_DIM { + problem.frozen.push(n, 0, problem.guess[(n, 0)]); + } let (config, tangent, success, history) = realize_gram( - &gram, guess.clone(), &frozen, - SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + &problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); - assert_eq!(config, guess); + assert_eq!(config, problem.guess); assert_eq!(success, true); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of // the solution variety const UNIFORM_DIM: usize = 4; - let element_dim = guess.nrows(); - let assembly_dim = guess.ncols(); + let element_dim = problem.guess.nrows(); + let assembly_dim = problem.guess.ncols(); let tangent_motions_unif = vec![ basis_matrix((0, 1), UNIFORM_DIM, assembly_dim), basis_matrix((1, 1), UNIFORM_DIM, assembly_dim), @@ -805,22 +898,17 @@ mod tests { fn proj_equivar_test() { // find a pair of spheres that meet at 120° const SCALED_TOL: f64 = 1.0e-12; - let gram = { - let mut gram_to_be = PartialMatrix::new(); - gram_to_be.push_sym(0, 0, 1.0); - gram_to_be.push_sym(1, 1, 1.0); - gram_to_be.push_sym(0, 1, 0.5); - gram_to_be - }; - let guess_orig = DMatrix::from_columns(&[ + let mut problem_orig = ConstraintProblem::from_guess(&[ sphere(0.0, 0.0, 0.5, 1.0), sphere(0.0, 0.0, -0.5, 1.0) ]); + problem_orig.gram.push_sym(0, 0, 1.0); + problem_orig.gram.push_sym(1, 1, 1.0); + problem_orig.gram.push_sym(0, 1, 0.5); let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram( - &gram, guess_orig.clone(), &[], - SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + &problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); - assert_eq!(config_orig, guess_orig); + assert_eq!(config_orig, problem_orig.guess); assert_eq!(success_orig, true); assert_eq!(history_orig.scaled_loss.len(), 1); @@ -833,11 +921,15 @@ mod tests { sphere(-a, 0.0, 7.0 - a, 1.0) ]) }; + let problem_tfm = ConstraintProblem { + gram: problem_orig.gram, + guess: guess_tfm, + frozen: problem_orig.frozen + }; let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram( - &gram, guess_tfm.clone(), &[], - SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + &problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); - assert_eq!(config_tfm, guess_tfm); + assert_eq!(config_tfm, problem_tfm.guess); assert_eq!(success_tfm, true); assert_eq!(history_tfm.scaled_loss.len(), 1); @@ -869,7 +961,7 @@ mod tests { // the comparison tolerance because the transformation seems to // introduce some numerical error const SCALED_TOL_TFM: f64 = 1.0e-9; - let tol_sq = ((guess_orig.nrows() * guess_orig.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM; + let tol_sq = ((problem_orig.guess.nrows() * problem_orig.guess.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM; assert!((motion_proj_tfm - motion_tfm_proj).norm_squared() < tol_sq); } } \ No newline at end of file diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 002baea..2446337 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -1,4 +1,5 @@ use itertools::Itertools; +use std::rc::Rc; use sycamore::prelude::*; use web_sys::{ KeyboardEvent, @@ -9,24 +10,38 @@ use web_sys::{ use crate::{ AppState, assembly, - assembly::{ElementKey, Regulator, RegulatorKey}, + assembly::{ + ElementKey, + HalfCurvatureRegulator, + InversiveDistanceRegulator, + Regulator, + RegulatorKey + }, specified::SpecifiedValue }; // an editable view of a regulator #[component(inline_props)] -fn RegulatorInput(regulator: Regulator) -> View { +fn RegulatorInput(regulator: Rc) -> View { + // get the regulator's measurement and set point signals + let measurement = regulator.measurement(); + let set_point = regulator.set_point(); + + // the `valid` signal tracks whether the last entered value is a valid set + // point specification let valid = create_signal(true); + + // the `value` signal holds the current set point specification let value = create_signal( - regulator.set_point.with_untracked(|set_pt| set_pt.spec.clone()) + set_point.with_untracked(|set_pt| set_pt.spec.clone()) ); - // this closure resets the input value to the regulator's set point - // specification + // this `reset_value` closure resets the input value to the regulator's set + // point specification let reset_value = move || { batch(|| { valid.set(true); - value.set(regulator.set_point.with(|set_pt| set_pt.spec.clone())); + value.set(set_point.with(|set_pt| set_pt.spec.clone())); }) }; @@ -39,7 +54,7 @@ fn RegulatorInput(regulator: Regulator) -> View { r#type="text", class=move || { if valid.get() { - regulator.set_point.with(|set_pt| { + set_point.with(|set_pt| { if set_pt.is_present() { "regulator-input constraint" } else { @@ -50,13 +65,13 @@ fn RegulatorInput(regulator: Regulator) -> View { "regulator-input invalid" } }, - placeholder=regulator.measurement.with(|result| result.to_string()), + placeholder=measurement.with(|result| result.to_string()), bind:value=value, on:change=move |_| { valid.set( match SpecifiedValue::try_from(value.get_clone_untracked()) { Ok(set_pt) => { - regulator.set_point.set(set_pt); + set_point.set(set_pt); true } Err(_) => false @@ -75,26 +90,53 @@ fn RegulatorInput(regulator: Regulator) -> View { } } +pub trait OutlineItem { + fn outline_item(self: Rc, element_key: ElementKey) -> View; +} + +impl OutlineItem for InversiveDistanceRegulator { + fn outline_item(self: Rc, element_key: ElementKey) -> View { + let state = use_context::(); + let other_subject = if self.subjects[0] == element_key { + self.subjects[1] + } else { + self.subjects[0] + }; + let other_subject_label = state.assembly.elements.with( + |elts| elts[other_subject].label.clone() + ); + view! { + li(class="regulator") { + div(class="regulator-label") { (other_subject_label) } + div(class="regulator-type") { "Inversive distance" } + RegulatorInput(regulator=self) + div(class="status") + } + } + } +} + +impl OutlineItem for HalfCurvatureRegulator { + fn outline_item(self: Rc, _element_key: ElementKey) -> View { + view! { + li(class="regulator") { + div(class="regulator-label") // for spacing + div(class="regulator-type") { "Half-curvature" } + RegulatorInput(regulator=self) + div(class="status") + } + } + } +} + // a list item that shows a regulator in an outline view of an element #[component(inline_props)] fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View { let state = use_context::(); - let assembly = &state.assembly; - let regulator = assembly.regulators.with(|regs| regs[regulator_key]); - let other_subject = if regulator.subjects.0 == element_key { - regulator.subjects.1 - } else { - regulator.subjects.0 - }; - let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); - view! { - li(class="regulator") { - div(class="regulator-label") { (other_subject_label) } - div(class="regulator-type") { "Inversive distance" } - RegulatorInput(regulator=regulator) - div(class="status") - } - } + let regulator = state.assembly.regulators.with( + |regs| regs[regulator_key].clone() + ); + regulator.outline_item(element_key) } // a list item that shows an element in an outline view of an assembly @@ -117,7 +159,15 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { }; let regulated = element.regulators.map(|regs| regs.len() > 0); let regulator_list = element.regulators.map( - |regs| regs.clone().into_iter().collect() + move |elt_reg_keys| elt_reg_keys + .clone() + .into_iter() + .sorted_by_key( + |®_key| state.assembly.regulators.with( + |regs| regs[reg_key].subjects().len() + ) + ) + .collect() ); let details_node = create_node_ref(); view! { From a2478febc128b218d833bc3e904482f2055865cb Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Thu, 1 May 2025 19:25:13 +0000 Subject: [PATCH 126/132] feat: Points (#82) Replaces the former sole Element entity by two, Sphere and Point, both implementing an Element trait. Adds Point display, uses the former Element display for Sphere. Adds a new "canned" configuration, and the ability to add, select, and nudge Point entities. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/82 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/main.css | 4 +- app-proto/src/add_remove.rs | 116 ++-- app-proto/src/assembly.rs | 435 ++++++++++----- app-proto/src/display.rs | 524 +++++++++++++----- app-proto/src/engine.rs | 1 - app-proto/src/outline.rs | 23 +- app-proto/src/point.frag | 18 + app-proto/src/point.vert | 24 + .../src/{inversive.frag => spheres.frag} | 0 9 files changed, 815 insertions(+), 330 deletions(-) create mode 100644 app-proto/src/point.frag create mode 100644 app-proto/src/point.vert rename app-proto/src/{inversive.frag => spheres.frag} (100%) diff --git a/app-proto/main.css b/app-proto/main.css index 4726a27..f787535 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -42,9 +42,7 @@ body { } #add-remove > button { - width: 32px; height: 32px; - font-size: large; } /* KLUDGE */ @@ -53,7 +51,9 @@ body { buttons need to be displayed in an emoji font */ #add-remove > button.emoji { + width: 32px; font-family: 'Noto Emoji', sans-serif; + font-size: large; } /* outline */ diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 14fcd41..ea86186 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -1,58 +1,59 @@ +use std::{f64::consts::FRAC_1_SQRT_2, rc::Rc}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ engine, AppState, - assembly::{Assembly, Element, InversiveDistanceRegulator} + assembly::{Assembly, InversiveDistanceRegulator, Point, Sphere} }; /* DEBUG */ // load an example assembly for testing. this code will be removed once we've // built a more formal test assembly system fn load_gen_assemb(assembly: &Assembly) { - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("gemini_a"), String::from("Castor"), [1.00_f32, 0.25_f32, 0.00_f32], engine::sphere(0.5, 0.5, 0.0, 1.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("gemini_b"), String::from("Pollux"), [0.00_f32, 0.25_f32, 1.00_f32], engine::sphere(-0.5, -0.5, 0.0, 1.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("ursa_major"), String::from("Ursa major"), [0.25_f32, 0.00_f32, 1.00_f32], engine::sphere(-0.5, 0.5, 0.0, 0.75) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("ursa_minor"), String::from("Ursa minor"), [0.25_f32, 1.00_f32, 0.00_f32], engine::sphere(0.5, -0.5, 0.0, 0.5) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("moon_deimos"), String::from("Deimos"), [0.75_f32, 0.75_f32, 0.00_f32], engine::sphere(0.0, 0.15, 1.0, 0.25) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("moon_phobos"), String::from("Phobos"), [0.00_f32, 0.75_f32, 0.50_f32], @@ -66,64 +67,64 @@ fn load_gen_assemb(assembly: &Assembly) { // built a more formal test assembly system fn load_low_curv_assemb(assembly: &Assembly) { let a = 0.75_f64.sqrt(); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "central".to_string(), "Central".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], engine::sphere(0.0, 0.0, 0.0, 1.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "assemb_plane".to_string(), "Assembly plane".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "side1".to_string(), "Side 1".to_string(), [1.00_f32, 0.00_f32, 0.25_f32], engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "side2".to_string(), "Side 2".to_string(), [0.25_f32, 1.00_f32, 0.00_f32], engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "side3".to_string(), "Side 3".to_string(), [0.00_f32, 0.25_f32, 1.00_f32], engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "corner1".to_string(), "Corner 1".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "corner2".to_string(), "Corner 2".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("corner3"), String::from("Corner 3"), [0.75_f32, 0.75_f32, 0.75_f32], @@ -132,6 +133,49 @@ fn load_low_curv_assemb(assembly: &Assembly) { ); } +fn load_pointed_assemb(assembly: &Assembly) { + let _ = assembly.try_insert_element( + Point::new( + format!("point_front"), + format!("Front point"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::point(0.0, 0.0, FRAC_1_SQRT_2) + ) + ); + let _ = assembly.try_insert_element( + Point::new( + format!("point_back"), + format!("Back point"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::point(0.0, 0.0, -FRAC_1_SQRT_2) + ) + ); + for index_x in 0..=1 { + for index_y in 0..=1 { + let x = index_x as f64 - 0.5; + let y = index_y as f64 - 0.5; + + let _ = assembly.try_insert_element( + Sphere::new( + format!("sphere{index_x}{index_y}"), + format!("Sphere {index_x}{index_y}"), + [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], + engine::sphere(x, y, 0.0, 1.0) + ) + ); + + let _ = assembly.try_insert_element( + Point::new( + format!("point{index_x}{index_y}"), + format!("Point {index_x}{index_y}"), + [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], + engine::point(x, y, 0.0) + ) + ); + } + } +} + #[component] pub fn AddRemove() -> View { /* DEBUG */ @@ -157,6 +201,7 @@ pub fn AddRemove() -> View { match name.as_str() { "general" => load_gen_assemb(assembly), "low-curv" => load_low_curv_assemb(assembly), + "pointed" => load_pointed_assemb(assembly), _ => () }; }); @@ -167,9 +212,15 @@ pub fn AddRemove() -> View { button( on:click=|_| { let state = use_context::(); - state.assembly.insert_new_sphere(); + state.assembly.insert_element_default::(); } - ) { "+" } + ) { "Add sphere" } + button( + on:click=|_| { + let state = use_context::(); + state.assembly.insert_element_default::(); + } + ) { "Add point" } button( class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button disabled={ @@ -190,7 +241,7 @@ pub fn AddRemove() -> View { .unwrap() ); state.assembly.insert_regulator( - InversiveDistanceRegulator::new(subjects, &state.assembly) + Rc::new(InversiveDistanceRegulator::new(subjects, &state.assembly)) ); state.selection.update(|sel| sel.clear()); } @@ -198,6 +249,7 @@ pub fn AddRemove() -> View { select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser option(value="general") { "General" } option(value="low-curv") { "Low-curvature" } + option(value="pointed") { "Pointed" } option(value="empty") { "Empty" } } } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 5c926ca..343cef8 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,15 +1,23 @@ -use nalgebra::{DMatrix, DVector, DVectorView, Vector3}; +use nalgebra::{DMatrix, DVector, DVectorView}; use rustc_hash::FxHashMap; use slab::Slab; -use std::{collections::BTreeSet, rc::Rc, sync::atomic::{AtomicU64, Ordering}}; +use std::{ + any::{Any, TypeId}, + cell::Cell, + collections::BTreeSet, + rc::Rc, + sync::atomic::{AtomicU64, Ordering} +}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use crate::{ + display::DisplayItem, engine::{ Q, change_half_curvature, local_unif_to_std, + point, realize_gram, sphere, ConfigSubspace, @@ -33,31 +41,87 @@ pub type ElementColor = [f32; 3]; static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0); pub trait ProblemPoser { - fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab); + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>); } -#[derive(Clone, PartialEq)] -pub struct Element { +pub trait Element: ProblemPoser + DisplayItem { + // the default identifier for an element of this type + fn default_id() -> String where Self: Sized; + + // create the default example of an element of this type + fn default(id: String, id_num: u64) -> Self where Self: Sized; + + // the regulators that should be created when an element of this type is + // inserted into the given assembly with the given storage key + /* KLUDGE */ + // right now, this organization makes sense because regulators identify + // their subjects by storage key, so the element has to be inserted before + // its regulators can be created. if we change the way regulators identify + // their subjects, we should consider refactoring + fn default_regulators(_key: ElementKey, _assembly: &Assembly) -> Vec> where Self: Sized { + Vec::new() + } + + fn id(&self) -> &String; + fn label(&self) -> &String; + fn representation(&self) -> Signal>; + + // the regulators the element is subject to. the assembly that owns the + // element is responsible for keeping this set up to date + fn regulators(&self) -> Signal>; + + // a serial number that uniquely identifies this element + fn serial(&self) -> u64; + + // take the next serial number, panicking if that was the last one left + fn next_serial() -> u64 where Self: Sized { + // the technique we use to panic on overflow is taken from _Rust Atomics + // and Locks_, by Mara Bos + // + // https://marabos.nl/atomics/atomics.html#example-handle-overflow + // + NEXT_ELEMENT_SERIAL.fetch_update( + Ordering::SeqCst, Ordering::SeqCst, + |serial| serial.checked_add(1) + ).expect("Out of serial numbers for elements") + } + + // the configuration matrix column index that was assigned to the element + // last time the assembly was realized, or `None` if the element has never + // been through a realization + fn column_index(&self) -> Option; + + // assign the element a configuration matrix column index. this method must + // be used carefully to preserve invariant (1), described in the comment on + // the `tangent` field of the `Assembly` structure + fn set_column_index(&self, index: usize); +} + +// the `Element` trait needs to be dyn-compatible, so its method signatures can +// only use `Self` in the type of the receiver. that means `Element` can't +// implement `PartialEq`. if you need partial equivalence for `Element` trait +// objects, use this wrapper +#[derive(Clone)] +pub struct ElementRc(pub Rc); + +impl PartialEq for ElementRc { + fn eq(&self, ElementRc(other): &Self) -> bool { + let ElementRc(rc) = self; + Rc::ptr_eq(rc, &other) + } +} + +pub struct Sphere { pub id: String, pub label: String, pub color: ElementColor, pub representation: Signal>, - - // the regulators this element is subject to. the assembly that owns the - // element is responsible for keeping this set up to date pub regulators: Signal>, - - // a serial number, assigned by `Element::new`, that uniquely identifies - // each element pub serial: u64, - - // the configuration matrix column index that was assigned to this element - // last time the assembly was realized, or `None` if the element has never - // been through a realization - column_index: Option + column_index: Cell> } -impl Element { +impl Sphere { const CURVATURE_COMPONENT: usize = 3; pub fn new( @@ -65,83 +129,161 @@ impl Element { label: String, color: ElementColor, representation: DVector - ) -> Element { - // take the next serial number, panicking if that was the last number we - // had left. the technique we use to panic on overflow is taken from - // _Rust Atomics and Locks_, by Mara Bos - // - // https://marabos.nl/atomics/atomics.html#example-handle-overflow - // - let serial = NEXT_ELEMENT_SERIAL.fetch_update( - Ordering::SeqCst, Ordering::SeqCst, - |serial| serial.checked_add(1) - ).expect("Out of serial numbers for elements"); - - Element { + ) -> Sphere { + Sphere { id: id, label: label, color: color, representation: create_signal(representation), regulators: create_signal(BTreeSet::default()), - serial: serial, - column_index: None - } - } - - // the smallest positive depth, represented as a multiple of `dir`, where - // the line generated by `dir` hits the element (which is assumed to be a - // sphere). returns `None` if the line misses the sphere. this function - // should be kept synchronized with `sphere_cast` in `inversive.frag`, which - // does essentially the same thing on the GPU side - pub fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix) -> Option { - // if `a/b` is less than this threshold, we approximate - // `a*u^2 + b*u + c` by the linear function `b*u + c` - const DEG_THRESHOLD: f64 = 1e-9; - - let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); - let a = -rep[3] * dir.norm_squared(); - let b = rep.rows_range(..3).dot(&dir); - let c = -rep[4]; - - let adjust = 4.0*a*c/(b*b); - if adjust < 1.0 { - // as long as `b` is non-zero, the linear approximation of - // - // a*u^2 + b*u + c - // - // at `u = 0` will reach zero at a finite depth `u_lin`. the root of - // the quadratic adjacent to `u_lin` is stored in `lin_root`. if - // both roots have the same sign, `lin_root` will be the one closer - // to `u = 0` - let square_rect_ratio = 1.0 + (1.0 - adjust).sqrt(); - let lin_root = -(2.0*c)/b / square_rect_ratio; - if a.abs() > DEG_THRESHOLD * b.abs() { - if lin_root > 0.0 { - Some(lin_root) - } else { - let other_root = -b/(2.*a) * square_rect_ratio; - (other_root > 0.0).then_some(other_root) - } - } else { - (lin_root > 0.0).then_some(lin_root) - } - } else { - // the line through `dir` misses the sphere completely - None + serial: Self::next_serial(), + column_index: None.into() } } } -impl ProblemPoser for Element { - fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab) { - let index = self.column_index.expect( - format!("Element \"{}\" should be indexed before writing problem data", self.id).as_str() +impl Element for Sphere { + fn default_id() -> String { + "sphere".to_string() + } + + fn default(id: String, id_num: u64) -> Sphere { + Sphere::new( + id, + format!("Sphere {id_num}"), + [0.75_f32, 0.75_f32, 0.75_f32], + sphere(0.0, 0.0, 0.0, 1.0) + ) + } + + fn default_regulators(key: ElementKey, assembly: &Assembly) -> Vec> { + vec![Rc::new(HalfCurvatureRegulator::new(key, assembly))] + } + + fn id(&self) -> &String { + &self.id + } + + fn label(&self) -> &String { + &self.label + } + + fn representation(&self) -> Signal> { + self.representation + } + + fn regulators(&self) -> Signal> { + self.regulators + } + + fn serial(&self) -> u64 { + self.serial + } + + fn column_index(&self) -> Option { + self.column_index.get() + } + + fn set_column_index(&self, index: usize) { + self.column_index.set(Some(index)); + } +} + +impl ProblemPoser for Sphere { + fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab>) { + let index = self.column_index().expect( + format!("Sphere \"{}\" should be indexed before writing problem data", self.id).as_str() ); problem.gram.push_sym(index, index, 1.0); problem.guess.set_column(index, &self.representation.get_clone_untracked()); } } +pub struct Point { + pub id: String, + pub label: String, + pub color: ElementColor, + pub representation: Signal>, + pub regulators: Signal>, + pub serial: u64, + column_index: Cell> +} + +impl Point { + const WEIGHT_COMPONENT: usize = 3; + + pub fn new( + id: String, + label: String, + color: ElementColor, + representation: DVector + ) -> Point { + Point { + id, + label, + color, + representation: create_signal(representation), + regulators: create_signal(BTreeSet::default()), + serial: Self::next_serial(), + column_index: None.into() + } + } +} + +impl Element for Point { + fn default_id() -> String { + "point".to_string() + } + + fn default(id: String, id_num: u64) -> Point { + Point::new( + id, + format!("Point {id_num}"), + [0.75_f32, 0.75_f32, 0.75_f32], + point(0.0, 0.0, 0.0) + ) + } + + fn id(&self) -> &String { + &self.id + } + + fn label(&self) -> &String { + &self.label + } + + fn representation(&self) -> Signal> { + self.representation + } + + fn regulators(&self) -> Signal> { + self.regulators + } + + fn serial(&self) -> u64 { + self.serial + } + + fn column_index(&self) -> Option { + self.column_index.get() + } + + fn set_column_index(&self, index: usize) { + self.column_index.set(Some(index)); + } +} + +impl ProblemPoser for Point { + fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab>) { + let index = self.column_index().expect( + format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str() + ); + problem.gram.push_sym(index, index, 0.0); + problem.frozen.push(Point::WEIGHT_COMPONENT, index, 0.5); + problem.guess.set_column(index, &self.representation.get_clone_untracked()); + } +} + pub trait Regulator: ProblemPoser + OutlineItem { fn subjects(&self) -> Vec; fn measurement(&self) -> ReadSignal; @@ -168,7 +310,7 @@ impl InversiveDistanceRegulator { pub fn new(subjects: [ElementKey; 2], assembly: &Assembly) -> InversiveDistanceRegulator { let measurement = assembly.elements.map( move |elts| { - let representations = subjects.map(|subj| elts[subj].representation); + let representations = subjects.map(|subj| elts[subj].representation()); representations[0].with(|rep_0| representations[1].with(|rep_1| rep_0.dot(&(&*Q * rep_1)) @@ -198,11 +340,11 @@ impl Regulator for InversiveDistanceRegulator { } impl ProblemPoser for InversiveDistanceRegulator { - fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab) { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { let [row, col] = self.subjects.map( - |subj| elts[subj].column_index.expect( + |subj| elts[subj].column_index().expect( "Subjects should be indexed before inversive distance regulator writes problem data" ) ); @@ -221,8 +363,8 @@ pub struct HalfCurvatureRegulator { impl HalfCurvatureRegulator { pub fn new(subject: ElementKey, assembly: &Assembly) -> HalfCurvatureRegulator { let measurement = assembly.elements.map( - move |elts| elts[subject].representation.with( - |rep| rep[Element::CURVATURE_COMPONENT] + move |elts| elts[subject].representation().with( + |rep| rep[Sphere::CURVATURE_COMPONENT] ) ); @@ -249,7 +391,7 @@ impl Regulator for HalfCurvatureRegulator { match self.set_point.with(|set_pt| set_pt.value) { Some(half_curv) => { let representation = assembly.elements.with_untracked( - |elts| elts[self.subject].representation + |elts| elts[self.subject].representation() ); representation.update( |rep| change_half_curvature(rep, half_curv) @@ -262,13 +404,13 @@ impl Regulator for HalfCurvatureRegulator { } impl ProblemPoser for HalfCurvatureRegulator { - fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab) { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { - let col = elts[self.subject].column_index.expect( + let col = elts[self.subject].column_index().expect( "Subject should be indexed before half-curvature regulator writes problem data" ); - problem.frozen.push(Element::CURVATURE_COMPONENT, col, val); + problem.frozen.push(Sphere::CURVATURE_COMPONENT, col, val); } }); } @@ -286,7 +428,7 @@ type AssemblyMotion<'a> = Vec>; #[derive(Clone)] pub struct Assembly { // elements and regulators - pub elements: Signal>, + pub elements: Signal>>, pub regulators: Signal>>, // solution variety tangent space. the basis vectors are stored in @@ -317,66 +459,61 @@ impl Assembly { // --- inserting elements and regulators --- - // insert a sphere into the assembly without checking whether we already + // insert an element into the assembly without checking whether we already // have an element with the same identifier. any element that does have the // same identifier will get kicked out of the `elements_by_id` index - fn insert_sphere_unchecked(&self, elt: Element) -> ElementKey { - // insert the sphere - let id = elt.id.clone(); - let key = self.elements.update(|elts| elts.insert(elt)); + fn insert_element_unchecked(&self, elt: T) -> ElementKey { + // insert the element + let id = elt.id().clone(); + let key = self.elements.update(|elts| elts.insert(Rc::new(elt))); self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); - // regulate the sphere's curvature - self.insert_regulator(HalfCurvatureRegulator::new(key, &self)); + // create and insert the element's default regulators + for reg in T::default_regulators(key, &self) { + self.insert_regulator(reg); + } key } - pub fn try_insert_sphere(&self, elt: Element) -> Option { + pub fn try_insert_element(&self, elt: impl Element + 'static) -> Option { let can_insert = self.elements_by_id.with_untracked( - |elts_by_id| !elts_by_id.contains_key(&elt.id) + |elts_by_id| !elts_by_id.contains_key(elt.id()) ); if can_insert { - Some(self.insert_sphere_unchecked(elt)) + Some(self.insert_element_unchecked(elt)) } else { None } } - pub fn insert_new_sphere(&self) { + pub fn insert_element_default(&self) { // find the next unused identifier in the default sequence + let default_id = T::default_id(); let mut id_num = 1; - let mut id = format!("sphere{}", id_num); + let mut id = format!("{default_id}{id_num}"); while self.elements_by_id.with_untracked( |elts_by_id| elts_by_id.contains_key(&id) ) { id_num += 1; - id = format!("sphere{}", id_num); + id = format!("{default_id}{id_num}"); } - // create and insert a sphere - let _ = self.insert_sphere_unchecked( - Element::new( - id, - format!("Sphere {}", id_num), - [0.75_f32, 0.75_f32, 0.75_f32], - sphere(0.0, 0.0, 0.0, 1.0) - ) - ); + // create and insert the default example of `T` + let _ = self.insert_element_unchecked(T::default(id, id_num)); } - pub fn insert_regulator(&self, regulator: T) { + pub fn insert_regulator(&self, regulator: Rc) { // add the regulator to the assembly's regulator list - let regulator_rc = Rc::new(regulator); let key = self.regulators.update( - |regs| regs.insert(regulator_rc.clone()) + |regs| regs.insert(regulator.clone()) ); // add the regulator to each subject's regulator list - let subjects = regulator_rc.subjects(); + let subjects = regulator.subjects(); let subject_regulators: Vec<_> = self.elements.with_untracked( |elts| subjects.into_iter().map( - |subj| elts[subj].regulators + |subj| elts[subj].regulators() ).collect() ); for regulators in subject_regulators { @@ -390,10 +527,10 @@ impl Assembly { /* DEBUG */ // log the regulator update console::log_1(&JsValue::from( - format!("Updated regulator with subjects {:?}", regulator_rc.subjects()) + format!("Updated regulator with subjects {:?}", regulator.subjects()) )); - if regulator_rc.try_activate(&self_for_effect) { + if regulator.try_activate(&self_for_effect) { self_for_effect.realize(); } }); @@ -427,7 +564,7 @@ impl Assembly { // index the elements self.elements.update_silent(|elts| { for (index, (_, elt)) in elts.into_iter().enumerate() { - elt.column_index = Some(index); + elt.set_column_index(index); } }); @@ -482,8 +619,8 @@ impl Assembly { if success { // read out the solution for (_, elt) in self.elements.get_clone_untracked() { - elt.representation.update( - |rep| rep.set_column(0, &config.column(elt.column_index.unwrap())) + elt.representation().update( + |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) ); } @@ -521,8 +658,8 @@ impl Assembly { let mut next_column_index = realized_dim; for elt_motion in motion.iter() { let moving_elt = &mut elts[elt_motion.key]; - if moving_elt.column_index.is_none() { - moving_elt.column_index = Some(next_column_index); + if moving_elt.column_index().is_none() { + moving_elt.set_column_index(next_column_index); next_column_index += 1; } } @@ -539,7 +676,7 @@ impl Assembly { // we can unwrap the column index because we know that every moving // element has one at this point let column_index = self.elements.with_untracked( - |elts| elts[elt_motion.key].column_index.unwrap() + |elts| elts[elt_motion.key].column_index().unwrap() ); if column_index < realized_dim { @@ -555,7 +692,7 @@ impl Assembly { let mut target_column = motion_proj.column_mut(column_index); let unif_to_std = self.elements.with_untracked( |elts| { - elts[elt_motion.key].representation.with_untracked( + elts[elt_motion.key].representation().with_untracked( |rep| local_unif_to_std(rep.as_view()) ) } @@ -567,26 +704,27 @@ impl Assembly { // step the assembly along the deformation. this changes the elements' // normalizations, so we restore those afterward /* KLUDGE */ - // since our test assemblies only include spheres, we assume that every - // element is on the 1 mass shell + // for now, we only restore the normalizations of spheres for (_, elt) in self.elements.get_clone_untracked() { - elt.representation.update_silent(|rep| { - match elt.column_index { + elt.representation().update_silent(|rep| { + match elt.column_index() { Some(column_index) => { // step the assembly along the deformation *rep += motion_proj.column(column_index); - // restore normalization by contracting toward the last - // coordinate axis - let q_sp = rep.fixed_rows::<3>(0).norm_squared(); - let half_q_lt = -2.0 * rep[3] * rep[4]; - let half_q_lt_sq = half_q_lt * half_q_lt; - let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); - rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling); + if elt.type_id() == TypeId::of::() { + // restore normalization by contracting toward the + // last coordinate axis + let q_sp = rep.fixed_rows::<3>(0).norm_squared(); + let half_q_lt = -2.0 * rep[3] * rep[4]; + let half_q_lt_sq = half_q_lt * half_q_lt; + let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); + rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling); + } }, None => { console::log_1(&JsValue::from( - format!("No velocity to unpack for fresh element \"{}\"", elt.id) + format!("No velocity to unpack for fresh element \"{}\"", elt.id()) )) } }; @@ -602,20 +740,14 @@ impl Assembly { #[cfg(test)] mod tests { - use crate::engine; - use super::*; #[test] - #[should_panic(expected = "Element \"sphere\" should be indexed before writing problem data")] + #[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")] fn unindexed_element_test() { let _ = create_root(|| { - Element::new( - "sphere".to_string(), - "Sphere".to_string(), - [1.0_f32, 1.0_f32, 1.0_f32], - engine::sphere(0.0, 0.0, 0.0, 1.0) - ).pose(&mut ConstraintProblem::new(1), &Slab::new()); + let elt = Sphere::default("sphere".to_string(), 0); + elt.pose(&mut ConstraintProblem::new(1), &Slab::new()); }); } @@ -623,18 +755,13 @@ mod tests { #[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")] fn unindexed_subject_test_inversive_distance() { let _ = create_root(|| { - let mut elts = Slab::new(); + let mut elts = Slab::>::new(); let subjects = [0, 1].map(|k| { elts.insert( - Element::new( - format!("sphere{k}"), - format!("Sphere {k}"), - [1.0_f32, 1.0_f32, 1.0_f32], - engine::sphere(0.0, 0.0, 0.0, 1.0) - ) + Rc::new(Sphere::default(format!("sphere{k}"), k)) ) }); - elts[subjects[0]].column_index = Some(0); + elts[subjects[0]].set_column_index(0); InversiveDistanceRegulator { subjects: subjects, measurement: create_memo(|| 0.0), diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 4e0c7e4..51b207d 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -4,17 +4,185 @@ use sycamore::{prelude::*, motion::create_raf}; use web_sys::{ console, window, - Element, KeyboardEvent, MouseEvent, WebGl2RenderingContext, + WebGlBuffer, WebGlProgram, WebGlShader, WebGlUniformLocation, wasm_bindgen::{JsCast, JsValue} }; -use crate::{AppState, assembly::{ElementKey, ElementMotion}}; +use crate::{ + AppState, + assembly::{ElementKey, ElementColor, ElementMotion, Point, Sphere} +}; + +// --- scene data --- + +struct SceneSpheres { + representations: Vec>, + colors: Vec, + highlights: Vec +} + +impl SceneSpheres { + fn new() -> SceneSpheres{ + SceneSpheres { + representations: Vec::new(), + colors: Vec::new(), + highlights: Vec::new() + } + } + + fn len_i32(&self) -> i32 { + self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer") + } + + fn push(&mut self, representation: DVector, color: ElementColor, highlight: f32) { + self.representations.push(representation); + self.colors.push(color); + self.highlights.push(highlight); + } +} + +struct ScenePoints { + representations: Vec>, + colors: Vec, + highlights: Vec, + selections: Vec +} + +impl ScenePoints { + fn new() -> ScenePoints { + ScenePoints { + representations: Vec::new(), + colors: Vec::new(), + highlights: Vec::new(), + selections: Vec::new() + } + } + + fn push(&mut self, representation: DVector, color: ElementColor, highlight: f32, selected: bool) { + self.representations.push(representation); + self.colors.push(color); + self.highlights.push(highlight); + self.selections.push(if selected { 1.0 } else { 0.0 }); + } +} + +pub struct Scene { + spheres: SceneSpheres, + points: ScenePoints +} + +impl Scene { + fn new() -> Scene { + Scene { + spheres: SceneSpheres::new(), + points: ScenePoints::new() + } + } +} + +pub trait DisplayItem { + fn show(&self, scene: &mut Scene, selected: bool); + + // the smallest positive depth, represented as a multiple of `dir`, where + // the line generated by `dir` hits the element. returns `None` if the line + // misses the element + fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, pixel_size: f64) -> Option; +} + +impl DisplayItem for Sphere { + fn show(&self, scene: &mut Scene, selected: bool) { + const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */ + let representation = self.representation.get_clone_untracked(); + let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; + let highlight = if selected { 1.0 } else { HIGHLIGHT }; + scene.spheres.push(representation, color, highlight); + } + + // this method should be kept synchronized with `sphere_cast` in + // `spheres.frag`, which does essentially the same thing on the GPU side + fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, _pixel_size: f64) -> Option { + // if `a/b` is less than this threshold, we approximate + // `a*u^2 + b*u + c` by the linear function `b*u + c` + const DEG_THRESHOLD: f64 = 1e-9; + + let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); + let a = -rep[3] * dir.norm_squared(); + let b = rep.rows_range(..3).dot(&dir); + let c = -rep[4]; + + let adjust = 4.0*a*c/(b*b); + if adjust < 1.0 { + // as long as `b` is non-zero, the linear approximation of + // + // a*u^2 + b*u + c + // + // at `u = 0` will reach zero at a finite depth `u_lin`. the root of + // the quadratic adjacent to `u_lin` is stored in `lin_root`. if + // both roots have the same sign, `lin_root` will be the one closer + // to `u = 0` + let square_rect_ratio = 1.0 + (1.0 - adjust).sqrt(); + let lin_root = -(2.0*c)/b / square_rect_ratio; + if a.abs() > DEG_THRESHOLD * b.abs() { + if lin_root > 0.0 { + Some(lin_root) + } else { + let other_root = -b/(2.*a) * square_rect_ratio; + (other_root > 0.0).then_some(other_root) + } + } else { + (lin_root > 0.0).then_some(lin_root) + } + } else { + // the line through `dir` misses the sphere completely + None + } + } +} + +impl DisplayItem for Point { + fn show(&self, scene: &mut Scene, selected: bool) { + const HIGHLIGHT: f32 = 0.5; /* SCAFFOLDING */ + let representation = self.representation.get_clone_untracked(); + let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; + let highlight = if selected { 1.0 } else { HIGHLIGHT }; + scene.points.push(representation, color, highlight, selected); + } + + /* SCAFFOLDING */ + fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, pixel_size: f64) -> Option { + let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); + if rep[2] < 0.0 { + // this constant should be kept synchronized with `point.frag` + const POINT_RADIUS_PX: f64 = 4.0; + + // find the radius of the point in screen projection units + let point_radius_proj = POINT_RADIUS_PX * pixel_size; + + // find the squared distance between the screen projections of the + // ray and the point + let dir_proj = -dir.fixed_rows::<2>(0) / dir[2]; + let rep_proj = -rep.fixed_rows::<2>(0) / rep[2]; + let dist_sq = (dir_proj - rep_proj).norm_squared(); + + // if the ray hits the point, return its depth + if dist_sq < point_radius_proj * point_radius_proj { + Some(rep[2] / dir[2]) + } else { + None + } + } else { + None + } + } +} + +// --- WebGL utilities --- fn compile_shader( context: &WebGl2RenderingContext, @@ -27,6 +195,45 @@ fn compile_shader( shader } +fn set_up_program( + context: &WebGl2RenderingContext, + vertex_shader_source: &str, + fragment_shader_source: &str +) -> WebGlProgram { + // compile the shaders + let vertex_shader = compile_shader( + &context, + WebGl2RenderingContext::VERTEX_SHADER, + vertex_shader_source, + ); + let fragment_shader = compile_shader( + &context, + WebGl2RenderingContext::FRAGMENT_SHADER, + fragment_shader_source, + ); + + // create the program and attach the shaders + let program = context.create_program().unwrap(); + context.attach_shader(&program, &vertex_shader); + context.attach_shader(&program, &fragment_shader); + context.link_program(&program); + + /* DEBUG */ + // report whether linking succeeded + let link_status = context + .get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS) + .as_bool() + .unwrap(); + let link_msg = if link_status { + "Linked successfully" + } else { + "Linking failed" + }; + console::log_1(&JsValue::from(link_msg)); + + program +} + fn get_uniform_array_locations( context: &WebGl2RenderingContext, program: &WebGlProgram, @@ -42,22 +249,39 @@ fn get_uniform_array_locations( }) } -// load the given data into the vertex input of the given name -fn bind_vertex_attrib( +// bind the given vertex buffer object to the given vertex attribute +fn bind_to_attribute( context: &WebGl2RenderingContext, - index: u32, - size: i32, - data: &[f32] + attr_index: u32, + attr_size: i32, + buffer: &Option ) { - // create a data buffer and bind it to ARRAY_BUFFER - let buffer = context.create_buffer().unwrap(); - context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer)); + context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref()); + context.vertex_attrib_pointer_with_i32( + attr_index, + attr_size, + WebGl2RenderingContext::FLOAT, + false, // don't normalize + 0, // zero stride + 0, // zero offset + ); +} + +// load the given data into a new vertex buffer object +fn load_new_buffer( + context: &WebGl2RenderingContext, + data: &[f32] +) -> Option { + // create a buffer and bind it to ARRAY_BUFFER + let buffer = context.create_buffer(); + context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref()); - // load the given data into the buffer. the function `Float32Array::view` - // creates a raw view into our module's `WebAssembly.Memory` buffer. - // allocating more memory will change the buffer, invalidating the view. - // that means we have to make sure we don't allocate any memory until the - // view is dropped + // load the given data into the buffer. this block is unsafe because + // `Float32Array::view` creates a raw view into our module's + // `WebAssembly.Memory` buffer. allocating more memory will change the + // buffer, invalidating the view, so we have to make sure we don't allocate + // any memory until the view is dropped. we're okay here because the view is + // used as soon as it's created unsafe { context.buffer_data_with_array_buffer_view( WebGl2RenderingContext::ARRAY_BUFFER, @@ -66,42 +290,43 @@ fn bind_vertex_attrib( ); } - // allow the target attribute to be used - context.enable_vertex_attrib_array(index); - - // take whatever's bound to ARRAY_BUFFER---here, the data buffer created - // above---and bind it to the target attribute - // - // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer - // - context.vertex_attrib_pointer_with_i32( - index, - size, - WebGl2RenderingContext::FLOAT, - false, // don't normalize - 0, // zero stride - 0, // zero offset - ); + buffer +} + +fn bind_new_buffer_to_attribute( + context: &WebGl2RenderingContext, + attr_index: u32, + attr_size: i32, + data: &[f32] +) { + let buffer = load_new_buffer(context, data); + bind_to_attribute(context, attr_index, attr_size, &buffer); } // the direction in camera space that a mouse event is pointing along -fn event_dir(event: &MouseEvent) -> Vector3 { - let target: Element = event.target().unwrap().unchecked_into(); +fn event_dir(event: &MouseEvent) -> (Vector3, f64) { + let target: web_sys::Element = event.target().unwrap().unchecked_into(); let rect = target.get_bounding_client_rect(); let width = rect.width(); let height = rect.height(); let shortdim = width.min(height); - // this constant should be kept synchronized with `inversive.frag` + // this constant should be kept synchronized with `spheres.frag` and + // `point.vert` const FOCAL_SLOPE: f64 = 0.3; - Vector3::new( - FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim, - FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim, - -1.0 + ( + Vector3::new( + FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim, + FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim, + -1.0 + ), + FOCAL_SLOPE * 2.0 / shortdim ) } +// --- display component --- + #[component] pub fn Display() -> View { let state = use_context::(); @@ -138,7 +363,7 @@ pub fn Display() -> View { create_effect(move || { state.assembly.elements.with(|elts| { for (_, elt) in elts { - elt.representation.track(); + elt.representation().track(); } }); state.selection.track(); @@ -170,7 +395,6 @@ pub fn Display() -> View { // display parameters const OPACITY: f32 = 0.5; /* SCAFFOLDING */ - const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */ const LAYER_THRESHOLD: i32 = 0; /* DEBUG */ const DEBUG_MODE: i32 = 0; /* DEBUG */ @@ -186,32 +410,26 @@ pub fn Display() -> View { .dyn_into::() .unwrap(); - // compile and attach the vertex and fragment shaders - let vertex_shader = compile_shader( + // disable depth testing + ctx.disable(WebGl2RenderingContext::DEPTH_TEST); + + // set blend mode + ctx.enable(WebGl2RenderingContext::BLEND); + ctx.blend_func(WebGl2RenderingContext::SRC_ALPHA, WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA); + + // set up the sphere rendering program + let sphere_program = set_up_program( &ctx, - WebGl2RenderingContext::VERTEX_SHADER, include_str!("identity.vert"), + include_str!("spheres.frag") ); - let fragment_shader = compile_shader( + + // set up the point rendering program + let point_program = set_up_program( &ctx, - WebGl2RenderingContext::FRAGMENT_SHADER, - include_str!("inversive.frag"), + include_str!("point.vert"), + include_str!("point.frag") ); - let program = ctx.create_program().unwrap(); - ctx.attach_shader(&program, &vertex_shader); - ctx.attach_shader(&program, &fragment_shader); - ctx.link_program(&program); - let link_status = ctx - .get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS) - .as_bool() - .unwrap(); - let link_msg = if link_status { - "Linked successfully" - } else { - "Linking failed" - }; - console::log_1(&JsValue::from(link_msg)); - ctx.use_program(Some(&program)); /* DEBUG */ // print the maximum number of vectors that can be passed as @@ -230,35 +448,33 @@ pub fn Display() -> View { &JsValue::from("uniform vectors available") ); - // find indices of vertex attributes and uniforms + // find the sphere program's vertex attribute + let viewport_position_attr = ctx.get_attrib_location(&sphere_program, "position") as u32; + + // find the sphere program's uniforms const SPHERE_MAX: usize = 200; - let position_index = ctx.get_attrib_location(&program, "position") as u32; - let sphere_cnt_loc = ctx.get_uniform_location(&program, "sphere_cnt"); + let sphere_cnt_loc = ctx.get_uniform_location(&sphere_program, "sphere_cnt"); let sphere_sp_locs = get_uniform_array_locations::( - &ctx, &program, "sphere_list", Some("sp") + &ctx, &sphere_program, "sphere_list", Some("sp") ); let sphere_lt_locs = get_uniform_array_locations::( - &ctx, &program, "sphere_list", Some("lt") + &ctx, &sphere_program, "sphere_list", Some("lt") ); - let color_locs = get_uniform_array_locations::( - &ctx, &program, "color_list", None + let sphere_color_locs = get_uniform_array_locations::( + &ctx, &sphere_program, "color_list", None ); - let highlight_locs = get_uniform_array_locations::( - &ctx, &program, "highlight_list", None + let sphere_highlight_locs = get_uniform_array_locations::( + &ctx, &sphere_program, "highlight_list", None ); - let resolution_loc = ctx.get_uniform_location(&program, "resolution"); - let shortdim_loc = ctx.get_uniform_location(&program, "shortdim"); - let opacity_loc = ctx.get_uniform_location(&program, "opacity"); - let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold"); - let debug_mode_loc = ctx.get_uniform_location(&program, "debug_mode"); + let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution"); + let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim"); + let opacity_loc = ctx.get_uniform_location(&sphere_program, "opacity"); + let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold"); + let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode"); - // create a vertex array and bind it to the graphics context - let vertex_array = ctx.create_vertex_array().unwrap(); - ctx.bind_vertex_array(Some(&vertex_array)); - - // set the vertex positions + // load the viewport vertex positions into a new vertex buffer object const VERTEX_CNT: usize = 6; - let positions: [f32; 3*VERTEX_CNT] = [ + let viewport_positions: [f32; 3*VERTEX_CNT] = [ // northwest triangle -1.0, -1.0, 0.0, -1.0, 1.0, 0.0, @@ -268,7 +484,13 @@ pub fn Display() -> View { 1.0, 1.0, 0.0, 1.0, -1.0, 0.0 ]; - bind_vertex_attrib(&ctx, position_index, 3, &positions); + let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions); + + // find the point program's vertex attributes + let point_position_attr = ctx.get_attrib_location(&point_program, "position") as u32; + let point_color_attr = ctx.get_attrib_location(&point_program, "color") as u32; + let point_highlight_attr = ctx.get_attrib_location(&point_program, "highlight") as u32; + let point_selection_attr = ctx.get_attrib_location(&point_program, "selected") as u32; // set up a repainting routine let (_, start_animation_loop, _) = create_raf(move || { @@ -362,6 +584,9 @@ pub fn Display() -> View { } if scene_changed.get() { + const SPACE_DIM: usize = 3; + const COLOR_SIZE: usize = 3; + /* INSTRUMENTS */ // measure mean frame interval frames_since_last_sample += 1; @@ -371,6 +596,10 @@ pub fn Display() -> View { frames_since_last_sample = 0; } + // --- get the assembly --- + + let mut scene = Scene::new(); + // find the map from assembly space to world space let location = { let u = -location_z; @@ -384,41 +613,27 @@ pub fn Display() -> View { }; let asm_to_world = &location * &orientation; - // get the assembly - let ( - elt_cnt, - reps_world, - colors, - highlights - ) = state.assembly.elements.with(|elts| { - ( - // number of elements - elts.len() as i32, - - // representation vectors in world coordinates - elts.iter().map( - |(_, elt)| elt.representation.with(|rep| &asm_to_world * rep) - ).collect::>(), - - // colors - elts.iter().map(|(key, elt)| { - if state.selection.with(|sel| sel.contains(&key)) { - elt.color.map(|ch| 0.2 + 0.8*ch) - } else { - elt.color - } - }).collect::>(), - - // highlight levels - elts.iter().map(|(key, _)| { - if state.selection.with(|sel| sel.contains(&key)) { - 1.0_f32 - } else { - HIGHLIGHT - } - }).collect::>() - ) - }); + // set up the scene + state.assembly.elements.with_untracked( + |elts| for (key, elt) in elts { + let selected = state.selection.with(|sel| sel.contains(&key)); + elt.show(&mut scene, selected); + } + ); + let sphere_cnt = scene.spheres.len_i32(); + + // --- draw the spheres --- + + // use the sphere rendering program + ctx.use_program(Some(&sphere_program)); + + // enable the sphere program's vertex attribute + ctx.enable_vertex_attrib_array(viewport_position_attr); + + // write the spheres in world coordinates + let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map( + |rep| (&asm_to_world * rep).cast::() + ).collect(); // set the resolution let width = canvas.width() as f32; @@ -426,25 +641,25 @@ pub fn Display() -> View { ctx.uniform2f(resolution_loc.as_ref(), width, height); ctx.uniform1f(shortdim_loc.as_ref(), width.min(height)); - // pass the assembly - ctx.uniform1i(sphere_cnt_loc.as_ref(), elt_cnt); - for n in 0..reps_world.len() { - let v = &reps_world[n]; - ctx.uniform3f( + // pass the scene data + ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_cnt); + for n in 0..sphere_reps_world.len() { + let v = &sphere_reps_world[n]; + ctx.uniform3fv_with_f32_array( sphere_sp_locs[n].as_ref(), - v[0] as f32, v[1] as f32, v[2] as f32 + v.rows(0, 3).as_slice() ); - ctx.uniform2f( + ctx.uniform2fv_with_f32_array( sphere_lt_locs[n].as_ref(), - v[3] as f32, v[4] as f32 + v.rows(3, 2).as_slice() ); ctx.uniform3fv_with_f32_array( - color_locs[n].as_ref(), - &colors[n] + sphere_color_locs[n].as_ref(), + &scene.spheres.colors[n] ); ctx.uniform1f( - highlight_locs[n].as_ref(), - highlights[n] + sphere_highlight_locs[n].as_ref(), + scene.spheres.highlights[n] ); } @@ -453,9 +668,56 @@ pub fn Display() -> View { ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); + // bind the viewport vertex position buffer to the position + // attribute in the vertex shader + bind_to_attribute(&ctx, viewport_position_attr, SPACE_DIM as i32, &viewport_position_buffer); + // draw the scene ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); + // disable the sphere program's vertex attribute + ctx.disable_vertex_attrib_array(viewport_position_attr); + + // --- draw the points --- + + if !scene.points.representations.is_empty() { + // use the point rendering program + ctx.use_program(Some(&point_program)); + + // enable the point program's vertex attributes + ctx.enable_vertex_attrib_array(point_position_attr); + ctx.enable_vertex_attrib_array(point_color_attr); + ctx.enable_vertex_attrib_array(point_highlight_attr); + ctx.enable_vertex_attrib_array(point_selection_attr); + + // write the points in world coordinates + let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM); + let point_positions = DMatrix::from_columns( + &scene.points.representations.into_iter().map( + |rep| &asm_to_world_sp * rep + ).collect::>().as_slice() + ).cast::(); + + // load the point positions and colors into new buffers and + // bind them to the corresponding attributes in the vertex + // shader + bind_new_buffer_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, point_positions.as_slice()); + bind_new_buffer_to_attribute(&ctx, point_color_attr, COLOR_SIZE as i32, scene.points.colors.concat().as_slice()); + bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice()); + bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice()); + + // draw the scene + ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32); + + // disable the point program's vertex attributes + ctx.disable_vertex_attrib_array(point_position_attr); + ctx.disable_vertex_attrib_array(point_color_attr); + ctx.disable_vertex_attrib_array(point_highlight_attr); + ctx.disable_vertex_attrib_array(point_selection_attr); + } + + // --- update the display state --- + // update the viewpoint assembly_to_world.set(asm_to_world); @@ -585,11 +847,11 @@ pub fn Display() -> View { }, on:click=move |event: MouseEvent| { // find the nearest element along the pointer direction - let dir = event_dir(&event); + let (dir, pixel_size) = event_dir(&event); console::log_1(&JsValue::from(dir.to_string())); let mut clicked: Option<(ElementKey, f64)> = None; for (key, elt) in state.assembly.elements.get_clone_untracked() { - match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world)) { + match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) { Some(depth) => match clicked { Some((_, best_depth)) => { if depth < best_depth { diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 869a7de..b0fa23d 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -4,7 +4,6 @@ use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ // --- elements --- -#[cfg(feature = "dev")] pub fn point(x: f64, y: f64, z: f64) -> DVector { DVector::from_column_slice(&[x, y, z, 0.5, 0.5*(x*x + y*y + z*z)]) } diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 2446337..2893b6d 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -9,9 +9,10 @@ use web_sys::{ use crate::{ AppState, - assembly, assembly::{ + Element, ElementKey, + ElementRc, HalfCurvatureRegulator, InversiveDistanceRegulator, Regulator, @@ -103,7 +104,7 @@ impl OutlineItem for InversiveDistanceRegulator { self.subjects[0] }; let other_subject_label = state.assembly.elements.with( - |elts| elts[other_subject].label.clone() + |elts| elts[other_subject].label().clone() ); view! { li(class="regulator") { @@ -141,14 +142,15 @@ fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> // a list item that shows an element in an outline view of an assembly #[component(inline_props)] -fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { +fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { let state = use_context::(); let class = state.selection.map( move |sel| if sel.contains(&key) { "selected" } else { "" } ); - let label = element.label.clone(); + let label = element.label().clone(); + let representation = element.representation().clone(); let rep_components = move || { - element.representation.with( + representation.with( |rep| rep.iter().map( |u| { let u_str = format!("{:.3}", u).replace("-", "\u{2212}"); @@ -157,8 +159,8 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { ).collect::>() ) }; - let regulated = element.regulators.map(|regs| regs.len() > 0); - let regulator_list = element.regulators.map( + let regulated = element.regulators().map(|regs| regs.len() > 0); + let regulator_list = element.regulators().map( move |elt_reg_keys| elt_reg_keys .clone() .into_iter() @@ -261,7 +263,8 @@ pub fn Outline() -> View { |elts| elts .clone() .into_iter() - .sorted_by_key(|(_, elt)| elt.id.clone()) + .sorted_by_key(|(_, elt)| elt.id().clone()) + .map(|(key, elt)| (key, ElementRc(elt))) .collect() ); @@ -275,10 +278,10 @@ pub fn Outline() -> View { ) { Keyed( list=element_list, - view=|(key, elt)| view! { + view=|(key, ElementRc(elt))| view! { ElementOutlineItem(key=key, element=elt) }, - key=|(_, elt)| elt.serial + key=|(_, ElementRc(elt))| elt.serial() ) } } diff --git a/app-proto/src/point.frag b/app-proto/src/point.frag new file mode 100644 index 0000000..3a361a8 --- /dev/null +++ b/app-proto/src/point.frag @@ -0,0 +1,18 @@ +#version 300 es + +precision highp float; + +in vec3 point_color; +in float point_highlight; +in float total_radius; + +out vec4 outColor; + +void main() { + float r = total_radius * length(2.*gl_PointCoord - vec2(1.)); + + const float POINT_RADIUS = 4.; + float border = smoothstep(POINT_RADIUS - 1., POINT_RADIUS, r); + vec3 color = mix(point_color, vec3(1.), border * point_highlight); + outColor = vec4(color, 1. - smoothstep(total_radius - 1., total_radius, r)); +} \ No newline at end of file diff --git a/app-proto/src/point.vert b/app-proto/src/point.vert new file mode 100644 index 0000000..6945010 --- /dev/null +++ b/app-proto/src/point.vert @@ -0,0 +1,24 @@ +#version 300 es + +in vec4 position; +in vec3 color; +in float highlight; +in float selected; + +out vec3 point_color; +out float point_highlight; +out float total_radius; + +// camera +const float focal_slope = 0.3; + +void main() { + total_radius = 5. + 0.5*selected; + + float depth = -focal_slope * position.z; + gl_Position = vec4(position.xy / depth, 0., 1.); + gl_PointSize = 2.*total_radius; + + point_color = color; + point_highlight = highlight; +} \ No newline at end of file diff --git a/app-proto/src/inversive.frag b/app-proto/src/spheres.frag similarity index 100% rename from app-proto/src/inversive.frag rename to app-proto/src/spheres.frag From 2adf4669f47ab8f2bff8d64ac011a2bd09f632fa Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Tue, 6 May 2025 19:17:30 +0000 Subject: [PATCH 127/132] Refactor: Use pointers to refer to elements and regulators (#84) Previously, dyna3 used storage keys to refer to elements, necessitating passing around element containers to various functions so that they could access the relevant elements. These storage keys have been replaced with reference-counted pointers, used for tasks like these: - Specifying the subjects of regulators. - Collecting the regulators each element is subject to - Handling selection. - Creating interface components. Also, systematizes the handling of serial numbers for entities, through a Serial trait. And updates to rust 1.86 and institutes explicit checking of the rust version. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/84 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- .../workflows/continuous-integration.yaml | 2 +- app-proto/Cargo.lock | 19 +- app-proto/Cargo.toml | 3 +- app-proto/src/add_remove.rs | 2 +- app-proto/src/assembly.rs | 396 ++++++++++-------- app-proto/src/display.rs | 23 +- app-proto/src/main.rs | 21 +- app-proto/src/outline.rs | 92 ++-- 8 files changed, 288 insertions(+), 270 deletions(-) diff --git a/.forgejo/workflows/continuous-integration.yaml b/.forgejo/workflows/continuous-integration.yaml index daf8923..f3b0130 100644 --- a/.forgejo/workflows/continuous-integration.yaml +++ b/.forgejo/workflows/continuous-integration.yaml @@ -11,7 +11,7 @@ jobs: test: runs-on: docker container: - image: cimg/rust:1.85-node + image: cimg/rust:1.86-node defaults: run: # set the default working directory for each `run` step, relative to the diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock index 9738589..3bf609c 100644 --- a/app-proto/Cargo.lock +++ b/app-proto/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ahash" @@ -89,8 +89,6 @@ dependencies = [ "lazy_static", "nalgebra", "readonly", - "rustc-hash", - "slab", "sycamore", "wasm-bindgen-test", "web-sys", @@ -365,12 +363,6 @@ dependencies = [ "syn", ] -[[package]] -name = "rustc-hash" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" - [[package]] name = "safe_arch" version = "0.7.2" @@ -414,15 +406,6 @@ dependencies = [ "wide", ] -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - [[package]] name = "slotmap" version = "1.0.7" diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 5ab7299..844a0a6 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -3,6 +3,7 @@ name = "dyna3" version = "0.1.0" authors = ["Aaron Fenyes", "Glen Whitney"] edition = "2021" +rust-version = "1.86" [features] default = ["console_error_panic_hook"] @@ -14,8 +15,6 @@ js-sys = "0.3.70" lazy_static = "1.5.0" nalgebra = "0.33.0" readonly = "0.2.12" -rustc-hash = "2.0.0" -slab = "0.4.9" sycamore = "0.9.0-beta.3" # The `console_error_panic_hook` crate provides better debugging of panics by diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index ea86186..f3bbc97 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -241,7 +241,7 @@ pub fn AddRemove() -> View { .unwrap() ); state.assembly.insert_regulator( - Rc::new(InversiveDistanceRegulator::new(subjects, &state.assembly)) + Rc::new(InversiveDistanceRegulator::new(subjects)) ); state.selection.update(|sel| sel.clear()); } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 343cef8..bd185c8 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,12 +1,14 @@ use nalgebra::{DMatrix, DVector, DVectorView}; -use rustc_hash::FxHashMap; -use slab::Slab; use std::{ any::{Any, TypeId}, cell::Cell, - collections::BTreeSet, + collections::{BTreeMap, BTreeSet}, + cmp::Ordering, + fmt, + fmt::{Debug, Formatter}, + hash::{Hash, Hasher}, rc::Rc, - sync::atomic::{AtomicU64, Ordering} + sync::{atomic, atomic::AtomicU64} }; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ @@ -27,49 +29,16 @@ use crate::{ specified::SpecifiedValue }; -// the types of the keys we use to access an assembly's elements and regulators -pub type ElementKey = usize; -pub type RegulatorKey = usize; - pub type ElementColor = [f32; 3]; /* KLUDGE */ // we should reconsider this design when we build a system for switching between // assemblies. at that point, we might want to switch to hierarchical keys, -// where each each element has a key that identifies it within its assembly and +// where each each item has a key that identifies it within its assembly and // each assembly has a key that identifies it within the sesssion -static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0); +static NEXT_SERIAL: AtomicU64 = AtomicU64::new(0); -pub trait ProblemPoser { - fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>); -} - -pub trait Element: ProblemPoser + DisplayItem { - // the default identifier for an element of this type - fn default_id() -> String where Self: Sized; - - // create the default example of an element of this type - fn default(id: String, id_num: u64) -> Self where Self: Sized; - - // the regulators that should be created when an element of this type is - // inserted into the given assembly with the given storage key - /* KLUDGE */ - // right now, this organization makes sense because regulators identify - // their subjects by storage key, so the element has to be inserted before - // its regulators can be created. if we change the way regulators identify - // their subjects, we should consider refactoring - fn default_regulators(_key: ElementKey, _assembly: &Assembly) -> Vec> where Self: Sized { - Vec::new() - } - - fn id(&self) -> &String; - fn label(&self) -> &String; - fn representation(&self) -> Signal>; - - // the regulators the element is subject to. the assembly that owns the - // element is responsible for keeping this set up to date - fn regulators(&self) -> Signal>; - +pub trait Serial { // a serial number that uniquely identifies this element fn serial(&self) -> u64; @@ -80,11 +49,62 @@ pub trait Element: ProblemPoser + DisplayItem { // // https://marabos.nl/atomics/atomics.html#example-handle-overflow // - NEXT_ELEMENT_SERIAL.fetch_update( - Ordering::SeqCst, Ordering::SeqCst, + NEXT_SERIAL.fetch_update( + atomic::Ordering::SeqCst, atomic::Ordering::SeqCst, |serial| serial.checked_add(1) ).expect("Out of serial numbers for elements") } +} + +impl Hash for dyn Serial { + fn hash(&self, state: &mut H) { + self.serial().hash(state) + } +} + +impl PartialEq for dyn Serial { + fn eq(&self, other: &Self) -> bool { + self.serial() == other.serial() + } +} + +impl Eq for dyn Serial {} + +impl PartialOrd for dyn Serial { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for dyn Serial { + fn cmp(&self, other: &Self) -> Ordering { + self.serial().cmp(&other.serial()) + } +} + +pub trait ProblemPoser { + fn pose(&self, problem: &mut ConstraintProblem); +} + +pub trait Element: Serial + ProblemPoser + DisplayItem { + // the default identifier for an element of this type + fn default_id() -> String where Self: Sized; + + // the default example of an element of this type + fn default(id: String, id_num: u64) -> Self where Self: Sized; + + // the default regulators that come with this element + fn default_regulators(self: Rc) -> Vec> { + Vec::new() + } + + fn id(&self) -> &String; + fn label(&self) -> &String; + fn representation(&self) -> Signal>; + + // the regulators the element is subject to. the assembly that owns the + // element is responsible for keeping this set up to date + fn regulators(&self) -> Signal>>; // the configuration matrix column index that was assigned to the element // last time the assembly was realized, or `None` if the element has never @@ -97,17 +117,35 @@ pub trait Element: ProblemPoser + DisplayItem { fn set_column_index(&self, index: usize); } -// the `Element` trait needs to be dyn-compatible, so its method signatures can -// only use `Self` in the type of the receiver. that means `Element` can't -// implement `PartialEq`. if you need partial equivalence for `Element` trait -// objects, use this wrapper -#[derive(Clone)] -pub struct ElementRc(pub Rc); +impl Debug for dyn Element { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { + self.id().fmt(f) + } +} -impl PartialEq for ElementRc { - fn eq(&self, ElementRc(other): &Self) -> bool { - let ElementRc(rc) = self; - Rc::ptr_eq(rc, &other) +impl Hash for dyn Element { + fn hash(&self, state: &mut H) { + ::hash(self, state) + } +} + +impl PartialEq for dyn Element { + fn eq(&self, other: &Self) -> bool { + ::eq(self, other) + } +} + +impl Eq for dyn Element {} + +impl PartialOrd for dyn Element { + fn partial_cmp(&self, other: &Self) -> Option { + ::partial_cmp(self, other) + } +} + +impl Ord for dyn Element { + fn cmp(&self, other: &Self) -> Ordering { + ::cmp(self, other) } } @@ -116,8 +154,8 @@ pub struct Sphere { pub label: String, pub color: ElementColor, pub representation: Signal>, - pub regulators: Signal>, - pub serial: u64, + pub regulators: Signal>>, + serial: u64, column_index: Cell> } @@ -135,7 +173,7 @@ impl Sphere { label: label, color: color, representation: create_signal(representation), - regulators: create_signal(BTreeSet::default()), + regulators: create_signal(BTreeSet::new()), serial: Self::next_serial(), column_index: None.into() } @@ -156,8 +194,8 @@ impl Element for Sphere { ) } - fn default_regulators(key: ElementKey, assembly: &Assembly) -> Vec> { - vec![Rc::new(HalfCurvatureRegulator::new(key, assembly))] + fn default_regulators(self: Rc) -> Vec> { + vec![Rc::new(HalfCurvatureRegulator::new(self))] } fn id(&self) -> &String { @@ -172,14 +210,10 @@ impl Element for Sphere { self.representation } - fn regulators(&self) -> Signal> { + fn regulators(&self) -> Signal>> { self.regulators } - fn serial(&self) -> u64 { - self.serial - } - fn column_index(&self) -> Option { self.column_index.get() } @@ -189,8 +223,14 @@ impl Element for Sphere { } } +impl Serial for Sphere { + fn serial(&self) -> u64 { + self.serial + } +} + impl ProblemPoser for Sphere { - fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab>) { + fn pose(&self, problem: &mut ConstraintProblem) { let index = self.column_index().expect( format!("Sphere \"{}\" should be indexed before writing problem data", self.id).as_str() ); @@ -204,8 +244,8 @@ pub struct Point { pub label: String, pub color: ElementColor, pub representation: Signal>, - pub regulators: Signal>, - pub serial: u64, + pub regulators: Signal>>, + serial: u64, column_index: Cell> } @@ -223,7 +263,7 @@ impl Point { label, color, representation: create_signal(representation), - regulators: create_signal(BTreeSet::default()), + regulators: create_signal(BTreeSet::new()), serial: Self::next_serial(), column_index: None.into() } @@ -256,14 +296,10 @@ impl Element for Point { self.representation } - fn regulators(&self) -> Signal> { + fn regulators(&self) -> Signal>> { self.regulators } - fn serial(&self) -> u64 { - self.serial - } - fn column_index(&self) -> Option { self.column_index.get() } @@ -273,8 +309,14 @@ impl Element for Point { } } +impl Serial for Point { + fn serial(&self) -> u64 { + self.serial + } +} + impl ProblemPoser for Point { - fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab>) { + fn pose(&self, problem: &mut ConstraintProblem) { let index = self.column_index().expect( format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str() ); @@ -284,8 +326,8 @@ impl ProblemPoser for Point { } } -pub trait Regulator: ProblemPoser + OutlineItem { - fn subjects(&self) -> Vec; +pub trait Regulator: Serial + ProblemPoser + OutlineItem { + fn subjects(&self) -> Vec>; fn measurement(&self) -> ReadSignal; fn set_point(&self) -> Signal; @@ -295,39 +337,65 @@ pub trait Regulator: ProblemPoser + OutlineItem { // preconditioning when the set point is present, and use its return value // to report whether the set is present. the default implementation does no // preconditioning - fn try_activate(&self, _assembly: &Assembly) -> bool { + fn try_activate(&self) -> bool { self.set_point().with(|set_pt| set_pt.is_present()) } } +impl Hash for dyn Regulator { + fn hash(&self, state: &mut H) { + ::hash(self, state) + } +} + +impl PartialEq for dyn Regulator { + fn eq(&self, other: &Self) -> bool { + ::eq(self, other) + } +} + +impl Eq for dyn Regulator {} + +impl PartialOrd for dyn Regulator { + fn partial_cmp(&self, other: &Self) -> Option { + ::partial_cmp(self, other) + } +} + +impl Ord for dyn Regulator { + fn cmp(&self, other: &Self) -> Ordering { + ::cmp(self, other) + } +} + pub struct InversiveDistanceRegulator { - pub subjects: [ElementKey; 2], + pub subjects: [Rc; 2], pub measurement: ReadSignal, - pub set_point: Signal + pub set_point: Signal, + serial: u64 } impl InversiveDistanceRegulator { - pub fn new(subjects: [ElementKey; 2], assembly: &Assembly) -> InversiveDistanceRegulator { - let measurement = assembly.elements.map( - move |elts| { - let representations = subjects.map(|subj| elts[subj].representation()); - representations[0].with(|rep_0| - representations[1].with(|rep_1| - rep_0.dot(&(&*Q * rep_1)) - ) + pub fn new(subjects: [Rc; 2]) -> InversiveDistanceRegulator { + let representations = subjects.each_ref().map(|subj| subj.representation()); + let measurement = create_memo(move || { + representations[0].with(|rep_0| + representations[1].with(|rep_1| + rep_0.dot(&(&*Q * rep_1)) ) - } - ); + ) + }); let set_point = create_signal(SpecifiedValue::from_empty_spec()); + let serial = Self::next_serial(); - InversiveDistanceRegulator { subjects, measurement, set_point } + InversiveDistanceRegulator { subjects, measurement, set_point, serial } } } impl Regulator for InversiveDistanceRegulator { - fn subjects(&self) -> Vec { - self.subjects.into() + fn subjects(&self) -> Vec> { + self.subjects.clone().into() } fn measurement(&self) -> ReadSignal { @@ -339,12 +407,18 @@ impl Regulator for InversiveDistanceRegulator { } } +impl Serial for InversiveDistanceRegulator { + fn serial(&self) -> u64 { + self.serial + } +} + impl ProblemPoser for InversiveDistanceRegulator { - fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>) { + fn pose(&self, problem: &mut ConstraintProblem) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { - let [row, col] = self.subjects.map( - |subj| elts[subj].column_index().expect( + let [row, col] = self.subjects.each_ref().map( + |subj| subj.column_index().expect( "Subjects should be indexed before inversive distance regulator writes problem data" ) ); @@ -355,28 +429,28 @@ impl ProblemPoser for InversiveDistanceRegulator { } pub struct HalfCurvatureRegulator { - pub subject: ElementKey, + pub subject: Rc, pub measurement: ReadSignal, - pub set_point: Signal + pub set_point: Signal, + serial: u64 } impl HalfCurvatureRegulator { - pub fn new(subject: ElementKey, assembly: &Assembly) -> HalfCurvatureRegulator { - let measurement = assembly.elements.map( - move |elts| elts[subject].representation().with( - |rep| rep[Sphere::CURVATURE_COMPONENT] - ) + pub fn new(subject: Rc) -> HalfCurvatureRegulator { + let measurement = subject.representation().map( + |rep| rep[Sphere::CURVATURE_COMPONENT] ); let set_point = create_signal(SpecifiedValue::from_empty_spec()); + let serial = Self::next_serial(); - HalfCurvatureRegulator { subject, measurement, set_point } + HalfCurvatureRegulator { subject, measurement, set_point, serial } } } impl Regulator for HalfCurvatureRegulator { - fn subjects(&self) -> Vec { - vec![self.subject] + fn subjects(&self) -> Vec> { + vec![self.subject.clone()] } fn measurement(&self) -> ReadSignal { @@ -387,13 +461,10 @@ impl Regulator for HalfCurvatureRegulator { self.set_point } - fn try_activate(&self, assembly: &Assembly) -> bool { + fn try_activate(&self) -> bool { match self.set_point.with(|set_pt| set_pt.value) { Some(half_curv) => { - let representation = assembly.elements.with_untracked( - |elts| elts[self.subject].representation() - ); - representation.update( + self.subject.representation().update( |rep| change_half_curvature(rep, half_curv) ); true @@ -403,11 +474,17 @@ impl Regulator for HalfCurvatureRegulator { } } +impl Serial for HalfCurvatureRegulator { + fn serial(&self) -> u64 { + self.serial + } +} + impl ProblemPoser for HalfCurvatureRegulator { - fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>) { + fn pose(&self, problem: &mut ConstraintProblem) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { - let col = elts[self.subject].column_index().expect( + let col = self.subject.column_index().expect( "Subject should be indexed before half-curvature regulator writes problem data" ); problem.frozen.push(Sphere::CURVATURE_COMPONENT, col, val); @@ -418,7 +495,7 @@ impl ProblemPoser for HalfCurvatureRegulator { // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { - pub key: ElementKey, + pub element: Rc, pub velocity: DVectorView<'a, f64> } @@ -428,8 +505,8 @@ type AssemblyMotion<'a> = Vec>; #[derive(Clone)] pub struct Assembly { // elements and regulators - pub elements: Signal>>, - pub regulators: Signal>>, + pub elements: Signal>>, + pub regulators: Signal>>, // solution variety tangent space. the basis vectors are stored in // configuration matrix format, ordered according to the elements' column @@ -444,16 +521,16 @@ pub struct Assembly { pub tangent: Signal, // indexing - pub elements_by_id: Signal> + pub elements_by_id: Signal>> } impl Assembly { pub fn new() -> Assembly { Assembly { - elements: create_signal(Slab::new()), - regulators: create_signal(Slab::new()), + elements: create_signal(BTreeSet::new()), + regulators: create_signal(BTreeSet::new()), tangent: create_signal(ConfigSubspace::zero(0)), - elements_by_id: create_signal(FxHashMap::default()) + elements_by_id: create_signal(BTreeMap::default()) } } @@ -462,29 +539,27 @@ impl Assembly { // insert an element into the assembly without checking whether we already // have an element with the same identifier. any element that does have the // same identifier will get kicked out of the `elements_by_id` index - fn insert_element_unchecked(&self, elt: T) -> ElementKey { + fn insert_element_unchecked(&self, elt: impl Element + 'static) { // insert the element let id = elt.id().clone(); - let key = self.elements.update(|elts| elts.insert(Rc::new(elt))); - self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); + let elt_rc = Rc::new(elt); + self.elements.update(|elts| elts.insert(elt_rc.clone())); + self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, elt_rc.clone())); // create and insert the element's default regulators - for reg in T::default_regulators(key, &self) { + for reg in elt_rc.default_regulators() { self.insert_regulator(reg); } - - key } - pub fn try_insert_element(&self, elt: impl Element + 'static) -> Option { + pub fn try_insert_element(&self, elt: impl Element + 'static) -> bool { let can_insert = self.elements_by_id.with_untracked( |elts_by_id| !elts_by_id.contains_key(elt.id()) ); if can_insert { - Some(self.insert_element_unchecked(elt)) - } else { - None + self.insert_element_unchecked(elt); } + can_insert } pub fn insert_element_default(&self) { @@ -505,19 +580,16 @@ impl Assembly { pub fn insert_regulator(&self, regulator: Rc) { // add the regulator to the assembly's regulator list - let key = self.regulators.update( + self.regulators.update( |regs| regs.insert(regulator.clone()) ); // add the regulator to each subject's regulator list - let subjects = regulator.subjects(); - let subject_regulators: Vec<_> = self.elements.with_untracked( - |elts| subjects.into_iter().map( - |subj| elts[subj].regulators() - ).collect() - ); + let subject_regulators: Vec<_> = regulator.subjects().into_iter().map( + |subj| subj.regulators() + ).collect(); for regulators in subject_regulators { - regulators.update(|regs| regs.insert(key)); + regulators.update(|regs| regs.insert(regulator.clone())); } // update the realization when the regulator becomes a constraint, or is @@ -530,7 +602,7 @@ impl Assembly { format!("Updated regulator with subjects {:?}", regulator.subjects()) )); - if regulator.try_activate(&self_for_effect) { + if regulator.try_activate() { self_for_effect.realize(); } }); @@ -539,7 +611,7 @@ impl Assembly { // print an updated list of regulators console::log_1(&JsValue::from("Regulators:")); self.regulators.with_untracked(|regs| { - for (_, reg) in regs.into_iter() { + for reg in regs.into_iter() { console::log_1(&JsValue::from(format!( " {:?}: {}", reg.subjects(), @@ -563,7 +635,7 @@ impl Assembly { pub fn realize(&self) { // index the elements self.elements.update_silent(|elts| { - for (index, (_, elt)) in elts.into_iter().enumerate() { + for (index, elt) in elts.iter().enumerate() { elt.set_column_index(index); } }); @@ -571,12 +643,12 @@ impl Assembly { // set up the constraint problem let problem = self.elements.with_untracked(|elts| { let mut problem = ConstraintProblem::new(elts.len()); - for (_, elt) in elts { - elt.pose(&mut problem, elts); + for elt in elts { + elt.pose(&mut problem); } self.regulators.with_untracked(|regs| { - for (_, reg) in regs { - reg.pose(&mut problem, elts); + for reg in regs { + reg.pose(&mut problem); } }); problem @@ -618,7 +690,7 @@ impl Assembly { if success { // read out the solution - for (_, elt) in self.elements.get_clone_untracked() { + for elt in self.elements.get_clone_untracked() { elt.representation().update( |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) ); @@ -654,17 +726,17 @@ impl Assembly { // in the process, we find out how many matrix columns we'll need to // hold the deformation let realized_dim = self.tangent.with(|tan| tan.assembly_dim()); - let motion_dim = self.elements.update_silent(|elts| { + let motion_dim = { let mut next_column_index = realized_dim; for elt_motion in motion.iter() { - let moving_elt = &mut elts[elt_motion.key]; + let moving_elt = &elt_motion.element; if moving_elt.column_index().is_none() { moving_elt.set_column_index(next_column_index); next_column_index += 1; } } next_column_index - }); + }; // project the element motions onto the tangent space of the solution // variety and sum them to get a deformation of the whole assembly. the @@ -675,9 +747,7 @@ impl Assembly { for elt_motion in motion { // we can unwrap the column index because we know that every moving // element has one at this point - let column_index = self.elements.with_untracked( - |elts| elts[elt_motion.key].column_index().unwrap() - ); + let column_index = elt_motion.element.column_index().unwrap(); if column_index < realized_dim { // this element had a column index when we started, so by @@ -690,12 +760,8 @@ impl Assembly { // this element didn't have a column index when we started, so // by invariant (2), it's unconstrained let mut target_column = motion_proj.column_mut(column_index); - let unif_to_std = self.elements.with_untracked( - |elts| { - elts[elt_motion.key].representation().with_untracked( - |rep| local_unif_to_std(rep.as_view()) - ) - } + let unif_to_std = elt_motion.element.representation().with_untracked( + |rep| local_unif_to_std(rep.as_view()) ); target_column += unif_to_std * elt_motion.velocity; } @@ -705,7 +771,7 @@ impl Assembly { // normalizations, so we restore those afterward /* KLUDGE */ // for now, we only restore the normalizations of spheres - for (_, elt) in self.elements.get_clone_untracked() { + for elt in self.elements.get_clone_untracked() { elt.representation().update_silent(|rep| { match elt.column_index() { Some(column_index) => { @@ -747,7 +813,7 @@ mod tests { fn unindexed_element_test() { let _ = create_root(|| { let elt = Sphere::default("sphere".to_string(), 0); - elt.pose(&mut ConstraintProblem::new(1), &Slab::new()); + elt.pose(&mut ConstraintProblem::new(1)); }); } @@ -755,18 +821,16 @@ mod tests { #[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")] fn unindexed_subject_test_inversive_distance() { let _ = create_root(|| { - let mut elts = Slab::>::new(); - let subjects = [0, 1].map(|k| { - elts.insert( - Rc::new(Sphere::default(format!("sphere{k}"), k)) - ) - }); - elts[subjects[0]].set_column_index(0); + let subjects = [0, 1].map( + |k| Rc::new(Sphere::default(format!("sphere{k}"), k)) as Rc + ); + subjects[0].set_column_index(0); InversiveDistanceRegulator { subjects: subjects, measurement: create_memo(|| 0.0), - set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()) - }.pose(&mut ConstraintProblem::new(2), &elts); + set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()), + serial: InversiveDistanceRegulator::next_serial() + }.pose(&mut ConstraintProblem::new(2)); }); } } \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 51b207d..a2fe4b6 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -1,5 +1,6 @@ use core::array; use nalgebra::{DMatrix, DVector, Rotation3, Vector3}; +use std::rc::Rc; use sycamore::{prelude::*, motion::create_raf}; use web_sys::{ console, @@ -16,7 +17,7 @@ use web_sys::{ use crate::{ AppState, - assembly::{ElementKey, ElementColor, ElementMotion, Point, Sphere} + assembly::{Element, ElementColor, ElementMotion, Point, Sphere} }; // --- scene data --- @@ -362,7 +363,7 @@ pub fn Display() -> View { let scene_changed = create_signal(true); create_effect(move || { state.assembly.elements.with(|elts| { - for (_, elt) in elts { + for elt in elts { elt.representation().track(); } }); @@ -548,7 +549,7 @@ pub fn Display() -> View { // manipulate the assembly if state.selection.with(|sel| sel.len() == 1) { let sel = state.selection.with( - |sel| *sel.into_iter().next().unwrap() + |sel| sel.into_iter().next().unwrap().clone() ); let translate_x = translate_pos_x_val - translate_neg_x_val; let translate_y = translate_pos_y_val - translate_neg_y_val; @@ -574,7 +575,7 @@ pub fn Display() -> View { assembly_for_raf.deform( vec![ ElementMotion { - key: sel, + element: sel, velocity: elt_motion.as_view() } ] @@ -615,8 +616,8 @@ pub fn Display() -> View { // set up the scene state.assembly.elements.with_untracked( - |elts| for (key, elt) in elts { - let selected = state.selection.with(|sel| sel.contains(&key)); + |elts| for elt in elts { + let selected = state.selection.with(|sel| sel.contains(elt)); elt.show(&mut scene, selected); } ); @@ -849,16 +850,16 @@ pub fn Display() -> View { // find the nearest element along the pointer direction let (dir, pixel_size) = event_dir(&event); console::log_1(&JsValue::from(dir.to_string())); - let mut clicked: Option<(ElementKey, f64)> = None; - for (key, elt) in state.assembly.elements.get_clone_untracked() { + let mut clicked: Option<(Rc, f64)> = None; + for elt in state.assembly.elements.get_clone_untracked() { match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) { Some(depth) => match clicked { Some((_, best_depth)) => { if depth < best_depth { - clicked = Some((key, depth)) + clicked = Some((elt, depth)) } }, - None => clicked = Some((key, depth)) + None => clicked = Some((elt, depth)) } None => () }; @@ -866,7 +867,7 @@ pub fn Display() -> View { // if we clicked something, select it match clicked { - Some((key, _)) => state.select(key, event.shift_key()), + Some((elt, _)) => state.select(&elt, event.shift_key()), None => state.selection.update(|sel| sel.clear()) }; } diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index e581997..b76859a 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -8,42 +8,41 @@ mod specified; #[cfg(test)] mod tests; -use rustc_hash::FxHashSet; +use std::{collections::BTreeSet, rc::Rc}; use sycamore::prelude::*; use add_remove::AddRemove; -use assembly::{Assembly, ElementKey}; +use assembly::{Assembly, Element}; use display::Display; use outline::Outline; #[derive(Clone)] struct AppState { assembly: Assembly, - selection: Signal> + selection: Signal>> } impl AppState { fn new() -> AppState { AppState { assembly: Assembly::new(), - selection: create_signal(FxHashSet::default()) + selection: create_signal(BTreeSet::default()) } } - // in single-selection mode, select the element with the given key. in - // multiple-selection mode, toggle whether the element with the given key - // is selected - fn select(&self, key: ElementKey, multi: bool) { + // in single-selection mode, select the given element. in multiple-selection + // mode, toggle whether the given element is selected + fn select(&self, element: &Rc, multi: bool) { if multi { self.selection.update(|sel| { - if !sel.remove(&key) { - sel.insert(key); + if !sel.remove(element) { + sel.insert(element.clone()); } }); } else { self.selection.update(|sel| { sel.clear(); - sel.insert(key); + sel.insert(element.clone()); }); } } diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 2893b6d..caf11e8 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -11,12 +11,9 @@ use crate::{ AppState, assembly::{ Element, - ElementKey, - ElementRc, HalfCurvatureRegulator, InversiveDistanceRegulator, - Regulator, - RegulatorKey + Regulator }, specified::SpecifiedValue }; @@ -92,20 +89,16 @@ fn RegulatorInput(regulator: Rc) -> View { } pub trait OutlineItem { - fn outline_item(self: Rc, element_key: ElementKey) -> View; + fn outline_item(self: Rc, element: &Rc) -> View; } impl OutlineItem for InversiveDistanceRegulator { - fn outline_item(self: Rc, element_key: ElementKey) -> View { - let state = use_context::(); - let other_subject = if self.subjects[0] == element_key { - self.subjects[1] + fn outline_item(self: Rc, element: &Rc) -> View { + let other_subject_label = if self.subjects[0] == element.clone() { + self.subjects[1].label() } else { - self.subjects[0] - }; - let other_subject_label = state.assembly.elements.with( - |elts| elts[other_subject].label().clone() - ); + self.subjects[0].label() + }.clone(); view! { li(class="regulator") { div(class="regulator-label") { (other_subject_label) } @@ -118,7 +111,7 @@ impl OutlineItem for InversiveDistanceRegulator { } impl OutlineItem for HalfCurvatureRegulator { - fn outline_item(self: Rc, _element_key: ElementKey) -> View { + fn outline_item(self: Rc, _element: &Rc) -> View { view! { li(class="regulator") { div(class="regulator-label") // for spacing @@ -130,23 +123,16 @@ impl OutlineItem for HalfCurvatureRegulator { } } -// a list item that shows a regulator in an outline view of an element -#[component(inline_props)] -fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View { - let state = use_context::(); - let regulator = state.assembly.regulators.with( - |regs| regs[regulator_key].clone() - ); - regulator.outline_item(element_key) -} - // a list item that shows an element in an outline view of an assembly #[component(inline_props)] -fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { +fn ElementOutlineItem(element: Rc) -> View { let state = use_context::(); - let class = state.selection.map( - move |sel| if sel.contains(&key) { "selected" } else { "" } - ); + let class = { + let element_for_class = element.clone(); + state.selection.map( + move |sel| if sel.contains(&element_for_class) { "selected" } else { "" } + ) + }; let label = element.label().clone(); let representation = element.representation().clone(); let rep_components = move || { @@ -161,14 +147,10 @@ fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { }; let regulated = element.regulators().map(|regs| regs.len() > 0); let regulator_list = element.regulators().map( - move |elt_reg_keys| elt_reg_keys + |regs| regs .clone() .into_iter() - .sorted_by_key( - |®_key| state.assembly.regulators.with( - |regs| regs[reg_key].subjects().len() - ) - ) + .sorted_by_key(|reg| reg.subjects().len()) .collect() ); let details_node = create_node_ref(); @@ -178,10 +160,11 @@ fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { summary( class=class.get(), on:keydown={ + let element_for_handler = element.clone(); move |event: KeyboardEvent| { match event.key().as_str() { "Enter" => { - state.select(key, event.shift_key()); + state.select(&element_for_handler, event.shift_key()); event.prevent_default(); }, "ArrowRight" if regulated.get() => { @@ -208,19 +191,10 @@ fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { div( class="element", on:click={ + let state_for_handler = state.clone(); + let element_for_handler = element.clone(); move |event: MouseEvent| { - if event.shift_key() { - state.selection.update(|sel| { - if !sel.remove(&key) { - sel.insert(key); - } - }); - } else { - state.selection.update(|sel| { - sel.clear(); - sel.insert(key); - }); - } + state_for_handler.select(&element_for_handler, event.shift_key()); event.stop_propagation(); event.prevent_default(); } @@ -234,13 +208,8 @@ fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { ul(class="regulators") { Keyed( list=regulator_list, - view=move |reg_key| view! { - RegulatorOutlineItem( - regulator_key=reg_key, - element_key=key - ) - }, - key=|reg_key| reg_key.clone() + view=move |reg| reg.outline_item(&element), + key=|reg| reg.serial() ) } } @@ -259,12 +228,15 @@ pub fn Outline() -> View { let state = use_context::(); // list the elements alphabetically by ID + /* TO DO */ + // this code is designed to generalize easily to other sort keys. if we only + // ever wanted to sort by ID, we could do that more simply using the + // `elements_by_id` index let element_list = state.assembly.elements.map( |elts| elts .clone() .into_iter() - .sorted_by_key(|(_, elt)| elt.id().clone()) - .map(|(key, elt)| (key, ElementRc(elt))) + .sorted_by_key(|elt| elt.id().clone()) .collect() ); @@ -278,10 +250,10 @@ pub fn Outline() -> View { ) { Keyed( list=element_list, - view=|(key, ElementRc(elt))| view! { - ElementOutlineItem(key=key, element=elt) + view=|elt| view! { + ElementOutlineItem(element=elt) }, - key=|(_, ElementRc(elt))| elt.serial() + key=|elt| elt.serial() ) } } From a671a8273ae183bf35b8d82e6cb4a024ff956f1d Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 2 Jun 2025 15:56:06 +0000 Subject: [PATCH 128/132] Introduce ghost mode for elements (#85) Allows any element to be put in "ghost mode," decreasing its opacity and making it insensitive to click-to-select. Ghost mode is toggled using a checkbox in the outline view. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/85 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/main.css | 4 +++ app-proto/src/assembly.rs | 13 ++++++++ app-proto/src/display.rs | 61 ++++++++++++++++++++++++++------------ app-proto/src/outline.rs | 6 +++- app-proto/src/point.frag | 7 +++-- app-proto/src/point.vert | 4 +-- app-proto/src/spheres.frag | 13 ++++---- 7 files changed, 77 insertions(+), 31 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index f787535..d56784f 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -90,6 +90,10 @@ summary > div, .regulator { padding-right: 8px; } +.element > input { + margin-left: 8px; +} + .element-switch { width: 18px; padding-left: 2px; diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index bd185c8..e48b802 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -101,6 +101,7 @@ pub trait Element: Serial + ProblemPoser + DisplayItem { fn id(&self) -> &String; fn label(&self) -> &String; fn representation(&self) -> Signal>; + fn ghost(&self) -> Signal; // the regulators the element is subject to. the assembly that owns the // element is responsible for keeping this set up to date @@ -154,6 +155,7 @@ pub struct Sphere { pub label: String, pub color: ElementColor, pub representation: Signal>, + pub ghost: Signal, pub regulators: Signal>>, serial: u64, column_index: Cell> @@ -173,6 +175,7 @@ impl Sphere { label: label, color: color, representation: create_signal(representation), + ghost: create_signal(false), regulators: create_signal(BTreeSet::new()), serial: Self::next_serial(), column_index: None.into() @@ -210,6 +213,10 @@ impl Element for Sphere { self.representation } + fn ghost(&self) -> Signal { + self.ghost + } + fn regulators(&self) -> Signal>> { self.regulators } @@ -244,6 +251,7 @@ pub struct Point { pub label: String, pub color: ElementColor, pub representation: Signal>, + pub ghost: Signal, pub regulators: Signal>>, serial: u64, column_index: Cell> @@ -263,6 +271,7 @@ impl Point { label, color, representation: create_signal(representation), + ghost: create_signal(false), regulators: create_signal(BTreeSet::new()), serial: Self::next_serial(), column_index: None.into() @@ -296,6 +305,10 @@ impl Element for Point { self.representation } + fn ghost(&self) -> Signal { + self.ghost + } + fn regulators(&self) -> Signal>> { self.regulators } diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index a2fe4b6..69a3659 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -20,11 +20,23 @@ use crate::{ assembly::{Element, ElementColor, ElementMotion, Point, Sphere} }; +// --- color --- + +const COLOR_SIZE: usize = 3; +type ColorWithOpacity = [f32; COLOR_SIZE + 1]; + +fn combine_channels(color: ElementColor, opacity: f32) -> ColorWithOpacity { + let mut color_with_opacity = [0.0; COLOR_SIZE + 1]; + color_with_opacity[..COLOR_SIZE].copy_from_slice(&color); + color_with_opacity[COLOR_SIZE] = opacity; + color_with_opacity +} + // --- scene data --- struct SceneSpheres { representations: Vec>, - colors: Vec, + colors_with_opacity: Vec, highlights: Vec } @@ -32,7 +44,7 @@ impl SceneSpheres { fn new() -> SceneSpheres{ SceneSpheres { representations: Vec::new(), - colors: Vec::new(), + colors_with_opacity: Vec::new(), highlights: Vec::new() } } @@ -41,16 +53,16 @@ impl SceneSpheres { self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer") } - fn push(&mut self, representation: DVector, color: ElementColor, highlight: f32) { + fn push(&mut self, representation: DVector, color: ElementColor, opacity: f32, highlight: f32) { self.representations.push(representation); - self.colors.push(color); + self.colors_with_opacity.push(combine_channels(color, opacity)); self.highlights.push(highlight); } } struct ScenePoints { representations: Vec>, - colors: Vec, + colors_with_opacity: Vec, highlights: Vec, selections: Vec } @@ -59,15 +71,15 @@ impl ScenePoints { fn new() -> ScenePoints { ScenePoints { representations: Vec::new(), - colors: Vec::new(), + colors_with_opacity: Vec::new(), highlights: Vec::new(), selections: Vec::new() } } - fn push(&mut self, representation: DVector, color: ElementColor, highlight: f32, selected: bool) { + fn push(&mut self, representation: DVector, color: ElementColor, opacity: f32, highlight: f32, selected: bool) { self.representations.push(representation); - self.colors.push(color); + self.colors_with_opacity.push(combine_channels(color, opacity)); self.highlights.push(highlight); self.selections.push(if selected { 1.0 } else { 0.0 }); } @@ -98,11 +110,16 @@ pub trait DisplayItem { impl DisplayItem for Sphere { fn show(&self, scene: &mut Scene, selected: bool) { - const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */ + /* SCAFFOLDING */ + const DEFAULT_OPACITY: f32 = 0.5; + const GHOST_OPACITY: f32 = 0.2; + const HIGHLIGHT: f32 = 0.2; + let representation = self.representation.get_clone_untracked(); let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; + let opacity = if self.ghost.get() { GHOST_OPACITY } else { DEFAULT_OPACITY }; let highlight = if selected { 1.0 } else { HIGHLIGHT }; - scene.spheres.push(representation, color, highlight); + scene.spheres.push(representation, color, opacity, highlight); } // this method should be kept synchronized with `sphere_cast` in @@ -148,11 +165,15 @@ impl DisplayItem for Sphere { impl DisplayItem for Point { fn show(&self, scene: &mut Scene, selected: bool) { - const HIGHLIGHT: f32 = 0.5; /* SCAFFOLDING */ + /* SCAFFOLDING */ + const GHOST_OPACITY: f32 = 0.4; + const HIGHLIGHT: f32 = 0.5; + let representation = self.representation.get_clone_untracked(); let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; + let opacity = if self.ghost.get() { GHOST_OPACITY } else { 1.0 }; let highlight = if selected { 1.0 } else { HIGHLIGHT }; - scene.points.push(representation, color, highlight, selected); + scene.points.push(representation, color, opacity, highlight, selected); } /* SCAFFOLDING */ @@ -365,6 +386,7 @@ pub fn Display() -> View { state.assembly.elements.with(|elts| { for elt in elts { elt.representation().track(); + elt.ghost().track(); } }); state.selection.track(); @@ -395,7 +417,6 @@ pub fn Display() -> View { const SHRINKING_SPEED: f64 = 0.15; // in length units per second // display parameters - const OPACITY: f32 = 0.5; /* SCAFFOLDING */ const LAYER_THRESHOLD: i32 = 0; /* DEBUG */ const DEBUG_MODE: i32 = 0; /* DEBUG */ @@ -469,7 +490,6 @@ pub fn Display() -> View { ); let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution"); let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim"); - let opacity_loc = ctx.get_uniform_location(&sphere_program, "opacity"); let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold"); let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode"); @@ -654,9 +674,9 @@ pub fn Display() -> View { sphere_lt_locs[n].as_ref(), v.rows(3, 2).as_slice() ); - ctx.uniform3fv_with_f32_array( + ctx.uniform4fv_with_f32_array( sphere_color_locs[n].as_ref(), - &scene.spheres.colors[n] + &scene.spheres.colors_with_opacity[n] ); ctx.uniform1f( sphere_highlight_locs[n].as_ref(), @@ -665,7 +685,6 @@ pub fn Display() -> View { } // pass the display parameters - ctx.uniform1f(opacity_loc.as_ref(), OPACITY); ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); @@ -703,7 +722,7 @@ pub fn Display() -> View { // bind them to the corresponding attributes in the vertex // shader bind_new_buffer_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, point_positions.as_slice()); - bind_new_buffer_to_attribute(&ctx, point_color_attr, COLOR_SIZE as i32, scene.points.colors.concat().as_slice()); + bind_new_buffer_to_attribute(&ctx, point_color_attr, (COLOR_SIZE + 1) as i32, scene.points.colors_with_opacity.concat().as_slice()); bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice()); bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice()); @@ -851,7 +870,11 @@ pub fn Display() -> View { let (dir, pixel_size) = event_dir(&event); console::log_1(&JsValue::from(dir.to_string())); let mut clicked: Option<(Rc, f64)> = None; - for elt in state.assembly.elements.get_clone_untracked() { + let tangible_elts = state.assembly.elements + .get_clone_untracked() + .into_iter() + .filter(|elt| !elt.ghost().get()); + for elt in tangible_elts { match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) { Some(depth) => match clicked { Some((_, best_depth)) => { diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index caf11e8..59bbdcc 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -202,7 +202,11 @@ fn ElementOutlineItem(element: Rc) -> View { ) { div(class="element-label") { (label) } div(class="element-representation") { (rep_components) } - div(class="status") + input( + r#type="checkbox", + bind:checked=element.ghost(), + on:click=|event: MouseEvent| event.stop_propagation() + ) } } ul(class="regulators") { diff --git a/app-proto/src/point.frag b/app-proto/src/point.frag index 3a361a8..194a072 100644 --- a/app-proto/src/point.frag +++ b/app-proto/src/point.frag @@ -2,7 +2,7 @@ precision highp float; -in vec3 point_color; +in vec4 point_color; in float point_highlight; in float total_radius; @@ -13,6 +13,7 @@ void main() { const float POINT_RADIUS = 4.; float border = smoothstep(POINT_RADIUS - 1., POINT_RADIUS, r); - vec3 color = mix(point_color, vec3(1.), border * point_highlight); - outColor = vec4(color, 1. - smoothstep(total_radius - 1., total_radius, r)); + float disk = 1. - smoothstep(total_radius - 1., total_radius, r); + vec4 color = mix(point_color, vec4(1.), border * point_highlight); + outColor = vec4(vec3(1.), disk) * color; } \ No newline at end of file diff --git a/app-proto/src/point.vert b/app-proto/src/point.vert index 6945010..0b76bc1 100644 --- a/app-proto/src/point.vert +++ b/app-proto/src/point.vert @@ -1,11 +1,11 @@ #version 300 es in vec4 position; -in vec3 color; +in vec4 color; in float highlight; in float selected; -out vec3 point_color; +out vec4 point_color; out float point_highlight; out float total_radius; diff --git a/app-proto/src/spheres.frag b/app-proto/src/spheres.frag index d50cb1e..fa317a8 100644 --- a/app-proto/src/spheres.frag +++ b/app-proto/src/spheres.frag @@ -17,7 +17,7 @@ struct vecInv { const int SPHERE_MAX = 200; uniform int sphere_cnt; uniform vecInv sphere_list[SPHERE_MAX]; -uniform vec3 color_list[SPHERE_MAX]; +uniform vec4 color_list[SPHERE_MAX]; uniform float highlight_list[SPHERE_MAX]; // view @@ -25,7 +25,6 @@ uniform vec2 resolution; uniform float shortdim; // controls -uniform float opacity; uniform int layer_threshold; uniform bool debug_mode; @@ -69,7 +68,7 @@ struct Fragment { vec4 color; }; -Fragment sphere_shading(vecInv v, vec3 pt, vec3 base_color) { +Fragment sphere_shading(vecInv v, vec3 pt, vec4 base_color) { // the expression for normal needs to be checked. it's supposed to give the // negative gradient of the lorentz product between the impact point vector // and the sphere vector with respect to the coordinates of the impact @@ -79,7 +78,7 @@ Fragment sphere_shading(vecInv v, vec3 pt, vec3 base_color) { float incidence = dot(normal, light_dir); float illum = mix(0.4, 1.0, max(incidence, 0.0)); - return Fragment(pt, normal, vec4(illum * base_color, opacity)); + return Fragment(pt, normal, vec4(illum * base_color.rgb, base_color.a)); } float intersection_dist(Fragment a, Fragment b) { @@ -192,10 +191,11 @@ void main() { vec3 color = vec3(0.); int layer = layer_cnt - 1; TaggedDepth hit = top_hits[layer]; + vec4 sphere_color = color_list[hit.id]; Fragment frag_next = sphere_shading( sphere_list[hit.id], hit.depth * dir, - hit.dimming * color_list[hit.id] + vec4(hit.dimming * sphere_color.rgb, sphere_color.a) ); float highlight_next = highlight_list[hit.id]; --layer; @@ -206,10 +206,11 @@ void main() { // shade the next fragment hit = top_hits[layer]; + sphere_color = color_list[hit.id]; frag_next = sphere_shading( sphere_list[hit.id], hit.depth * dir, - hit.dimming * color_list[hit.id] + vec4(hit.dimming * sphere_color.rgb, sphere_color.a) ); highlight_next = highlight_list[hit.id]; From e447e7ea966880d24bada011bf5df08e927b3871 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Wed, 4 Jun 2025 21:01:12 +0000 Subject: [PATCH 129/132] Dispatch normalization routines correctly (#87) Addresses issue #86 by correctly dispatching the routine used to normalize spheres during nudging. Adds a test that would have detected the issue. Since the tests aren't built for WebAssembly, we have to replace `console::log` with `console_log!` in all of the functions used by `assembly::curvature_drift_test`. We'll eventually want to do this replacement everywhere. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/87 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.toml | 7 +++ app-proto/src/assembly.rs | 123 +++++++++++++++++++++++++------------- app-proto/src/engine.rs | 34 ++++++++--- 3 files changed, 112 insertions(+), 52 deletions(-) diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 844a0a6..9b46b2b 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -46,6 +46,13 @@ features = [ dyna3 = { path = ".", default-features = false, features = ["dev"] } wasm-bindgen-test = "0.3.34" +# turn off spurious warnings about the custom config that Sycamore uses +# +# https://sycamore.dev/book/troubleshooting#unexpected-cfg-condition-name--sycamore-force-ssr +# +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(sycamore_force_ssr)"] } + [profile.release] opt-level = "s" # optimize for small code size debug = true # include debug symbols diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index e48b802..6c91fc0 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,6 +1,5 @@ use nalgebra::{DMatrix, DVector, DVectorView}; use std::{ - any::{Any, TypeId}, cell::Cell, collections::{BTreeMap, BTreeSet}, cmp::Ordering, @@ -20,6 +19,8 @@ use crate::{ change_half_curvature, local_unif_to_std, point, + project_point_to_normalized, + project_sphere_to_normalized, realize_gram, sphere, ConfigSubspace, @@ -107,6 +108,10 @@ pub trait Element: Serial + ProblemPoser + DisplayItem { // element is responsible for keeping this set up to date fn regulators(&self) -> Signal>>; + // project a representation vector for this kind of element onto its + // normalization variety + fn project_to_normalized(&self, rep: &mut DVector); + // the configuration matrix column index that was assigned to the element // last time the assembly was realized, or `None` if the element has never // been through a realization @@ -221,6 +226,10 @@ impl Element for Sphere { self.regulators } + fn project_to_normalized(&self, rep: &mut DVector) { + project_sphere_to_normalized(rep); + } + fn column_index(&self) -> Option { self.column_index.get() } @@ -313,6 +322,10 @@ impl Element for Point { self.regulators } + fn project_to_normalized(&self, rep: &mut DVector) { + project_point_to_normalized(rep); + } + fn column_index(&self) -> Option { self.column_index.get() } @@ -611,9 +624,7 @@ impl Assembly { create_effect(move || { /* DEBUG */ // log the regulator update - console::log_1(&JsValue::from( - format!("Updated regulator with subjects {:?}", regulator.subjects()) - )); + console_log!("Updated regulator with subjects {:?}", regulator.subjects()); if regulator.try_activate() { self_for_effect.realize(); @@ -622,10 +633,10 @@ impl Assembly { /* DEBUG */ // print an updated list of regulators - console::log_1(&JsValue::from("Regulators:")); + console_log!("Regulators:"); self.regulators.with_untracked(|regs| { for reg in regs.into_iter() { - console::log_1(&JsValue::from(format!( + console_log!( " {:?}: {}", reg.subjects(), reg.set_point().with_untracked( @@ -638,7 +649,7 @@ impl Assembly { } } ) - ))); + ); } }); } @@ -669,19 +680,11 @@ impl Assembly { /* DEBUG */ // log the Gram matrix - console::log_1(&JsValue::from("Gram matrix:")); - problem.gram.log_to_console(); + console_log!("Gram matrix:\n{}", problem.gram); /* DEBUG */ // log the initial configuration matrix - console::log_1(&JsValue::from("Old configuration:")); - for j in 0..problem.guess.nrows() { - let mut row_str = String::new(); - for k in 0..problem.guess.ncols() { - row_str.push_str(format!(" {:>8.3}", problem.guess[(j, k)]).as_str()); - } - console::log_1(&JsValue::from(row_str)); - } + console_log!("Old configuration:{:>8.3}", problem.guess); // look for a configuration with the given Gram matrix let (config, tangent, success, history) = realize_gram( @@ -690,16 +693,14 @@ impl Assembly { /* DEBUG */ // report the outcome of the search - console::log_1(&JsValue::from( - if success { - "Target accuracy achieved!" - } else { - "Failed to reach target accuracy" - } - )); - console::log_2(&JsValue::from("Steps:"), &JsValue::from(history.scaled_loss.len() - 1)); - console::log_2(&JsValue::from("Loss:"), &JsValue::from(*history.scaled_loss.last().unwrap())); - console::log_2(&JsValue::from("Tangent dimension:"), &JsValue::from(tangent.dim())); + if success { + console_log!("Target accuracy achieved!") + } else { + console_log!("Failed to reach target accuracy") + } + console_log!("Steps: {}", history.scaled_loss.len() - 1); + console_log!("Loss: {}", *history.scaled_loss.last().unwrap()); + console_log!("Tangent dimension: {}", tangent.dim()); if success { // read out the solution @@ -782,29 +783,17 @@ impl Assembly { // step the assembly along the deformation. this changes the elements' // normalizations, so we restore those afterward - /* KLUDGE */ - // for now, we only restore the normalizations of spheres for elt in self.elements.get_clone_untracked() { elt.representation().update_silent(|rep| { match elt.column_index() { Some(column_index) => { - // step the assembly along the deformation + // step the element along the deformation and then + // restore its normalization *rep += motion_proj.column(column_index); - - if elt.type_id() == TypeId::of::() { - // restore normalization by contracting toward the - // last coordinate axis - let q_sp = rep.fixed_rows::<3>(0).norm_squared(); - let half_q_lt = -2.0 * rep[3] * rep[4]; - let half_q_lt_sq = half_q_lt * half_q_lt; - let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); - rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling); - } + elt.project_to_normalized(rep); }, None => { - console::log_1(&JsValue::from( - format!("No velocity to unpack for fresh element \"{}\"", elt.id()) - )) + console_log!("No velocity to unpack for fresh element \"{}\"", elt.id()) } }; }); @@ -821,6 +810,8 @@ impl Assembly { mod tests { use super::*; + use crate::engine; + #[test] #[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")] fn unindexed_element_test() { @@ -846,4 +837,50 @@ mod tests { }.pose(&mut ConstraintProblem::new(2)); }); } + + #[test] + fn curvature_drift_test() { + const INITIAL_RADIUS: f64 = 0.25; + let _ = create_root(|| { + // set up an assembly containing a single sphere centered at the + // origin + let assembly = Assembly::new(); + let sphere_id = "sphere0"; + let _ = assembly.try_insert_element( + // we create the sphere by hand for two reasons: to choose the + // curvature (which can affect drift rate) and to make the test + // independent of `Sphere::default` + Sphere::new( + String::from(sphere_id), + String::from("Sphere 0"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS) + ) + ); + + // nudge the sphere repeatedly along the `z` axis + const STEP_SIZE: f64 = 0.0025; + const STEP_CNT: usize = 400; + let sphere = assembly.elements_by_id.with(|elts_by_id| elts_by_id[sphere_id].clone()); + let velocity = DVector::from_column_slice(&[0.0, 0.0, STEP_SIZE, 0.0]); + for _ in 0..STEP_CNT { + assembly.deform( + vec![ + ElementMotion { + element: sphere.clone(), + velocity: velocity.as_view() + } + ] + ); + } + + // check how much the sphere's curvature has drifted + const INITIAL_HALF_CURV: f64 = 0.5 / INITIAL_RADIUS; + const DRIFT_TOL: f64 = 0.015; + let final_half_curv = sphere.representation().with_untracked( + |rep| rep[Sphere::CURVATURE_COMPONENT] + ); + assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL); + }); + } } \ No newline at end of file diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index b0fa23d..c5d7b00 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -1,5 +1,6 @@ use lazy_static::lazy_static; use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen}; +use std::fmt::{Display, Error, Formatter}; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ // --- elements --- @@ -34,6 +35,21 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 ]) } +// project a sphere's representation vector to the normalization variety by +// contracting toward the last coordinate axis +pub fn project_sphere_to_normalized(rep: &mut DVector) { + let q_sp = rep.fixed_rows::<3>(0).norm_squared(); + let half_q_lt = -2.0 * rep[3] * rep[4]; + let half_q_lt_sq = half_q_lt * half_q_lt; + let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); + rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling); +} + +// normalize a point's representation vector by scaling +pub fn project_point_to_normalized(rep: &mut DVector) { + rep.scale_mut(0.5 / rep[3]); +} + // given a sphere's representation vector, change the sphere's half-curvature to // `half-curv` and then restore normalization by contracting the representation // vector toward the curvature axis @@ -94,15 +110,6 @@ impl PartialMatrix { } } - /* DEBUG */ - pub fn log_to_console(&self) { - for &MatrixEntry { index: (row, col), value } in self { - console::log_1(&JsValue::from( - format!(" {} {} {}", row, col, value) - )); - } - } - fn freeze(&self, a: &DMatrix) -> DMatrix { let mut result = a.clone(); for &MatrixEntry { index, value } in self { @@ -128,6 +135,15 @@ impl PartialMatrix { } } +impl Display for PartialMatrix { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + for &MatrixEntry { index: (row, col), value } in self { + writeln!(f, " {row} {col} {value}")?; + } + Ok(()) + } +} + impl IntoIterator for PartialMatrix { type Item = MatrixEntry; type IntoIter = std::vec::IntoIter; From 4cb32625556743de30b49eed4b6bfce8ab9190dc Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Thu, 26 Jun 2025 22:11:02 +0000 Subject: [PATCH 130/132] chore: Update Sycamore to 0.9.1 (#91) Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/91 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.lock | 25 +++++++++++++------------ app-proto/Cargo.toml | 2 +- app-proto/src/outline.rs | 4 ++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock index 3bf609c..55e8686 100644 --- a/app-proto/Cargo.lock +++ b/app-proto/Cargo.lock @@ -423,9 +423,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "sycamore" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dedaf7237c05913604a5b0b2536b613f6c8510c6b213d2583b1294869755cabd" +checksum = "5f38201dcb10aa609e81ca6f7547758a7eb602240a5ff682e668909fd0f7b2cc" dependencies = [ "hashbrown", "indexmap", @@ -440,9 +440,9 @@ dependencies = [ [[package]] name = "sycamore-core" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ddddc3d1bcb38c04ad55d2d1ab4f6a358e4daaeae0a0436892f1fade9fb31a" +checksum = "41dc04bf0de321c6486356b2be751fac82fabb06c992d25b6748587561bba187" dependencies = [ "hashbrown", "paste", @@ -451,9 +451,9 @@ dependencies = [ [[package]] name = "sycamore-macro" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77181c27cb753e86065308901871ccc7456fb19527b6a4ffacad3b63175ed014" +checksum = "a0c1d2eddc94db6d03e67eb832df5512b967e81053a573cd01bf3e1c3db00137" dependencies = [ "once_cell", "proc-macro2", @@ -465,20 +465,21 @@ dependencies = [ [[package]] name = "sycamore-reactive" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aa6870203507c07e850687c0ccf528eb0f04240e3596bac9137007ffb6c50b1" +checksum = "f2bacf810535efc2701187a716a5652197ad241d620d5b00fb12caa6dfa23add" dependencies = [ "paste", "slotmap", "smallvec", + "wasm-bindgen", ] [[package]] name = "sycamore-view-parser" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6144640af2eafffc68a92f3aacbbfaa21f7fd31906e2336fe304fd100fe226b" +checksum = "6c22875843db83cd4d49c0123a195e433bdc74e13ed0fff4ace0e77bb0a67033" dependencies = [ "proc-macro2", "quote", @@ -487,9 +488,9 @@ dependencies = [ [[package]] name = "sycamore-web" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca93dcf1b1830bf1aac93508ed51babcda92c1d32d96067ab416d94e4b7c475" +checksum = "4b17aa5875f59f541cdf6fb58751ec702a6ed9801f30dd2b4d5f2279025b98bd" dependencies = [ "html-escape", "js-sys", diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 9b46b2b..6932b72 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -15,7 +15,7 @@ js-sys = "0.3.70" lazy_static = "1.5.0" nalgebra = "0.33.0" readonly = "0.2.12" -sycamore = "0.9.0-beta.3" +sycamore = "0.9.1" # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 59bbdcc..77d8575 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -151,7 +151,7 @@ fn ElementOutlineItem(element: Rc) -> View { .clone() .into_iter() .sorted_by_key(|reg| reg.subjects().len()) - .collect() + .collect::>() ); let details_node = create_node_ref(); view! { @@ -241,7 +241,7 @@ pub fn Outline() -> View { .clone() .into_iter() .sorted_by_key(|elt| elt.id().clone()) - .collect() + .collect::>() ); view! { From 5864017e6fd4e45f138b3fa4dac4cbd362a95641 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 21 Jul 2025 04:18:49 +0000 Subject: [PATCH 131/132] feat: Engine diagnostics (#92) Adds a `Diagnostics` component that shows the following diagnostics from the last realization: - Confirmation of success or a short description of what failed. - The value of the loss function at each step. - The spectrum of the Hessian at each step. The loss and spectrum plots are shown on switchable panels. Also includes some refactoring/renaming of existing code. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/92 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.lock | 565 +++++++++++++++++++++++++- app-proto/Cargo.toml | 3 + app-proto/examples/common/print.rs | 36 ++ app-proto/examples/irisawa-hexlet.rs | 28 +- app-proto/examples/kaleidocycle.rs | 48 +-- app-proto/examples/point-on-sphere.rs | 32 +- app-proto/examples/three-spheres.rs | 29 +- app-proto/index.html | 6 + app-proto/main.css | 66 ++- app-proto/run-examples | 12 - app-proto/run-examples.sh | 20 + app-proto/src/add_remove.rs | 4 +- app-proto/src/assembly.rs | 65 ++- app-proto/src/diagnostics.rs | 258 ++++++++++++ app-proto/src/display.rs | 1 + app-proto/src/engine.rs | 94 +++-- app-proto/src/main.rs | 3 + 17 files changed, 1120 insertions(+), 150 deletions(-) create mode 100644 app-proto/examples/common/print.rs delete mode 100755 app-proto/run-examples create mode 100644 app-proto/run-examples.sh create mode 100644 app-proto/src/diagnostics.rs diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock index 55e8686..4f75c45 100644 --- a/app-proto/Cargo.lock +++ b/app-proto/Cargo.lock @@ -20,6 +20,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "approx" version = "0.5.1" @@ -35,6 +50,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -68,6 +98,35 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "charming" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ffae2e616ae7d66b2e9ea369f1c7650042bdcdc1dc08b04b027107007b4f09" +dependencies = [ + "handlebars", + "js-sys", + "serde", + "serde-wasm-bindgen", + "serde_json", + "serde_with", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -78,10 +137,122 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dyna3" version = "0.1.0" dependencies = [ + "charming", "console_error_panic_hook", "dyna3", "itertools", @@ -106,6 +277,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -117,6 +304,28 @@ dependencies = [ "wasi", ] +[[package]] +name = "handlebars" +version = "6.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" +dependencies = [ + "derive_builder", + "log", + "num-order", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -127,6 +336,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "html-escape" version = "0.2.13" @@ -136,6 +351,47 @@ dependencies = [ "utf8-width", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.5.0" @@ -143,7 +399,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", + "serde", ] [[package]] @@ -155,6 +412,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" version = "0.3.70" @@ -192,6 +455,12 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "minicov" version = "0.3.5" @@ -248,6 +517,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -257,6 +532,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" + +[[package]] +name = "num-order" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" +dependencies = [ + "num-modular", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -289,6 +579,57 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pest" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -363,6 +704,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "safe_arch" version = "0.7.2" @@ -387,6 +734,90 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.5.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -421,14 +852,20 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "sycamore" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f38201dcb10aa609e81ca6f7547758a7eb602240a5ff682e668909fd0f7b2cc" dependencies = [ - "hashbrown", - "indexmap", + "hashbrown 0.14.5", + "indexmap 2.5.0", "paste", "sycamore-core", "sycamore-macro", @@ -444,7 +881,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41dc04bf0de321c6486356b2be751fac82fabb06c992d25b6748587561bba187" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", "paste", "sycamore-reactive", ] @@ -506,21 +943,78 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.13" @@ -677,6 +1171,65 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 6932b72..1230b47 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -17,6 +17,9 @@ nalgebra = "0.33.0" readonly = "0.2.12" sycamore = "0.9.1" +# We use Charming to help display engine diagnostics +charming = { version = "0.5.1", features = ["wasm"] } + # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for diff --git a/app-proto/examples/common/print.rs b/app-proto/examples/common/print.rs new file mode 100644 index 0000000..2aa6a39 --- /dev/null +++ b/app-proto/examples/common/print.rs @@ -0,0 +1,36 @@ +#![allow(dead_code)] + +use nalgebra::DMatrix; + +use dyna3::engine::{Q, DescentHistory, Realization}; + +pub fn title(title: &str) { + println!("─── {title} ───"); +} + +pub fn realization_diagnostics(realization: &Realization) { + let Realization { result, history } = realization; + println!(); + if let Err(ref message) = result { + println!("❌️ {message}"); + } else { + println!("✅️ Target accuracy achieved!"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); +} + +pub fn gram_matrix(config: &DMatrix) { + println!("\nCompleted Gram matrix:{}", (config.tr_mul(&*Q) * config).to_string().trim_end()); +} + +pub fn config(config: &DMatrix) { + println!("\nConfiguration:{}", config.to_string().trim_end()); +} + +pub fn loss_history(history: &DescentHistory) { + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } +} \ No newline at end of file diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs index 639a494..0d710ff 100644 --- a/app-proto/examples/irisawa-hexlet.rs +++ b/app-proto/examples/irisawa-hexlet.rs @@ -1,25 +1,23 @@ -use dyna3::engine::{Q, examples::realize_irisawa_hexlet}; +#[path = "common/print.rs"] +mod print; + +use dyna3::engine::{ConfigNeighborhood, examples::realize_irisawa_hexlet}; fn main() { const SCALED_TOL: f64 = 1.0e-12; - let (config, _, success, history) = realize_irisawa_hexlet(SCALED_TOL); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - if success { + let realization = realize_irisawa_hexlet(SCALED_TOL); + print::title("Irisawa hexlet"); + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood { config, .. }) = realization.result { + // print the diameters of the chain spheres println!("\nChain diameters:"); println!(" {} sun (given)", 1.0 / config[(3, 3)]); for k in 4..9 { println!(" {} sun", 1.0 / config[(3, k)]); } + + // print the completed Gram matrix + print::gram_matrix(&config); } - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); - } + print::loss_history(&realization.history); } \ No newline at end of file diff --git a/app-proto/examples/kaleidocycle.rs b/app-proto/examples/kaleidocycle.rs index 2779ab1..7ca1f97 100644 --- a/app-proto/examples/kaleidocycle.rs +++ b/app-proto/examples/kaleidocycle.rs @@ -1,30 +1,32 @@ +#[path = "common/print.rs"] +mod print; + use nalgebra::{DMatrix, DVector}; -use dyna3::engine::{Q, examples::realize_kaleidocycle}; +use dyna3::engine::{ConfigNeighborhood, examples::realize_kaleidocycle}; fn main() { const SCALED_TOL: f64 = 1.0e-12; - let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); - print!("Completed Gram matrix:{}", config.tr_mul(&*Q) * &config); - print!("Configuration:{}", config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); + let realization = realize_kaleidocycle(SCALED_TOL); + print::title("Kaleidocycle"); + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood { config, nbhd: tangent }) = realization.result { + // print the completed Gram matrix and the realized configuration + print::gram_matrix(&config); + print::config(&config); + + // find the kaleidocycle's twist motion by projecting onto the tangent + // space + const N_POINTS: usize = 12; + let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); + let down = -&up; + let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( + |n| [ + tangent.proj(&up.as_view(), n), + tangent.proj(&down.as_view(), n+1) + ] + ).sum(); + let normalization = 5.0 / twist_motion[(2, 0)]; + println!("\nTwist motion:{}", (normalization * twist_motion).to_string().trim_end()); } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}\n", history.scaled_loss.last().unwrap()); - - // find the kaleidocycle's twist motion by projecting onto the tangent space - const N_POINTS: usize = 12; - let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); - let down = -&up; - let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( - |n| [ - tangent.proj(&up.as_view(), n), - tangent.proj(&down.as_view(), n+1) - ] - ).sum(); - let normalization = 5.0 / twist_motion[(2, 0)]; - print!("Twist motion:{}", normalization * twist_motion); } \ No newline at end of file diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs index 880d7b0..89dee76 100644 --- a/app-proto/examples/point-on-sphere.rs +++ b/app-proto/examples/point-on-sphere.rs @@ -1,4 +1,13 @@ -use dyna3::engine::{Q, point, realize_gram, sphere, ConstraintProblem}; +#[path = "common/print.rs"] +mod print; + +use dyna3::engine::{ + point, + realize_gram, + sphere, + ConfigNeighborhood, + ConstraintProblem +}; fn main() { let mut problem = ConstraintProblem::from_guess(&[ @@ -11,21 +20,14 @@ fn main() { } } problem.frozen.push(3, 0, problem.guess[(3, 0)]); - println!(); - let (config, _, success, history) = realize_gram( + let realization = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - print!("Configuration:{}", config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); + print::title("Point on a sphere"); + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood{ config, .. }) = realization.result { + print::gram_matrix(&config); + print::config(&config); } + print::loss_history(&realization.history); } \ No newline at end of file diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs index 3f3cc44..aa5a105 100644 --- a/app-proto/examples/three-spheres.rs +++ b/app-proto/examples/three-spheres.rs @@ -1,4 +1,12 @@ -use dyna3::engine::{Q, realize_gram, sphere, ConstraintProblem}; +#[path = "common/print.rs"] +mod print; + +use dyna3::engine::{ + realize_gram, + sphere, + ConfigNeighborhood, + ConstraintProblem +}; fn main() { let mut problem = ConstraintProblem::from_guess({ @@ -14,20 +22,13 @@ fn main() { problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); } } - println!(); - let (config, _, success, history) = realize_gram( + let realization = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); + print::title("Three spheres"); + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood{ config, .. }) = realization.result { + print::gram_matrix(&config); } + print::loss_history(&realization.history); } \ No newline at end of file diff --git a/app-proto/index.html b/app-proto/index.html index 92238f4..4fbe52f 100644 --- a/app-proto/index.html +++ b/app-proto/index.html @@ -6,6 +6,12 @@ + + + diff --git a/app-proto/main.css b/app-proto/main.css index d56784f..7981285 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -18,6 +18,17 @@ body { font-family: 'Fira Sans', sans-serif; } +.invalid { + color: var(--text-invalid); +} + +.status { + width: 20px; + text-align: center; + font-family: 'Noto Emoji'; + font-style: normal; +} + /* sidebar */ #sidebar { @@ -138,6 +149,7 @@ details[open]:has(li) .element-switch::after { } .regulator-input { + margin-right: 4px; color: inherit; background-color: inherit; border: 1px solid var(--border); @@ -159,22 +171,56 @@ details[open]:has(li) .element-switch::after { border-color: var(--border-invalid); } -.status { - width: 20px; - padding-left: 4px; - text-align: center; - font-family: 'Noto Emoji'; - font-style: normal; -} - .regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after { content: '⚠'; color: var(--text-invalid); } +/* diagnostics */ + +#diagnostics { + margin: 10px; +} + +#diagnostics-bar { + display: flex; +} + +#realization-status { + display: flex; + flex-grow: 1; +} + +#realization-status .status { + margin-right: 4px; +} + +#realization-status :not(.status) { + flex-grow: 1; +} + +#realization-status .status::after { + content: '✓'; +} + +#realization-status.invalid .status::after { + content: '⚠'; +} + +.diagnostics-panel { + margin-top: 10px; + min-height: 180px; +} + +.diagnostics-chart { + background-color: var(--display-background); + border: 1px solid var(--border); + border-radius: 8px; +} + /* display */ -canvas { +#display { float: left; margin-left: 20px; margin-top: 20px; @@ -183,7 +229,7 @@ canvas { border-radius: 16px; } -canvas:focus { +#display:focus { border-color: var(--border-focus-dark); outline: none; } diff --git a/app-proto/run-examples b/app-proto/run-examples deleted file mode 100755 index 52173b0..0000000 --- a/app-proto/run-examples +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -# run all Cargo examples, as described here: -# -# Karol Kuczmarski. "Add examples to your Rust libraries" -# http://xion.io/post/code/rust-examples.html -# - -cargo run --example irisawa-hexlet -cargo run --example three-spheres -cargo run --example point-on-sphere -cargo run --example kaleidocycle \ No newline at end of file diff --git a/app-proto/run-examples.sh b/app-proto/run-examples.sh new file mode 100644 index 0000000..861addf --- /dev/null +++ b/app-proto/run-examples.sh @@ -0,0 +1,20 @@ +# run all Cargo examples, as described here: +# +# Karol Kuczmarski. "Add examples to your Rust libraries" +# http://xion.io/post/code/rust-examples.html +# +# you should invoke this script by calling `sh` or another interpreter, rather +# than calling `souce`, to ensure that the script can find the manifest file for +# the application prototype + +# find the manifest file for the application prototype +MANIFEST="$(dirname -- $0)/Cargo.toml" + +# set up the command that runs each example +RUN_EXAMPLE="cargo run --manifest-path $MANIFEST --example" + +# run the examples +$RUN_EXAMPLE irisawa-hexlet; echo +$RUN_EXAMPLE three-spheres; echo +$RUN_EXAMPLE point-on-sphere; echo +$RUN_EXAMPLE kaleidocycle \ No newline at end of file diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index f3bbc97..d737c79 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -3,8 +3,9 @@ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ - engine, AppState, + engine, + engine::DescentHistory, assembly::{Assembly, InversiveDistanceRegulator, Point, Sphere} }; @@ -195,6 +196,7 @@ pub fn AddRemove() -> View { assembly.regulators.update(|regs| regs.clear()); assembly.elements.update(|elts| elts.clear()); assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); + assembly.descent_history.set(DescentHistory::new()); state.selection.update(|sel| sel.clear()); // load assembly diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 6c91fc0..c3b0c6b 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -23,8 +23,11 @@ use crate::{ project_sphere_to_normalized, realize_gram, sphere, + ConfigNeighborhood, ConfigSubspace, - ConstraintProblem + ConstraintProblem, + DescentHistory, + Realization }, outline::OutlineItem, specified::SpecifiedValue @@ -547,7 +550,11 @@ pub struct Assembly { pub tangent: Signal, // indexing - pub elements_by_id: Signal>> + pub elements_by_id: Signal>>, + + // realization diagnostics + pub realization_status: Signal>, + pub descent_history: Signal } impl Assembly { @@ -556,7 +563,9 @@ impl Assembly { elements: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()), tangent: create_signal(ConfigSubspace::zero(0)), - elements_by_id: create_signal(BTreeMap::default()) + elements_by_id: create_signal(BTreeMap::default()), + realization_status: create_signal(Ok(())), + descent_history: create_signal(DescentHistory::new()) } } @@ -687,31 +696,49 @@ impl Assembly { console_log!("Old configuration:{:>8.3}", problem.guess); // look for a configuration with the given Gram matrix - let (config, tangent, success, history) = realize_gram( + let Realization { result, history } = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); /* DEBUG */ - // report the outcome of the search - if success { - console_log!("Target accuracy achieved!") + // report the outcome of the search in the browser console + if let Err(ref message) = result { + console_log!("❌️ {message}"); } else { - console_log!("Failed to reach target accuracy") + console_log!("✅️ Target accuracy achieved!"); } console_log!("Steps: {}", history.scaled_loss.len() - 1); - console_log!("Loss: {}", *history.scaled_loss.last().unwrap()); - console_log!("Tangent dimension: {}", tangent.dim()); + console_log!("Loss: {}", history.scaled_loss.last().unwrap()); - if success { - // read out the solution - for elt in self.elements.get_clone_untracked() { - elt.representation().update( - |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) - ); + // report the loss history + self.descent_history.set(history); + + match result { + Ok(ConfigNeighborhood { config, nbhd: tangent }) => { + /* DEBUG */ + // report the tangent dimension + console_log!("Tangent dimension: {}", tangent.dim()); + + // report the realization status + self.realization_status.set(Ok(())); + + // read out the solution + for elt in self.elements.get_clone_untracked() { + elt.representation().update( + |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) + ); + } + + // save the tangent space + self.tangent.set_silent(tangent); + }, + Err(message) => { + // report the realization status. the `Err(message)` we're + // setting the status to has a different type than the + // `Err(message)` we received from the match: we're changing the + // `Ok` type from `Realization` to `()` + self.realization_status.set(Err(message)) } - - // save the tangent space - self.tangent.set_silent(tangent); } } diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/diagnostics.rs new file mode 100644 index 0000000..a2f090a --- /dev/null +++ b/app-proto/src/diagnostics.rs @@ -0,0 +1,258 @@ +use charming::{ + Chart, + WasmRenderer, + component::{Axis, DataZoom, Grid}, + element::{AxisType, Symbol}, + series::{Line, Scatter}, +}; +use sycamore::prelude::*; + +use crate::AppState; + +#[derive(Clone)] +struct DiagnosticsState { + active_tab: Signal +} + +impl DiagnosticsState { + fn new(initial_tab: String) -> DiagnosticsState { + DiagnosticsState { + active_tab: create_signal(initial_tab) + } + } +} + +// a realization status indicator +#[component] +fn RealizationStatus() -> View { + let state = use_context::(); + let realization_status = state.assembly.realization_status; + view! { + div( + id="realization-status", + class=realization_status.with( + |status| match status { + Ok(_) => "", + Err(_) => "invalid" + } + ) + ) { + div(class="status") + div { + (realization_status.with( + |status| match status { + Ok(_) => "Target accuracy achieved".to_string(), + Err(message) => message.clone() + } + )) + } + } + } +} + +fn into_log10_time_point((step, value): (usize, f64)) -> Vec> { + vec![ + Some(step as f64), + if value == 0.0 { None } else { Some(value.abs().log10()) } + ] +} + +// the loss history from the last realization +#[component] +fn LossHistory() -> View { + const CONTAINER_ID: &str = "loss-history"; + let state = use_context::(); + let renderer = WasmRenderer::new_opt(None, Some(178)); + + on_mount(move || { + create_effect(move || { + // get the loss history + let scaled_loss: Vec<_> = state.assembly.descent_history.with( + |history| history.scaled_loss + .iter() + .enumerate() + .map(|(step, &loss)| (step, loss)) + .map(into_log10_time_point) + .collect() + ); + + // initialize the chart axes + let step_axis = Axis::new() + .type_(AxisType::Category) + .boundary_gap(false); + let scaled_loss_axis = Axis::new(); + + // load the chart data. when there's no history, we load the data + // point (0, None) to clear the chart. it would feel more natural to + // load empty data vectors, but that turns out not to clear the + // chart: it instead leads to previous data being re-used + let scaled_loss_series = Line::new().data( + if scaled_loss.len() > 0 { + scaled_loss + } else { + vec![vec![Some(0.0), None::]] + } + ); + let chart = Chart::new() + .animation(false) + .data_zoom(DataZoom::new().y_axis_index(0).right(40)) + .x_axis(step_axis) + .y_axis(scaled_loss_axis) + .grid(Grid::new().top(20).right(80).bottom(30).left(60)) + .series(scaled_loss_series); + renderer.render(CONTAINER_ID, &chart).unwrap(); + }); + }); + + view! { + div(id=CONTAINER_ID, class="diagnostics-chart") + } +} + +// the spectrum of the Hessian during the last realization +#[component] +fn SpectrumHistory() -> View { + const CONTAINER_ID: &str = "spectrum-history"; + let state = use_context::(); + let renderer = WasmRenderer::new(478, 178); + + on_mount(move || { + create_effect(move || { + // get the spectrum of the Hessian at each step, split into its + // positive, negative, and strictly-zero parts + let ( + hess_eigvals_zero, + hess_eigvals_nonzero + ): (Vec<_>, Vec<_>) = state.assembly.descent_history.with( + |history| history.hess_eigvals + .iter() + .enumerate() + .map( + |(step, eigvals)| eigvals.iter().map( + move |&val| (step, val) + ) + ) + .flatten() + .partition(|&(_, val)| val == 0.0) + ); + let zero_level = hess_eigvals_nonzero + .iter() + .map(|(_, val)| val.abs()) + .reduce(f64::min) + .map(|val| 0.1 * val) + .unwrap_or(1.0); + let ( + hess_eigvals_pos, + hess_eigvals_neg + ): (Vec<_>, Vec<_>) = hess_eigvals_nonzero + .into_iter() + .partition(|&(_, val)| val > 0.0); + + // initialize the chart axes + let step_axis = Axis::new() + .type_(AxisType::Category) + .boundary_gap(false); + let eigval_axis = Axis::new(); + + // load the chart data. when there's no history, we load the data + // point (0, None) to clear the chart. it would feel more natural to + // load empty data vectors, but that turns out not to clear the + // chart: it instead leads to previous data being re-used + let eigval_series_pos = Scatter::new() + .symbol_size(4.5) + .data( + if hess_eigvals_pos.len() > 0 { + hess_eigvals_pos + .into_iter() + .map(into_log10_time_point) + .collect() + } else { + vec![vec![Some(0.0), None::]] + } + ); + let eigval_series_neg = Scatter::new() + .symbol(Symbol::Diamond) + .symbol_size(6.0) + .data( + if hess_eigvals_neg.len() > 0 { + hess_eigvals_neg + .into_iter() + .map(into_log10_time_point) + .collect() + } else { + vec![vec![Some(0.0), None::]] + } + ); + let eigval_series_zero = Scatter::new() + .symbol(Symbol::Triangle) + .symbol_size(5.0) + .data( + if hess_eigvals_zero.len() > 0 { + hess_eigvals_zero + .into_iter() + .map(|(step, _)| (step, zero_level)) + .map(into_log10_time_point) + .collect() + } else { + vec![vec![Some(0.0), None::]] + } + ); + let chart = Chart::new() + .animation(false) + .data_zoom(DataZoom::new().y_axis_index(0).right(40)) + .x_axis(step_axis) + .y_axis(eigval_axis) + .grid(Grid::new().top(20).right(80).bottom(30).left(60)) + .series(eigval_series_pos) + .series(eigval_series_neg) + .series(eigval_series_zero); + renderer.render(CONTAINER_ID, &chart).unwrap(); + }); + }); + + view! { + div(id=CONTAINER_ID, class="diagnostics-chart") + } +} + +#[component(inline_props)] +fn DiagnosticsPanel(name: &'static str, children: Children) -> View { + let diagnostics_state = use_context::(); + view! { + div( + class="diagnostics-panel", + "hidden"=diagnostics_state.active_tab.with( + |active_tab| { + if active_tab == name { + None + } else { + Some("") + } + } + ) + ) { + (children) + } + } +} + +#[component] +pub fn Diagnostics() -> View { + let diagnostics_state = DiagnosticsState::new("loss".to_string()); + let active_tab = diagnostics_state.active_tab.clone(); + provide_context(diagnostics_state); + + view! { + div(id="diagnostics") { + div(id="diagnostics-bar") { + RealizationStatus {} + select(bind:value=active_tab) { + option(value="loss") { "Loss" } + option(value="spectrum") { "Spectrum" } + } + } + DiagnosticsPanel(name="loss") { LossHistory {} } + DiagnosticsPanel(name="spectrum") { SpectrumHistory {} } + } + } +} \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 69a3659..1646c4e 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -806,6 +806,7 @@ pub fn Display() -> View { // again canvas( ref=display, + id="display", width="600", height="600", tabindex="0", diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index c5d7b00..e6ffa25 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -256,18 +256,18 @@ pub struct DescentHistory { pub config: Vec>, pub scaled_loss: Vec, pub neg_grad: Vec>, - pub min_eigval: Vec, + pub hess_eigvals: Vec::>, pub base_step: Vec>, pub backoff_steps: Vec } impl DescentHistory { - fn new() -> DescentHistory { + pub fn new() -> DescentHistory { DescentHistory { config: Vec::>::new(), scaled_loss: Vec::::new(), neg_grad: Vec::>::new(), - min_eigval: Vec::::new(), + hess_eigvals: Vec::>::new(), base_step: Vec::>::new(), backoff_steps: Vec::::new(), } @@ -393,6 +393,17 @@ fn seek_better_config( None } +// a first-order neighborhood of a configuration +pub struct ConfigNeighborhood { + pub config: DMatrix, + pub nbhd: ConfigSubspace +} + +pub struct Realization { + pub result: Result, + pub history: DescentHistory +} + // seek a matrix `config` that matches the partial matrix `problem.frozen` and // has `config' * Q * config` matching the partial matrix `problem.gram`. start // at `problem.guess`, set the frozen entries to their desired values, and then @@ -405,7 +416,7 @@ pub fn realize_gram( reg_scale: f64, max_descent_steps: i32, max_backoff_steps: i32 -) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { +) -> Realization { // destructure the problem data let ConstraintProblem { gram, guess, frozen @@ -457,11 +468,12 @@ pub fn realize_gram( hess = DMatrix::from_columns(hess_cols.as_slice()); // regularize the Hessian - let min_eigval = hess.symmetric_eigenvalues().min(); + let hess_eigvals = hess.symmetric_eigenvalues(); + let min_eigval = hess_eigvals.min(); if min_eigval <= 0.0 { hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim); } - history.min_eigval.push(min_eigval); + history.hess_eigvals.push(hess_eigvals); // project the negative gradient and negative Hessian onto the // orthogonal complement of the frozen subspace @@ -480,30 +492,40 @@ pub fn realize_gram( if state.loss < tol { break; } // compute the Newton step + /* TO DO */ /* - we need to either handle or eliminate the case where the minimum - eigenvalue of the Hessian is zero, so the regularized Hessian is - singular. right now, this causes the Cholesky decomposition to return - `None`, leading to a panic when we unrap + we should change our regularization to ensure that the Hessian is + is positive-definite, rather than just positive-semidefinite. ideally, + that would guarantee the success of the Cholesky decomposition--- + although we'd still need the error-handling routine in case of + numerical hiccups */ - let base_step_stacked = hess.clone().cholesky().unwrap().solve(&neg_grad_stacked); + let hess_cholesky = match hess.clone().cholesky() { + Some(cholesky) => cholesky, + None => return Realization { + result: Err("Cholesky decomposition failed".to_string()), + history + } + }; + let base_step_stacked = hess_cholesky.solve(&neg_grad_stacked); let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim)); history.base_step.push(base_step.clone()); // use backtracking line search to find a better configuration - match seek_better_config( + if let Some((better_state, backoff_steps)) = seek_better_config( gram, &state, &base_step, neg_grad.dot(&base_step), min_efficiency, backoff, max_backoff_steps ) { - Some((better_state, backoff_steps)) => { - state = better_state; - history.backoff_steps.push(backoff_steps); - }, - None => return (state.config, ConfigSubspace::zero(assembly_dim), false, history) + state = better_state; + history.backoff_steps.push(backoff_steps); + } else { + return Realization { + result: Err("Line search failed".to_string()), + history + } }; } - let success = state.loss < tol; - let tangent = if success { + let result = if state.loss < tol { // express the uniform basis in the standard basis const UNIFORM_DIM: usize = 4; let total_dim_unif = UNIFORM_DIM * assembly_dim; @@ -516,11 +538,13 @@ pub fn realize_gram( } // find the kernel of the Hessian. give it the uniform inner product - ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim) + let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim); + + Ok(ConfigNeighborhood { config: state.config, nbhd: tangent }) } else { - ConfigSubspace::zero(assembly_dim) + Err("Failed to reach target accuracy".to_string()) }; - (state.config, tangent, success, history) + Realization { result, history } } // --- tests --- @@ -539,7 +563,7 @@ pub mod examples { // "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki // https://www.nippon.com/en/japan-topics/c12801/ // - pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + pub fn realize_irisawa_hexlet(scaled_tol: f64) -> Realization { let mut problem = ConstraintProblem::from_guess( [ sphere(0.0, 0.0, 0.0, 15.0), @@ -590,7 +614,7 @@ pub mod examples { // set up a kaleidocycle, made of points with fixed distances between them, // and find its tangent space - pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + pub fn realize_kaleidocycle(scaled_tol: f64) -> Realization { const N_HINGES: usize = 6; let mut problem = ConstraintProblem::from_guess( (0..N_HINGES).step_by(2).flat_map( @@ -714,10 +738,10 @@ mod tests { } problem.frozen.push(3, 0, problem.guess[(3, 0)]); problem.frozen.push(3, 1, 0.5); - let (config, _, success, history) = realize_gram( + let Realization { result, history } = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - assert_eq!(success, true); + let config = result.unwrap().config; for base_step in history.base_step.into_iter() { for &MatrixEntry { index, .. } in &problem.frozen { assert_eq!(base_step[index], 0.0); @@ -732,7 +756,7 @@ mod tests { fn irisawa_hexlet_test() { // solve Irisawa's problem const SCALED_TOL: f64 = 1.0e-12; - let (config, _, _, _) = realize_irisawa_hexlet(SCALED_TOL); + let config = realize_irisawa_hexlet(SCALED_TOL).result.unwrap().config; // check against Irisawa's solution let entry_tol = SCALED_TOL.sqrt(); @@ -759,11 +783,11 @@ mod tests { for n in 0..ELEMENT_DIM { problem.frozen.push(n, 0, problem.guess[(n, 0)]); } - let (config, tangent, success, history) = realize_gram( + let Realization { result, history } = realize_gram( &problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); assert_eq!(config, problem.guess); - assert_eq!(success, true); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of @@ -831,8 +855,8 @@ mod tests { fn tangent_test_kaleidocycle() { // set up a kaleidocycle and find its tangent space const SCALED_TOL: f64 = 1.0e-12; - let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); - assert_eq!(success, true); + let Realization { result, history } = realize_kaleidocycle(SCALED_TOL); + let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of @@ -920,11 +944,11 @@ mod tests { problem_orig.gram.push_sym(0, 0, 1.0); problem_orig.gram.push_sym(1, 1, 1.0); problem_orig.gram.push_sym(0, 1, 0.5); - let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram( + let Realization { result: result_orig, history: history_orig } = realize_gram( &problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let ConfigNeighborhood { config: config_orig, nbhd: tangent_orig } = result_orig.unwrap(); assert_eq!(config_orig, problem_orig.guess); - assert_eq!(success_orig, true); assert_eq!(history_orig.scaled_loss.len(), 1); // find another pair of spheres that meet at 120°. we'll think of this @@ -941,11 +965,11 @@ mod tests { guess: guess_tfm, frozen: problem_orig.frozen }; - let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram( + let Realization { result: result_tfm, history: history_tfm } = realize_gram( &problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let ConfigNeighborhood { config: config_tfm, nbhd: tangent_tfm } = result_tfm.unwrap(); assert_eq!(config_tfm, problem_tfm.guess); - assert_eq!(success_tfm, true); assert_eq!(history_tfm.scaled_loss.len(), 1); // project a nudge to the tangent space of the solution variety at the diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index b76859a..f905c46 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -1,5 +1,6 @@ mod add_remove; mod assembly; +mod diagnostics; mod display; mod engine; mod outline; @@ -13,6 +14,7 @@ use sycamore::prelude::*; use add_remove::AddRemove; use assembly::{Assembly, Element}; +use diagnostics::Diagnostics; use display::Display; use outline::Outline; @@ -60,6 +62,7 @@ fn main() { div(id="sidebar") { AddRemove {} Outline {} + Diagnostics {} } Display {} } From 0801200210094b9514974d63bb09eaad88ba74de Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Tue, 22 Jul 2025 22:01:37 +0000 Subject: [PATCH 132/132] Add more test assemblies (#103) This PR helps probe the capabilities of the engine. Also adjusts the realization triggering system to reduce redundant realizations as we set an assembly's regulators during loading. Specificially, consolidates all calls to `realize()` into a single effect, which is triggered by the `needs_realization` signal. Also introduces a `keep_realized` signal and use it to pause realization while loading assemblies, but this signal is planned for removal as ultimately we do not want a separate "mode" of interpreting commands during loading, for maximal reproducibility of results (and simplicity of system). Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/103 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/src/add_remove.rs | 259 ----- app-proto/src/assembly.rs | 42 +- app-proto/src/components.rs | 5 + app-proto/src/components/add_remove.rs | 54 + app-proto/src/{ => components}/diagnostics.rs | 0 app-proto/src/{ => components}/display.rs | 0 app-proto/src/{ => components}/identity.vert | 0 app-proto/src/{ => components}/outline.rs | 0 app-proto/src/{ => components}/point.frag | 0 app-proto/src/{ => components}/point.vert | 0 app-proto/src/{ => components}/spheres.frag | 0 .../src/components/test_assembly_chooser.rs | 947 ++++++++++++++++++ app-proto/src/main.rs | 15 +- 13 files changed, 1045 insertions(+), 277 deletions(-) delete mode 100644 app-proto/src/add_remove.rs create mode 100644 app-proto/src/components.rs create mode 100644 app-proto/src/components/add_remove.rs rename app-proto/src/{ => components}/diagnostics.rs (100%) rename app-proto/src/{ => components}/display.rs (100%) rename app-proto/src/{ => components}/identity.vert (100%) rename app-proto/src/{ => components}/outline.rs (100%) rename app-proto/src/{ => components}/point.frag (100%) rename app-proto/src/{ => components}/point.vert (100%) rename app-proto/src/{ => components}/spheres.frag (100%) create mode 100644 app-proto/src/components/test_assembly_chooser.rs diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs deleted file mode 100644 index d737c79..0000000 --- a/app-proto/src/add_remove.rs +++ /dev/null @@ -1,259 +0,0 @@ -use std::{f64::consts::FRAC_1_SQRT_2, rc::Rc}; -use sycamore::prelude::*; -use web_sys::{console, wasm_bindgen::JsValue}; - -use crate::{ - AppState, - engine, - engine::DescentHistory, - assembly::{Assembly, InversiveDistanceRegulator, Point, Sphere} -}; - -/* DEBUG */ -// load an example assembly for testing. this code will be removed once we've -// built a more formal test assembly system -fn load_gen_assemb(assembly: &Assembly) { - let _ = assembly.try_insert_element( - Sphere::new( - String::from("gemini_a"), - String::from("Castor"), - [1.00_f32, 0.25_f32, 0.00_f32], - engine::sphere(0.5, 0.5, 0.0, 1.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("gemini_b"), - String::from("Pollux"), - [0.00_f32, 0.25_f32, 1.00_f32], - engine::sphere(-0.5, -0.5, 0.0, 1.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("ursa_major"), - String::from("Ursa major"), - [0.25_f32, 0.00_f32, 1.00_f32], - engine::sphere(-0.5, 0.5, 0.0, 0.75) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("ursa_minor"), - String::from("Ursa minor"), - [0.25_f32, 1.00_f32, 0.00_f32], - engine::sphere(0.5, -0.5, 0.0, 0.5) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("moon_deimos"), - String::from("Deimos"), - [0.75_f32, 0.75_f32, 0.00_f32], - engine::sphere(0.0, 0.15, 1.0, 0.25) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("moon_phobos"), - String::from("Phobos"), - [0.00_f32, 0.75_f32, 0.50_f32], - engine::sphere(0.0, -0.15, -1.0, 0.25) - ) - ); -} - -/* DEBUG */ -// load an example assembly for testing. this code will be removed once we've -// built a more formal test assembly system -fn load_low_curv_assemb(assembly: &Assembly) { - let a = 0.75_f64.sqrt(); - let _ = assembly.try_insert_element( - Sphere::new( - "central".to_string(), - "Central".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, 0.0, 0.0, 1.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "assemb_plane".to_string(), - "Assembly plane".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "side1".to_string(), - "Side 1".to_string(), - [1.00_f32, 0.00_f32, 0.25_f32], - engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "side2".to_string(), - "Side 2".to_string(), - [0.25_f32, 1.00_f32, 0.00_f32], - engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "side3".to_string(), - "Side 3".to_string(), - [0.00_f32, 0.25_f32, 1.00_f32], - engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "corner1".to_string(), - "Corner 1".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "corner2".to_string(), - "Corner 2".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("corner3"), - String::from("Corner 3"), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0) - ) - ); -} - -fn load_pointed_assemb(assembly: &Assembly) { - let _ = assembly.try_insert_element( - Point::new( - format!("point_front"), - format!("Front point"), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::point(0.0, 0.0, FRAC_1_SQRT_2) - ) - ); - let _ = assembly.try_insert_element( - Point::new( - format!("point_back"), - format!("Back point"), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::point(0.0, 0.0, -FRAC_1_SQRT_2) - ) - ); - for index_x in 0..=1 { - for index_y in 0..=1 { - let x = index_x as f64 - 0.5; - let y = index_y as f64 - 0.5; - - let _ = assembly.try_insert_element( - Sphere::new( - format!("sphere{index_x}{index_y}"), - format!("Sphere {index_x}{index_y}"), - [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], - engine::sphere(x, y, 0.0, 1.0) - ) - ); - - let _ = assembly.try_insert_element( - Point::new( - format!("point{index_x}{index_y}"), - format!("Point {index_x}{index_y}"), - [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], - engine::point(x, y, 0.0) - ) - ); - } - } -} - -#[component] -pub fn AddRemove() -> View { - /* DEBUG */ - let assembly_name = create_signal("general".to_string()); - create_effect(move || { - // get name of chosen assembly - let name = assembly_name.get_clone(); - console::log_1( - &JsValue::from(format!("Showing assembly \"{}\"", name.clone())) - ); - - batch(|| { - let state = use_context::(); - let assembly = &state.assembly; - - // clear state - assembly.regulators.update(|regs| regs.clear()); - assembly.elements.update(|elts| elts.clear()); - assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); - assembly.descent_history.set(DescentHistory::new()); - state.selection.update(|sel| sel.clear()); - - // load assembly - match name.as_str() { - "general" => load_gen_assemb(assembly), - "low-curv" => load_low_curv_assemb(assembly), - "pointed" => load_pointed_assemb(assembly), - _ => () - }; - }); - }); - - view! { - div(id="add-remove") { - button( - on:click=|_| { - let state = use_context::(); - state.assembly.insert_element_default::(); - } - ) { "Add sphere" } - button( - on:click=|_| { - let state = use_context::(); - state.assembly.insert_element_default::(); - } - ) { "Add point" } - button( - class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button - disabled={ - let state = use_context::(); - state.selection.with(|sel| sel.len() != 2) - }, - on:click=|_| { - let state = use_context::(); - let subjects: [_; 2] = state.selection.with( - // the button is only enabled when two elements are - // selected, so we know the cast to a two-element array - // will succeed - |sel| sel - .clone() - .into_iter() - .collect::>() - .try_into() - .unwrap() - ); - state.assembly.insert_regulator( - Rc::new(InversiveDistanceRegulator::new(subjects)) - ); - state.selection.update(|sel| sel.clear()); - } - ) { "🔗" } - select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser - option(value="general") { "General" } - option(value="low-curv") { "Low-curvature" } - option(value="pointed") { "Pointed" } - option(value="empty") { "Empty" } - } - } - } -} \ No newline at end of file diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index c3b0c6b..68fcd8b 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -13,7 +13,7 @@ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use crate::{ - display::DisplayItem, + components::{display::DisplayItem, outline::OutlineItem}, engine::{ Q, change_half_curvature, @@ -29,7 +29,6 @@ use crate::{ DescentHistory, Realization }, - outline::OutlineItem, specified::SpecifiedValue }; @@ -552,6 +551,10 @@ pub struct Assembly { // indexing pub elements_by_id: Signal>>, + // realization control + pub keep_realized: Signal, + pub needs_realization: Signal, + // realization diagnostics pub realization_status: Signal>, pub descent_history: Signal @@ -559,14 +562,30 @@ pub struct Assembly { impl Assembly { pub fn new() -> Assembly { - Assembly { + // create an assembly + let assembly = Assembly { elements: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()), tangent: create_signal(ConfigSubspace::zero(0)), elements_by_id: create_signal(BTreeMap::default()), + keep_realized: create_signal(true), + needs_realization: create_signal(false), realization_status: create_signal(Ok(())), descent_history: create_signal(DescentHistory::new()) - } + }; + + // realize the assembly whenever it becomes simultaneously true that + // we're trying to keep it realized and it needs realization + let assembly_for_effect = assembly.clone(); + create_effect(move || { + let should_realize = assembly_for_effect.keep_realized.get() + && assembly_for_effect.needs_realization.get(); + if should_realize { + assembly_for_effect.realize(); + } + }); + + assembly } // --- inserting elements and regulators --- @@ -627,7 +646,7 @@ impl Assembly { regulators.update(|regs| regs.insert(regulator.clone())); } - // update the realization when the regulator becomes a constraint, or is + // request a realization when the regulator becomes a constraint, or is // edited while acting as a constraint let self_for_effect = self.clone(); create_effect(move || { @@ -636,7 +655,7 @@ impl Assembly { console_log!("Updated regulator with subjects {:?}", regulator.subjects()); if regulator.try_activate() { - self_for_effect.realize(); + self_for_effect.needs_realization.set(true); } }); @@ -731,6 +750,9 @@ impl Assembly { // save the tangent space self.tangent.set_silent(tangent); + + // clear the realization request flag + self.needs_realization.set(false); }, Err(message) => { // report the realization status. the `Err(message)` we're @@ -826,10 +848,10 @@ impl Assembly { }); } - // bring the configuration back onto the solution variety. this also - // gets the elements' column indices and the saved tangent space back in - // sync - self.realize(); + // request a realization to bring the configuration back onto the + // solution variety. this also gets the elements' column indices and the + // saved tangent space back in sync + self.needs_realization.set(true); } } diff --git a/app-proto/src/components.rs b/app-proto/src/components.rs new file mode 100644 index 0000000..7387d58 --- /dev/null +++ b/app-proto/src/components.rs @@ -0,0 +1,5 @@ +pub mod add_remove; +pub mod diagnostics; +pub mod display; +pub mod outline; +pub mod test_assembly_chooser; \ No newline at end of file diff --git a/app-proto/src/components/add_remove.rs b/app-proto/src/components/add_remove.rs new file mode 100644 index 0000000..3b0f9e0 --- /dev/null +++ b/app-proto/src/components/add_remove.rs @@ -0,0 +1,54 @@ +use std::rc::Rc; +use sycamore::prelude::*; + +use super::test_assembly_chooser::TestAssemblyChooser; +use crate::{ + AppState, + assembly::{InversiveDistanceRegulator, Point, Sphere} +}; + +#[component] +pub fn AddRemove() -> View { + view! { + div(id="add-remove") { + button( + on:click=|_| { + let state = use_context::(); + state.assembly.insert_element_default::(); + } + ) { "Add sphere" } + button( + on:click=|_| { + let state = use_context::(); + state.assembly.insert_element_default::(); + } + ) { "Add point" } + button( + class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button + disabled={ + let state = use_context::(); + state.selection.with(|sel| sel.len() != 2) + }, + on:click=|_| { + let state = use_context::(); + let subjects: [_; 2] = state.selection.with( + // the button is only enabled when two elements are + // selected, so we know the cast to a two-element array + // will succeed + |sel| sel + .clone() + .into_iter() + .collect::>() + .try_into() + .unwrap() + ); + state.assembly.insert_regulator( + Rc::new(InversiveDistanceRegulator::new(subjects)) + ); + state.selection.update(|sel| sel.clear()); + } + ) { "🔗" } + TestAssemblyChooser {} + } + } +} \ No newline at end of file diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/components/diagnostics.rs similarity index 100% rename from app-proto/src/diagnostics.rs rename to app-proto/src/components/diagnostics.rs diff --git a/app-proto/src/display.rs b/app-proto/src/components/display.rs similarity index 100% rename from app-proto/src/display.rs rename to app-proto/src/components/display.rs diff --git a/app-proto/src/identity.vert b/app-proto/src/components/identity.vert similarity index 100% rename from app-proto/src/identity.vert rename to app-proto/src/components/identity.vert diff --git a/app-proto/src/outline.rs b/app-proto/src/components/outline.rs similarity index 100% rename from app-proto/src/outline.rs rename to app-proto/src/components/outline.rs diff --git a/app-proto/src/point.frag b/app-proto/src/components/point.frag similarity index 100% rename from app-proto/src/point.frag rename to app-proto/src/components/point.frag diff --git a/app-proto/src/point.vert b/app-proto/src/components/point.vert similarity index 100% rename from app-proto/src/point.vert rename to app-proto/src/components/point.vert diff --git a/app-proto/src/spheres.frag b/app-proto/src/components/spheres.frag similarity index 100% rename from app-proto/src/spheres.frag rename to app-proto/src/components/spheres.frag diff --git a/app-proto/src/components/test_assembly_chooser.rs b/app-proto/src/components/test_assembly_chooser.rs new file mode 100644 index 0000000..232cda3 --- /dev/null +++ b/app-proto/src/components/test_assembly_chooser.rs @@ -0,0 +1,947 @@ +use itertools::izip; +use std::{f64::consts::{FRAC_1_SQRT_2, PI}, rc::Rc}; +use nalgebra::Vector3; +use sycamore::prelude::*; +use web_sys::{console, wasm_bindgen::JsValue}; + +use crate::{ + AppState, + engine, + engine::DescentHistory, + assembly::{ + Assembly, + Element, + ElementColor, + InversiveDistanceRegulator, + Point, + Sphere + }, + specified::SpecifiedValue +}; + +// --- loaders --- + +/* DEBUG */ +// each of these functions loads an example assembly for testing. once we've +// done more work on saving and loading assemblies, we should come back to this +// code to see if it can be simplified + +fn load_gen_assemb(assembly: &Assembly) { + let _ = assembly.try_insert_element( + Sphere::new( + String::from("gemini_a"), + String::from("Castor"), + [1.00_f32, 0.25_f32, 0.00_f32], + engine::sphere(0.5, 0.5, 0.0, 1.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("gemini_b"), + String::from("Pollux"), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere(-0.5, -0.5, 0.0, 1.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("ursa_major"), + String::from("Ursa major"), + [0.25_f32, 0.00_f32, 1.00_f32], + engine::sphere(-0.5, 0.5, 0.0, 0.75) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("ursa_minor"), + String::from("Ursa minor"), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere(0.5, -0.5, 0.0, 0.5) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("moon_deimos"), + String::from("Deimos"), + [0.75_f32, 0.75_f32, 0.00_f32], + engine::sphere(0.0, 0.15, 1.0, 0.25) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("moon_phobos"), + String::from("Phobos"), + [0.00_f32, 0.75_f32, 0.50_f32], + engine::sphere(0.0, -0.15, -1.0, 0.25) + ) + ); +} + +fn load_low_curv_assemb(assembly: &Assembly) { + // create the spheres + let a = 0.75_f64.sqrt(); + let _ = assembly.try_insert_element( + Sphere::new( + "central".to_string(), + "Central".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "assemb_plane".to_string(), + "Assembly plane".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "side1".to_string(), + "Side 1".to_string(), + [1.00_f32, 0.00_f32, 0.25_f32], + engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "side2".to_string(), + "Side 2".to_string(), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "side3".to_string(), + "Side 3".to_string(), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "corner1".to_string(), + "Corner 1".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "corner2".to_string(), + "Corner 2".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("corner3"), + String::from("Corner 3"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0) + ) + ); + + // impose the desired tangencies and make the sides planar + let index_range = 1..=3; + let [central, assemb_plane] = ["central", "assemb_plane"].map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[id].clone() + ) + ); + let sides = index_range.clone().map( + |k| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("side{k}")].clone() + ) + ); + let corners = index_range.map( + |k| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("corner{k}")].clone() + ) + ); + for plane in [assemb_plane.clone()].into_iter().chain(sides.clone()) { + // fix the curvature of each plane + let curvature = plane.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + curvature.set_point().set(SpecifiedValue::try_from("0".to_string()).unwrap()); + } + let all_perpendicular = [central.clone()].into_iter() + .chain(sides.clone()) + .chain(corners.clone()); + for sphere in all_perpendicular { + // make each side and packed sphere perpendicular to the assembly plane + let right_angle = InversiveDistanceRegulator::new([sphere, assemb_plane.clone()]); + right_angle.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(right_angle)); + } + for sphere in sides.clone().chain(corners.clone()) { + // make each side and corner sphere tangent to the central sphere + let tangency = InversiveDistanceRegulator::new([sphere.clone(), central.clone()]); + tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(tangency)); + } + for (side_index, side) in sides.enumerate() { + // make each side tangent to the two adjacent corner spheres + for (corner_index, corner) in corners.clone().enumerate() { + if side_index != corner_index { + let tangency = InversiveDistanceRegulator::new([side.clone(), corner]); + tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(tangency)); + } + } + } +} + +fn load_pointed_assemb(assembly: &Assembly) { + let _ = assembly.try_insert_element( + Point::new( + format!("point_front"), + format!("Front point"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::point(0.0, 0.0, FRAC_1_SQRT_2) + ) + ); + let _ = assembly.try_insert_element( + Point::new( + format!("point_back"), + format!("Back point"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::point(0.0, 0.0, -FRAC_1_SQRT_2) + ) + ); + for index_x in 0..=1 { + for index_y in 0..=1 { + let x = index_x as f64 - 0.5; + let y = index_y as f64 - 0.5; + + let _ = assembly.try_insert_element( + Sphere::new( + format!("sphere{index_x}{index_y}"), + format!("Sphere {index_x}{index_y}"), + [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], + engine::sphere(x, y, 0.0, 1.0) + ) + ); + + let _ = assembly.try_insert_element( + Point::new( + format!("point{index_x}{index_y}"), + format!("Point {index_x}{index_y}"), + [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], + engine::point(x, y, 0.0) + ) + ); + } + } +} + +// to finish describing the tridiminished icosahedron, set the inversive +// distance regulators as follows: +// A-A -0.25 +// A-B " +// B-C " +// C-C " +// A-C -0.25 * φ^2 = -0.6545084971874737 +fn load_tridim_icosahedron_assemb(assembly: &Assembly) { + // create the vertices + const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.25_f32]; + const COLOR_B: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; + const COLOR_C: ElementColor = [0.25_f32, 0.50_f32, 1.00_f32]; + let vertices = [ + Point::new( + "a1".to_string(), + "A₁".to_string(), + COLOR_A, + engine::point(0.25, 0.75, 0.75) + ), + Point::new( + "a2".to_string(), + "A₂".to_string(), + COLOR_A, + engine::point(0.75, 0.25, 0.75) + ), + Point::new( + "a3".to_string(), + "A₃".to_string(), + COLOR_A, + engine::point(0.75, 0.75, 0.25) + ), + Point::new( + "b1".to_string(), + "B₁".to_string(), + COLOR_B, + engine::point(0.75, -0.25, -0.25) + ), + Point::new( + "b2".to_string(), + "B₂".to_string(), + COLOR_B, + engine::point(-0.25, 0.75, -0.25) + ), + Point::new( + "b3".to_string(), + "B₃".to_string(), + COLOR_B, + engine::point(-0.25, -0.25, 0.75) + ), + Point::new( + "c1".to_string(), + "C₁".to_string(), + COLOR_C, + engine::point(0.0, -1.0, -1.0) + ), + Point::new( + "c2".to_string(), + "C₂".to_string(), + COLOR_C, + engine::point(-1.0, 0.0, -1.0) + ), + Point::new( + "c3".to_string(), + "C₃".to_string(), + COLOR_C, + engine::point(-1.0, -1.0, 0.0) + ) + ]; + for vertex in vertices { + let _ = assembly.try_insert_element(vertex); + } + + // create the faces + const COLOR_FACE: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; + let frac_1_sqrt_6 = 1.0 / 6.0_f64.sqrt(); + let frac_2_sqrt_6 = 2.0 * frac_1_sqrt_6; + let faces = [ + Sphere::new( + "face1".to_string(), + "Face 1".to_string(), + COLOR_FACE, + engine::sphere_with_offset(frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0) + ), + Sphere::new( + "face2".to_string(), + "Face 2".to_string(), + COLOR_FACE, + engine::sphere_with_offset(-frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0) + ), + Sphere::new( + "face3".to_string(), + "Face 3".to_string(), + COLOR_FACE, + engine::sphere_with_offset(-frac_1_sqrt_6, -frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, 0.0) + ) + ]; + for face in faces { + face.ghost().set(true); + let _ = assembly.try_insert_element(face); + } + + let index_range = 1..=3; + for j in index_range.clone() { + // make each face planar + let face = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("face{j}")].clone() + ); + let curvature_regulator = face.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + curvature_regulator.set_point().set( + SpecifiedValue::try_from("0".to_string()).unwrap() + ); + + // put each A vertex on the face it belongs to + let vertex_a = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("a{j}")].clone() + ); + let incidence_a = InversiveDistanceRegulator::new([face.clone(), vertex_a.clone()]); + incidence_a.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(incidence_a)); + + // regulate the B-C vertex distances + let vertices_bc = ["b", "c"].map( + |series| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("{series}{j}")].clone() + ) + ); + assembly.insert_regulator( + Rc::new(InversiveDistanceRegulator::new(vertices_bc)) + ); + + // get the pair of indices adjacent to `j` + let adjacent_indices = [j % 3 + 1, (j + 1) % 3 + 1]; + + for k in adjacent_indices.clone() { + for series in ["b", "c"] { + // put each B and C vertex on the faces it belongs to + let vertex = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("{series}{k}")].clone() + ); + let incidence = InversiveDistanceRegulator::new([face.clone(), vertex.clone()]); + incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(incidence)); + + // regulate the A-B and A-C vertex distances + assembly.insert_regulator( + Rc::new(InversiveDistanceRegulator::new([vertex_a.clone(), vertex])) + ); + } + } + + // regulate the A-A and C-C vertex distances + let adjacent_pairs = ["a", "c"].map( + |series| adjacent_indices.map( + |index| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("{series}{index}")].clone() + ) + ) + ); + for pair in adjacent_pairs { + assembly.insert_regulator( + Rc::new(InversiveDistanceRegulator::new(pair)) + ); + } + } +} + +// to finish describing the dodecahedral circle packing, set the inversive +// distance regulators to -1. some of the regulators have already been set +fn load_dodeca_packing_assemb(assembly: &Assembly) { + // add the substrate + let _ = assembly.try_insert_element( + Sphere::new( + "substrate".to_string(), + "Substrate".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ) + ); + let substrate = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id["substrate"].clone() + ); + + // fix the substrate's curvature + substrate.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ).set_point().set( + SpecifiedValue::try_from("0.5".to_string()).unwrap() + ); + + // add the circles to be packed + const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.00_f32]; + const COLOR_B: ElementColor = [1.00_f32, 0.00_f32, 0.25_f32]; + const COLOR_C: ElementColor = [0.25_f32, 0.00_f32, 1.00_f32]; + let phi = 0.5 + 1.25_f64.sqrt(); /* TO DO */ // replace with std::f64::consts::PHI when that gets stabilized + let phi_inv = 1.0 / phi; + let coord_scale = (phi + 2.0).sqrt(); + let face_scales = [phi_inv, (13.0 / 12.0) / coord_scale]; + let face_radii = [phi_inv, 5.0 / 12.0]; + let mut faces = Vec::>::new(); + let subscripts = ["₀", "₁"]; + for j in 0..2 { + for k in 0..2 { + let small_coord = face_scales[k] * (2.0*(j as f64) - 1.0); + let big_coord = face_scales[k] * (2.0*(k as f64) - 1.0) * phi; + + let id_num = format!("{j}{k}"); + let label_sub = format!("{}{}", subscripts[j], subscripts[k]); + + // add the A face + let id_a = format!("a{id_num}"); + let _ = assembly.try_insert_element( + Sphere::new( + id_a.clone(), + format!("A{label_sub}"), + COLOR_A, + engine::sphere(0.0, small_coord, big_coord, face_radii[k]) + ) + ); + faces.push( + assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&id_a].clone() + ) + ); + + // add the B face + let id_b = format!("b{id_num}"); + let _ = assembly.try_insert_element( + Sphere::new( + id_b.clone(), + format!("B{label_sub}"), + COLOR_B, + engine::sphere(small_coord, big_coord, 0.0, face_radii[k]) + ) + ); + faces.push( + assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&id_b].clone() + ) + ); + + // add the C face + let id_c = format!("c{id_num}"); + let _ = assembly.try_insert_element( + Sphere::new( + id_c.clone(), + format!("C{label_sub}"), + COLOR_C, + engine::sphere(big_coord, 0.0, small_coord, face_radii[k]) + ) + ); + faces.push( + assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&id_c].clone() + ) + ); + } + } + + // make each face sphere perpendicular to the substrate + for face in faces { + let right_angle = InversiveDistanceRegulator::new([face, substrate.clone()]); + right_angle.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(right_angle)); + } + + // set up the tangencies that define the packing + for [long_edge_plane, short_edge_plane] in [["a", "b"], ["b", "c"], ["c", "a"]] { + for k in 0..2 { + let long_edge_ids = [ + format!("{long_edge_plane}{k}0"), + format!("{long_edge_plane}{k}1") + ]; + let short_edge_ids = [ + format!("{short_edge_plane}0{k}"), + format!("{short_edge_plane}1{k}") + ]; + let [long_edge, short_edge] = [long_edge_ids, short_edge_ids].map( + |edge_ids| edge_ids.map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&id].clone() + ) + ) + ); + + // set up the short-edge tangency + let short_tangency = InversiveDistanceRegulator::new(short_edge.clone()); + if k == 0 { + short_tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); + } + assembly.insert_regulator(Rc::new(short_tangency)); + + // set up the side tangencies + for i in 0..2 { + for j in 0..2 { + let side_tangency = InversiveDistanceRegulator::new( + [long_edge[i].clone(), short_edge[j].clone()] + ); + if i == 0 && k == 0 { + side_tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); + } + assembly.insert_regulator(Rc::new(side_tangency)); + } + } + } + } +} + +// the initial configuration of this test assembly deliberately violates the +// constraints, so loading the assembly will trigger a non-trivial realization +fn load_balanced_assemb(assembly: &Assembly) { + // create the spheres + const R_OUTER: f64 = 10.0; + const R_INNER: f64 = 4.0; + let spheres = [ + Sphere::new( + "outer".to_string(), + "Outer".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, R_OUTER) + ), + Sphere::new( + "a".to_string(), + "A".to_string(), + [1.00_f32, 0.00_f32, 0.25_f32], + engine::sphere(0.0, 4.0, 0.0, R_INNER) + ), + Sphere::new( + "b".to_string(), + "B".to_string(), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere(0.0, -4.0, 0.0, R_INNER) + ), + ]; + for sphere in spheres { + let _ = assembly.try_insert_element(sphere); + } + + // get references to the spheres + let [outer, a, b] = ["outer", "a", "b"].map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[id].clone() + ) + ); + + // fix the diameters of the outer, sun, and moon spheres + for (sphere, radius) in [ + (outer.clone(), R_OUTER), + (a.clone(), R_INNER), + (b.clone(), R_INNER) + ] { + let curvature_regulator = sphere.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + let curvature = 0.5 / radius; + curvature_regulator.set_point().set( + SpecifiedValue::try_from(curvature.to_string()).unwrap() + ); + } + + // set the inversive distances between the spheres. as described above, the + // initial configuration deliberately violates these constraints + for inner in [a, b] { + let tangency = InversiveDistanceRegulator::new([outer.clone(), inner]); + tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(tangency)); + } +} + +// the initial configuration of this test assembly deliberately violates the +// constraints, so loading the assembly will trigger a non-trivial realization +fn load_off_center_assemb(assembly: &Assembly) { + // create a point almost at the origin and a sphere centered on the origin + let _ = assembly.try_insert_element( + Point::new( + "point".to_string(), + "Point".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::point(1e-9, 0.0, 0.0) + ), + ); + let _ = assembly.try_insert_element( + Sphere::new( + "sphere".to_string(), + "Sphere".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ), + ); + + // get references to the elements + let point_and_sphere = ["point", "sphere"].map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[id].clone() + ) + ); + + // put the point on the sphere + let incidence = InversiveDistanceRegulator::new(point_and_sphere); + incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(incidence)); +} + +// setting the inversive distances between the vertices to -2 gives a regular +// tetrahedron with side length 1, whose insphere and circumsphere have radii +// sqrt(1/6) and sqrt(3/2), respectively. to measure those radii, set an +// inversive distance of -1 between the insphere and each face, and then set an +// inversive distance of 0 between the circumsphere and each vertex +fn load_radius_ratio_assemb(assembly: &Assembly) { + let index_range = 1..=4; + + // create the spheres + const GRAY: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; + let spheres = [ + Sphere::new( + "sphere_faces".to_string(), + "Insphere".to_string(), + GRAY, + engine::sphere(0.0, 0.0, 0.0, 0.5) + ), + Sphere::new( + "sphere_vertices".to_string(), + "Circumsphere".to_string(), + GRAY, + engine::sphere(0.0, 0.0, 0.0, 0.25) + ) + ]; + for sphere in spheres { + let _ = assembly.try_insert_element(sphere); + } + + // create the vertices + let vertices = izip!( + index_range.clone(), + [ + [1.00_f32, 0.50_f32, 0.75_f32], + [1.00_f32, 0.75_f32, 0.50_f32], + [1.00_f32, 1.00_f32, 0.50_f32], + [0.75_f32, 0.50_f32, 1.00_f32] + ].into_iter(), + [ + engine::point(-0.6, -0.8, -0.6), + engine::point(-0.6, 0.8, 0.6), + engine::point(0.6, -0.8, 0.6), + engine::point(0.6, 0.8, -0.6) + ].into_iter() + ).map( + |(k, color, representation)| { + Point::new( + format!("v{k}"), + format!("Vertex {k}"), + color, + representation + ) + } + ); + for vertex in vertices { + let _ = assembly.try_insert_element(vertex); + } + + // create the faces + let base_dir = Vector3::new(1.0, 0.75, 1.0).normalize(); + let offset = base_dir.dot(&Vector3::new(-0.6, 0.8, 0.6)); + let faces = izip!( + index_range.clone(), + [ + [1.00_f32, 0.00_f32, 0.25_f32], + [1.00_f32, 0.25_f32, 0.00_f32], + [0.75_f32, 0.75_f32, 0.00_f32], + [0.25_f32, 0.00_f32, 1.00_f32] + ].into_iter(), + [ + engine::sphere_with_offset(base_dir[0], base_dir[1], base_dir[2], offset, 0.0), + engine::sphere_with_offset(base_dir[0], -base_dir[1], -base_dir[2], offset, 0.0), + engine::sphere_with_offset(-base_dir[0], base_dir[1], -base_dir[2], offset, 0.0), + engine::sphere_with_offset(-base_dir[0], -base_dir[1], base_dir[2], offset, 0.0) + ].into_iter() + ).map( + |(k, color, representation)| { + Sphere::new( + format!("f{k}"), + format!("Face {k}"), + color, + representation + ) + } + ); + for face in faces { + face.ghost().set(true); + let _ = assembly.try_insert_element(face); + } + + // impose the constraints + for j in index_range.clone() { + let [face_j, vertex_j] = [ + format!("f{j}"), + format!("v{j}") + ].map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&id].clone() + ) + ); + + // make the faces planar + let curvature_regulator = face_j.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + curvature_regulator.set_point().set( + SpecifiedValue::try_from("0".to_string()).unwrap() + ); + + for k in index_range.clone().filter(|&index| index != j) { + let vertex_k = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("v{k}")].clone() + ); + + // fix the distances between the vertices + if j < k { + let distance_regulator = InversiveDistanceRegulator::new( + [vertex_j.clone(), vertex_k.clone()] + ); + assembly.insert_regulator(Rc::new(distance_regulator)); + } + + // put the vertices on the faces + let incidence_regulator = InversiveDistanceRegulator::new([face_j.clone(), vertex_k.clone()]); + incidence_regulator.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(incidence_regulator)); + } + } +} + +// to finish setting up the problem, fix the following curvatures: +// sun 1 +// moon 5/3 = 1.666666666666666... +// chain1 2 +// a tiny `x` or `z` nudge of the outer sphere reliably prevents realization +// failures before they happen, or resolves them after they happen. the result +// depends sensitively on the translation direction, suggesting that realization +// is failing because the engine is having trouble breaking a symmetry +// /* TO DO */ +// the engine's performance on this problem is scale-dependent! with the current +// initial conditions, realization fails for any order of imposing the remaining +// curvature constraints. scaling everything up by a factor of ten, as done in +// the original problem, makes realization succeed reliably. one potentially +// relevant difference is that a lot of the numbers in the current initial +// conditions are exactly representable as floats, unlike the analogous numbers +// in the scaled-up problem. the inexact representations might break the +// symmetry that's getting the engine stuck +fn load_irisawa_hexlet_assemb(assembly: &Assembly) { + let index_range = 1..=6; + let colors = [ + [1.00_f32, 0.00_f32, 0.25_f32], + [1.00_f32, 0.25_f32, 0.00_f32], + [0.75_f32, 0.75_f32, 0.00_f32], + [0.25_f32, 1.00_f32, 0.00_f32], + [0.00_f32, 0.25_f32, 1.00_f32], + [0.25_f32, 0.00_f32, 1.00_f32] + ].into_iter(); + + // create the spheres + let spheres = [ + Sphere::new( + "outer".to_string(), + "Outer".to_string(), + [0.5_f32, 0.5_f32, 0.5_f32], + engine::sphere(0.0, 0.0, 0.0, 1.5) + ), + Sphere::new( + "sun".to_string(), + "Sun".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, -0.75, 0.0, 0.75) + ), + Sphere::new( + "moon".to_string(), + "Moon".to_string(), + [0.25_f32, 0.25_f32, 0.25_f32], + engine::sphere(0.0, 0.75, 0.0, 0.75) + ), + ].into_iter().chain( + index_range.clone().zip(colors).map( + |(k, color)| { + let ang = (k as f64) * PI/3.0; + Sphere::new( + format!("chain{k}"), + format!("Chain {k}"), + color, + engine::sphere(1.0 * ang.sin(), 0.0, 1.0 * ang.cos(), 0.5) + ) + } + ) + ); + for sphere in spheres { + let _ = assembly.try_insert_element(sphere); + } + + // put the outer sphere in ghost mode and fix its curvature + let outer = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id["outer"].clone() + ); + outer.ghost().set(true); + let outer_curvature_regulator = outer.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + outer_curvature_regulator.set_point().set( + SpecifiedValue::try_from((1.0 / 3.0).to_string()).unwrap() + ); + + // impose the desired tangencies + let [outer, sun, moon] = ["outer", "sun", "moon"].map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[id].clone() + ) + ); + let chain = index_range.map( + |k| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("chain{k}")].clone() + ) + ); + for (chain_sphere, chain_sphere_next) in chain.clone().zip(chain.cycle().skip(1)) { + for (other_sphere, inversive_distance) in [ + (outer.clone(), "1"), + (sun.clone(), "-1"), + (moon.clone(), "-1"), + (chain_sphere_next.clone(), "-1") + ] { + let tangency = InversiveDistanceRegulator::new([chain_sphere.clone(), other_sphere]); + tangency.set_point.set(SpecifiedValue::try_from(inversive_distance.to_string()).unwrap()); + assembly.insert_regulator(Rc::new(tangency)); + } + } + + let outer_sun_tangency = InversiveDistanceRegulator::new([outer.clone(), sun]); + outer_sun_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(outer_sun_tangency)); + + let outer_moon_tangency = InversiveDistanceRegulator::new([outer.clone(), moon]); + outer_moon_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(outer_moon_tangency)); +} + +// --- chooser --- + +/* DEBUG */ +#[component] +pub fn TestAssemblyChooser() -> View { + // create an effect that loads the selected test assembly + let assembly_name = create_signal("general".to_string()); + create_effect(move || { + // get name of chosen assembly + let name = assembly_name.get_clone(); + console::log_1( + &JsValue::from(format!("Showing assembly \"{}\"", name.clone())) + ); + + batch(|| { + let state = use_context::(); + let assembly = &state.assembly; + + // pause realization + assembly.keep_realized.set(false); + + // clear state + assembly.regulators.update(|regs| regs.clear()); + assembly.elements.update(|elts| elts.clear()); + assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); + assembly.descent_history.set(DescentHistory::new()); + state.selection.update(|sel| sel.clear()); + + // load assembly + match name.as_str() { + "general" => load_gen_assemb(assembly), + "low-curv" => load_low_curv_assemb(assembly), + "pointed" => load_pointed_assemb(assembly), + "tridim-icosahedron" => load_tridim_icosahedron_assemb(assembly), + "dodeca-packing" => load_dodeca_packing_assemb(assembly), + "balanced" => load_balanced_assemb(assembly), + "off-center" => load_off_center_assemb(assembly), + "radius-ratio" => load_radius_ratio_assemb(assembly), + "irisawa-hexlet" => load_irisawa_hexlet_assemb(assembly), + _ => () + }; + + // resume realization + assembly.keep_realized.set(true); + }); + }); + + // build the chooser + view! { + select(bind:value=assembly_name) { + option(value="general") { "General" } + option(value="low-curv") { "Low-curvature" } + option(value="pointed") { "Pointed" } + option(value="tridim-icosahedron") { "Tridiminished icosahedron" } + option(value="dodeca-packing") { "Dodecahedral packing" } + option(value="balanced") { "Balanced" } + option(value="off-center") { "Off-center" } + option(value="radius-ratio") { "Radius ratio" } + option(value="irisawa-hexlet") { "Irisawa hexlet" } + option(value="empty") { "Empty" } + } + } +} \ No newline at end of file diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index f905c46..152d11c 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -1,9 +1,6 @@ -mod add_remove; mod assembly; -mod diagnostics; -mod display; +mod components; mod engine; -mod outline; mod specified; #[cfg(test)] @@ -12,11 +9,13 @@ mod tests; use std::{collections::BTreeSet, rc::Rc}; use sycamore::prelude::*; -use add_remove::AddRemove; use assembly::{Assembly, Element}; -use diagnostics::Diagnostics; -use display::Display; -use outline::Outline; +use components::{ + add_remove::AddRemove, + diagnostics::Diagnostics, + display::Display, + outline::Outline +}; #[derive(Clone)] struct AppState {