diff --git a/CHANGELOG.md b/CHANGELOG.md index f859512..182f83b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added + - A package extension for interoperability with [Unitful.jl] (only supported on julia 1.9 and up). + It provides support for wedge products for `Quantity`, including `Quantity{<:Real}` and + `Quantity{<:AbstractCliffordNumber}` (geometric products were already supported). - `nonzero_grades(::Complex)` is defined and returns `0:0`, like `nonzero_grades(::Real)`. - `print(::IO, ::AbstractCliffordNumber)` shows a prettier (but not parseable) representation. @@ -128,3 +131,4 @@ Initial release of CliffordNumbers.jl [0.1.2]: https://github.com/brainandforce/CliffordNumbers.jl/releases/tag/v0.1.2 [0.1.1]: https://github.com/brainandforce/CliffordNumbers.jl/releases/tag/v0.1.1 [0.1.0]: https://github.com/brainandforce/CliffordNumbers.jl/releases/tag/v0.1.0 +[Unitful.jl]: https://github.com/PainterQubits/Unitful.jl diff --git a/Project.toml b/Project.toml index 7dcb3f6..bf38a10 100644 --- a/Project.toml +++ b/Project.toml @@ -3,14 +3,22 @@ uuid = "3998ac73-6bd4-4031-8035-f167dd3ed523" authors = ["Brandon Flores"] version = "0.1.9-dev" +[weakdeps] +Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" + +[extensions] +CliffordNumbersUnitfulExt = "Unitful" + [compat] Aqua = "0.8" Test = "1" +Unitful = "1" julia = "1.8" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [targets] -test = ["Aqua", "Test"] +test = ["Aqua", "Test", "Unitful"] diff --git a/docs/make.jl b/docs/make.jl index b527475..1461000 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -26,6 +26,7 @@ makedocs(; "Indexing" => "indexing.md", "Operations" => "operations.md", "Performance tips" => "performance.md", + "Extensions" => "extensions.md", "API" => Any[ "CliffordNumbers" => "api/clifford.md", "Indexing" => "api/indexing.md", diff --git a/docs/src/extensions.md b/docs/src/extensions.md new file mode 100644 index 0000000..7366bad --- /dev/null +++ b/docs/src/extensions.md @@ -0,0 +1,37 @@ +# Extensions + +CliffordNumbers.jl provides some extensions to allow for interoperability with other packages which +we anticipate to be commonly used alongside it. + +!!! note + Extensions require Julia 1.9; support for previous versions will be dropped with the 0.2.0 + release of CliffordNumbers.jl. + +## [Unitful.jl] + +[Unitful.jl] provides support for quantities with associated units. + +To make a Clifford number into a unitful quantity, simply multiply it by a unit or other unitful +quantity: +```julia-repl +julia> KVector{1, VGA(3)}(1, 2, 3)u"m" +(1e₁ + 2e₂ + 3e₃) m + +julia> KVector{1, VGA(3)}(1, 2, 3) * 1.5u"m" +(1.5e₁ + 3.0e₂ + 4.5e₃) m +``` + +!!! warn "Constructing Clifford numbers from `Quantity` coefficients fails" + Attempts to construct a Clifford number from `Quantity` objects will fail, because `Quantity` + does not subtype `Real` or `Complex`: + ```julia-repl + julia> KVector{1,VGA(3)}(1u"m", 2u"m", 3u"m") + ERROR: ArgumentError: + ... + ``` + For this reason, Clifford numbers with mixed units cannot be constructed, and this is + intentional: `AbstractCliffordNumber` assumes an orthonormal basis. + +Supported operations include the geometric product and wedge product. + +[Unitful.jl]: https://github.com/PainterQubits/Unitful.jl diff --git a/ext/CliffordNumbersUnitfulExt.jl b/ext/CliffordNumbersUnitfulExt.jl new file mode 100644 index 0000000..7ef486e --- /dev/null +++ b/ext/CliffordNumbersUnitfulExt.jl @@ -0,0 +1,74 @@ +module CliffordNumbersUnitfulExt + +using CliffordNumbers +using Unitful + +import CliffordNumbers: ∧, ∨, ⨼, ⨽, dot, ×, ⨰ +import Unitful: AbstractQuantity, AffineError, AffineQuantity + +#---Constructors-----------------------------------------------------------------------------------# + +# Error thrown when units are incommensurate +function (::Type{<:AbstractCliffordNumber})(::Tuple{Vararg{Quantity}}) + throw( + ArgumentError( + "\n$(Quantity) does not subtype $(Real) or $(Complex), so it cannot be used as the " * + "scalar type for $(AbstractCliffordNumber) instances.\n" * + "Additionally, mixed units are disallowed because $(AbstractCliffordNumber) " * + "currently assumes an orthonormal basis." + ) + ) +end + +# Error thrown when units are commensurate +function (::Type{C})(x::Tuple{Vararg{Quantity{<:Number,D,U}}}) where {C<:AbstractCliffordNumber,D,U} + unit_string = "u\"" * replace(repr(unit(first(x))), " " => " * ") * "\"" + multivector_string = "$C$(ustrip.(x))" + throw( + ArgumentError( + "\n$(Quantity) does not subtype $(Real) or $(Complex), so it cannot be used as the " * + "scalar type for $(AbstractCliffordNumber) instances.\n" * + "Instead, construct a $(Quantity{<:C}) by multiplying a $C by a unit:\n" * + "\n " * multivector_string * unit_string * "\n" + ) + ) +end + +(::Type{C})(x::Quantity...) where C<:AbstractCliffordNumber = C(x) + +#---Fixing printing methods------------------------------------------------------------------------# + +# Probably not needed, but included anyway +Unitful.BracketStyle(::Type{<:AbstractCliffordNumber}) = Unitful.RoundBrackets() + +function Base.print(io::IO, x::AbstractQuantity{<:AbstractCliffordNumber}) + # Functions with qualified names are part of the internal Unitful API. + # This should be fine for now, but may break unexpectedly. + if unit(x) isa Unitful.Units{()} + print(io, x.val, " (no units)") + else + print(io, '(', x.val, ')') + # This one is based on SI convention and should not change. I hope. + Unitful.has_unit_spacing(unit(x)) && print(io, ' ') + print(io, unit(x)) + end +end + +Base.show(io::IO, ::MIME"text/plain", x::Quantity{<:AbstractCliffordNumber}) = print(io, x) + +#---Definitions for different products-------------------------------------------------------------# +∧(x::AbstractQuantity, y::AbstractQuantity) = Quantity(x.val ∧ y.val, unit(x) * unit(y)) + +function ∧(x::Number, y::AbstractQuantity) + y isa AffineQuantity && + throw(AffineError("an invalid operation was attempted with affine quantities: $x ∧ $y")) + return Quantity(x ∧ y.val, unit(y)) +end + +function ∧(x::AbstractQuantity, y::Number) + x isa AffineQuantity && + throw(AffineError("an invalid operation was attempted with affine quantities: $x ∧ $y")) + return Quantity(x.val ∧ y, unit(x)) +end + +end diff --git a/test/ext/Unitful.jl b/test/ext/Unitful.jl new file mode 100644 index 0000000..cb75648 --- /dev/null +++ b/test/ext/Unitful.jl @@ -0,0 +1,20 @@ +@testset "Unitful extension" begin + x = KVector{1,VGA(3)}(1, 0, 0) + y = KVector{1,VGA(3)}(0, 1, 0) + @test_throws ArgumentError KVector{1,VGA(3)}(1u"m", 0u"m", 0u"m") + @test_throws ArgumentError KVector{1,VGA(3)}(0u"m", 1u"m/s", 0u"K") + xu = x*u"m" + yu = y*u"m" + @test xu ∧ yu === KVector{2,VGA(3)}(1, 0, 0) * u"m^2" + @test 2 ∧ xu === KVector{1,VGA(3)}(2, 0, 0) * u"m" + @test yu ∧ 2 === KVector{1,VGA(3)}(0, 2, 0) * u"m" + @test_throws Unitful.AffineError 2 ∧ 2u"°F" + @test_throws Unitful.AffineError 2u"°F" ∧ 2 + # Printing + @test Unitful.BracketStyle(x) === Unitful.RoundBrackets() + @test string(xu) == "(1e₁) m" + @test string(xu ∧ yu) == "(1e₁e₂) m^2" + unitless = Quantity{KVector{1,VGA(3),Int,3}, NoDims, Unitful.FreeUnits{(), NoDims, nothing}}(y) + @test string(unitless) == "1e₂ (no units)" + @test repr("text/plain", unitless) == string(unitless) +end diff --git a/test/runtests.jl b/test/runtests.jl index a8eebd2..ea8f88b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,5 @@ using CliffordNumbers -using Aqua, Test +using Aqua, Test, Unitful Aqua.test_all(CliffordNumbers; unbound_args = false) @@ -15,4 +15,7 @@ Aqua.test_all(CliffordNumbers; unbound_args = false) include("indexing.jl") include("conversion.jl") include("operations.jl") + if VERSION >= v"1.9.0" + include("ext/Unitful.jl") + end end