#############################################################################
##
#W  selfsimgroup.gi           automgrp package                 Yevgen Muntyan
#W                                                             Dmytro Savchuk
##
#Y  Copyright (C) 2003 - 2018 Yevgen Muntyan, Dmytro Savchuk
##

###############################################################################
##
#M  SelfSimilarGroup(<list>)
##
InstallMethod(SelfSimilarGroup, "for [IsList]", [IsList],
function(list)
  return SelfSimilarGroup(list, false);
end);

###############################################################################
##
#M  SelfSimilarGroup(<list>, <bind_vars>)
##
InstallMethod(SelfSimilarGroup, "for [IsList, IsBool]", [IsList, IsBool],
function(list, bind_vars)
  if not AG_IsCorrectRecurList(list, true) then
    Error("in SelfSimilarGroup(IsList, IsBool):\n",
          "  given list is not a correct list representing self-similar group\n");
  fi;

  return GroupOfSelfSimFamily(SelfSimFamily(list, bind_vars));
end);

###############################################################################
##
#M  SelfSimilarGroup(<list>, <names>)
##
InstallMethod(SelfSimilarGroup, "for [IsList, IsList]", [IsList, IsList],
function(list, names)
  return SelfSimilarGroup(list, names, AG_Globals.bind_vars_autom_family);
end);

###############################################################################
##
#M  SelfSimilarGroup(<list>, <names>, <bind_vars>)
##
InstallMethod(SelfSimilarGroup,
              "for [IsList, IsList, IsBool]", [IsList, IsList, IsBool],
function(list, names, bind_vars)
  if not AG_IsCorrectRecurList(list, true) then
    Error("error in SelfSimilarGroup(IsList, IsList, IsBool):\n",
          "  given list is not a correct list representing self-similar group\n");
  fi;

  return GroupOfSelfSimFamily(SelfSimFamily(list, names, bind_vars));
end);

###############################################################################
##
#M  SelfSimilarGroup(<string>)
#M  SelfSimilarGroup(<string>, <bind_vars>)
##
InstallMethod(SelfSimilarGroup, "for [IsString]", [IsString],
function(string)
  return SelfSimilarGroup(string, AG_Globals.bind_vars_autom_family);
end);

InstallMethod(SelfSimilarGroup, "for [IsString, IsBool]", [IsString, IsBool],
function(string, bind_vars)
  local s;
  s := AG_ParseAutomatonStringFR(string);
  return SelfSimilarGroup(s[2], s[1], bind_vars);
end);

###############################################################################
##
#M  SelfSimilarGroup(<A>)
#M  SelfSimilarGroup(<A>, <bind_vars>)
##
InstallMethod(SelfSimilarGroup, "for [IsMealyAutomaton]", [IsMealyAutomaton],
function(A)
  if not IsInvertible(A) then
    Error("Automaton <A> is not invertible");
  fi;
  return SelfSimilarGroup(AutomatonList(A), A!.states);
end);

InstallMethod(SelfSimilarGroup, "for [IsMealyAutomaton, IsBool]", [IsMealyAutomaton, IsBool],
function(A, bind_vars)
  if not IsInvertible(A) then
    Error("Automaton <A> is not invertible");
  fi;
  return SelfSimilarGroup(AutomatonList(A), A!.states, bind_vars);
end);

###############################################################################
##
#M  GroupOfSelfSimFamily(<G>)
##
InstallMethod(GroupOfSelfSimFamily, "for [IsSelfSimGroup]",
                   [IsSelfSimGroup],
function(G)
  return GroupOfSelfSimFamily(UnderlyingSelfSimFamily(G));
end);

###############################################################################
##
#M  IsGroupOfSelfSimFamily(<G>)
##
InstallMethod(IsGroupOfSelfSimFamily, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  return G = GroupOfSelfSimFamily(G);
end);

###############################################################################
##
#M  UseSubsetRelation(<G>)
##
InstallMethod(UseSubsetRelation,
              "for [IsSelfSimGroup, IsSelfSimGroup]",
              [IsSelfSimGroup, IsSelfSimGroup],
function(super, sub)
  ## the full group is self similar, so if <super> is smaller than the full
  ##  group then sub is smaller either
  if HasIsGroupOfSelfSimFamily(super) then
    if not IsGroupOfSelfSimFamily(super) then
      SetIsGroupOfSelfSimFamily(sub, false); fi; fi;
  TryNextMethod();
end);

###############################################################################
##
#M  __AG_SubgroupOnLevel(<G>, <gens>, <level>)
##
InstallMethod(__AG_SubgroupOnLevel, [IsSelfSimGroup,
                                    IsList and IsTreeAutomorphismCollection,
                                    IsPosInt],
function(G, gens, level)
  local overgroup;

  if IsEmpty(gens) or (Length(gens) = 1 and IsOne(gens[1])) then
    return TrivialSubgroup(G);
  fi;

  if HasIsGroupOfSelfSimFamily(G) and IsGroupOfSelfSimFamily(G) then
    overgroup := G;
  else
    overgroup := GroupOfSelfSimFamily(UnderlyingSelfSimFamily(G));
  fi;

  return SubgroupNC(overgroup, gens);
end);

InstallOtherMethod(__AG_SubgroupOnLevel, [IsSelfSimGroup, IsList and IsEmpty, IsPosInt],
function(G, gens, level)
  return TrivialSubgroup(G);
end);

InstallMethod(__AG_SubgroupOnLevel, [IsTreeAutomorphismGroup,
                                    IsList and IsSelfSimCollection,
                                    IsPosInt],
function(G, gens, level)
  local overgroup;

  overgroup := GroupOfSelfSimFamily(FamilyObj(gens[1]));

  if Length(gens) = 1 and IsOne(gens[1]) then
    return TrivialSubgroup(overgroup);
  fi;

  return SubgroupNC(overgroup, gens);
end);

InstallMethod(__AG_SimplifyGroupGenerators, "for [IsList and IsInvertibleSelfSimCollection]",
                          [IsList and IsInvertibleSelfSimCollection],
function(gens)
  local words, fam;

  if IsEmpty(gens) then
    return [];
  fi;

  fam := FamilyObj(gens[1]);
  words := FreeGeneratorsOfGroup(Group(List(gens, a -> a!.word)));

  if fam!.use_rws and not IsEmpty(words) then
    words := AG_ReducedForm(fam!.rws, words);
    if IsEmpty(words) then
      return [];
    fi;
    words := FreeGeneratorsOfGroup(Group(words));
  fi;

  return List(words, w -> SelfSim(w, fam));
end);

###############################################################################
##
#M  PrintObj(<G>)
##
InstallMethod(PrintObj, "for [IsSelfSimilarGroup]",
              [IsSelfSimilarGroup],
function(G)
  Print("SelfSimilarGroup(\"", String(G), "\")");
end);

###############################################################################
##
#M  Display(<G>)
##
InstallMethod(Display, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  local i, gens, printone;

  printone := function(a)
    Print(a, " = ", Decompose(a));
  end;

  gens := GeneratorsOfGroup(G);
  if gens = [] then Print("< >"); fi;
  if Length(gens) = 1 then
    Print("< "); printone(gens[1]); Print(" >");
  else
    Print("< "); printone(gens[1]); Print(", \n");
    for i in [2..Length(gens)-1] do
      Print("  "); printone(gens[i]); Print(", \n");
    od;
    Print("  "); printone(gens[Length(gens)]); Print(" >");
  fi;
end);

#############################################################################
##
#M  String(<G>)
##
InstallMethod(String, "for [IsSelfSimGroup]", [IsSelfSimGroup],
function(G)
  local i, gens, formatone, s;

  formatone := function(a)
    return Concatenation(String(a), " = ", String(Decompose(a)));
  end;

  gens := GeneratorsOfGroup(G);

  s := "";
  for i in [1..Length(gens)] do
    Append(s, formatone(gens[i]));
    if i <> Length(gens) then
      Append(s, ", ");
    fi;
  od;

  return s;
end);

###############################################################################
##
#M  ViewObj(<G>)
##
InstallMethod(ViewObj, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  local i, gens;
  gens := List(GeneratorsOfGroup(G), g -> Word(g));
  if gens = [] then Print("< >"); fi;
  Print("< ");
  for i in [1..Length(gens)-1] do
    if IsOne(gens[i]) then
      Print(AG_Globals.identity_symbol, ", ");
    else
      Print(gens[i], ", ");
    fi;
  od;
  if IsOne(gens[Length(gens)]) then
    Print(AG_Globals.identity_symbol, " >");
  else
    Print(gens[Length(gens)], " >");
  fi;
end);

###############################################################################
##
#M  IsFractalByWords(G)
##
InstallMethod(IsFractalByWords, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function (G)
  local freegens, stab, i, sym, f;

  sym := GroupWithGenerators(List(GeneratorsOfGroup(G), g -> Perm(g)));
  if not IsTransitive(sym, [1..DegreeOfTree(G)]) then
    Info(InfoAutomGrp, 1, "group is not transitive on first level");
    return false;
  fi;

  f := GroupWithGenerators(List(GeneratorsOfGroup(G), g -> Word(g)));
  stab := StabilizerOfFirstLevel(G);
  stab := List(GeneratorsOfGroup(stab), a -> StatesWords(a));

  for i in [1..DegreeOfTree(G)] do
    if f <> GroupWithGenerators(List(stab, s -> s[i])) then
      return false;
    fi;
  od;
  return true;
end);

###############################################################################
##
#M  Size(G)
##
InstallMethod(Size, "for [IsSelfSimGroup]", [IsSelfSimGroup],
function (G)
  local f;
  if IsTrivial(G) then
    Info(InfoAutomGrp, 3, "Size(G): 1, G is trivial");
    return 1;
  fi;

  if CanEasilyTestSphericalTransitivity(G) and IsSphericallyTransitive(G) then
    Info(InfoAutomGrp, 3, "Size(G): infinity, G is spherically transitive");
    return infinity;
  fi;

  if IsFractalByWords(G) then
    Info(InfoAutomGrp, 3, "Size(G): infinity, G is fractal by words");
    return infinity;
  fi;

  if HasIsFractal(G) and IsFractal(G) then
    Info(InfoAutomGrp, 3, "Size(G): infinity, G is fractal");
    return infinity;
  fi;

  if IsSelfSimilarGroup(G) and LevelOfFaithfulAction(G, 8)<>fail then
    return Size(G);
  fi;

  f := FindElementOfInfiniteOrder(G, 10, 10);

  if HasSize(G) or f <> fail then
    return Size(G);
  fi;

  Info(InfoAutomGrp, 1, "You can try to use IsomorphismPermGroup(<G>) or\n",
                        "   FindElementOfInfiniteOrder( <G>, <length>, <depth> ) with bigger bounds");
  TryNextMethod();
end);

InstallOtherMethod(LevelOfFaithfulAction, "for [IsSelfSimGroup and IsSelfSimilar, IsCyclotomic]",
              [IsSelfSimGroup and IsSelfSimilar, IsCyclotomic],
function(G, max_lev)
  local s, s_next, lev;
  if HasIsFinite(G) and not IsFinite(G) then return fail; fi;
  if HasLevelOfFaithfulAction(G) then return LevelOfFaithfulAction(G); fi;
  lev := 0; s := 1; s_next := Size(PermGroupOnLevel(G, 1));
  while s<s_next and lev<max_lev do
    lev := lev+1;
    s := s_next;
    s_next := Size(PermGroupOnLevel(G, lev+1));
  od;
  if s=s_next then
    SetSize(G, s);
    SetLevelOfFaithfulAction(G, lev);
    return lev;
  else
    return fail;
  fi;
end);

InstallMethod(LevelOfFaithfulAction, "for [IsSelfSimGroup and IsSelfSimilar]",
              [IsSelfSimGroup and IsSelfSimilar],
function(G)
  return LevelOfFaithfulAction(G, infinity);
end);

################################################################################
##
#O  IsomorphismPermGroup (<G>)
#O  IsomorphismPermGroup (<G>, <max_lev>)
##
##  For a given finite group <G> generated by initial automata or by elements defined by
##  wreath recursion
##  computes an isomorphism from <G> into a finite permutational group.
##  If <G> is not known to be self-similar (see "IsSelfSimilar") the isomorphism is based on the
##  regular representation, which works generally much slower. If <G> is self-similar
##  there is a level of the tree (see "LevelOfFaithfulAction"), where <G> acts faithfully.
##  The corresponding representation is returned in this case. If <max_lev> is given
##  it finds only the first <max_lev> quotients by stabilizers and if all of them have
##  different size it returns `fail'.
##  If <G> is infinite and <max_lev> is not specified it will loop forever.
##
##  For example, consider a subgroup $\langle a, b\rangle$ of Grigorchuk group.
##  \beginexample
##  gap> Grigorchuk_Group := AutomatonGroup("a=(1,1)(1,2),b=(a,c),c=(a,d),d=(1,b)");
##  < a, b, c, d >
##  gap> f := IsomorphismPermGroup(Group(a, b));
##  MappingByFunction( < a, b >, Group(
##  [ (1,2)(3,5)(4,6)(7,9)(8,10)(11,13)(12,14)(15,17)(16,18)(19,21)(20,22)(23,
##      25)(24,26)(27,29)(28,30)(31,32), (1,3)(2,4)(5,7)(6,8)(9,11)(10,12)(13,
##      15)(14,16)(17,19)(18,20)(21,23)(22,24)(25,27)(26,28)(29,31)(30,32)
##   ]), function( g ) ... end, function( b ) ... end )
##  gap> Size(Image(f));
##  32
##  gap> H := SelfSimilarGroup("a=(a*b,1)(1,2), b=(1,b*a^-1)(1,2), c=(b, a*b)");
##  < a, b, c >
##  gap> f1 := IsomorphismPermGroup(H);
##  MappingByFunction( < a, b, c >, Group([ (1,3)(2,4), (1,3)(2,4), (1,2)
##   ]), function( g ) ... end, function( b ) ... end )
##  gap> Size(Image(f1));
##  8
##  gap> PreImagesRepresentative(f1, (1,3,2,4));
##  a*c
##  gap> (a*c)^f1;
##  (1,3,2,4)
##  \endexample
##
InstallOtherMethod(IsomorphismPermGroup, "for [IsSelfSimilarGroup, IsCyclotomic]",
                   [IsSelfSimGroup and IsSelfSimilar, IsCyclotomic],
function (G, n)
  local H, lev;
  lev := LevelOfFaithfulAction(G, n);
  if lev <> fail then
    H := PermGroupOnLevel(G, LevelOfFaithfulAction(G));
    return AG_GroupHomomorphismByImagesNC(G, H, GeneratorsOfGroup(G), GeneratorsOfGroup(H));
  fi;
  return fail;
end);

###############################################################################
##
#M  IsSphericallyTransitive(G)
##
InstallMethod(IsSphericallyTransitive, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function (G)
  local x, rat_gens, abel_hom, lev;

  if IsFractalByWords(G) then
    Info(InfoAutomGrp, 3, "IsSphericallyTransitive(G): true");
    Info(InfoAutomGrp, 3, "  G is fractal");
    return true;
  fi;

  if IsTrivial(G) then
    Info(InfoAutomGrp, 3, "IsSphericallyTransitive(G): false");
    Info(InfoAutomGrp, 3, "  G is trivial: G = ", G);
    return false;
  fi;

  if HasIsFinite(G) and IsFinite(G) then
    Info(InfoAutomGrp, 3, "IsSphericallyTransitive(G): false");
    Info(InfoAutomGrp, 3, "  IsFinite(G): G = ", G);
    return false;
  fi;

  if DegreeOfTree(G) = 2 and TestSelfSimilarity(G) and IsSelfSimilar(G) then
    if HasIsFinite(G) and IsFinite(G)=false then
      Info(InfoAutomGrp, 3, "IsSphericallyTransitive(G): true");
      Info(InfoAutomGrp, 3, "  <G> is infinite self-similar acting on binary tree");
      return true;
    fi;
    if PermGroupOnLevel(G, 2)=Group((1, 4, 2, 3)) then
      Info(InfoAutomGrp, 3, "IsSphericallyTransitive(G): true");
      Info(InfoAutomGrp, 3, "  any element which acts transitively on the first level acts spherically transitively");
      return true;
    fi;
  fi;

  for lev in [1..8] do
    if not IsTransitiveOnLevel(G,lev) then
      Info(InfoAutomGrp, 3, "IsSphericallyTransitive(G): false");
      Info(InfoAutomGrp, 3, "  the group does not act transitively on level ", lev);
      return false;
    fi;
  od;

  TryNextMethod();
end);

###############################################################################
##
#M  DiagonalPower(<G>, <n>)
##
InstallOtherMethod( DiagonalPower,
                    "for [IsSelfSimGroup and IsGroupOfSelfSimFamily, IsPosInt]",
                    [IsSelfSimGroup and IsGroupOfSelfSimFamily, IsPosInt],
function(G, n)
  return DiagonalPower(UnderlyingSelfSimFamily(G), n);
end);

###############################################################################
##
#M  MultAutomAlphabet(<G>, <n>)
##
InstallOtherMethod( MultAutomAlphabet,
                    "for [IsSelfSimGroup and IsGroupOfSelfSimFamily, IsPosInt]",
                    [IsSelfSimGroup and IsGroupOfSelfSimFamily, IsPosInt],
function(G, n)
  return MultAutomAlphabet(UnderlyingSelfSimFamily(G), n);
end);

###############################################################################
##
#M  \= (<G>, <H>)
##
InstallMethod(\=, "for [IsSelfSimGroup, IsSelfSimGroup]",
              IsIdenticalObj, [IsSelfSimGroup, IsSelfSimGroup],
function(G, H)
  local fgens1, fgens2, fam;

  if HasIsGroupOfSelfSimFamily(G) and HasIsGroupOfSelfSimFamily(H) then
    if IsGroupOfSelfSimFamily(G) <> IsGroupOfSelfSimFamily(H) then
      Info(InfoAutomGrp, 3, "G = H: false, exactly one is GroupOfSelfSimFamily");
      return false;
    fi;
    if IsGroupOfSelfSimFamily(G) then
      Info(InfoAutomGrp, 3, "G = H: true, both are GroupOfSelfSimFamily");
      return true;
    fi;
  fi;

  fgens1 := List(GeneratorsOfGroup(G), g -> Word(g));
  fgens2 := List(GeneratorsOfGroup(H), g -> Word(g));
  fam := UnderlyingSelfSimFamily(G);

  if fam!.rws <> fail then
    fgens1 := AG_ReducedForm(fam!.rws, fgens1);
    fgens2 := AG_ReducedForm(fam!.rws, fgens2);
  fi;

  if GroupWithGenerators(fgens1) = GroupWithGenerators(fgens2) then
    Info(InfoAutomGrp, 3, "G = H: true, by subgroups of free group");
    return true;
  fi;

  TryNextMethod();
end);

###############################################################################
##
#M  IsSubset (<G>, <H>)
##
InstallMethod(IsSubset, "for [IsSelfSimGroup, IsSelfSimGroup]",
              IsIdenticalObj, [IsSelfSimGroup, IsSelfSimGroup],
function(G, H)
  local h, fam, fgens1, fgens2;

  if HasIsGroupOfSelfSimFamily(G) and IsGroupOfSelfSimFamily(G) then
    Info(InfoAutomGrp, 3, "IsSubgroup(G, H): true");
    Info(InfoAutomGrp, 3, "  G is GroupOfSelfSimFamily");
    return true;
  fi;

  fgens1 := List(GeneratorsOfGroup(G), g -> Word(g));
  fgens2 := List(GeneratorsOfGroup(H), g -> Word(g));
  fam := UnderlyingSelfSimFamily(G);

  if fam!.rws <> fail then
    fgens1 := AG_ReducedForm(fam!.rws, fgens1);
    fgens2 := AG_ReducedForm(fam!.rws, fgens2);
  fi;

  if IsSubgroup(GroupWithGenerators(fgens1), GroupWithGenerators(fgens2)) then
    Info(InfoAutomGrp, 3, "IsSubgroup(G, H): true");
    Info(InfoAutomGrp, 3, "  by subgroups of free group");
    return true;
  fi;

  TryNextMethod();
end);

###############################################################################
##
#M  <g> in <G>
##
InstallMethod(\in, "for [IsSelfSim, IsSelfSimGroup]",
              [IsSelfSim, IsSelfSimGroup],
function(g, G)
  local fam, fgens, w;

  if HasIsGroupOfSelfSimFamily(G) and IsGroupOfSelfSimFamily(G) then
    return true;
  fi;

  fgens := List(GeneratorsOfGroup(G), g -> Word(g));
  w := Word(g);

  fam := UnderlyingSelfSimFamily(G);

  if fam!.rws <> fail then
    fgens := AG_ReducedForm(fam!.rws, fgens);
    w := AG_ReducedForm(fam!.rws, w);
  fi;

  if w in GroupWithGenerators(fgens) then
    Info(InfoAutomGrp, 3, "g in G: true");
    Info(InfoAutomGrp, 3, "  by elements of free group");
    Info(InfoAutomGrp, 3, "  g = ", g, "; G = ", G);
    return true;
  fi;

  TryNextMethod();
end);

###############################################################################
##
#O  Random(<G>)
##
##  Returns a random element of a group (semigroup) <G>. The operation is based
##  on the generator of random elements in free groups and semigroups.
##
##  \beginexample
##  gap> Basilica := AutomatonGroup( "u=(v,1)(1,2), v=(u,1)" );
##  < u, v >
##  gap> Random( Basilica );
##  v*u^-3
##  \endexample
##
InstallMethodWithRandomSource(Random, "for a random source and [IsSelfSimGroup]",
              [IsRandomSource, IsSelfSimGroup],
function(rs, G)
  local F, gens, pi;

  if IsTrivial(G) then
    return One(G);
  elif IsSelfSimilarGroup(G) then
    return SelfSim(Random(rs, UnderlyingFreeGroup(G)), UnderlyingSelfSimFamily(G));
  else
    gens := GeneratorsOfGroup(G);
    F := FreeGroup(Length(gens));
    pi := GroupHomomorphismByImagesNC(F, G,  GeneratorsOfGroup(F), gens);
    return Random(rs, F)^pi;
  fi;
end);

###############################################################################
##
#M  UnderlyingFreeSubgroup(<G>)
##
InstallMethod(UnderlyingFreeSubgroup, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  local f;
  if HasIsGroupOfSelfSimFamily(G) and IsGroupOfSelfSimFamily(G) then
    return UnderlyingFreeGroup(G);
  fi;
  f := Subgroup(UnderlyingFreeGroup(G), UnderlyingFreeGenerators(G));
  if f = UnderlyingFreeGroup(G) then
    SetIsGroupOfSelfSimFamily(G, true);
  fi;
  return f;
end);

###############################################################################
##
#M  UnderlyingFreeGenerators(<G>)
##
InstallMethod(UnderlyingFreeGenerators, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  return List(GeneratorsOfGroup(G), g -> Word(g));
end);

###############################################################################
##
#M  TrivialSubmagmaWithOne(<G>)
##
InstallMethod(TrivialSubmagmaWithOne, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  return Subgroup(G, [One(G)]);
end);

###############################################################################
##
#M  IsSelfSimilarGroup(<G>)
##
##  Returns `true' if generators of <G> coincide with generators of the family
InstallImmediateMethod(IsSelfSimilarGroup, IsSelfSimGroup, 0,
function(G)
  local fam;
  fam := UnderlyingSelfSimFamily(G);
  return fam!.numstates = 0 or
         GeneratorsOfGroup(G) = fam!.recurgens{[1..fam!.numstates]};
end);

###############################################################################
##
#M  RecurList(<G>)
##
InstallMethod(RecurList, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  if IsSelfSimilarGroup(G) then
    return RecurList(GroupOfSelfSimFamily(UnderlyingSelfSimFamily(G)));
  else
    Error("Group <G> is not necessarily self-similar");
  fi;
end);

###############################################################################
##
#M  IsSelfSimilar(<G>)
##
InstallMethod(IsSelfSimilar, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  local g, i, res;
  res := true;
  for g in GeneratorsOfGroup(G) do
    for i in [1..UnderlyingSelfSimFamily(G)!.deg] do
      res := Section(g, i) in G;
      if res = fail then
        TryNextMethod();
      elif not res then
        return false;
      fi;
    od;
  od;
  return true;
end);

###############################################################################
##
#M  UnderlyingSelfSimFamily(<G>)
##
InstallMethod(UnderlyingSelfSimFamily, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  return FamilyObj(GeneratorsOfGroup(G)[1]);
end);

###############################################################################
##
#M  IsFiniteState(<G>)
##
InstallMethod(IsFiniteState, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  local states, MealyAutomatonLocal, aut_list, gens, images, H, g, hom_function, \
        inv_hom_function, hom, free_groups_hom, inv_free_groups_hom, inv_hom, \
        gens_in_freegrp, images_in_freegrp, preimages_in_freegrp, F, pi, pi_bar, \
        preimage_in_freegrp, MealyAutomatonLocalFinite;

# if we do not know much, we compare just words in free group
  MealyAutomatonLocal := function(g)
    local cur_state;
    if g!.word in states then return Position(states, g!.word); fi;
    Add(states, g!.word);
    cur_state := Length(states);
    aut_list[cur_state] := List([1..g!.deg], x -> MealyAutomatonLocal(Section(g, x)));
    Add(aut_list[cur_state], g!.perm);
    return cur_state;
  end;

# if we do know that the groups is finite, we compare actual elements of the group
  MealyAutomatonLocalFinite := function(g)
    local cur_state;
    if g in states then return Position(states, g); fi;
    Add(states, g);
    cur_state := Length(states);
    aut_list[cur_state] := List([1..g!.deg], x -> MealyAutomatonLocalFinite(Section(g, x)));
    Add(aut_list[cur_state], g!.perm);
    return cur_state;
  end;

  if IsTrivial(G) then return true; fi;

  states := [];
  aut_list := [];
  gens := GeneratorsOfGroup(G);
  images := [];

  if HasIsFinite(G) and IsFinite(G) then
    for g in gens do
      Add(images, MealyAutomatonLocalFinite(g));
    od;
    states := List(states, Word);
  else
    for g in gens do
      Add(images, MealyAutomatonLocal(g));
    od;
  fi;

  H := AutomatonGroup(aut_list);

  if IsTrivial(H) then
    SetIsTrivial( G, true);
    return true;
  fi;

  images := UnderlyingAutomFamily(H)!.oldstates{images};

  SetIsomorphicAutomGroup(G, GroupWithGenerators(UnderlyingAutomFamily(H)!.automgens{images}));
  SetUnderlyingAutomatonGroup(G, H);

# preimages of generators of G in UnderlyingFreeGroup(G)
  gens_in_freegrp := List(GeneratorsOfGroup(G), Word);

# preimages of generators of a subgroup of H isomorphic to G in UnderlyingFreeGroup(H)
  images_in_freegrp := List(UnderlyingAutomFamily(H)!.automgens{images}, Word);

  preimage_in_freegrp := function(x)
    local w;
    w := LetterRepAssocWord(x!.word)[1];
    if w > 0 then
      return states[ Position( UnderlyingAutomFamily(H)!.oldstates, w)];
    else
      return states[ Position( UnderlyingAutomFamily(H)!.oldstates, -w+UnderlyingAutomFamily(H)!.numstates)];
    fi;
  end;

#  preimages of generators of H in UnderlyingFreeGroup(G)
#  preimages_in_freegrp := List([1..Length(GeneratorsOfGroup(H))], x->states[Position(UnderlyingAutomFamily(H)!.oldstates, x)]);
  preimages_in_freegrp := List(GeneratorsOfGroup(H), x -> preimage_in_freegrp(x));

  if IsSelfSimilarGroup(G) then
    free_groups_hom :=
       GroupHomomorphismByImagesNC( Group(gens_in_freegrp), UnderlyingFreeGroup(H),
                                    gens_in_freegrp, images_in_freegrp );

    inv_free_groups_hom :=
       GroupHomomorphismByImagesNC( UnderlyingFreeGroup(H), UnderlyingFreeGroup(G),
                                    UnderlyingFreeGenerators(H), preimages_in_freegrp );

    hom_function := function(a)
      return Autom(Image(free_groups_hom, a!.word), UnderlyingAutomFamily(H));
    end;

    inv_hom_function :=  function(b)
      return SelfSim(Image(inv_free_groups_hom, b!.word), UnderlyingSelfSimFamily(G));
    end;

    hom := GroupHomomorphismByFunction(G, GroupWithGenerators(UnderlyingAutomFamily(H)!.automgens{images}), hom_function, inv_hom_function);

    SetMonomorphismToAutomatonGroup(G, hom);
  else
    F := FreeGroup(Length(GeneratorsOfGroup(G)));

#        pi
#    F ------> G ----> UnderlyingFreeGroup(H)
#      -------------->
#            pi_bar

    pi := GroupHomomorphismByImages(F,                     Group(gens_in_freegrp),
                                    GeneratorsOfGroup(F),  gens_in_freegrp);

    pi_bar := GroupHomomorphismByImages(F,                     UnderlyingFreeGroup(H),
                                        GeneratorsOfGroup(F),  images_in_freegrp);

    hom_function := function(g)
      return Autom(Image(pi_bar, PreImagesRepresentative(pi, g!.word)), UnderlyingAutomFamily(H));
    end;

    inv_hom_function :=  function(b)
      return SelfSim(Image(pi, PreImagesRepresentative(pi_bar, b!.word)), UnderlyingSelfSimFamily(G));
    end;

    hom := GroupHomomorphismByFunction(G, GroupWithGenerators(UnderlyingAutomFamily(H)!.automgens{images}), hom_function, inv_hom_function);

    SetMonomorphismToAutomatonGroup(G, hom);
  fi;

  return true;
end);

###############################################################################
##
#M  IsomorphicAutomGroup( <G> )
##
InstallMethod(IsomorphicAutomGroup, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  if IsFiniteState(G) then return IsomorphicAutomGroup(G); fi;
end);

###############################################################################
##
#M  UnderlyingAutomatonGroup( <G> )
##
InstallMethod(UnderlyingAutomatonGroup, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  if IsFiniteState(G) then return UnderlyingAutomatonGroup(G); fi;
end);

###############################################################################
##
#M  MonomorphismToAutomatonGroup( <G> )
##
InstallMethod(MonomorphismToAutomatonGroup, "for [IsSelfSimGroup]",
              [IsSelfSimGroup],
function(G)
  if IsFiniteState(G) then return MonomorphismToAutomatonGroup(G); fi;
end);

###############################################################################
##
#M  IsContracting( <G> )
##
InstallMethod(IsContracting, "for [IsSelfSimilarGroup]",
              [IsSelfSimilarGroup],
function(G)
  local res;
  if not IsFiniteState(G) then
#   every contracting self-similar group is finite-state
    return false;
  fi;

  res := IsContracting(GroupOfAutomFamily(UnderlyingAutomFamily(UnderlyingAutomatonGroup(G))));

  UnderlyingSelfSimFamily(G)!.use_contraction := true;
  UnderlyingAutomFamily(UnderlyingAutomatonGroup(G))!.use_contraction := true;

  return res;
end);

###############################################################################
##
#M  GroupNucleus( <G> )
##
InstallMethod(GroupNucleus, "for [IsSelfSimilarGroup]",
              [IsSelfSimilarGroup],
function(G)
  local H;
  if not IsFiniteState(G) then
#   every contracting self-similar group is finite-state
    Error("Group <G> is not finite-state");
  fi;

  if not IsContracting(G) then
    Error("Group <G> is not contracting");
  fi;

  H := GroupOfAutomFamily( UnderlyingAutomFamily( UnderlyingAutomatonGroup(G)));

  return List( GroupNucleus(H), x -> PreImagesRepresentative( MonomorphismToAutomatonGroup(G), x));
end);

###############################################################################
##
#M  UseContraction( <G> )
##
InstallMethod(UseContraction, "for [IsSelfSimGroup]", true,
              [IsSelfSimGroup],
function(G)
  if not IsSelfSimilarGroup(G) then
    Print("Error in UseContraction(<G>): The method is implemented only for IsSelfSimilarGroup\n");
    return fail;
  fi;

  if not HasIsContracting(G) then
    Print("Error in UseContraction(<G>): It is not known whether the group <G> is contracting\n");
    return fail;
  elif not IsContracting(G) then
    Print("Error in UseContraction(<G>): The group <G> is not contracting");
    return fail;
  fi;
  #  IsContracting returns either true or false or an error (it can not return fail)

  UnderlyingSelfSimFamily(G)!.use_contraction := true;
  UnderlyingAutomFamily( UnderlyingAutomatonGroup(G))!.use_contraction := true;

  return true;
end);

###############################################################################
##
#M  DoNotUseContraction( <G> )
##
InstallMethod(DoNotUseContraction, "for [IsSelfSimGroup]", true,
              [IsSelfSimGroup],
function(G)
  UnderlyingAutomFamily(G)!.use_contraction := false;

  if HasUnderlyingAutomatonGroup(G) then
    UnderlyingAutomFamily( UnderlyingAutomatonGroup(G))!.use_contraction := false;
  fi;
  return true;
end);

###############################################################################
##
#M  FindNucleus( <G> )
##
InstallMethod(FindNucleus, "for [IsSelfSimilarGroup, IsCyclotomic]",
              [IsSelfSimilarGroup, IsCyclotomic],
function(G, max_nucl)
  local H, nuclH, nuclG;
  if not IsFiniteState(G) then
#   every contracting self-similar group is finite-state
    Error("Group <G> is not finite-state");
  fi;

  if HasIsContracting(G) and not IsContracting(G) then
    Error("Group <G> is not contracting");
  fi;

  H := GroupOfAutomFamily( UnderlyingAutomFamily( UnderlyingAutomatonGroup( G )));

  if HasIsContracting(H) and not IsContracting(H) then
    Error("Group <G> is not contracting");
  fi;

  nuclH := FindNucleus(H, max_nucl);

  if nuclH=fail then return fail; fi;

  nuclG := [];
  Add(nuclG, List( GeneratingSetWithNucleus(H), x -> PreImagesRepresentative( MonomorphismToAutomatonGroup( G ), x )));
  Add(nuclG, List( GroupNucleus(H), x -> PreImagesRepresentative( MonomorphismToAutomatonGroup( G ), x )));
  Add(nuclG, GeneratingSetWithNucleusAutom(H));

  SetGroupNucleus(G, nuclG[1]);
  SetGeneratingSetWithNucleus(G, nuclG[2]);
  SetGeneratingSetWithNucleusAutom(G, nuclG[3]);
  SetContractingLevel(G, ContractingLevel(H));

  return nuclG;
end);

InstallMethod(FindNucleus, "for [IsSelfSimilarGroup]", true,
              [IsSelfSimilarGroup],
function(G)
  return FindNucleus(G, infinity);
end);

InstallMethod(GeneratingSetWithNucleus, "for [IsSelfSimilarGroup]", true,
              [IsSelfSimilarGroup],
function(G)
  if IsContracting(G) then return GeneratingSetWithNucleus(G); fi;
end);

InstallMethod(GeneratingSetWithNucleusAutom, "for [IsSelfSimilarGroup]", true,
              [IsSelfSimilarGroup],
function(G)
  if IsContracting(G) then return GeneratingSetWithNucleusAutom(G); fi;
end);

#E


# Copyright (c) 2018-2021, Carnegie Mellon University
# See LICENSE for details

Class(Codegen, HierarchicalVisitor, rec(
    initXY := function(x, y, opts)
        if IsBound(opts.XType) and not IsList(x) then
            if not IsArrayT(opts.XType) and not IsPtrT(opts.XType)
                then Error("opts.XType must be a pointer or array type. This has recently changed.",
                           "If you used TReal before, use TPtr(TReal) now"); fi;
            x.t := opts.XType;
            x.t := When(IsBound(opts.useRestrict) and opts.useRestrict, x.t.restrict(), x.t);
        fi;
        if IsBound(opts.YType) and not IsList(y) then
            if not IsArrayT(opts.YType) and not IsPtrT(opts.YType)
                then Error("opts.YType must be a pointer or array type. This has recently changed.",
                           "If you used TReal before, use TPtr(TReal) now"); fi;
            y.t := opts.YType;
            y.t := When(IsBound(opts.useRestrict) and opts.useRestrict, y.t.restrict(), y.t);
        fi;
        return [x, y];
    end,

    # NOTE: get rid of _acc_

    # This function substitutes assign by assign_acc (eliminated in a subsequent pass)
    # This is done to achieve accumulation, and assign_acc is used to
    #   prevent double accumulation.. Used in Codegen.ISumAcc and Codegen.SUMAcc.
    # Must be handled better somehow.
    #
    _acc := (icode, y) ->
        SubstTopDownNR(icode, [assign, [@(0,nth), @(1), @(2)], @(3)],
            e -> assign_acc(@(0).val, @(3).val)),

    _interleave := function(codes)
        local decls, c, i, j, cmds;
        decls := Set([]);
        for i in [1..Length(codes)] do
            c := codes[i];
            while ObjId(c)=decl do UniteSet(decls, c.vars); c := c.cmd; od;
            if ObjId(c)<>chain then
                codes[i] := [c];
            else
                codes[i] := c.cmds;
            fi;
        od;

        cmds := [];
        for i in [1..Maximum(List(codes, Length))] do
            for j in [1..Length(codes)] do
                if IsBound(codes[j][i]) then Add(cmds, codes[j][i]); fi;
            od;
        od;
        return decl(decls, chain(cmds));
    end
));

#fAdd.rlambda := self >> let(i:=Ind(), Lambda(i,i+1));
#H.rlambda := self >> let(i:=Ind(), Lambda(i,i+self.params[4]));

# a version of the Dat1d call that also sets the 'no scalarize' flag on the variable 't'.
_dat1d_donotscalarize := function(a,b)
    local t;

    t := Dat1d(a,b);
    t.doNotScalarize := true;

    return t;
end;

Class(DefaultCodegen, Codegen, rec(
    Formula := meth(self, o, y, x, opts)
        local icode, datas, prog, params, sub, initsub, destroysub, io, t, initcode, initparams;

        o := SumsUnification(o.child(1), opts);

        [x, y] := self.initXY(x, y, opts);

        #o :=  Process_fPrecompute(o, opts);

        params := Set(Concatenation(Collect(o, param), Filtered(Collect(o, var), IsParallelLoopIndex)));

        datas := Collect(o, FDataOfs);
        [o,t] := UTimedAction(BlockSumsOpts(o, opts)); #PrintLine("BlockSums ", t);
        [icode,t] := UTimedAction(self(o, y, x, opts)); #PrintLine("codegen ", t);
        #[icode,t] := UTimedAction(ESReduce(icode, opts)); #PrintLine("ESReduce ", t);
        icode := RemoveAssignAcc(icode);
        Unbind(Compile.times);
        [icode,t] := UTimedAction(BlockUnroll(icode, opts)); #PrintLine("BlockUnroll ", t);
        #PrintLine("---compile--");
        #DoForAll([1..Length(Compile.times)], i -> PrintLine(i, " ", Compile.times[i], " ",
        #        let(f:=opts.compileStrategy[i], When(IsFunc(f) or IsMeth(f), "---", f))));

        # icode := PowerOpt(icode);
        icode := DeclareHidden(icode);
        if IsBound(opts.isFixedPoint) and opts.isFixedPoint then
            icode := FixedPointCode(icode, opts.bits, opts.fracbits);
        fi;

        initparams := Copy(params);
        if IsBound(opts.symbol) then
          params := Concatenation(params, opts.symbol);
        fi;

        if IsBound(opts.accStrategy) then
          icode.iy := y;
          icode.iy.n := o.dims()[1];
          icode.ix := x;
          icode.ix.n := o.dims()[2];
          icode.ivars := Concatenation(params, List(datas, x->x.var));
          icode := opts.accStrategy(icode);
        fi;

        io := When(x=y, [x], [y, x]);
        sub := Cond(IsBound(opts.subName), opts.subName, "transform");
        initsub := Cond(IsBound(opts.subName), Concat("init_", opts.subName), "init");
        destroysub := Cond(IsBound(opts.subName), Concat("destroy_", opts.subName), "destroy");
        icode := func(TVoid, sub, Concatenation(io, params), icode);

        if IsBound(opts.generateInitFunc) and opts.generateInitFunc then
	    initcode := chain(List(Filtered(datas, e -> IsBound(e.var.init)), x -> SReduce(x.var.init, opts)));
            prog := program(
                decl(List(datas, x->x.var),
                    chain(
                        func(TVoid, initsub, initparams :: Set(Collect(initcode, param)), initcode),
                        icode,
                        func(TVoid, destroysub, [], skip())
                    )));
        else
            prog := program( func(TVoid, initsub, params, chain()), icode);
        fi;
        prog.dimensions := o.dims();
        return prog;
    end,

    Cross := meth(self, o, y, x, opts)
        local i, mychain, xdims, ydims, myYdims, myXdims;
        mychain:=[];xdims:=1;ydims:=1;
        for i in [1..Length(o._children)] do
            myYdims:=DimLength(o._children[i].dims()[1]);
            myXdims:=DimLength(o._children[i].dims()[2]);
            Add(mychain,self(o._children[i],
                    StripList(y{[ydims..ydims+myYdims-1]}),
                    StripList(x{[xdims..xdims+myXdims-1]}),opts));
            ydims:=ydims+myYdims;
            xdims:=xdims+myXdims;
        od;
        return chain(mychain);
    end,

    Glue:= meth(self,o,y,x,opts)
        local size, als,iterator,n;
        size := o.element[2];
        n := EvalScalar(o.element[1]);
        iterator :=Ind(size);
        als  := List([0..n-1],t -> assign(nth(StripList(y),add(iterator,t*size)),nth(x[t+1],iterator)));
        return loop(iterator, size, chain(als));
    end,

    Split:= meth(self,o,y,x,opts)
        local size, als,iterator,n;
        n := EvalScalar(o.element[2]);
        size := o.element[1];
        iterator :=Ind(idiv(size, n));
        als  := List([0..(n-1)],t -> assign(nth(y[t+1],iterator),nth(StripList(x),add(iterator,t*size/n))));
        return loop(iterator, idiv(size, n), chain(als));
     end,

    BB := (self,o,y,x,opts) >> MarkForUnrolling(
        When(IsBound(o.bbnum),
            o.bbnum,
            0
        ),
        self(o.child(1), y, x, opts),
        opts
    ),
    IDirSum := (self,o,y,x,opts) >> self(o.sums(), y, x, opts),
    TTag := (self,o,y,x,opts) >> self(o.params[1], y, x, opts),
    DPWrapper := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),

    Buf := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),
    NoPull := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),
    PushL := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),
    PushR := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),
    PushLR := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),
    Grp := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),
    NoDiagPullin := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),
    NoDiagPullinLeft := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),
    NoDiagPullinRight := (self,o,y,x,opts) >> self(o.child(1), y, x, opts),

    COND := (self,o,y,x,opts) >> IF(
        When(IsFunction(o.cond), o.cond.at(0), o.cond),
        self(o.child(1), y, x, opts), self(o.child(2), y, x, opts)),

    Diag := (self, o, y, x, opts) >> let(i := Ind(), elt := o.element.lambda(),
        loop(i, elt.domain(), assign(nth(y,i), elt.at(i) * nth(x,i)))),

    DiagCpxSplit := (self, o, y, x, opts) >> let(i := Ind(), elt := o.element.lambda(),
        re := elt.at(2*i), im := elt.at(2*i+1),
        loop(i, elt.domain()/2,
             assign(nth(y,i),   re * nth(x,i) + E(4) * im * nth(x,i)))),

    TCast := (self, o, y, x, opts) >> let(i := Ind(),
        loop(i, o.params[1], assign(nth(y,i), tcast(o.params[2], nth(x,i))))),

    RCDiag := (self, o, y, x, opts) >> let(i := Ind(), elt := o.element.lambda(),
        re := elt.at(2*i), im := elt.at(2*i+1),
        loop(i, elt.domain()/2, chain(
             assign(nth(y,2*i),   re * nth(x,2*i) - im * nth(x,2*i+1)),
             assign(nth(y,2*i+1), im * nth(x,2*i) + re * nth(x,2*i+1))))),

    ColVec := (self, o, y, x, opts) >> let(i := Ind(), func := o.element.lambda(),
        loop(i, func.domain(), assign(nth(y,i), mul(func.at(i), nth(x,0))))),

    RowVec := (self, o, y, x, opts) >> let(i := Ind(), func := o.element.lambda(),
        t := TempVar(x.t.t),
        chain(assign(t,0),
            loop(i, func.domain(), assign(t, add(t, mul(func.at(i), nth(x,i))))),
            assign(nth(y,0), t))),

    Scale := (self, o, y, x, opts) >> let(i := Ind(),
        chain(self(o.child(1), y, x, opts),
              loop(i, Rows(o), assign(nth(y,i), mul(o.scalar, nth(y,i)))))),

    I := (self, o, y, x, opts) >> Cond(x<>y,let(i := Ind(Rows(o)),
            loop(i, i.range, assign(nth(y,i), nth(x,i)))),skip()),

    2DI := (self, o, y, x, opts) >> Cond(x<>y, Error("Should not happen"), skip()),

    # this one will always unroll Blk's
    Blk := (self, o, y, x, opts) >> let(
	# this is a hack, and it is needed, because without it we will be assigning to a
	# variable of type TArray (after binsplit), and some compilers (=icc) don't like that
	# (not an l-value)
	# NOTE: suggestion -- move this somewhere, as a compatibility patch (maybe postprocess?)
	tcst := Cond(IsSymbolic(o.element), x->tcast(TPtr(o.element.t.t.t), x), x->x),
	Cond(
            not IsSymbolic(o.element) and Rows(o)=2 and Cols(o)=2, Blk2code(o, y, x),
            not IsSymbolic(o.element) and Rows(o)=4 and Cols(o)=4, Blk4code(o, y, x),
            chain(
		List([0..Rows(o)-1], j -> let(
		    row := tcst(nth(o.element, j)),
                    assign(nth(y,j), ApplyFunc(add, List([0..Cols(o)-1], i -> nth(row, i) * nth(x,i))))
		))
	    )
	)
    ),

# This one can loop Blk's if needed, but is much slower for large matrices, and eventually blows up in storage requirements
#    Blk := (self, o, y, x, opts) >> Cond(
#        Rows(o)=2 and Cols(o)=2, Blk2code(o, y, x),
#        Rows(o)=4 and Cols(o)=4, Blk4code(o, y, x),
#        let(j:=Ind(), i:=Ind(), t:=TempVar(x.t.t), mat:=V(o.element), d:=Dat(mat.t),
#            data(d, mat,
#                loop(j, Rows(o), decl(t, chain(
#                     assign(t, 0),
#                     loop(i, Cols(o), assign(t, t + nth(nth(d,j),i) * nth(x,i))),
#                     assign(nth(y,j), t))))))),

    toeplitz := (self, o, y, x, opts) >> self.Blk(o.obj, y, x, opts),

    Blk1 := (self, o, y, x, opts) >> assign(nth(y,0), mul(toExpArg(o.element), nth(x,0))),

    BlkConj := (self, o, y, x, opts) >> assign(nth(y,0), conj(nth(x,0))),

    Prm := (self, o, y, x, opts) >> Cond(
        x = y,
            When(ObjId(o.func) = fId, skip(), Error("Inplace Permutation is not dealt with...")),

        let(i:=Ind(), func:=o.func.lambda(),
            loop(i, Rows(o), assign(nth(y, i), nth(x, func.at(i)))))
    ),

    O := (self, o, y, x, opts) >> let(i:=Ind(),
        loop(i, o.params[1], assign(nth(y, i), V(0)))),

    Gath := meth(self, o, y, x, opts)
        local i, func, rfunc, ix;
        i := Ind(); func := o.func.lambda();

        if IsBound(o.func.rlambda) then
            rfunc := o.func.rlambda();
            ix := var.fresh_t("ix", TInt);
            return decl(ix, chain(
                    assign(ix, func.at(0)),
                    assign(nth(y, 0), nth(x, ix)),
                    loop(i, o.func.domain()-1,
                        chain(assign(ix, rfunc.at(ix)),
                            assign(nth(y, i+1), nth(x, ix))))));
        else
            return loop(i, o.func.domain(), assign(nth(y,i), nth(x, func.at(i))));
        fi;
    end,

    Scat := meth(self, o, y, x, opts)
        local i, func, rfunc, ix;
        i := Ind(); func := o.func.lambda();
        if IsBound(o.func.rlambda) then
        rfunc := o.func.rlambda();
        ix := var.fresh_t("ix", TInt);
        return decl(ix, chain(
                assign(ix, func.at(0)),
                assign(nth(y, ix), nth(x, 0)),
                loop(i, o.func.domain()-1,
                    chain(assign(ix, rfunc.at(ix)),
                        assign(nth(y, ix), nth(x, i+1))))));
        else
            return loop(i, o.func.domain(), assign(nth(y,func.at(i)), nth(x, i)));
        fi;
    end,

    ScatGath := meth(self, o, y, x, opts)
        local i, sfunc, gfunc, decls;

        i := Ind();
        sfunc := o.sfunc.lambda();
        gfunc := o.gfunc.lambda();
        decls := Set(Concat(sfunc.free(),gfunc.free()));
        return loopn(i, o.sfunc.domain(), assign(nth(y,sfunc.at(i)), nth(x, gfunc.at(i))));
    end,

    SUM := (self, o, y, x, opts) >> chain(List(o.children(), c -> self(c, y, x, opts))),

    ISum := (self, o, y, x, opts) >> let(
        myloop := When(IsSymbolic(o.domain), loopn, loop),
        myloop(o.var, o.domain,
            self(o.child(1), y, x, opts))),

    JamISum := (self, o, y, x, opts) >> let(its := EvalScalar(o.domain),
        Cond(IsSymbolic(its),
                 self.ISum(o, y, x, opts),
             its > 32, Error("<o.domain> is too big (> 32), probably something went wrong. ",
                             "If you know what you are doing, then change DefaultCodegen.JamISum"),
             let(s := o.child(1),
                 bodies := List([0..its-1], j ->
                     Compile(self(SubstVars(Copy(s), rec((o.var.id):= V(j))), y, x, opts), opts)),
                 self._interleave(bodies)))),

    # NOTE: get rid of _acc
    SUMAcc := (self, o, y, x, opts) >> let(ii := Ind(),
        When(not Same(ObjId(o.child(1)), Gath),  # NOTE: come up with a general condition
            chain(
                loop(ii, Rows(o), assign(nth(y, ii), V(0))),
                List(o.children(), c -> self._acc(self(c, y, x, opts), y))),
            chain(
                self(o.child(1), y, x, opts),
                List(Drop(o.children(), 1), c -> self._acc(self(c, y, x, opts), y))))),

    ISumAcc := (self, o, y, x, opts) >> let(ii := Ind(), chain(
            loop(ii, Rows(o), assign(nth(y, ii), V(0))),
            loop(o.var, o.domain, self._acc(self(o.child(1), y, x, opts), y)))),

    ScatAcc := (self, o, y, x, opts) >> self._acc(self(Scat(o.func),y,x,opts),y),

    # _composePropagate(<ch>, <y>, <x>, <mfunc>) - <x> and <y> and temp arrays propagation through
    # identity/inplace operators and creating intermediate arrays using <mfunc> which
    # has (c) -> ... signature (<c> is child operator). There is no assumption made on what is <x>,
    # <y> and <mfunc> return so this function can be used to propagate other information same way
    # as arrays in composition.

    _composePropagate := function(ch, y, x, mfunc)
        local numch, vecs, i, j, cmd, code, isI, crossCh, apos;
        numch := Length(ch);
        vecs  := [y];

        isI   := (x) -> IsIdentitySPL(x) or x.isInplace();

        # we like to evaluate right (last) to left (first), where the values
        # in parens refer to the position of the elements in the array.
        #
        # So here, we start at the output (first entry in array) and
        # walk towards the input, creating temporary arrays as necessary

        for i in [1..numch-1] do
            vecs[i+1] := When(isI(ch[i]),
                vecs[i],
                mfunc(ch[i])
            );
            if ObjId(ch[i]) = Cross then
                apos := [ 1, 1 ]; # apos holds starting input/output indexes
                                  # as children's arity on input may be different from output arity.
                crossCh := ch[i].rChildren();
                for j in [1 .. Length(crossCh)] do
                    if isI(crossCh[j]) then
                        vecs[i+1][apos[2]] := vecs[i][apos[1]];
                    fi;
                    apos := apos + crossCh[j].arity();
                od;
            fi;
        od;

        # the last entry must be the input.
        vecs[numch+1] := x;

        # now we walk in the opposite direction, copying through
        # input as far as we can.
        # if there is a cross and one of the inputs is an identity,
        # there's no point actually doing it
        for i in Reversed([1..numch]) do
            if isI(ch[i]) then
                vecs[i] := vecs[i+1];
            elif ObjId(ch[i]) = Cross then
                apos := [ 1, 1 ]; # apos holds starting input/output indexes
                                  # as children's arity on input may be different from output arity.
                crossCh := ch[i].rChildren();
                for j in [1 .. Length(crossCh)] do
                    if isI(crossCh[j]) then
                        vecs[i][apos[1]] := vecs[i+1][apos[2]];
                    fi;
                    apos := apos + crossCh[j].arity();
                od;
            fi;
        od;

        # If all children were evaluated inplace, the output will be in x
        # Make it go from x -> y as expected
        if vecs[1] = vecs[numch+1] then vecs[1] := y; fi;
        if vecs[1] = vecs[numch+1] then vecs[numch+1] := x; fi;

        return vecs;
    end,

    Compose := meth(self, o,y,x,opts)
        local ch, numch, vecs;
        ch    := o.children();
        numch := Length(ch);

        # propagate x and y arrays and create temporary arrays
        vecs  := self._composePropagate(ch, y, x, c -> TempArray(y,x,c));

        # order them so that first to be evaluated is first in array.
        vecs := Reversed(vecs);
        ch   := Reversed(ch);

        # Wrap code in variable declaration. Each entry in vecs will contain multiple
        # arrays in the case of multi-input/output (i.e. OL)
        return decl(
            Difference(Flat(vecs{[2..Length(vecs)-1]}), Flat([x,y])),
            chain(
                List([1..numch], i ->
                     self(ch[i], vecs[i+1], vecs[i], opts)))
        );
    end,

    # ComposeDists is meant to be like a Compose, but for parallel ISums. We
    # need it for 2 reasons: a) The arrays declared are distributed among the
    # nodes, so we need them to be of size N/p, and not N. b) We can ping-pong
    # between 2 parallel buffers instead of declaring a ton of temp arrays, as
    # long as a barrier-sync exists between the stages. NOTE: This won't work
    # if we do full overlapping with micro-barriers. NOTE: This also might not
    # work too well for partial/dirty-overlap.

    ComposeDists := meth(self, o,y,x,opts)
        local ch, numch, vecs, i, cmd, code, pt1, pt2;
        ch    := o.children();
        numch := Length(ch);

        # order them so that first to be evaluated is first in array.
        ch   := Reversed(ch);

        # NOTE: We assume that the # of procs doesn't change across the ComposeDists!
        pt1 := TempArraySeq(y,x,ch[1]);
        pt2 := TempArraySeq(y,x,ch[1]);

        return(
            decl([pt1, pt2], chain(
            List([1..numch], i ->
                let(ppx := When(i mod 2 = 0, pt1, pt2),
                    ppy := When(i mod 2 = 0, pt2, pt1),
                    px  := When(i=1,     x, ppx),
                    py  := When(i=numch, y, ppy),
                    self(ch[i], py, px, opts))
                )
            ))
        );
    end,

    ComposeStreams:= meth(self, o,y,x,opts)

        # This is a compose of 2 or more multibuffered streams. The streams can
        # ping-pong data between X and Y. This is okay only if we're allowed to
        # clobber the input (reasonable to assume only when Inplace is
        # requested). Also, whether the last stage is a ping or a pong will
        # determine where the output is written to (which is not great).
        # Currently, just a hack.

        local ch, numch;
        ch    := o.children();
        numch := Length(ch);

        # order them so that first to be evaluated is first in array.
        ch   := Reversed(ch);

        return chain( List([1..numch], i ->
            let(ppy := When(i mod 2 = 0, x, y),
                ppx := When(i mod 2 = 0, y, x),
                self(ch[i], ppy, ppx, opts))
            )
        );
    end,

    Inplace := (self, o, y, x, opts) >>
        self(o.child(1), y, x, opts), # Compose will handle these somehow
#MRT: it really should do this... but it doesn't
#        self(o.child(1), x, x, opts),

    Data := meth(self, o, y, x, opts)
        local val;
        o.var.isData := true;
        val := When(IsFunction(o.value), o.value.tolist(), o.value);
        val := When(IsValue(val), val, o.var.t.value(val));
        return data(o.var, val, self(o.child(1), y, x, opts));
    end,

    #   NOTE: use pointers to do pingpong?
    ICompose := (self, o, y, x, opts) >> let(
        t      := Dat1d(x.t.t, Rows(o)),
        its    := o.domain,
        newind := Ind( Int((its-1)/2) ),
        # if orig loops has even # iterations, we peel 2 iterations, otherwise peel 1
        peel_its := Cond(IsEvenInt(o.domain), 2, 1),

        decl([t], chain(
           Cond(IsOddInt(o.domain),
                SubstVars(Copy(self(o.child(1), y, x, opts)), tab((o.var.id) := (V(its-1)))),

                chain(
                   SubstVars(Copy(self(o.child(1), t, x, opts)), tab((o.var.id) := (V(its-1)))),
                   SubstVars(Copy(self(o.child(1), y, t, opts)), tab((o.var.id) := (V(its-2)))))),

           When(o.domain <= 2, [],
               loop(newind, newind.range,
                   chain(
                       SubstVars(Copy(self(o.child(1), t, y, opts)), tab((o.var.id) := (its-1-peel_its)-2*newind)),
                       SubstVars(Copy(self(o.child(1), y, t, opts)), tab((o.var.id) := (its-2-peel_its)-2*newind)))))
       ))),

    Multiplication:= meth(self, o, y, x, opts)
        local iterator;

        iterator:=Ind();
        return loop(iterator, [ 0 .. o.element[2]-1 ],
            assign(nth(StripList(y), iterator), mul(nth(x[1], iterator),nth(x[2], iterator))));
    end,

    OLMultiplication := meth(self, o, y, x, opts)
        local iterator;
        iterator:=Ind();
        return loop(iterator, [ 0 .. o.rChildren()[2]-1 ],
            assign(nth(StripList(y), iterator),
                ApplyFunc(mul, List([1..o.rChildren()[1]], i -> nth(x[i], iterator)))));
    end,

    OLConjMultiplication := meth(self, o, y, x, opts)
        local iterator;
        iterator:=Ind();
        return loop(iterator, [ 0 .. o.rChildren()[2]-1 ],
            assign(nth(StripList(y), iterator),
                ApplyFunc(mul, [nth(x[1], iterator)] :: List([2..o.rChildren()[1]], i -> conj(nth(x[i], iterator))))));
    end,

    __RCOLMultiplication := (self, o, y, x, conj) >> let(
        i  := Ind(),
        n  := o.rChildren()[2],
        m  := o.rChildren()[1],
        yy := StripList(y),
        re := List([1..m], e -> var.fresh_t("re", yy.t.t)),
        im := List([1..m], e -> var.fresh_t("re", yy.t.t)),
        loop(i, n, decl( re :: im, chain(
            assign( re[1], nth(x[1], 2*i) ),
            assign( im[1], nth(x[1], 2*i+1) ),
            chain( List( [2..m], j ->
                chain(
                    assign(re[j], re[j-1] * nth(x[j],2*i) - conj * im[j-1] * nth(x[j],2*i+1)),
                    assign(im[j], im[j-1] * nth(x[j],2*i) + conj * re[j-1] * nth(x[j],2*i+1))
                ))),
            assign(nth(yy,2*i),   re[m]),
            assign(nth(yy,2*i+1), im[m]))))),

    RCOLMultiplication     := (self, o, y, x, conj) >> self.__RCOLMultiplication(o, y, x, 1),
    RCOLConjMultiplication := (self, o, y, x, conj) >> self.__RCOLMultiplication(o, y, x, -1),

    OLDup := (self, o, y, x, opts) >> let(
        i := Ind(o.params[2]),
        loop(i, i.range, chain(
            List( Flat([y]), yy -> assign(nth(yy, i), nth(x, i)))
        ))
    ),

    SMAP := (self, o, y, x, opts) >> assign(nth(y, 0), o.at(List([1..Cols(o)], i -> nth(x, i-1)))),

    ParSeqWrap := (self, o, y, x, opts) >> let( yy := Flat([y]), xx := Flat([x]),
        self( o.p.child(o.ci),
            StripList(yy :: o.p.filtSUMR(o.y)),
            StripList(xx :: o.p.filtSUMR(o.x)),
            opts)),

    ParSeq := (self, o, y, x, opts) >> let( ch := o.children(), yy := Flat([y]), xx := Flat([x]),
        self( Compose(List([1..Length(ch)], i -> ParSeqWrap(o, i, yy, xx))),
            StripList(o.filtCompR(yy)), StripList(o.filtCompL(xx)), opts)),

    IParSeq := (self, o, y, x, opts) >> let(
            its := Ind(o.domain),
            xs  := o.filtSUML(Flat([x])),
            ys  := o.filtSUMR(Flat([y])),
            xc  := o.filtCompL(Flat([x])),
            yc  := o.filtCompR(Flat([y])),

            rc  := o.filtCompR(Flat([Rows(o)])),
            tc  := List(Zip2(yc, rc), a -> Dat1d(a[1].t.t, a[2])),

            src := List( xc, e -> var.fresh_t("pX", TPtr(e.t.t))),
            dst := List( yc, e -> var.fresh_t("pY", TPtr(e.t.t))),

            ptr := (p) -> When(IsPtrT(p.t), p, nth(p, 0).toPtr(p.t.t)),

            decl(src :: dst :: tc, chain(chain(
                List( TransposedMat([src, xc]),
                    e -> assign(e[1], ptr(e[2])) ) ::
                List( TransposedMat([dst, tc, yc]),
                    e -> assign(e[1], cond( eq(imod(o.domain, 2), 0), ptr(e[2]), ptr(e[3]))) )),
                loopn( its, its.range, chain(
                    self( SubstVars(Copy(o.child(1)), tab((o.var.id) := its)), StripList(dst :: ys), StripList(src :: xs), opts ),
                    chain(
                        List( TransposedMat([src, dst]),
                            e -> assign(e[1], e[2])) ::
                        List( TransposedMat([dst, yc, tc]),
                            e -> assign(e[1], cond( eq(imod(add(o.domain, its), 2), 0), ptr(e[2]), ptr(e[3]) )))
                )))
            ))
    ),

    Cvt := (self, o, y, x, opts) >> o.params[1].code(y, x, opts),
));

StackAllocsToPtrs := function(icode, x, size, vars)
    local arrayvars, replvars, offset, i;

    # extract all temp array definitions
    arrayvars := Flat(List(
        Collect(icode, @(1, decl, e -> ForAny(e.vars, i -> ObjId(i.t) = TArray))),
        f -> Filtered(f.vars, g -> ObjId(g.t) = TArray)
    ));

    # build a set of replacement pointers for these variables
    replvars := List(arrayvars, e -> var.fresh_t("T", TPtr(e.t.t)));

    offset := 2*size;

    for i in [1..Length(arrayvars)] do

        # remove declaration of array
        icode := SubstTopDown(icode, @(1, decl, e -> arrayvars[i] in e.vars), ee -> ee.cmd);

        # change variables from TArray -> TPtr
        icode := SubstTopDown(icode, arrayvars[i], e -> replvars[i]);

        # prepend declaration and setup of ptr
        icode := decl(replvars[i], chain(
            assign(replvars[i], add(x, offset)),
            icode
        ));

        offset := offset + size;
    od;

    Append(vars, arrayvars);

    return icode;
end;

Class(SingleAllocCodegen, DefaultCodegen, rec(
    Formula := meth(self, o, y, x, opts)
        local icode, datas, prog, params, sub, initsub, io, vars, size;

        o := SumsUnification(o.child(1), opts);

        [x, y] := self.initXY(x, y, opts);

        size := Maximum(o.dimensions);
        params := Set(Collect(o, param));

        datas := Collect(o, FDataOfs);
        o := BlockSums(opts.globalUnrolling, o);
        icode := self(o, y, x, opts);
        icode := RemoveAssignAcc(icode);
        icode := BlockUnroll(icode, opts);

        # replace all stack allocated arrays with offsets into input array
        vars := [];
        icode := StackAllocsToPtrs(icode, x, size, vars);

        # icode := PowerOpt(icode);
        icode := DeclareHidden(icode);
        if IsBound(opts.isFixedPoint) and opts.isFixedPoint then
            icode := FixedPointCode(icode, opts.bits, opts.fracbits);
        fi;

        io := When(x=y, [x], [y, x]);
        sub := Cond(IsBound(opts.subName), opts.subName, "transform");
        initsub := Cond(IsBound(opts.subName), Concat("init_", opts.subName), "init");
        icode := func(TVoid, sub, Concatenation(io, params), icode);

        if IsBound(opts.generateInitFunc) and opts.generateInitFunc then
            prog := program(
                decl(List(datas, x->x.var),
                    chain(
                        func(TVoid, initsub, params, chain(List(datas, x -> SReduce(x.var.init)))),
                        icode
                    )));
        else
            prog := program( func(TVoid, initsub, params, chain()), icode);
        fi;
        prog.dimensions := o.dimensions;
        return prog;
    end,
));

Class(RecCodegenMixin, rec(
    RecursStep := (self, o, y, x, opts) >> let(
        name := spiral.libgen.CodeletName(spiral.libgen.CodeletShape(o.child(1))),
        ApplyFunc(call, Concatenation(
                [ var(name), y + o.yofs, x + o.xofs ],
                spiral.libgen.CodeletParams(o.child(1))))),

    RecursStepCall := (self, o, y, x, opts) >>
        ApplyFunc(call, Concatenation(Flat([var(o.func), y, x,]), List(o.bindings, x->x[2]))),

    Codelet := meth(self, o, y, x, opts)
        local code;
        [x, y] := self.initXY(x, y, opts);
        o := o.child(1);
        ## Generating code : main body
        o := BlockSums(opts.libgen.basesUnrolling, o);
        code := SReduce(self(o, y, x, opts), opts);
        code := BlockUnroll(RemoveAssignAcc(code), opts);
        code := DeclareHidden(code);
        return code;
    end,
));


# Copyright (c) 2018-2021, Carnegie Mellon University
# See LICENSE for details

Declare(ICompose, ISum, fTensor, fBase, fId, fCompose);
Declare(RC);

# ===========================================================================
# SPL Sums Notation
# ===========================================================================
# Blk - block
# Blk1 - 1x1 block
# Data(var, value, expr) - data
# ISum - iterative sum
# SUM - sum
#
# Gath(N,n,func) - gather matrix
# Scat(N,n,func) - scatter matrix
# Prm(N, read_func, write_func) - read: output->input, write: input->output
# Conj, ConjL, ConjR, ConjLR - generalized conjugation (arbitrary/no matrix to left and right)
# ConjDiag to conjugate block diagonals
# ===========================================================================

Class(SumsBase, rec(
    isSums := true,
    area := self >> Sum(self.children(), x->x.area()),
    sums := meth(self)
       local children, i, res;
       res := Copy(self);
       children := Map(res.rChildren(), c -> Cond(IsSPL(c), c.sums(), c));
       for i in [1..Length(children)] do
           res.rSetChild(i, children[i]);
       od;
       return res;
    end
));

Compose.area   := self >> Sum(self.children(), x->x.area());
Diag.area      := self >> Rows(self);
Scale.area     := self >> 0;
IsSumsSPL := o -> IsRec(o) and
                ((IsBound(o.isSums) and o.isSums) or
                 (Same(ObjId(o), Compose) and ForAll(o.children(), IsSumsSPL)));

# ==========================================================================
# TCast(<n>, <to_type>, <from_type>) - type conversion on <n> elements
# ==========================================================================
Class(TCast, SumsBase, Sym, rec(
    abbrevs := [
        (n, to_type)            -> Checked(IsPosInt0Sym(n), IsType(to_type), [n, to_type, TUnknown]),
        (n, to_type, from_type) -> Checked(IsPosInt0Sym(n), IsType(to_type), [n, to_type, from_type])
    ],
    def := (n, to_type, from_type) -> Perm((), n),
    dmn := self >> [ TArray(self.params[3], self.params[1]) ],
    rng := self >> [ TArray(self.params[2], self.params[1]) ],

    transpose := self >> self,
    conjTranspose := self >> self,
    inverse := self >> self,
    isSymmetric := True,
    isPermutation := False,
));

# ==========================================================================
# BB(<spl>) - basic block container, serves as barrier for rule application
# ==========================================================================
Class(BB, SumsBase, BaseContainer, rec(isBlock:=true,
    rng := meth(self) return self._children[1].rng(); end,
    dmn := meth(self) return self._children[1].dmn(); end,
));

Class(Buf, SumsBase, BaseContainer, rec(
    rng := meth(self) return self._children[1].rng(); end,
    dmn := meth(self) return self._children[1].dmn(); end
));

Declare(PushL, PushR, PushLR, NoPullLeft, NoPullRight);

# Forces propagation into *left* construct (e.g. ISum, RecursStep, BB, etc)
# Also .sums() conversion always returns same object, ie conversion of
# child is not forced. This is done to avoid Sigma-SPLizing constructs
# that we want to pull in .
Class(PushL, Buf, rec(
    sums := self >> self,
    transpose := self >> PushR(self.child(1).transpose()),
    normalizedArithCost := self >> self.child(1).normalizedArithCost(),
));

# Forces propagation into *right* construct (e.g. ISum, RecursStep, BB, etc)
# Also .sums() conversion always returns same object, ie conversion of
# child is not forced. This is done to avoid Sigma-SPLizing constructs
# that we want to pull in .
Class(PushR, Buf, rec(
    sums := self >> self,
    transpose := self >> PushL(self.child(1).transpose()),
    normalizedArithCost := self >> self.child(1).normalizedArithCost(),
));

# Allows propagation into either right or left construct (e.g. ISum, RecursStep, BB, etc)
# Also .sums() conversion always returns same object, ie conversion of
# child is not forced. This is done to avoid Sigma-SPLizing constructs
# that we want to pull in .
Class(PushLR, Buf, rec(sums := self >> self));

# Prevents propagation into constructs on both sides (e.g. ISum, RecursStep, BB, etc)
Class(NoPull, Buf);

# Prevents pull-in. Used for distributed stuff.
Class(NoPull_Dist, Buf);

# Prevents propagation pulling in of diags
Declare(NoDiagPullinRight);
Class(NoDiagPullin, Buf);
Class(NoDiagPullinLeft, Buf, rec(
    transpose := self >> NoDiagPullinRight(self.child(1).transpose())
));
Class(NoDiagPullinRight, Buf, rec(
    transpose := self >> NoDiagPullinLeft(self.child(1).transpose())
));

# Prevents propagation into left construct (e.g. ISum, RecursStep, BB, etc)
Class(NoPullLeft, Buf, rec(transpose := self >> NoPullRight(self.child(1).transpose())));

# Prevents propagation into right construct (e.g. ISum, RecursStep, BB, etc)
Class(NoPullRight, Buf, rec(transpose := self >> NoPullLeft(self.child(1).transpose())));

# Top level wrapper for Sigma-SPL formulas
Class(Formula, BaseContainer, SumsBase);

#F RecursStep(<expr>)
#F RecursStep(<yofs>, <xofs>, <expr>) - with implicit
#F     fAdd on Gath (xofs) and Scat (yofs) side
#F
Class(RecursStep, SumsBase, BaseContainer, rec(
    abbrevs := [ ch -> [0,0,ch],
                 (yofs,xofs,ch) -> [yofs, xofs, ch] ],
    new := (self, yofs, xofs, ch) >> SPL(WithBases(self,
    rec(yofs:=yofs, xofs:=xofs, _children := [ch], dimensions := ch.dims()))),
    rChildren := self >> [self.yofs, self.xofs, self._children[1]],
    rSetChild := meth(self, n, what)
        if n=1 then self.yofs := what;
        elif n=2 then self.xofs := what;
        elif n=3 then self._children[1] := what;
        else Error("<n> must be in [1..3]");
        fi;
    end,
));

Class(Inplace, SumsBase, BaseContainer, rec(
  rng:=self>>self._children[1].rng(),
  dmn:=self>>self._children[1].dmn(),
  numops:=self >>0, # YSV: what is this? pls remove or document
  toNonInplace := self >> self._children[1],
  isInplace := self >> true,
  normalizedArithCost := self >> self._children[1].normalizedArithCost(),
));

Class(LStep, SumsBase, BaseContainer, rec(
    toAMat := self >> AMatMat(Sum([I(Rows(self)), self.child(1)], MatSPL))
));

Declare(RTWrap); # to avoid complaints in .transpose

Class(RTWrap, SumsBase, BaseContainer, rec(
    new := (self, rt) >> Checked(Global.formgen.IsRuleTree(rt),
    SPL(WithBases(self, rec(
        rt   := rt,
        root := rt.node))).setDims()),

    area := self >> Rows(self) * Cols(self),
    children := self >> [self.rt],
    child := (self, n) >> When(n=1, self.rt, Error("<n> must be 1")),
    setChild := rSetChildFields("rt"),
    rSetChild := ~.setChild,
    rChildren := ~.children,

    dims          := self >> self.rt.dims(),
    isPermutation := self >> self.rt.node.isPermutation(),
    isReal        := self >> self.rt.node.isReal(),
    toAMat        := self >> self.rt.node.toAMat(),

    transpose     := self >> RTWrap(self.rt.transpose()),
    conjTranspose := self >> InertConjTranspose(self),
    isInertConjTranspose := True,
));

# ==========================================================================
# COND(<spl>) - This is a 'switch' statement for SPLs
# ==========================================================================
Class(COND, SumsBase, BaseContainer, rec(
    abbrevs := [ arg -> let(f:=Flat(arg), [f[1], Drop(f, 1)]) ],
    new := (self, cond, spls) >> SPL(WithBases(self, rec(
        _children := spls,
        dimensions := spls[1].dimensions,
        cond := cond))),

    toAMat := self >> When(self.cond.ev()=V(true) or self.cond.ev()=1,
        self.child(1).toAMat(),
        self.child(2).toAMat()),

    rChildren := self >> Concatenation([self.cond], self._children),
    rSetChild := meth(self, n, newC)
        if n = 1 then self.cond := newC;
        else self.setChild(n-1, newC);
        fi;
    end,

    area := self >> Maximum(List(self.children(), x->x.area())),

    sums := self >> CopyFields(self, rec(_children := List(self._children, x->x.sums()))),
    transpose := self >> CopyFields(self, rec(_children := List(self._children, x->x.transpose()))),
));

Class(RC, SumsBase, BaseContainer, rec(
    dims := self >> let(d:=self.child(1).dims(), [2*d[1], 2*d[2]]),
    isReal := self >> true,

    # when RC(M) is transposed with M - complex, not only M is transposed, but also
    # each complex element of M as a 2x2 matrix is transposed == complex conjugation
    transpose := self >> CopyFields(self, rec(_children := [self.child(1).conjTranspose()],
        dimensions := [self.dimensions[2], self.dimensions[1]])),

    # RC(.) is real, conjTranspose is just a regular transpose
    conjTranspose := self >> self.transpose(),
    inverse := self >> CopyFields(self, rec(_children := [self.child(1).inverse()],
        dimensions := [self.dimensions[2], self.dimensions[1]])),

    sums := self >> CopyFields(self, rec(_children := [self.child(1).sums()])),
    area := self >> 2*self.child(1).area(),
    toAMat := self >> AMatMat(RealMatComplexMat(MatSPL(self.child(1)))),
    createCode := self >> Cond(IsBound(self.child(1).createCode), RC(self.child(1).createCode()), self),

    # assume that normalizedArithCost() always returns cost in real ops
    normalizedArithCost := self >> self.child(1).normalizedArithCost(),

));

# This takes a real matrix that can be seen as RC(A) and returns A as complex matrix
Class(CR, SumsBase, BaseContainer, rec(
    dims := self >> List(self.child(1).dimensions, e -> _unwrap(div(e,2))),

    # the derived matrix is real, but over the complex field
    isReal := self >> false,

    transpose := self >> CopyFields(self, rec(
	_children := [self.child(1).transpose()],
        dimensions := [self.dimensions[2], self.dimensions[1]])),

    # CR(.) is complex, but all entries are real, conjTranspose is just a regular transpose
    conjTranspose := self >> self.transpose(),

    inverse := self >> CopyFields(self, rec(_children := [self.child(1).inverse()],
        dimensions := [self.dimensions[2], self.dimensions[1]])),

    sums := self >> CopyFields(self, rec(_children := [self.child(1).sums()])),

    area := self >> 1/2*self.child(1).area(),

    toAMat := self >> let(mat := MatSPL(self.child(1)),
        rmat := List(mat{2*[1..Length(mat)/2]}, m -> m{2*[1..Length(m)/2]}),
        AMatMat(rmat)
        ),

    # assume that normalizedArithCost() always returns cost in real ops
    normalizedArithCost := self >> self.child(1).normalizedArithCost(),
    vcost := self >> self.child(1).vcost()

));

# ==========================================================================
# Blk(<mat>) - matrix block
# ==========================================================================
# Note: Blk should not check for M being a matrix,
#   otherwise cant reuse Blk for vector code
Class(Blk, SumsBase, Mat, rec(
    new := (self, M) >> SPL(WithBases(self, rec(
            element := M,
            TType   := Cond( # NOTE: add checks to M
                            IsList(M),     UnifyTypes(List(Flat(M), InferType)),
                            IsValue(M),    M.t.t,
			    IsSymbolic(M), M.t.t),
			))).setDims(),
    area := self >> Length(Filtered(Flat(self.element), k -> k<>0)),
    new  := (self, M) >> SPL(WithBases(self, rec(element := M))).setDims(),
    dims := self >> Dimensions(self.element)
));

# ==========================================================================
# Blk1(<val>) - 1x1 block
# ==========================================================================
Class(Blk1, SumsBase, BaseMat, rec(
    # Compare mathematically Blks disregarding differences in way to express code-level elements.
#    new := (self, val) >> SPL(WithBases(self, rec(dimensions:=[1,1], element:=val))),
#    toAMat := self >> AMatMat([[EvalScalar(Eval(self.element))]]),
    new := (self, val) >> SPL(WithBases(self, rec(dimensions:=[1,1], element:=EvalScalar(Eval(val))))),
    toAMat := self >> AMatMat([[self.element]]),
    transpose := self >> self,
    conjTranspose := self >> CopyFields(self, rec(element := Global.Conjugate(self.element))),
    inverse := self >> CopyFields(self, rec(element := 1 / self.element)),
    area := self >> 1,
    dims := self >> [1,1],
));

# ==========================================================================
# BlkConj() - pseudo 1x1 matrix when multiplied w/ complex number conjugates it
# ==========================================================================
Class(BlkConj, SumsBase, BaseMat, rec(
    rChildren := self >> [],
    rSetChild := (self, n, what) >> Error("no children"),
    new := (self) >> SPL(WithBases(self, rec(dimensions:=[1,1]))),
    toAMat := self >> AMatMat([[1]]),
    transpose := self >> self,
    conjTranspose := self >> self,
    inverse := self >> self,
    area := self >> 1
));

# ==========================================================================
# Data(<var>, <value>, <spl>) - introduces a data constant bound in <spl>
# ==========================================================================
Class(Data, SumsBase, BaseContainer, rec(
    new := (self, var, value, spl) >> Checked(IsVar(var), IsSPL(spl),
	SPL(WithBases(self, rec(
	    var := var, value := value, _children := [spl],
            dimensions := spl.dims())))),
    #-----------------------------------------------------------------------
    area := self >> self.child(1).area(),
    #-----------------------------------------------------------------------
    eval := meth(self)
        local d, c;
        if IsBound(self._evaluated) then return self._evaluated;
        else
            d := Cond(
                IsValue(self.value) or IsSymbolic(self.value), self.value,
                IsBound(self.value.tolist), V(self.value.tolist()),
                IsSPL(self.value), V(MatSPL(self.value)),
                self.value);
            c := Copy(self._children[1]);
            self._evaluated := SubstBottomUp(c, @(1, var, e->Same(e,self.var)), e -> d);
            return self._evaluated;
        fi;
    end,

    rChildren := self >> [self.var, self.value, self.child(1)],
    rSetChild := meth(self, n, newC)
        if n=1 then self.var := newC;
        elif n=2 then self.value := newC;
        elif n=3 then self._children[1] := newC;
        else Error("<n> must be between [1..3]");
        fi;
    end,
    from_rChildren := (self, rch) >> CopyFields(self, rec(
        var := rch[1], value := rch[2], _children := [rch[3]])),
    #-----------------------------------------------------------------------
    uneval := meth(self) Unbind(self._evaluated); return self; end,
    #-----------------------------------------------------------------------
    transpose := self >> CopyFields(self, rec(
        _children := [self.child(1).transpose()])).uneval(),
    #-----------------------------------------------------------------------
    toAMat := self >> self.eval().toAMat(),
    #-----------------------------------------------------------------------
    sums := self >> CopyFields(self, rec(_children := [self.child(1).sums()])),
));

Declare(Scat);

#F ==========================================================================
#F Gath(<func>) - gather (read) matrix
#F NOTE: implements affine transformations via funcExp hack.

#F as of Dec '2010, <func> can contain fInsert/fPad, which will create
#F funcExp(..) in the code. This is used for simulating affine (rather
#F than linear) transformation.
#F
#F Affine transformations can only be experessed using matrices, if we
#F use homogeneous coordinates, i.e., instead of [x_1, ..., x_n] use
#F always [x_1, ..., x_n, 1], for input/output vectors
#F
#F Gath.toAMat and everything else does NOT use homogeneous
#F coordinates, and thus we can't represent affine "gathers" with a
#F proper matrix, the only special case is when funcExp(0) is used to
#F insert 0s (this preserves linearity).
#F
#F Currently, we use the following semantics (to implement affine transf. using a hack)
#F
#F  nth(X, i)          == X[i]
#F  nth(X, funcExp(i)) == i
#F
#F The proper way of doing this would be instead (using h. coords, X[len(x)] = 1)
#F nth(X, funcExp(i)) -> i * nth(X, len(X)) = i * X[len(X)] = i
#F
#F See http://en.wikipedia.org/wiki/Transformation_matrix#Affine_transformations
# ==========================================================================
Class(Gath, SumsBase, BaseMat, rec(
    #-----------------------------------------------------------------------
    rChildren := self >> [self.func],
    rSetChild := rSetChildFields("func"),
    #-----------------------------------------------------------------------
    new := (self, func) >> SPL(WithBases(self, rec(
      	func := Checked(IsFunction(func) or IsFuncExp(func), func)))).setDims(),
    #-----------------------------------------------------------------------
    dims := self >> [self.func.domain(), self.func.range()],
    sums := self >> self,
    area := self >> Sum(Flat([self.func.domain()])),
    isReal := self >> true,
    transpose := self >> Scat(self.func),
    conjTranspose := self >> self.transpose(),
    inverse := self >> self.transpose(),
    #-----------------------------------------------------------------------
    toAMat := self >> let(
	n := EvalScalar(self.func.domain()),
        N := EvalScalar(self.func.range()),
        func := self.func.lambda(),
        AMatMat(List([0..n-1], row -> let(
            idx := EvalScalar(func.at(row)),
	    Cond(idx _is funcExp,
		     When(idx.args[1]=0, Replicate(N, 0),
			 Error("<self> is an affine (non-linear) transformation ",
			       "and can't be represented as a matrix")),
		 BasisVec(N, idx)))))
    ),
    #-----------------------------------------------------------------------
    toloop := (self, bksize) >> let(
	i := Ind(self.func.domain()),
	ISum(i,
            Scat(fTensor(fBase(i), fId(1))) *
            Gath(fCompose(self.func, fTensor(fBase(i), fId(1))))
	).split(bksize)
    ),
    #-----------------------------------------------------------------------
    normalizedArithCost := self >> 0,
    #-----------------------------------------------------------------------
    isIdentity := self >> IsIdentity(func),
));

#F ==========================================================================
#F Prm(<func>) - permutation, semantically same as Gath(<func>), but square
#F
#F NB: Prm should not be used with fPad/fInsert, which lead to affine
#F     transformations when used with Gath.
#F
#F Prm(f).transpose() = Prm(f.transpose())
#F
#F ==========================================================================
Class(Prm, Gath, rec(
    transpose := self >> CopyFields(self, rec(func:=self.func.transpose())),
    toAMat := self >> Perm(PermList(List(self.func.lambda().tolist(), e->e.v)+1),
                           self.func.domain()).toAMat(),
    #-----------------------------------------------------------------------
    normalizedArithCost := self >> 0
));

#   special perm to be gotten rid of in rewriting
Class(DelayedPrm, Prm);
Class(FormatPrm, Prm);

#F ==========================================================================
#F Scat(<func>) - scatter (write) matrix,  Scat(f) = Gath(f).transpose()
#F
#F NOTE: implements affine transformations via funcExp workaround.
#F        See Doc(Gath) for explanation
#F ==========================================================================
Class(Scat, SumsBase, BaseMat, rec(
    #-----------------------------------------------------------------------
    rChildren := self >> [self.func],
    rSetChild := rSetChildFields("func"),
    #-----------------------------------------------------------------------
    new := (self, func) >> SPL(WithBases(self, rec(
	func := Checked(IsFunction(func) or IsFuncExp(func), func)))).setDims(),
    #-----------------------------------------------------------------------
    dims := self >> [self.func.range(), self.func.domain()],
    sums := self >> self,
    area := self >> Sum(Flat([self.func.domain()])),
    isReal := self >> true,
    transpose := self >> Gath(self.func),
    conjTranspose := self >> self.transpose(),
    inverse := self >> self.transpose(),
    #-----------------------------------------------------------------------
    toAMat := self >> TransposedAMat(Gath(self.func).toAMat()),
    #-----------------------------------------------------------------------
    toloop := (self, bksize) >> Gath(self.func).toloop(bksize).transpose(),
    #-----------------------------------------------------------------------
    normalizedArithCost := self >> 0,
    #-----------------------------------------------------------------------
    isIdentity := self >> IsIdentity(self.func),
));

Class(ScatAcc, Scat, rec(
    codeletName:="SA",
    toloop := (self, bksize) >> Error("Not implemented")
));

Declare(ScatGath);

# ==========================================================================
# ScatGath(<sfunc>, <gfunc>)
# ==========================================================================
Class(ScatGath, SumsBase, BaseMat, rec(
    rChildren := self >> [self.sfunc, self.gfunc],
    rSetChild := rSetChildFields("sfunc", "gfunc"),
    #-----------------------------------------------------------------------
    new := (self, sfunc, gfunc) >> SPL(WithBases(self,
        rec(dimensions := [sfunc.range(), gfunc.range()],
	    sfunc := Checked(IsFunction(sfunc) or IsFuncExp(sfunc), sfunc),
	    gfunc := Checked(IsFunction(gfunc) or IsFuncExp(gfunc), gfunc)))),
    #-----------------------------------------------------------------------
    dims := self >> [self.sfunc.range(), self.gfunc.range()],
    area := self >> self.sfunc.domain(),
    isReal := self >> true,
    transpose := self >> ScatGath(self.gfunc, self.sfunc),
    conjTranspose := self >> self.transpose(),
    inverse := self >> self.transpose(),
    #-----------------------------------------------------------------------
    toAMat := meth(self)
        local s, gfunc, g, sdomain, gdomain, idx;
        # NOTE: FF: this is a temporary solution to work around Lamda
	#        problems with symbolic domains and variable substitution
        s := Scat(self.sfunc);
        sdomain := EvalScalar(self.sfunc.domain());
        gdomain := spiral.code.RulesStrengthReduce(self.gfunc.domain());
        if ObjId(self.gfunc) = Lambda and IsExp(gdomain) then
            idx := Ind(sdomain);
            gfunc := Lambda(idx, self.gfunc.at(idx)).setRange(self.gfunc.range());
        else
            gfunc := self.gfunc;
        fi;
        g :=Gath (gfunc);
        return s.toAMat() * g.toAMat();
    end,
       # Correct semantics: Scat(self.sfunc).toAMat() * Gath(self.gfunc).toAMat(),
    #-----------------------------------------------------------------------
    sums := self >> self, #self.toloop(self.maxBkSize()),
    #-----------------------------------------------------------------------
    maxBkSize := meth(self)
        local exp, l, d;
        exp := self.gfunc.domain();

        if IsValue(exp) or IsInt(exp) then return exp; fi;

        if IsExp(exp) and ObjId(exp)=mul and IsValue(exp.args[1]) then
            return exp.args[1];
        fi;

        if IsInt(exp.eval()) or IsValue(exp.eval()) then return EvalScalar(exp); fi;
        l := Lambda(Filtered(exp.free(), IsLoopIndex), exp);
        if Length(l.vars) > 1 then return 1; fi;
        d := List(spiral.sigma.GenerateData(l).tolist(), EvalScalar);
        return Gcd(d);
    end,
    #-----------------------------------------------------------------------
    toloop := (self, bksize) >> let(
	i := Ind(self.gfunc.domain()),
        ISum(i,
            Scat(fCompose(self.sfunc, fTensor(fBase(i), fId(1)))) *
            Gath(fCompose(self.gfunc, fTensor(fBase(i), fId(1))))
        ).split(bksize)
    )
));

# ==========================================================================
# SUM(<spl1>, <spl2>, ...) - non-overlapping matrix sum
# ==========================================================================
Declare(SUM, SUMAcc);
Class(SUM, SumsBase, BaseOperation, rec(
    area := self >> Sum(self.children(), x->x.area()),
    abbrevs := [ arg ->
    [ Flat(List(Flat(arg),
        s -> When(IsSPL(s) and Same(ObjId(s), SUM), s.children(), s))) ] ],
    #-----------------------------------------------------------------------
    new := meth(self, L)
        local dims;
        Constraint(Length(L) >= 1); Constraint(ForAll(L, IsSPL));
        if Length(L) = 1 then return L[1]; fi;
        dims := L[1].dims();
        if not (IsSymbolic(dims[1]) or IsSymbolic(dims[2])) and
           not ForAll(Drop(L, 1), x -> let(d:=x.dims(),
                   IsSymbolic(d[1]) or IsSymbolic(d[2]) or d = dims))
            then Error("Dimensions of summands do not match"); fi;
        return SPL(WithBases(self, rec( _children := L, dimensions := dims)));
    end,
    #-----------------------------------------------------------------------
    rng := self >> self.child(1).rng(),
    #-----------------------------------------------------------------------
    dmn := self >> self.child(1).dmn(),

    advdims := self >> self._children[1].advdims(),
    #-----------------------------------------------------------------------
#    dims := self >> self.child(1).dimensions,
    #-----------------------------------------------------------------------
    toAMat := self >> AMatMat(Sum(self._children, MatSPL)),
    #-----------------------------------------------------------------------
    isPermutation := self >> false,
    #-----------------------------------------------------------------------
    transpose := self >>   # we use CopyFields to copy all fields of self
        CopyFields(self, rec(
           _children := List(self._children, x->x.transpose()),
           dimensions := Reversed(self.dimensions))),
    inverse := self >>   # we use CopyFields to copy all fields of self
        CopyFields(self, rec(
           _children := List(self._children, x->x.inverse()),
           dimensions := Reversed(self.dimensions))),
    conjTranspose := self >>   # we use CopyFields to copy all fields of self
        CopyFields(self, rec(
           _children := List(self._children, x->x.conjTranspose()),
           dimensions := Reversed(self.dimensions)))
));

# ==========================================================================
# SUMAcc(<spl1>, <spl2>, ...) - overlapping matrix sum
# ==========================================================================
Class(SUMAcc, SUM, rec(
   abbrevs := [ arg ->
    [ Flat(List(Flat(arg),
        s -> When(IsSPL(s) and Same(ObjId(s), SUMAcc), s.children(), s))) ] ]
 ));

# ==========================================================================
# ISum(<var>, <domain>, <spl>) - non-overlapping iterative matrix sum
# ==========================================================================
Class(ISum, SumsBase, BaseIterative, rec(
    needInterleavedLeft := self >> self.child(1).needInterleavedLeft(),
    needInterleavedRight := self >> self.child(1).needInterleavedRight(),
    cannotChangeDataFormat := self >> self.child(1).cannotChangeDataFormat(),
    totallyCannotChangeDataFormat := self >> self.child(1).totallyCannotChangeDataFormat(),

    directOper := SUM,
    area := self >> let(ac:=self._children[1].area(), ac * self.domain),
    #-----------------------------------------------------------------------
    rng := self >> self._children[1].rng(),
    dmn := self >> self._children[1].dmn(),
    dims := self >> [StripList(List(self.rng(),l->l.size)),StripList(List(self.dmn(),l->l.size))],

    advdims := self >> self._children[1].advdims(),

    #-----------------------------------------------------------------------
    transpose := self >> CopyFields(self, rec(
           _children := [self._children[1].transpose()],
           dimensions := Reversed(self.dimensions))),
    conjTranspose := self >> CopyFields(self, rec(
           _children := [self._children[1].conjTranspose()],
           dimensions := Reversed(self.dimensions))),
    inverse := self >> CopyFields(self, rec(
           _children := [self._children[1].inverse()],
           dimensions := Reversed(self.dimensions))),
    #-----------------------------------------------------------------------
    unroll := self >> SUM(self.unrolledChildren()),
    #-----------------------------------------------------------------------
    sums := self >> CopyFields(self, rec(
        _children := [self._children[1].sums()]))
));

Class(ISumLS, ISum);
Class(JamISum, ISum, rec(isBlockTransitive := true));
Class(Grp, Buf);

# ==========================================================================
# ICompose(<var>, <domain>, <spl>) - iterative matrix product
# ==========================================================================
Class(ICompose, SumsBase, BaseIterative, rec(
    area := self >> self._children[1].area() * self.domain,
    #-----------------------------------------------------------------------
    dims := self >> self._children[1].dimensions,
    #-----------------------------------------------------------------------
    unroll := self >> Compose(self.unrolledChildren()),
    #-----------------------------------------------------------------------
    transpose := self >> ICompose(self.var, self.domain,
        SubstVars(Copy(self._children[1].transpose()),
	          tab((self.var.id) := self.domain-1-self.var))),

    createCode := self >> Cond(IsBound(self._children[1].createCode),
        ICompose(self.var, self.domain, self._children[1].createCode()), self),

    prods := self >> let(base := self.__bases__[1],
        base(self.var, self.domain, self._children[1].prods())),

#    rChildren := self >> [self.var, self.domain, self._children[1]],
#
#    from_rChildren := (self, rch) >> ApplyFunc(ObjId(self), rch),
#
#    rSetChild := meth(self, n, newChild)
#        if n=1 then self.var := newChild;
#        elif n=2 then self.domain := newChild;
#        elif n=3 then self._children := [newChild];
#        else Error("<n> must be in [1..3]");
#        fi;
#    end
));

# ==========================================================================
# ISumAcc(<var>, <domain>, <spl>) - overlapping iterative matrix sum
# ==========================================================================
Class(ISumAcc, ISum);

# ==========================================================================
# IParSeq(<var>, <domain>, <fb>, <spl>)
# ==========================================================================
Declare(ParSeq);
Class(IParSeq, SumsBase, BaseIterative, rec(
    abbrevs := [ (v, fb_cnt, expr) -> [v, v.range, fb_cnt, expr]  ],
    new := meth(self, v, domain, fb_cnt, expr)
        local obj;
        #NOTE: check dimensions
        obj := Inherited(v, domain, expr);
        obj.fb_cnt := fb_cnt;
        return obj;
    end,

    dims := self >> self._children[1].dims(),
    unroll := self >> ParSeq( self.fb_cnt, Reversed(self.unrolledChildren())),

    filtCompL := (self, lst) >> lst{[1..self.fb_cnt]},
    filtCompR := (self, lst) >> lst{[1..self.fb_cnt]},
    filtSUML  := (self, lst) >> lst{[self.fb_cnt+1..Length(lst)]},
    filtSUMR  := (self, lst) >> lst{[self.fb_cnt+1..Length(lst)]},

    # area doesn take into account that we have composition and sum
    area := self >> self._children[1].area() * self.domain,

    print := (self, i, is) >> Print(
        self.name, "(", self.var, ", ", self.domain, ", ", self.fb_cnt, ",\n",
        Blanks(i+is), self._children[1].print(i+is, is), "\n",
        Blanks(i), ")", self.printA(),
        When(IsBound(self._setDims), Print(".overrideDims(", self._setDims, ")"), Print(""))
    ),
));

##############################################################################
Declare(Conj, ConjL, ConjR, ConjLR, ConjDiag);

Class(Conj, SumsBase, BaseOperation, rec(
    new    := (self, spl) >> SPL(WithBases(self, rec(_children:=[spl], dimensions := spl.dimensions))),
    dims   := self >> self._children[1].dims(),
    toAMat := self >> self.child(1).toAMat(),
    sums   := self >> self,
    transpose := self >> Conj(self.child(1).transpose())
));

Class(ConjL, Conj, rec(
    new    := (self, spl, lprm) >> SPL(WithBases(self, rec(_children:=[spl, lprm]))).setDims(),
    dims   := self >> [self.child(2).dims()[1], self.child(1).dims()[2]],
    toAMat := self >> self.child(2).toAMat() * self.child(1).toAMat(),
    sums   := self >> ConjL(self.child(1).sums(), self.child(2)),
    transpose := self >> ConjR(self.child(1).transpose(), self.child(2).transpose()),
));

Class(ConjR, Conj, rec(
    new    := (self, spl, rprm) >> SPL(WithBases(self, rec(_children:=[spl, rprm]))).setDims(),
    dims   := self >> [self._children[1].dims()[1], self._children[2].dims()[2]],
    toAMat := self >> self.child(1).toAMat() * self.child(2).toAMat(),
    sums   := self >> ConjR(self.child(1).sums(), self.child(2)),
    transpose := self >> ConjL(self.child(1).transpose(), self.child(2).transpose()),
));

Class(ConjLR, Conj, rec(
    new    := (self, spl, lprm, rprm) >> SPL(WithBases(self, rec(_children:=[spl, lprm, rprm]))).setDims(),
    dims   := self >> [ self.child(2).dims()[1], self.child(3).dims()[2] ],
    toAMat := self >> self.child(2).toAMat() * self.child(1).toAMat() * self.child(3).toAMat(),
    sums   := self >> ConjLR(self.child(1).sums(), self.child(2), self.child(3)),
    transpose := self >> ConjLR(self.child(1).transpose(), self.child(3).transpose(), self.child(2).transpose()),
));

Class(ConjDiag, Conj, rec(
    new    := (self, spl, lprm, rprm) >> SPL(WithBases(self, rec(_children:=[spl, lprm, rprm]))).setDims(),
    dims   := self >> [Rows(self.child(2)), Cols(self.child(3))],
    toAMat := self >> self.child(2).toAMat() * self.child(1).toAMat() * self.child(3).toAMat(),
    sums   := self >> ConjDiag(self.child(1).sums(), self.child(2), self.child(3)),
    transpose := self >> ConjDiag(self.child(1).transpose(), self.child(3).transpose(), self.child(2).transpose()),
));

Class(NeedInterleavedComplex, BaseContainer, rec(
    needInterleavedLeft := True,
    needInterleavedRight := True,
    sums := self >> self,
    area:= self >> self.child(1).area()
));