From 34cfcf1e6158d9502c3283b91cc7e96e34abd9de Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Mon, 16 Feb 2026 09:17:50 -0500 Subject: [PATCH] mjo/clan: start implementing clan operators This is a moral prerequisite for the Vinberg inner product. --- mjo/clan/clan_element.py | 61 +++++++++++++++++++ mjo/clan/clan_operator.py | 121 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 mjo/clan/clan_operator.py diff --git a/mjo/clan/clan_element.py b/mjo/clan/clan_element.py index eedf481..6b08e53 100644 --- a/mjo/clan/clan_element.py +++ b/mjo/clan/clan_element.py @@ -45,6 +45,67 @@ class ClanElement(IndexedFreeModuleElement): for (bj, yj) in other.items() ) + def leftreg(self): + r""" + Return the left-regular representation of this element. + + SETUP:: + + sage: from mjo.clan.unital_clan import VinbergClan + + EXAMPLES:: + + sage: C = VinbergClan() + sage: C.one().leftreg() + [1 0 0 0 0] + [0 1 0 0 0] + [0 0 1 0 0] + [0 0 0 1 0] + [0 0 0 0 1] + + """ + P = self.parent() + left_mult_by_self = lambda y: self*y + L = P.module_morphism(function=left_mult_by_self, codomain=P) + return L.matrix() + + def operator(self): + """ + Return the left-multiplication-by-me operator on the + ambient clan. + + SETUP:: + + sage: from mjo.clan.unital_clan import VinbergClan + + EXAMPLES:: + + sage: C = VinbergClan() + sage: C.one().operator() + Clan operator represented by the matrix: + [1 0 0 0 0] + [0 1 0 0 0] + [0 0 1 0 0] + [0 0 0 1 0] + [0 0 0 0 1] + Domain: Vinberg clan over Rational Field + Codomain: Vinberg clan over Rational Field + + TESTS:: + + sage: C = VinbergClan() + sage: x = C.random_element() + sage: y = C.random_element() + sage: x.operator()(y) == x*y + True + sage: y.operator()(x) == y*x + True + + """ + from mjo.clan.clan_operator import ClanOperator + P = self.parent() + return ClanOperator(P, P, self.leftreg()) + class NormalDecompositionElement(ClanElement): """ diff --git a/mjo/clan/clan_operator.py b/mjo/clan/clan_operator.py new file mode 100644 index 0000000..6a058e5 --- /dev/null +++ b/mjo/clan/clan_operator.py @@ -0,0 +1,121 @@ +from sage.categories.all import FreeModules +from sage.categories.map import Map + +class ClanOperator(Map): + r""" + An operator between two clans. + + Defined for *unital* clans at the moment, because that's all we + have. + + SETUP:: + + sage: from mjo.clan.unital_clan import SnClan + sage: from mjo.clan.clan_operator import ClanOperator + + EXAMPLES: + + The domain and codomain must be unital clans; if either is not, + then an error is raised:: + + sage: J = SnClan(2) + sage: V = VectorSpace(J.base_ring(), 3) + sage: M = matrix.identity(J.base_ring(), 3) + sage: ClanOperator(V,J,M) + Traceback (most recent call last): + ... + TypeError: domain must be a unital clan + sage: ClanOperator(J,V,M) + Traceback (most recent call last): + ... + TypeError: codomain must be a unital clan + + """ + + def __init__(self, domain, codomain, mat): + from mjo.clan.unital_clan import UnitalClan + + if not isinstance(domain, UnitalClan): + raise TypeError('domain must be a unital clan') + if not isinstance(codomain, UnitalClan): + raise TypeError('codomain must be a unital clan') + + F = domain.base_ring() + if not (F == codomain.base_ring()): + raise ValueError("domain and codomain must have the same base ring") + if not (F == mat.base_ring()): + raise ValueError("domain and matrix must have the same base ring") + + # We need to supply something here to avoid getting the + # default Homset of the parent FiniteDimensionalAlgebra class, + # which messes up e.g. equality testing. We use FreeModules(F) + # instead of VectorSpaces(F) because our characteristic polynomial + # algorithm will need to F to be a polynomial ring at some point. + # When F is a field, FreeModules(F) returns VectorSpaces(F) anyway. + parent = domain.Hom(codomain, FreeModules(F)) + + # The Map initializer will set our parent to a homset, which + # is explicitly NOT what we want, because these ain't algebra + # homomorphisms. + super().__init__(parent) + + # Keep a matrix around to do all of the real work. It would + # be nice if we could use a VectorSpaceMorphism instead, but + # those use row vectors that we don't want to accidentally + # expose to our users. + self._matrix = mat + + + def _call_(self, x): + """ + Allow this operator to be called only on elements of a clan. + + SETUP:: + + sage: from mjo.clan.clan_operator import ClanOperator + sage: from mjo.clan.unital_clan import VinbergClan + + EXAMPLES:: + + sage: C = VinbergClan() + sage: x = C.linear_combination(zip(C.gens(),range(len(C.gens())))) + sage: id = identity_matrix(C.base_ring(), C.dimension()) + sage: f = ClanOperator(C,C,id) + sage: f(x) == x + True + + """ + return self.codomain().from_vector(self._matrix*x.to_vector()) + + def _repr_(self): + r""" + + A text representation of this linear operator on a Euclidean + Jordan Algebra. + + SETUP:: + + sage: from mjo.clan.clan_operator import ClanOperator + sage: from mjo.clan.unital_clan import HnClan + + EXAMPLES:: + + sage: C = HnClan(2) + sage: id = identity_matrix(C.base_ring(), C.dimension()) + sage: ClanOperator(C,C,id) + Clan operator represented by the matrix: + [1 0 0 0] + [0 1 0 0] + [0 0 1 0] + [0 0 0 1] + Domain: Clan H^2 over Rational Field + Codomain: Clan H^2 over Rational Field + + """ + msg = ("Clan operator represented by the matrix:\n", + "{!r}\n", + "Domain: {}\n", + "Codomain: {}") + return ''.join(msg).format(self._matrix, + self.domain(), + self.codomain()) -- 2.51.0