Miscellaneous
Printing options
AbstractAlgebra supports printing to LaTeX using the MIME type "text/latex". To enable LaTeX rendering in Jupyter notebooks and query for the current state, use the following functions:
set_html_as_latex
— Functionset_html_as_latex(fl::Bool)
Toggles whether MIME type text/html
should be printed as text/latex
. Note that this is a global option. The return value is the old value.
get_html_as_latex
— Functionget_html_as_latex()
Returns whether MIME type text/html
is printed as text/latex
.
Updating the type diagrams
Updating the diagrams of the documentation can be done by modifying and running the script docs/create_type_diagrams.jl
. Note that this requires the package Kroki
.
Attributes
Often it is desirable to have a flexible way to attach additional data to mathematical structures such as groups, rings, fields, etc. beyond what the original implementation covers. To facilitate this, we provide an attributes system: for objects of suitable types, one may use set_attribute!
to attach key-value pairs to the object, and query them using has_attribute
, get_attribute
and get_attribute!
.
Attributes are supported for all singletons (i.e., instances of an empty struct
type), as well as for instances of mutable struct type for which attribute storage was enabled. There are two ways to enable attribute storage for such types:
- By applying
@attributes
to a mutable struct declaration, storage is reserved inside that struct type itself (this increases the size of each struct by 8 bytes if no attributes are set). - By applying
@attributes
to the name of a mutable struct type, methods are installed which store attributes to instances of the type in aWeakKeyDict
outside the struct.
@attributes
— Macro@attributes typedef
This is a helper macro that ensures that there is storage for attributes in the type declared in the expression typedef
, which must be either a mutable struct
definition expression, or the name of a mutable struct
type.
The latter variant is useful to enable attribute storage for types defined in other packages. Note that @attributes
is idempotent: when applied to a type for which attribute storage is already available, it does nothing.
For singleton types, attribute storage is also supported, and in fact always enabled. Thus it is not necessary to apply this macro to such a type.
When applied to a struct definition this macro adds a new field to the struct. For structs without constructor, this will change the signature of the default inner constructor, which requires explicit values for every field, including the attribute storage field this macro adds. Usually it is thus preferable to add an explicit default constructor, as in the example below.
Examples
Applying the macro to a struct definition results in internal storage of the attributes:
julia> @attributes mutable struct MyGroup
order::Int
MyGroup(order::Int) = new(order)
end
julia> G = MyGroup(5)
MyGroup(5, #undef)
julia> set_attribute!(G, :isfinite, :true)
julia> get_attribute(G, :isfinite)
true
Applying the macro to a typename results in external storage of the attributes:
julia> mutable struct MyOtherGroup
order::Int
MyOtherGroup(order::Int) = new(order)
end
julia> @attributes MyOtherGroup
julia> G = MyOtherGroup(5)
MyOtherGroup(5)
julia> set_attribute!(G, :isfinite, :true)
julia> get_attribute(G, :isfinite)
true
@attr
— Macro@attr RetType funcdef
This macro is applied to the definition of a unary function, and enables caching ("memoization") of its return values based on the argument. This assumes the argument supports attribute storing (see @attributes
) via get_attribute!
.
The name of the function is used as name for the underlying attribute.
Effectively, this turns code like this:
@attr RetType function myattr(obj::Foo)
# ... expensive computation
return result
end
into something essentially equivalent to this:
function myattr(obj::Foo)
return get_attribute!(obj, :myattr) do
# ... expensive computation
return result
end::RetType
end
Examples
julia> @attributes mutable struct Foo
x::Int
Foo(x::Int) = new(x)
end;
julia> @attr Int function myattr(obj::Foo)
println("Performing expensive computation")
return factorial(obj.x)
end;
julia> obj = Foo(5);
julia> myattr(obj)
Performing expensive computation
120
julia> myattr(obj) # second time uses the cached result
120
has_attribute
— Functionhas_attribute(G::Any, attr::Symbol)
Return a boolean indicating whether G
has a value stored for the attribute attr
.
get_attribute
— Functionget_attribute(f::Function, G::Any, attr::Symbol)
Return the value stored for the attribute attr
, or if no value has been set, return f()
.
This is intended to be called using do
block syntax.
get_attribute(obj, attr) do
# default value calculated here if needed
...
end
get_attribute(G::Any, attr::Symbol, default::Any = nothing)
Return the value stored for the attribute attr
, or if no value has been set, return default
.
get_attribute!
— Functionget_attribute!(f::Function, G::Any, attr::Symbol)
Return the value stored for the attribute attr
of G
, or if no value has been set, store key => f()
and return f()
.
This is intended to be called using do
block syntax.
get_attribute!(obj, attr) do
# default value calculated here if needed
...
end
get_attribute!(G::Any, attr::Symbol, default::Any)
Return the value stored for the attribute attr
of G
, or if no value has been set, store key => default
, and return default
.
set_attribute!
— Functionset_attribute!(G::Any, data::Pair{Symbol, <:Any}...)
Attach the given sequence of key=>value
pairs as attributes of G
.
set_attribute!(G::Any, attr::Symbol, value::Any)
Attach the given value
as attribute attr
of G
.
Advanced printing
Self-given names
We provide macros @show_name
, @show_special
and @show_special_elem
to change the way certain objects are printed.
In compact and terse printing mode, @show_name
tries to determine a suitable name to print instead of the object (see AbstractAlgebra.get_name
).
@show_special
checks if an attribute :show
is present. If so, it has to be a function taking IO
, optionally a MIME-type, and the object. This is then called instead of the usual show
function.
Similarly, @show_special_elem
checks if an attribute :show_elem
is present in the object's parent. The semantics are the same as for @show_special
.
All are supposed to be used within the usual show
function, where @show_special_elem
is only relevant for element types of algebraic structures.
@attributes MyObj
function show(io::IO, A::MyObj)
@show_name(io, A)
@show_special(io, A)
# ... usual stuff
end
function show(io::IO, mime::MIME"text/plain", A::MyObj)
@show_name(io, A)
@show_special(io, mime, A)
# ... usual stuff
end
function show(io::IO, A::MyObjElem)
@show_name(io, A)
@show_special_elem(io, A)
# ... usual stuff
end
function show(io::IO, mime::MIME"text/plain", A::MyObjElem)
@show_name(io, A)
@show_special_elem(io, mime, A)
# ... usual stuff
end
Documentation
@show_special
— Macro@show_special(io::IO, obj)
If the obj
has a show
attribute, this gets called with io
and obj
and returns from the current scope. Otherwise, does nothing.
If obj
does not have attribute storage available, this macro does nothing.
It is supposed to be used at the start of show
methods as shown in the documentation.
Examples
julia> R = @polynomial_ring(QQ, :x; cached=false)
Univariate polynomial ring in x over rationals
julia> AbstractAlgebra.@show_special(stdout, R)
julia> set_attribute!(R, :show, (i,o) -> print(i, "=> The One True Ring <="))
julia> AbstractAlgebra.@show_special(stdout, R)
=> The One True Ring <=
julia> R # show for R uses @show_special, so we can observe the effect directly
=> The One True Ring <=
@show_special(io::IO, mime, obj)
If the obj
has a show
attribute, this gets called with io
, mime
and obj
(if applicable) and io
and obj
otherwise, and returns from the current scope. Otherwise, does nothing.
If obj
does not have attribute storage available, this macro does nothing.
It is supposed to be used at the start of show
methods as shown in the documentation.
Examples
julia> R = @polynomial_ring(QQ, :x; cached=false)
Univariate polynomial ring in x over rationals
julia> AbstractAlgebra.@show_special(stdout, MIME"text/plain"(), R)
julia> myshow(i,o) = print(i, "=> The One True Ring <=");
julia> myshow(i,m,o) = print(i, "=> The One True Ring with mime type $m <=");
julia> set_attribute!(R, :show, myshow)
julia> AbstractAlgebra.@show_special(stdout, MIME"text/plain"(), R)
=> The One True Ring with mime type text/plain <=
julia> R # show for R uses @show_special, so we can observe the effect directly
=> The One True Ring <=
@show_special_elem
— Macro@show_special_elem(io::IO, obj)
If the parent
of obj
has a show_elem
attribute, this gets called with io
and obj
and returns from the current scope. Otherwise, does nothing.
If parent(obj)
does not have attribute storage available, this macro does nothing.
It is supposed to be used at the start of show
methods as shown in the documentation.
Examples
julia> R = @polynomial_ring(QQ, :x; cached=false)
Univariate polynomial ring in x over rationals
julia> AbstractAlgebra.@show_special_elem(stdout, x)
julia> set_attribute!(R, :show_elem, (i,o) -> print(i, "=> $o <="))
julia> AbstractAlgebra.@show_special_elem(stdout, x)
=> x <=
julia> x # show for x does not uses @show_special_elem, so x prints as before
x
@show_special_elem(io::IO, mime, obj)
If the parent
of obj
has a show_elem
attribute, this gets called with io
, mime
and obj
(if applicable) and io
and obj
otherwise, and returns from the current scope. Otherwise, does nothing.
If parent(obj)
does not have attribute storage available, this macro does nothing.
It is supposed to be used at the start of show
methods as shown in the documentation.
Examples
julia> R = @polynomial_ring(QQ, :x; cached=false)
Univariate polynomial ring in x over rationals
julia> AbstractAlgebra.@show_special_elem(stdout, MIME"text/plain"(), x)
julia> set_attribute!(R, :show_elem, (i,m,o) -> print(i, "=> $o with mime type $m <="))
julia> AbstractAlgebra.@show_special_elem(stdout, MIME"text/plain"(), x)
=> x with mime type text/plain <=
julia> x # show for x does not uses @show_special_elem, so x prints as before
x
@show_name
— Macro@show_name(io::IO, obj)
If either is_terse(io)
is true or property :compact
is set to true
for io
(see IOContext
), print the name get_name(obj)
of the object obj
to the io
stream, then return from the current scope. Otherwise, do nothing.
It is supposed to be used at the start of show
methods as shown in the documentation.
get_name
— Functionget_name(obj) -> Union{String,Nothing}
Returns the name of the object obj
if it is set, or nothing
otherwise. This function tries to find a name in the following order:
- The name set by
AbstractAlgebra.set_name!
. - The name of a variable in global (
Main
module) namespace with value bound to the objectobj
(seeAbstractAlgebra.PrettyPrinting.find_name
). - The name returned by
AbstractAlgebra.extra_name
.
set_name!
— Functionset_name!(obj, name::String; override::Bool=true)
Sets the name of the object obj
to name
. This name is used for printing using AbstractAlgebra.@show_name
. If override
is false
, the name is only set if there is no name already set.
This function errors if obj
does not support attribute storage.
set_name!(obj; override::Bool=true)
Sets the name of the object obj
to the name of a variable in global (Main
module) namespace with value bound to the object obj
, if such a variable exists (see AbstractAlgebra.PrettyPrinting.find_name
). This name is used for printing using AbstractAlgebra.@show_name
. If override
is false
, the name is only set if there is no name already set.
This function errors if obj
does not support attribute storage.
extra_name
— Functionextra_name(obj) -> Union{String,Nothing}
May be overloaded to provide a fallback name for the object obj
in AbstractAlgebra.get_name
. The default implementation returns nothing
.
find_name
— Functionfind_name(obj, M = Main; all::Bool = false) -> Union{String,Nothing}
Return name of a variable in M
's namespace with value bound to the object obj
, or nothing
if no such variable exists. If all
is true
, private and non-exported variables are also searched.
If the object is stored in several variables, the first one will be used, but a name returned once is kept until the variable no longer contains this object.
For this to work in doctests, one should call AbstractAlgebra.set_current_module(@__MODULE__)
in the value
argument of Documenter.DocMeta.setdocmeta!
and keep the default value of M = Main
here.
This function should not be used directly, but rather through AbstractAlgebra.get_name
.
Indentation and Decapitalization
To facilitate printing of nested mathematical structures, we provide a modified IOCustom
object, that supports indentation and decapitalization.
Example
We illustrate this with an example
struct A{T}
x::T
end
function Base.show(io::IO, a::A)
io = AbstractAlgebra.pretty(io)
println(io, "Something of type A")
print(io, AbstractAlgebra.Indent(), "over ", AbstractAlgebra.Lowercase(), a.x)
print(io, AbstractAlgebra.Dedent()) # don't forget to undo the indentation!
end
struct B
end
function Base.show(io::IO, b::B)
io = AbstractAlgebra.pretty(io)
print(io, LowercaseOff(), "Hilbert thing")
end
At the REPL, this will then be printed as follows:
julia> A(2)
Something of type A
over 2
julia> A(A(2))
Something of type A
over something of type A
over 2
julia> A(B())
Something of type A
over Hilbert thing
Documentation
pretty
— Functionpretty(io::IO) -> IOCustom
Wrap io
into an IOCustom
object.
Examples
julia> io = AbstractAlgebra.pretty(stdout);
Indent
— TypeIndent
When printed to an IOCustom
object, increases the indentation level by one.
Examples
julia> io = AbstractAlgebra.pretty(stdout);
julia> print(io, AbstractAlgebra.Indent(), "This is indented")
This is indented
Dedent
— TypeDedent
When printed to an IOCustom
object, decreases the indentation level by one.
Examples
julia> io = AbstractAlgebra.pretty(stdout);
julia> print(io, AbstractAlgebra.Indent(), AbstractAlgebra.Dedent(), "This is indented")
This is indented
Lowercase
— TypeLowercase
When printed to an IOCustom
object, the next letter printed will be lowercase.
Examples
julia> io = AbstractAlgebra.pretty(stdout);
julia> print(io, AbstractAlgebra.Lowercase(), "Foo")
foo
LowercaseOff
— TypeLowercaseOff
When printed to an IOCustom
object, the case of the next letter will not be changed when printed.
Examples
julia> io = AbstractAlgebra.pretty(stdout);
julia> print(io, AbstractAlgebra.Lowercase(), AbstractAlgebra.LowercaseOff(), "Foo")
Foo
terse
— Functionterse(io::IO) -> IO
Return a new IO objects derived from io
for which "terse" printing mode has been enabled.
See https://docs.oscar-system.org/stable/DeveloperDocumentation/printing_details/ for details.
Examples
julia> AbstractAlgebra.is_terse(stdout)
false
julia> io = AbstractAlgebra.terse(stdout);
julia> AbstractAlgebra.is_terse(io)
true
is_terse
— Functionis_terse(io::IO) -> Bool
Test whether "terse" printing mode is enabled for io
.
See https://docs.oscar-system.org/stable/DeveloperDocumentation/printing_details/ for details.
Examples
julia> AbstractAlgebra.is_terse(stdout)
false
julia> io = AbstractAlgebra.terse(stdout);
julia> AbstractAlgebra.is_terse(io)
true
Linear solving interface for developers
AbstractAlgebra has a generic interface for linear solving and we describe here how one may extend this interface. For the user-facing functionality of linear solving, see Linear Solving.
Notice that the functionality is implemented in the module AbstractAlgebra.Solve
and the internal functions are not exported from there.
Matrix normal forms
To distinguish between different algorithms, we use type traits of abstract type MatrixNormalFormTrait
which usually correspond to a certain matrix normal form. The available algorithms/normal forms are
HowellFormTrait
: uses a Howell form;HermiteFormTrait
: uses a Hermite normal form;RREFTrait
: uses a row-reduced echelon form over fields;LUTrait
: uses a LU factoring of the matrix;FFLUTrait
: uses a "fraction-free" LU factoring of the matrix over fraction fields;MatrixInterpolateTrait
: uses interpolation of polynomials for fraction fields of polynomial rings.
To select a normal form type for rings of type NewRing
, implement the function
Solve.matrix_normal_form_type(::NewRing) = Bla()
where Bla <: MatrixNormalFormTrait
. A new type trait can be added via
struct NewTrait <: Solve.MatrixNormalFormTrait end
Internal solving functionality
If a new ring type NewRing
can make use of one of the available MatrixNormalFormTrait
s, then it suffices to specify this normal form as described above to use the generic solving functionality. (However, for example HermiteFormTrait
requires that the function hermite_form_with_transformation
is implemented.)
For a new trait NewTrait <: MatrixNormalFormTrait
, one needs to implement the function
Solve._can_solve_internal_no_check(
::NewTrait, A::MatElem{T}, b::MatElem{T}, task::Symbol; side::Symbol = :left
) where T
Inside this function, one can assume that A
and b
have the same base ring and have compatible dimensions. Further, task
and side
are set to "legal" options. (All this is checked in Solve._can_solve_internal
.) This function should then (try to) solve Ax = b
(side == :right
) or xA = b
(side == :left
) possibly with kernel. The function must always return a tuple (::Bool, ::MatElem{T}, ::MatElem{T})
consisting of:
true
/false
whether a solution exists or not- the solution (or a placeholder if no solution exists or a solution is not requested)
- the kernel (or a placeholder if the kernel is not requested)
The input task
may be:
:only_check
: Only test whether there is a solution, the second and third return value are only for type stability;:with_solution
: Compute a solution, if it exists, the last return value is only for type stability;:with_kernel
: Compute a solution and a kernel.
One should further implement the function
kernel(::NewTrait, A::MatElem; side::Symbol = :left)
which computes a left (or right) kernel of A
.
Internal solve context functionality
To efficiently solve several linear systems with the same matrix A
, we provide the "solve contexts objects" of type Solve.SolveCtx
. These can be extended for a ring of type NewRing
as follows.
Solve context type
For a new ring type, one may have to define the type parameters of a SolveCtx
object. First of all, one needs to implement the function
function Solve.solve_context_type(::NewRing)
return Solve.solve_context_type(::NormalFormTrait, elem_type(NewRing))
end
to pick a MatrixNormalFormTrait
.
Usually, nothing else should be necessary. However, if for example the normal form of a matrix does not live over the same ring as the matrix itself, one might also need to implement
function Solve.solve_context_type(NF::NormalFormTrait, T::Type{NewRingElem})
return Solve.SolveCtx{T, typeof(NF), MatType, RedMatType, TranspMatType}
end
where MatType
is the dense matrix type over NewRing
, RedMatType
the type of a matrix in reduced/normal form and TranspMatType
the type of the reduced/normal form of the transposed matrix.
Initialization
To initialize the solve context functionality for a new normal form NewTrait
, one needs to implement the functions
Solve._init_reduce(C::SolveCtx{T, NewTrait}) where T
Solve._init_reduce_transpose(C::SolveCtx{T, NewTrait}) where T
These should fill the corresponding fields of the solve context C
with a "reduced matrix" (that is, a matrix in normal form) of matrix(C)
, respectively transpose(matrix(C))
, and other information necessary to solve a linear system. The fields can be accessed via reduced_matrix
, reduced_matrix_of_transpose
, etc. New fields may also be added via attributes.
Internal solving functionality
As above, one finally needs to implement the functions
Solve._can_solve_internal_no_check(
::NewTrait, C::SolveCtx{T, NewTrait}, b::MatElem{T}, task::Symbol;
side::Symbol = :left
) where T
and
kernel(::NewTrait, C::SolveCtx{T, NewTrait}; side::Symbol = :left)