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

# NOTE: Support odd sizes ie with (I_4 dirsum J_3)
condIJ := (N, m) -> let(k := N/m, Checked(IsSymbolic(k) or IsEvenInt(k),
    fCond(diagTensor(II(k/2), II(2,0,1), II(m)), fTensor(fId(k), fId(m)),
                                                 fTensor(fId(k), J(m)))
));

# NOTE: Support odd sizes ie with (I_4 dirsum OS_3)
condIOS := (N, m) -> let(k := N/m, Checked(IsSymbolic(k) or IsEvenInt(k),
    fCond(diagTensor(II(k/2), II(2,0,1), II(m)), fTensor(fId(k), fId(m)),
                                                 fTensor(fId(k), OS(m, -1)))
));

condM := (N, m) -> L(N, m) * Prm(condIJ(N, m));
condK := (N, m) -> Prm(condIJ(N, N/m)) * L(N, m);

condMp := (N, m) -> L(N, m) * Prm(condIOS(N, m));
condKp := (N, m) -> Prm(condIOS(N, N/m)) * L(N, m);

condB := (N, m) -> Checked(IsEvenInt(m), let(k := N/m,
    condK(N, m) * Tensor(I(N/m), condM(m,m/2))));

condBp := (N, m) -> Checked(IsEvenInt(m), let(k := N/m,
    condKp(N, m) * DirectSum(L(m,m/2), Tensor(I(k-1), condM(m,m/2)))));

condBpu := (N, m) -> Checked(IsEvenInt(m), let(k := N/m,
    condKp(N, m) * DirectSum(I(m), Tensor(I(k-1), condM(m,m/2)))));

# condBpu(k*m,m) == Scat(Refl0_u(k,m)) = Refl0_u(k,m).transpose()
# u = stands for URDFT. i.e. this is for URDFT based algorithms
# NOTE: not valid for odd <k> (but will need new ASPF rule anyway with an extra child)
# GOOD:  valid for odd <m>.
Class(Refl0_u, PermClass, rec(
    def := (k, m) -> rec(),
    domain := self >> self.params[1] * self.params[2],
    range := self >> self.params[1] * self.params[2],
    lambda := self >> let(k := self.params[1], m := self.params[2], i := Ind(k*m),
        Lambda(i, cond(leq(i, m-1), k*i,
                                    cond(leq(imod(i, m), idiv(m-1,2)), 2*k*imod(i,m) + idiv(i,m),
                                                                      2*k*m - 2*k*imod(i,m) - idiv(i,m)))))
));

# Valid for odd <k>, even <m>
# <omit> is how many initial points we want to omit from the permutation
# the only sensible values for it are 0 and 1
Class(Refl0_odd, PermClass, rec(
    def := (k, m, omit) -> rec(), #size := k*m+idiv(m+1,2)-omit),
    range := self >> self.domain(),
    domain := self >> let(k:=self.params[1], m:=self.params[2], omit:=self.params[3],
        k*m + idiv(m+1, 2) - omit),

    lambda := self >> let(
        k := self.params[1],   m := self.params[2], mc := idiv(m+1, 2),
        N := self.domain(), omit := self.params[3], mf := idiv(m, 2),
        N2:= 2*k*m+m,         ii := Ind(N-omit),    i  := ii+omit,

        Lambda(ii, cond(leq(i, mc-1), (2*k+1)*i,
                                     cond(leq(imod(i-mc, m), mc-1),       (2*k+1)*imod(i-mc,m) + idiv(i+mf,m),
                                                                    N2  - (2*k+1)*imod(i-mc,m) - idiv(i+mf,m)))
            -omit))
));

# condB(k*m,m) == Scat(Refl1(k,m)) = Refl1(k,m).transpose()
# NOTE: not valid for odd <k> (but will need new ASPF rule anyway with an extra child)
# GOOD:  valid for odd <m>.
Class(Refl1, PermClass, rec(
    def := (k, m) -> rec(), #size := k*m),
    domain := self >> self.params[1]*self.params[2],
    range := self >> self.params[1]*self.params[2],
    lambda := self >> let(k := self.params[1], m := self.params[2], i := Ind(k*m),
        Lambda(i, cond(leq(imod(i, m), idiv(m-1,2)), 2*k*imod(i,m) + idiv(i,m),
                                       2*k*m - 1 - 2*k*imod(i,m) - idiv(i,m))))
));

Declare(KK, MM, KKp, MMp);

Class(KK, PermClass, rec(
    def := (k, m) -> rec(), #size := k*m),
    domain := self >> self.params[1]*self.params[2],
    range := self >> self.params[1]*self.params[2],
    transpose := self >> MM(self.params[2], self.params[1]),
    lambda := self >> let(k := self.params[1], m := self.params[2],
        fCompose(Tr(k,m), condIJ(k*m, k)).lambda()),
));

Class(MM, PermClass, rec(
    def := (k, m) -> rec(), #size := k*m),
    domain := self >> self.params[1]*self.params[2],
    range := self >> self.params[1]*self.params[2],
    transpose := self >> KK(self.params[2], self.params[1]),
    lambda := self >> let(k := self.params[1], m := self.params[2],
        fCompose(condIJ(k*m, m), Tr(k, m)).lambda()),
));

Class(KKp, PermClass, rec(
    def := (k, m) -> rec(), #size := k*m),
    domain := self >> self.params[1]*self.params[2],
    range := self >> self.params[1]*self.params[2],
    transpose := self >> MMp(self.params[2], self.params[1]),
    lambda := self >> let(k := self.params[1], m := self.params[2],
        fCompose(Tr(k,m), condIOS(k*m, k)).lambda()),
));

Class(MMp, PermClass, rec(
    def := (k, m) -> rec(), #size := k*m),
    domain := self >> self.params[1]*self.params[2],
    range := self >> self.params[1]*self.params[2],
    transpose := self >> KKp(self.params[2], self.params[1]),
    lambda := self >> let(k := self.params[1], m := self.params[2],
        fCompose(condIOS(k*m, m), Tr(k, m)).lambda()),
));

Declare(IP);

libB := (k, m) -> Checked(IsSymbolic(m) or IsEvenInt(m),
    KK(k, m) * Tensor(I(k), MM(2,m/2)));

libBp := (k, m) -> Checked(IsSymbolic(m) or IsEvenInt(m),
    KKp(k, m) * DirectSum(Tr(2,m/2), Tensor(I(k-1), MM(2,m/2))));

libBpu := (k, m) -> Checked(IsSymbolic(m) or IsEvenInt(m),
    KKp(k, m) * DirectSum(I(m), Tensor(I(k-1), MM(2,m/2))));

#F IJ(<size>, <n>) - I_n dirsum J_n dirsum I_n dirsum ... (repeats N/n times, n|N)
#F
#
# Subst(i -> BinXor(i, BinAnd(i,$n) - BinShr(BinAnd(i,$n), $(LogInt(n,2))))),
# Class(IJ, PermClass, rec(
#   def := (N, n) -> Checked(IsInt(N), IsInt(n), N mod n = 0,
#       rec(
#       size := N,
#       direct  := Subst(x -> x + (QuoInt(x,$n) mod 2) * ($(n-1) - 2*(x mod $n))),
#       inverse := Subst(x -> x + (QuoInt(x,$n) mod 2) * ($(n-1) - 2*(x mod $n)))
#       )),
#   transpose := self >> self
#);
IJ := (N, n) -> IP(N, J(n));

#F IP(<N>, <splperm>) - I_n dirsum P_n dirsum I_n dirsum ... (repeats N/n times, n|N)
#F
#F    P_n given by <splperm>, which is a permutation function
#F
Class(IP, PermClass, rec(
    def := (N, splperm) -> Checked(IsInt(N), IsSPL(splperm), N mod Rows(splperm) = 0, rec()),
    range := self >> self.params[1],
    domain := self >> self.params[1],

    lambda := self >> let(
	N := self.params[1],
	P := self.params[2],
	Pl := P.lambda(),
	n := P.domain(),
	i := Ind(N),
	Lambda(i, n*idiv(i,n) + imod(1+idiv(i,n), 2)*imod(i, n) +
                                imod(  idiv(i,n), 2)*Pl.at(imod(i,n)))
    ),

    transpose := self >> IP(self.params[1], self.params[2].transpose())
));

# -----------------------------------------------------------------------------#
#F LIJ(<N>) - L(N, N/2) * (I(N/2) dirsum J(N/2))
##   works also for odd sizes
Class(LIJ, PermClass, rec(
    def := N -> Checked(IsInt(N), rec()),
    range := self >> self.params[1],
    domain := self >> self.params[1],
    lambda := self >> let(
	N := self.params[1],
	N1 := When(IsOddInt(N), N, N-1),
	i := Ind(N),
	When(not self.transposed,
            Lambda(i, QuoInt(N,2) + idiv(N-i, 2)*cond(neq(imod(N1-i,2),0), -1, 1)),
            Lambda(i, 2*i + idiv(i, QuoInt(N+1,2)) * (2*N - 1 - 4*i))
        ))
));

# -- Originals
# K ( <n>, <m> )
#   returns the equivalent of the stride permutation for Cooley-Tukey FFT
#   for the DCT3 and 4. <m> has to divide <n>
#     (I_n/m dirsum J_n/m dirsum I_n/m ....) * L^n_m  (direct sum alternately)
#   This permutation is to be used if it occurs on the left side in the rule.
#   It is K(n, m)^-1 =  M(n, n/m).
K := (mn, m) -> Checked(IsPosInt(mn), IsPosInt(m), mn mod m = 0,
    IJ(mn, mn/m) * L(mn, m));

M := (mn, m) -> Checked(IsPosInt(mn), IsPosInt(m), mn mod m = 0,
    L(mn, m) * IJ(mn, m));
# -------------

########################
# Generating functions
########################

L_or_OS := (n, str) -> When((n mod str) <> 0, OS(n, str), L(n, str));

Kp := (NN, m) -> let(n := NN / m,
    Prm(IP(NN, OS(n, -1)))* L(NN, m));

Mp := (NN, m) -> Kp(NN, NN/m).transpose();

# Type 0 symmetries  (ce0, co0) aka whole-point
# BH0(<Nreal>, <n>, <base>, <stride>), Nreal = number of output real pts = Rows(PRDFT(.))
# BH0 := (Nreal,n,base,stride) -> BH(Int(Nreal/2)+1, Nreal, n, base, stride);

# Type 1 symmetries  (ce1, co1) aka half-point
# BH1(<Nreal>, <n>, <base>, <stride>), Nreal = number of output real pts = Rows(PRDFT3(.))
# BH1 := (Nreal,n,base,stride) -> BH(Int((Nreal+1)/2), Nreal-1, n, base, stride);

# BH(<N>, <reflect>, <n>, <b>, <s>) - Reflective stride function
#   Defined as  i -> When(i < ceil(n/2)-1, si + b, reflect - si - b)
#
Class(BH, FuncClass, rec(
    def := (N,reflect,n,base,stride) -> rec(), #N := N, n := n),
    domain := self >> self.params[3],
    range  := self >> self.params[1],
    lambda := self >> let(
	N:=self.params[1], reflect:=self.params[2], n:=self.params[3], b:=self.params[4],
	s:=self.params[5], i:=Ind(n),
	Lambda(i, cond(leq(i, idiv(n-1, 2)), s*i + b, reflect - s*i - b)))
));

Declare(Refl);
# Refl(<N>, <reflect>, <n>, <func>) - Reflective arbitrary function
#   Defined as  i -> When(func(i) < N, func(i), reflect - func(i))
#   with domain of <n> points (usually smaller than domain(func))
#
Class(Refl, FuncClass, rec(
    def := (N,reflect,n,func) -> rec(), #N := N, n := n),
    domain := self >> self.params[3],
    range  := self >> self.params[1],
    transpose := self >> Refl(self.params[1], self.params[2], self.params[3], self.params[4].transpose()),
    lambda := self >> let(
	func:=self.params[4].lambda(), reflect:=self.params[2], N:=self.params[1],
	i := Ind(self.params[3]),
	Lambda(i, cond(leq(func.at(i), N-1), func.at(i), reflect-func.at(i))))
));


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

Declare(ScatH, VScat_L);

Class(GathH, SumsBase, BaseMat, rec(
    rChildren := self >> [self.N, self.n, self.base, self.stride],
    rSetChild := rSetChildFields("N", "n", "base", "stride"),
    new := (self, N, n, base, stride) >>
	SPL(WithBases(self, rec(
      	N:= N, n := n, base := base, stride := stride, dimensions := [n, N]))),
    dims := self >> self.dimensions,
    sums := self >> self,
    area := self >> self.n,
    isReal := self >> true,
    transpose := self >> ScatH(self.N, self.n, self.base, self.stride),
    conjTranspose := self >> self.transpose(),
    inverse := self >> self.transpose(),
    toAMat := self >> let(
	   n := self.n,
       N := self.N,
       base := self.base,
       stride := self.stride,
       AMatMat(List([0..n-1], row -> let(
         idx := base+row,
		 BasisVec(N, idx))))
    ),
    isIdentity := self >> (self.n=self.N) and self.base=0 and self.stride = 1,
));

Class(ScatH, SumsBase, BaseMat, rec(
    rChildren := self >> [self.N, self.n, self.base, self.stride],
    rSetChild := rSetChildFields("N", "n", "base", "stride"),
    new := (self, N, n, base, stride) >>
SPL(WithBases(self, rec(
      	N:= N, n := n, base := base, stride := stride, dimensions := [N, n]))),
    dims := self >> self.dimensions,
    sums := self >> self,
    area := self >> self.n,
    isReal := self >> true,
    transpose := self >> GathH(self.N, self.n, self.base, self.stride),
    conjTranspose := self >> self.transpose(),
    inverse := self >> self.transpose(),
    toAMat := self >> let(
	   n := self.n,
       N := self.N,
       base := self.base,
       stride := self.stride,
       TransposedAMat(AMatMat(List([0..n-1], row -> let(
         idx := base+row*stride,
		 BasisVec(N, idx))))
    )),
    isIdentity := self >> (self.n=self.N) and self.base=0 and self.stride = 1,
));

Class(VGath_L, SumsBase, BaseMat, rec(
    rChildren := self >> [self.N, self.n, self.base],
    rSetChild := rSetChildFields("N", "n", "base"),
    new := (self, N, n, base) >>
	SPL(WithBases(self, rec(
      	N:= N, n := n, base := base, dimensions := [2, N]))),
    dims := self >> self.dimensions,
    sums := self >> self,
    area := self >> self.n,
    isReal := self >> true,
    transpose := self >> VScat_L(self.N, self.n, self.base),
    conjTranspose := self >> self.transpose(),
    inverse := self >> self.transpose(),
    toAMat := self >> let(
	   n := self.n,
       N := self.N,
       base := self.base,
       AMatSPL(L(2*n, 2) * Tensor(SPLAMat(AMatMat(List([0..n-1], row -> let(
         idx := base+row,
		 BasisVec(N, idx))))), I(2)))
    ),
    isIdentity := False
));

Class(VScat_L, SumsBase, BaseMat, rec(
    rChildren := self >> [self.N, self.n, self.base],
    rSetChild := rSetChildFields("N", "n", "base"),
    new := (self, N, n, base) >>
SPL(WithBases(self, rec(
      	N:= N, n := n, base := base, dimensions := [N, 2]))),
    dims := self >> self.dimensions,
    sums := self >> self,
    area := self >> self.n,
    isReal := self >> true,
    transpose := self >> VGath_L(self.N, self.n, self.base),
    conjTranspose := self >> self.transpose(),
    inverse := self >> self.transpose(),
    toAMat := self >> TransposedAMat(self.transpose().toAMat()),
    isIdentity := False
));


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

# NOTE: The first section is Intel SSE/2/3 specific, and has to be ported
# over to SPU

unpacklo := (l1,l2,n,k) -> Flat(List([1..n/(2*k)], i-> [
        List([1..k], j->l1[(i-1)*k+j]),
        List([1..k], j->l2[(i-1)*k+j])
]));

unpackhi := (l1,l2,n,k) -> Flat(List([1..n/(2*k)], i -> [
        List([1..k], j->l1[n/2+(i-1)*k+j]),
        List([1..k], j->l2[n/2+(i-1)*k+j])
]));

sparams := (l,n) -> List([1..l], i->[1..n]);

shuffle := (in1, in2, p, n, k) -> Flat([
    List([1..n/(2*k)],     i->List([1..k], j->in1[(p[i]-1)*k+j])),
    List([n/(2*k)+1..n/k], i->List([1..k], j->in2[(p[i]-1)*k+j]))
]);

iperm4 := self >> Filtered(Cartesian(self.params()), i->i[1]<>i[2] and i[3] <> i[4]);


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

# ==========================================================================
# RowDirectSum
# ==========================================================================
Class(RowDirectSum, BaseOverlap, rec(
    #-----------------------------------------------------------------------
    abbrevs := [
        function(arg)
          arg:=Flat(arg);
          return [arg[1], arg{[2..Length(arg)]}];
        end ],
    #-----------------------------------------------------------------------
    new := meth(self, overlap, spls)
       return self._new(0, overlap, spls);
    end,
    #-----------------------------------------------------------------------
    _dims := meth(self)
        local ovdim;
    ovdim := self.ovDim(self.overlap,
                        List(self._children, t -> t.dimensions[2]));
    return [ Sum(self._children, t -> t.dimensions[1]),
             ovdim[3] - ovdim[2] + 1 # max - min + 1
    ];
    end,
    dims := self >> When(IsBound(self._setDims), self._setDims, let(d := Try(self._dims()),
	    When(d[1], d[2], [errExp(TInt), errExp(TInt)]))),
    #-----------------------------------------------------------------------
    toAMat := meth(self)
        return
      DirectSumAMat(List(self._children, AMatSPL)) *
      AMatSPL(
          Sparse(
          self._rowoverlap(
              List(self._children, t -> t.dimensions[2]),
              self.overlap
          )
          )
      );
    end,
    #-----------------------------------------------------------------------
    arithmeticCost := meth(self, costMul, costAddMul)
        return Sum(List(self.children(), x -> x.arithmeticCost(costMul, costAddMul)));
    end
));


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

NewRulesFor(TConjEven, rec(
    TConjEven_vec := rec(
        switch := true,
        applicable := (self, t) >> IsEvenInt(t.params[1]) and t.isTag(1, AVecReg) and
	    t.params[1] mod (2*t.getTag(1).v) = 0,

        apply := (self, t, C, Nonterms) >> let(N := t.params[1], v := t.getTag(1).v, rot := t.params[2],
            m := Mat([[1,0],[0,-1]]),
#            d1 := Diag(diagDirsum(fConst(TReal, 2, 1.0), fConst(TReal, N-2, 1/2))),
            d1 := VDiag(diagDirsum(fConst(TReal, 1, 1.0), fConst(TReal, v-1, 0.5), fConst(TReal, 1, 1.0), fConst(TReal, N-v-1, 0.5)), v),
            #m1 := DirectSum(m, SUM(I(N-2), Tensor(J((N-2)/2), m))),
            #dv1 := Diag(diagDirsum(fConst(TReal, v, 1.0), fConst(TReal, 1,-1.0), fConst(TReal, N-v-1, 1.0))),
            dv1 := DirectSum(VTensor(I(1), v),
		             VBlk([[ [-1.0] :: Replicate(v-1, 1.0) ]], v),
			     VTensor(I(N/v-2), v)),
            P1 := VPrm_x_I(L(N/v, N/(2*v)), v),
            Q1 := VPrm_x_I(L(N/v, 2), v),
            jv1 := P1*DirectSum(VO1dsJ(N/2, v), VScale(VO1dsJ(N/2, v), -1, v))*Q1,
            mv1 := _VSUM([dv1,jv1], v),
            #m2 := DirectSum(J(2), SUM(I(N-2), Tensor(J((N-2)/2), -m))),
            e0 := [0.0] :: Replicate(v-1, 1.0),
            e1 := [1.0] :: Replicate(v-1, 0.0),
            blk := VBlk([[e0,e1],[e1,e0]], v).setSymmetric(),
            dv2 := DirectSum(blk, VTensor(I(N/v-2), v)),
            jv2 := P1 * DirectSum(VScale(VO1dsJ(N/2, v), -1, v), VO1dsJ(N/2, v))*Q1,
            mv2 := _VSUM([dv2,jv2], v),
            #
            i := VTensor(I(N/v), v),
            d2 := VRCLR(Diag(fPrecompute(diagDirsum(fConst(TReal, 1, 1.0), diagMul(fConst(TComplex, N/2-1, omegapi(-1/2)), fCompose(dOmega(N, rot), fAdd(N/2, N/2-1, 1)))))), v),
            #
            #d1 *
	    VBlkInt(d1 * _VHStack([mv1, mv2], v) * VStack(i, d2), v))
    ),

    TConjEven_vec_tr := rec(
        switch := true,
        applicable := (self, t) >> IsEvenInt(t.params[1]) and t.isTag(1, AVecReg) and t.params[1] mod (2*t.getTag(AVecReg).v)=0,
	transposed := true,

        apply := (self, t, C, Nonterms) >> let(N := t.params[1], v := t.getTag(1).v, rot := t.params[2],
            m := Mat([[1,0],[0,-1]]),
#            d1 := Diag(diagDirsum(fConst(TReal, 2, 1.0), fConst(TReal, N-2, 1/2))),
            d1 := VDiag(diagDirsum(fConst(TReal, 1, 1.0), fConst(TReal, v-1, 0.5), fConst(TReal, 1, 1.0), fConst(TReal, N-v-1, 0.5)), v),
            #m1 := DirectSum(m, SUM(I(N-2), Tensor(J((N-2)/2), m))),
            #dv1 := Diag(diagDirsum(fConst(TReal, v, 1.0), fConst(TReal, 1,-1.0), fConst(TReal, N-v-1, 1.0))),
            dv1 := DirectSum(VTensor(I(1), v),
		             VBlk([[ [-1.0] :: Replicate(v-1, 1.0) ]], v),
			     VTensor(I(N/v-2), v)),
            P1 := VPrm_x_I(L(N/v, N/(2*v)), v),
            Q1 := VPrm_x_I(L(N/v, 2), v),
            jv1 := P1*DirectSum(VO1dsJ(N/2, v), VScale(VO1dsJ(N/2, v), -1, v))*Q1,
            mv1 := _VSUM([dv1,jv1], v),
            #m2 := DirectSum(J(2), SUM(I(N-2), Tensor(J((N-2)/2), -m))),
            e0 := [0.0] :: Replicate(v-1, 1.0),
            e1 := [1.0] :: Replicate(v-1, 0.0),
            blk := VBlk([[e0,e1],[e1,e0]], v).setSymmetric(),
            dv2 := DirectSum(blk, VTensor(I(N/v-2), v)),
            jv2 := P1 * DirectSum(VScale(VO1dsJ(N/2, v), -1, v), VO1dsJ(N/2, v))*Q1,
            mv2 := _VSUM([dv2,jv2], v),
            #
            i := VTensor(I(N/v), v),
            d2 := VRCLR(Diag(fPrecompute(diagDirsum(fConst(TReal, 1, 1.0), diagMul(fConst(TComplex, N/2-1, omegapi(-1/2)), fCompose(dOmega(N, rot), fAdd(N/2, N/2-1, 1)))))), v),
            #
            #d1 *
	    VBlkInt(_VHStack([i, d2.transpose()], v) * VStack(mv1, mv2) * d1, v))
    )
));

NewRulesFor(TXMatDHT, rec(
    TXMatDHT_vec := rec(
        switch := true,
        applicable := (self, t) >> IsEvenInt(t.params[1]) and t.isTag(1, AVecReg),
        apply := (self, t, C, Nonterms) >> let(N := t.params[1], v := t.getTag(1).v,

            f0 := Replicate(v, 1.0),
            f1 := [0.0] :: Replicate(v-1, 1.0),
            f2 := [1.0] :: Replicate(v-1, -1.0),
            fblk := VBlk([[f0,f1],[f1,f2]], v).setSymmetric(),
            fdiag := VTensor(Tensor(I(N/(2*v)-1), F(2)), v),
            ff := DirectSum(fblk, fdiag),

            ###
            m := Mat([[1,0],[0,-1]]),
            d1 := Diag(diagDirsum(fConst(TReal, 2, 1.0), fConst(TReal, N-2, 1/2))),
            #m1 := DirectSum(m, SUM(I(N-2), Tensor(J((N-2)/2), m))),
            #dv1 := Diag(diagDirsum(fConst(TReal, v,1), fConst(TReal, 1,-1), fConst(TReal, N-v-1, 1))),
            dv1 := DirectSum(VTensor(I(1), v),
		             VBlk([[ [-1.0] :: Replicate(v-1, 1.0) ]], v),
			     VTensor(I(N/v-2), v)),
            P1 := VPrm_x_I(L(N/v, N/(2*v)), v),
            Q1 := VPrm_x_I(L(N/v, 2), v),
            jv1 := P1*DirectSum(VO1dsJ(N/2, v), VScale(VO1dsJ(N/2, v), -1, v))*Q1,
            mv1 := _VSUM([dv1,jv1], v),
            #m2 := DirectSum(J(2), SUM(I(N-2), Tensor(J((N-2)/2), -m))),
            e0 := [0.0] :: Replicate(v-1, 1.0),
            e1 := [1.0] :: Replicate(v-1, 0.0),
            blk := VBlk([[e0,e1],[e1,e0]], v).setSymmetric(),
            dv2 := DirectSum(blk, VTensor(I(N/v-2), v)),
            jv2 := P1*DirectSum(VScale(VO1dsJ(N/2, v), -1, v), VO1dsJ(N/2, v))*Q1,
            mv2 := _VSUM([dv2,jv2], v),
            #
            i := VTensor(I(N/v), v),
            d2 := VRCLR(Diag(fPrecompute(diagDirsum(fConst(TReal, 1, 1.0), diagMul(fConst(TComplex, N/2-1, Complex(0,-1)), fCompose(dOmega(N, 1), fAdd(N/2, N/2-1, 1)))))), v),
            #
            d1 * VBlkInt(ff * _VHStack([mv1, mv2], v) * VStack(i, d2), v)
    ))
));


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

Class(PrunedPRDFT, TaggedNonTerminal, rec(
    abbrevs := [
        (n,blk,pat) -> Checked(IsPosIntSym(n), IsPosIntSym(blk), IsList(pat),
            AnySyms(n,blk) or (IsInt(_unwrap(n)/_unwrap(blk)) and ForAll(pat, i->IsInt(i) and i < n/blk)),
            [_unwrap(n), 1, _unwrap(blk), pat]),
        (n,k,blk,pat) -> Checked(IsPosIntSym(n), IsIntSym(k), IsPosIntSym(blk), IsList(pat),
            AnySyms(n,k) or Gcd(_unwrap(n),_unwrap(k))=1,
            AnySyms(n,blk) or (IsInt(_unwrap(n)/_unwrap(blk)) and ForAll(pat, i->IsInt(i) and i < n/blk)),
            [_unwrap(n), When(AnySyms(n,k), k, k mod _unwrap(n)), _unwrap(blk), pat])
        ],

    dims := self >> let(
        size := let(n:=self.params[1], 2*(Int(n/2)+1)),
        d := [size, self.params[3]*Length(self.params[4])],
        When(self.transposed, [d[2], d[1]], d)
    ),

    terminate := self >> let(
        size := self.params[1], blk := self.params[3], pat := self.params[4],
        res := Compose(PRDFT(size, self.params[2]),
               Tensor(Mat(List(pat, i->BasisVec(size/blk, i))).transpose(), I(blk))).terminate(),
        When(self.transposed, res.transpose(), res)
    ),

    isReal    := True,
    normalizedArithCost := self >> let(n := self.params[1], IntDouble(2.5 * n * d_log(n) / d_log(2))),
    TType := TReal
));

_pruned_PRF12_CT_Children := (N,k,PRFt,DFTt,PRFtp,PRF1) -> Map2(DivisorPairs(N),
    (m,n) -> When(IsEvenInt(n),
	[ PRFt(m,k), DFTt(m,k), PRFtp(m,k) ] :: PRF1(m,n,k),
	[ PRFt(m,k), DFTt(m,k) ] :: PRF1(m,n,k))
);

_pruned_PRF12_CT_Stage1 := (N,k,C,Conj,Tw) ->
    let(m:=Cols(C[1]),
        n:=N/m,
        Nf:=Int(N/2),
        nf:=Int(n/2),
        nc:=Int((n+1)/2),
        mf:=Int(m/2),
        mc:=Int((m+1)/2),
        j:=Ind(nc-1),
        SUM(
            RC(Scat(H(Nf+1,mf+1,0,n))) * C[1] * Gath(H(2*(nf+1)*m, m, 0, 2*(nf+1))),
            When(nc=1, [],
                ISum(j,
                    RC(Scat(BH(Nf+1,N,m,j+1,n))) *
                    Conj * RC(C[2]) * Tw(j) *
                    RC(Gath(H((nf+1)*m, m, j+1, nf+1)))
                )
            ),
            When(IsOddInt(n), [],
                RC(Scat(H(Nf+1,mc,nf,n))) * C[3] * Gath(H(2*(nf+1)*m, m, 2*nf, 2*(nf+1)))
            )
        )
    );

_pruned_IPRF12_CT_Stage2 := (N,k,C,Conj,Tw) ->
    let(m:=Rows(C[1]),
        n:=N/m,
        Nf:=Int(N/2),
        nf:=Int(n/2),
        nc:=Int((n+1)/2),
        mf:=Int(m/2),
        mc:=Int((m+1)/2),
        j:=Ind(nc-1),
        SUM(
            Scat(H(2*(nf+1)*m, m, 0, 2*(nf+1))) * C[1] * RC(Gath(H(Nf+1,mf+1,0,n))),
        	When(nc=1, [],
                ISum(j,
                    RC(Scat(H((nf+1)*m, m, j+1, nf+1))) *
                    Tw(j) * RC(C[2]) * Conj *
    	            RC(Gath(BH(Nf+1,N,m,j+1,n)))
                )
            ),
        	When(IsOddInt(n), [],
    	       Scat(H(2*(nf+1)*m, m, 2*nf, 2*(nf+1))) * C[3] * RC(Gath(H(Nf+1,mc,nf,n))))
        )
    );

NewRulesFor(PrunedPRDFT, rec(
    PrunedPRDFT_base := rec(
       forTransposition := false,
       maxSize := 256,
       applicable := (self, nt) >> not nt.hasTags() and nt.params[1] <= self.maxSize and nt.params[3] = 1,
       children := nt -> [[PRDFT(nt.params[1], nt.params[2])]],
       apply := (nt, C, cnt) -> let(size := nt.params[1], blk := nt.params[3], pat := nt.params[4],
            C[1] * Tensor(Mat(List(pat, i->BasisVec(size/blk, i))).transpose(), I(blk)).terminate())
    ),
    PrunedPRDFT_DFT := rec(
       forTransposition := false,
       applicable := (self, nt) >> nt.params[1] = nt.params[3] and nt.params[4] = [0],
       children := nt -> [[ PRDFT(nt.params[1], nt.params[2]).withTags(nt.getTags()) ]],
       apply := (nt, C, cnt) -> C[1]
    ),
    PrunedPRDFT_CT_rec_block := rec(
       forTransposition := true,
       fPrecompute := f->fPrecompute(f),
       applicable := (self, nt) >> nt.params[1] > 2
            and not nt.hasTags()
            and not IsPrime(nt.params[1])
            and nt.params[3] = 1
            and Last(nt.params[4])+1-nt.params[4][1] = Length(Set(nt.params[4]))
            and ForAny(Map2(DivisorPairs(nt.params[1]), (m,n)->_ctpr_applicable(m, n, nt.params[4])), i->i),
       children := (nt) -> Filtered(_pruned_PRF12_CT_Children(nt.params[1],nt.params[2],PRDFT1, DFT1, PRDFT3,
                                        (m, n, k)->_pruned_children(m, n, nt.params[4], (r, sp) -> PrunedPRDFT(r, k mod n, nt.params[3], sp))),
                              lst -> ForAll(Filtered(lst, i->ObjId(i) = PrunedPRDFT), j->j.params[4] <> [])),
       apply := (self, nt, C, cnt) >> let(N:=nt.params[1], k:=nt.params[2], m:=Cols(C[1]), n := N/m,
            _pruned_PRF12_CT_Stage1(N, k, C, Diag(BHD(m,1,-1)), j->RC(Diag(self.fPrecompute(Twid(N,m,k,0,0,j+1))))) *
            _build_stack(m, n, nt.params[4], let(s1len := When(IsEvenInt(n), 3, 2), C{[s1len+1..Length(C)]}), nt.dims()[2], Gath, IterVStack, VStack, (a,b)->a*b))
    )
));

Class(PrunedIPRDFT, TaggedNonTerminal, rec(
    abbrevs := [
        (n,blk,pat) -> Checked(IsPosIntSym(n), IsPosIntSym(blk), IsList(pat),
            AnySyms(n,blk) or (IsInt(_unwrap(n)/_unwrap(blk)) and ForAll(pat, i->IsInt(i) and i < n/blk)),
            [_unwrap(n), 1, _unwrap(blk), pat]),
        (n,k,blk,pat) -> Checked(IsPosIntSym(n), IsIntSym(k), IsPosIntSym(blk), IsList(pat),
            AnySyms(n,k) or Gcd(_unwrap(n),_unwrap(k))=1,
            AnySyms(n,blk) or (IsInt(_unwrap(n)/_unwrap(blk)) and ForAll(pat, i->IsInt(i) and i < n/blk)),
            [_unwrap(n), When(AnySyms(n,k), k, k mod _unwrap(n)), _unwrap(blk), pat])
        ],

    dims := self >> let(
        size := let(n:=self.params[1], 2*(Int(n/2)+1)),
        d := [self.params[3]*Length(self.params[4]), size],
        When(self.transposed, [d[2], d[1]], d)
    ),

    terminate := self >> let(
        size := self.params[1], blk := self.params[3], pat := self.params[4],
        res := Compose(Tensor(Mat(List(pat, i->BasisVec(size/blk, i))), I(blk)),
                       IPRDFT(size, self.params[2])).terminate(),
        When(self.transposed, res.transpose(), res)
    ),

    isReal    := True,
    normalizedArithCost := self >> let(n := self.params[1], IntDouble(2.5 * n * d_log(n) / d_log(2))),
    TType := TReal
));

NewRulesFor(PrunedIPRDFT, rec(
    PrunedIPRDFT_base := rec(
       forTransposition := false,
       maxSize := 256,
       applicable := (self, nt) >> not nt.hasTags() and nt.params[1] <= self.maxSize and nt.params[3] = 1,
       children := nt -> [[IPRDFT(nt.params[1], nt.params[2])]],
       apply := (nt, C, cnt) -> let(size := nt.params[1], blk := nt.params[3], pat := nt.params[4],
            Tensor(Mat(List(pat, i->BasisVec(size/blk, i))), I(blk)).terminate() * C[1] )
    ),
    PrunedIPRDFT_DFT := rec(
       forTransposition := false,
       applicable := (self, nt) >> nt.params[1] = nt.params[3] and nt.params[4] = [0],
       children := nt -> [[ IPRDFT(nt.params[1], nt.params[2]).withTags(nt.getTags()) ]],
       apply := (nt, C, cnt) -> C[1]
    ),
    PrunedIPRDFT_CT_rec_block := rec(
       forTransposition := true,
       applicable := (self, nt) >> nt.params[1] > 2
            and not nt.hasTags()
            and not IsPrime(nt.params[1])
            and nt.params[3] = 1
            and Last(nt.params[4])+1-nt.params[4][1] = Length(Set(nt.params[4]))
            and ForAny(Map2(DivisorPairs(nt.params[1]), (m,n)->_ctpr_applicable(m, n, nt.params[4])), i->i),
       children := (nt) -> Filtered(_pruned_PRF12_CT_Children(nt.params[1],nt.params[2], IPRDFT1, DFT1, IPRDFT2,
                                        (m, n, k)->_pruned_children(m, n, nt.params[4], (r, sp) -> PrunedIPRDFT(r, k mod n, nt.params[3], sp))),
                              lst -> ForAll(Filtered(lst, i->ObjId(i) = PrunedIPRDFT), j->j.params[4] <> [])),
       apply := (nt, C, cnt) -> let(N:=nt.params[1], k:=nt.params[2], m:=Rows(C[1]), n := N/m,
            _build_stack(m, n, nt.params[4], let(s1len := When(IsEvenInt(n), 3, 2), C{[s1len+1..Length(C)]}), nt.dims()[1], Scat, IterHStack1, HStack1, (a,b)->b*a) *
            _pruned_IPRF12_CT_Stage2(N, k, C, Diag(BHD(m,1,-1)), j->RC(Diag(fPrecompute(Twid(N,m,k,0,0,j+1)))))
       )
    ),
));

Class(IOPrunedConv, TaggedNonTerminal, rec(
    abbrevs := [
        (n,h,oblk,opat,iblk,ipat) -> Checked(IsPosIntSym(n), IsPosIntSym(oblk), IsList(opat), IsPosIntSym(iblk), IsList(ipat),
            AnySyms(n,iblk,oblk) or (IsInt(_unwrap(n)/_unwrap(iblk)) and IsInt(_unwrap(n)/_unwrap(oblk)) and
                ForAll(ipat, i->IsInt(i) and i < n/iblk) and ForAll(opat, i->IsInt(i) and i < n/oblk)),
            [_unwrap(n), h, _unwrap(oblk), opat, _unwrap(iblk), ipat, false]),
        (n,h,oblk,opat,iblk,ipat,isFreqData) -> Checked(IsPosIntSym(n), IsPosIntSym(oblk), IsList(opat), IsPosIntSym(iblk), IsList(ipat),
            AnySyms(n,iblk,oblk) or (IsInt(_unwrap(n)/_unwrap(iblk)) and IsInt(_unwrap(n)/_unwrap(oblk)) and
                ForAll(ipat, i->IsInt(i) and i < n/iblk) and ForAll(opat, i->IsInt(i) and i < n/oblk)),
            [_unwrap(n), h, _unwrap(oblk), opat, _unwrap(iblk), ipat, isFreqData])
        ],
    dims      := self >> [self.params[3]*Length(self.params[4]), self.params[5]*Length(self.params[6])],
    terminate := self >> let(size := self.params[1],
        taps := When(self.params[7], MatSPL(DFT(self.params[1], -1)) *
               Cond(IsList(self.params[2]), self.params[2], IsLambda(self.params[2]), self.params[2].tolist(), IsBound(self.params[2].list), self.params[2].list),
           self.params[2]),
        oblk := self.params[3], opat := self.params[4], iblk := self.params[5], ipat := self.params[6],
        Tensor(Mat(List(opat, i->BasisVec(size/oblk, i))), I(oblk)).terminate() *
        spiral.transforms.filtering.Circulant(size, taps, -size).terminate() *
        Tensor(Mat(List(ipat, i->BasisVec(size/iblk, i))).transpose(), I(iblk)).terminate()),
    isReal    := self >> false,
    normalizedArithCost := self >> let(n := self.params[1], IntDouble(5 * n * d_log(n) / d_log(2))),
    TType := TComplex,
    hashAs := self >> ApplyFunc(ObjId(self), [self.params[1], fUnk(self.params[2].range(), self.params[2].domain())]::Drop(self.params, 2))
));

NewRulesFor(IOPrunedConv, rec(
    IOPrunedConv_PrunedDFT_IPrunedDFT := rec(
       forTransposition := false,
       applicable :=  (self, nt) >> not nt.hasTags(),
       children := nt -> [[ PrunedIDFT(nt.params[1], 1, nt.params[3], nt.params[4]),
                            PrunedDFT(nt.params[1], -1, nt.params[5], nt.params[6]) ]],
       apply := (nt, C, cnt) -> Cond(ObjId(nt.params[2]) = var, # a variable is frequency domain data -> FData
                                    Checked(nt.params[7], C[1] * Diag(FData(nt.params[2])) * C[2]), # if data is provided it needs to be frequency domain
                                    ObjId(nt.params[2]) = Lambda,  # data is inside a larger array
                                    Checked(nt.params[7], C[1] * Diag(nt.params[2]) * C[2]),
                                    nt.params[7],   # if params[7] is true then data is frequency domain data
                                    C[1] * Diag(When(IsList(nt.params[2]), FList(TComplex, nt.params[2]), nt.params[2])) * C[2],
                                    let(cxfftdiag := 1/nt.params[1]*ComplexFFT(List(nt.params[2].tolist(), i->ComplexAny(_unwrap(i)))),
                                        C[1] * Diag(FData(cxfftdiag)) * C[2]))
    ),
));

#===========================================

Class(IOPrunedRConv, TaggedNonTerminal, rec(
    abbrevs := [
        (n,h,oblk,opat,iblk,ipat) -> Checked(IsPosIntSym(n), IsPosIntSym(oblk), IsList(opat), IsPosIntSym(iblk), IsList(ipat),
            AnySyms(n,iblk,oblk) or (IsInt(_unwrap(n)/_unwrap(iblk)) and IsInt(_unwrap(n)/_unwrap(oblk)) and
                ForAll(ipat, i->IsInt(i) and i < n/iblk) and ForAll(opat, i->IsInt(i) and i < n/oblk)),
            [_unwrap(n), h, _unwrap(oblk), opat, _unwrap(iblk), ipat, false]),
        (n,h,oblk,opat,iblk,ipat,isFreqData) -> Checked(IsPosIntSym(n), IsPosIntSym(oblk), IsList(opat), IsPosIntSym(iblk), IsList(ipat),
            AnySyms(n,iblk,oblk) or (IsInt(_unwrap(n)/_unwrap(iblk)) and IsInt(_unwrap(n)/_unwrap(oblk)) and
                ForAll(ipat, i->IsInt(i) and i < n/iblk) and ForAll(opat, i->IsInt(i) and i < n/oblk)),
            [_unwrap(n), h, _unwrap(oblk), opat, _unwrap(iblk), ipat, isFreqData])
        ],
    dims      := self >> [self.params[3]*Length(self.params[4]), self.params[5]*Length(self.params[6])],
    terminate := self >> let(size := self.params[1],
        taps := When(self.params[7], MatSPL(IPRDFT(self.params[1], -1)) * When(IsList(self.params[2]), self.params[2], self.params[2].list), self.params[2]),
        oblk := self.params[3], opat := self.params[4], iblk := self.params[5], ipat := self.params[6],
        Tensor(Mat(List(opat, i->BasisVec(size/oblk, i))), I(oblk)).terminate() *
        spiral.transforms.filtering.Circulant(size, taps, -size).terminate() *
        Tensor(Mat(List(ipat, i->BasisVec(size/iblk, i))).transpose(), I(iblk)).terminate()),
    isReal    := self >> true,
    normalizedArithCost := self >> let(n := self.params[1], IntDouble(5 * n * d_log(n) / d_log(2))),
    TType := TReal
));

NewRulesFor(IOPrunedRConv, rec(
    IOPrunedRConv_PrunedIPRDFT_PrunedPRDFT := rec(
       forTransposition := false,
       applicable :=  (self, nt) >> not nt.hasTags(),
       children := nt -> [[ PrunedIPRDFT(nt.params[1], 1, nt.params[3], nt.params[4]),
                            PrunedPRDFT(nt.params[1], -1, nt.params[5], nt.params[6]) ]],
       apply := (nt, C, cnt) -> Cond(ObjId(nt.params[2]) = var, # a variable is frequency domain data -> FData
                                Checked(nt.params[7], C[1] * RCDiag(FData(nt.params[2])) * C[2]), # if data is provided it needs to be frequency domain
                                nt.params[7],   # if params[7] is true then data is frequency domain data
                                C[1] *  RCDiag(When(IsList(nt.params[2]), FList(TReal, nt.params[2]), nt.params[2])) * C[2],
                                let(cxfftdiag := 1/nt.params[1]*ComplexFFT(List(nt.params[2].tolist(), i->ComplexAny(_unwrap(i)))),
                                    rcdiag := cxfftdiag{[1..Rows(PRDFT(nt.params[1], -1))/2]},
                                    rlist := Flat(List(rcdiag, i->[ReComplex(_unwrap(i)), ImComplex(_unwrap(i))])),
                                    C[1] * RCDiag(FList(TReal, rlist)) * C[2]))
    ),
));

_complexify := l -> List([1..Length(l)/2], i->l[2*i-1] + E(4) * l[2*i]);

#=========================================================================================================================================================
Declare(IOPrunedMDRConv);
Class(IOPrunedMDRConv, TaggedNonTerminal, rec(
    abbrevs := [
        (n,h,oblk,opat,iblk,ipat) -> Checked(ForAll(n, IsPosIntSym), IsPosIntSym(oblk), ForAll(opat, IsList), IsPosIntSym(iblk), ForAll(ipat, IsList),
            [_unwrap(n), h, _unwrap(oblk), opat, _unwrap(iblk), ipat, false]),
        (n,h,oblk,opat,iblk,ipat,isFreqData) -> Checked(ForAll(n, IsPosIntSym), IsPosIntSym(oblk), ForAll(opat, IsList), IsPosIntSym(iblk), ForAll(ipat, IsList),
            [_unwrap(n), h, _unwrap(oblk), opat, _unwrap(iblk), ipat, isFreqData])
        ],
    dims      := self >> [self.params[3]*Product(self.params[4], i->Length(i)), self.params[5]*Product(self.params[6], i->Length(i))],
    isReal    := self >> true,
    normalizedArithCost := self >> let(n := self.params[1], IntDouble(5 * n * d_log(n) / d_log(2))),
    TType := TReal,
    terminate := self >> When(self.params[7],
                            let(nlist := self.params[1],
                                n := nlist[1],
                                nfreq := n/2+1,
                                nrem := Drop(nlist, 1),
                                idft := Tensor(IPRDFT(n, -1), I(Product(nrem))) *
                                    Tensor(I(nfreq), L(2*Product(nrem), 2)) * Tensor(I(nfreq), RC(MDDFT(nrem, -1))),
                                tlist := MatSPL(idft) *
                                    Cond(IsList(self.params[2]), self.params[2], IsLambda(self.params[2]), self.params[2].tolist(), IsBound(self.params[2].list), self.params[2].list),
                                IOPrunedMDRConv(self.params[1], FList(TReal, tlist), self.params[3], self.params[4],
                                    self.params[5], self.params[6], false).terminate()
                            ),
                            let(nlist := self.params[1],
                                oblk := self.params[3], opat_md := self.params[4],
                                iblk := self.params[5], ipat_md := self.params[6],
                                scat3d := Tensor(List(Zip2(ipat_md, nlist), j->let(ipat := j[1], size := j[2],
                                    Tensor(Mat(List(ipat, i->BasisVec(size/iblk, i))).transpose(), I(iblk)).terminate()))::[Mat([[1], [0]])]),
                                gath3d := Tensor(List(Zip2(opat_md, nlist), j->let(opat := j[1], size := j[2],
                                    Tensor(Mat(List(opat, i->BasisVec(size/oblk, i))), I(oblk)).terminate()))::[Mat([[1, 0]])]),

                                dft3dr := RC(MDDFT(nlist, -1)),
                                idft3dr := RC(MDDFT(nlist, 1)),
                                gfd := List(1/Product(nlist) * MatSPL(MDDFT(nlist, 1)) * self.params[2].list, i->ComplexAny(_unwrap(i))),
                                gdiagr := RC(Diag(gfd)),
                                t := gath3d * idft3dr * gdiagr * dft3dr * scat3d,
                                t.terminate()
                            )
                        ),
    hashAs := self >> ApplyFunc(ObjId(self), [self.params[1], fUnk(self.params[2].range(), self.params[2].domain())]::Drop(self.params, 2))
));

NewRulesFor(IOPrunedMDRConv, rec(
    IOPrunedMDRConv_time2freq := rec(
       forTransposition := false,
       applicable :=  (self, nt) >> not nt.hasTags() and not nt.params[7],
       children := nt -> let(htime := nt.params[2].list,
                            nlist := nt.params[1],
                            dft := 1/Product(nlist) * MatSPL(MDDFT(nlist, 1)),
                            nfreq := nlist[1]/2+1,
                            hfreq := dft*htime,
                            hfreqc := List(hfreq, ComplexAny),
                            hfreqce := hfreqc{[1..nfreq*Product(Drop(nlist,1))]},
                            hfreqr := Flat(List(hfreqce, i->[ReComplex(i), ImComplex(i)])),
                            [[IOPrunedMDRConv(nt.params[1], FList(TReal, hfreqr), nt.params[3], nt.params[4], nt.params[5], nt.params[6], true)]]
                        ),
       apply := (nt, C, cnt) -> C[1]
    ),

    ## GPU/TITAN V Hockney algotithm variant
    ## 2-trip, 5-step, ZYX ====================================================
    IOPrunedMDRConv_3D_2trip_zyx_freqdata := rec(
       forTransposition := false,
       applicable :=  (self, nt) >> not nt.hasTags() and Length(nt.params[1]) = 3 and nt.params[7],
       children := nt -> let(nlist := nt.params[1],
                            diag := nt.params[2],
                            oblk := nt.params[3],
                            opats := nt.params[4],
                            iblk := nt.params[5],
                            ipats := nt.params[6],
                            nfreq := nlist[1]/2+1,
                            i := Ind(nfreq*nlist[2]),
                            hfunc := Cond(ObjId(diag) = Lambda,
                                let(j := Ind(nlist[3]),
                                    # Lambda(j, cxpack(diag.at(2*(j + i*nlist[3])), diag.at(2*(j + i*nlist[3])+1)))
                                    pos := i +j*nfreq*nlist[2],
                                    Lambda(j, cxpack(diag.at(2*pos), diag.at(2*pos+1)))
                                ),
                                ObjId(diag) = fUnk,
                                fUnk(TComplex, nlist[3]),
                                let(list := nt.params[2].list,  # here we assume FList(TReal, [...])
                                    clist := List([1..Length(list)/2], i->Cplx(list[2*i-1], list[2*i])),
                                    fc := FList(TComplex, clist),
                                    gf := fTensor(fBase(i), fId(nlist[3])),
                                    fCompose(fc, gf)
                                )
                            ),
                            [[ PrunedPRDFT(nlist[1], -1, iblk, ipats[1]),  # stage 1: PRDFT z
                                PrunedDFT(nlist[2], -1, iblk, ipats[2]),    # stage 2: DFT y
                                IOPrunedConv(nlist[3], hfunc, oblk, opats[3], iblk, ipats[3], true), # stage 3+4+5: complex conv in x
                                PrunedIDFT(nlist[2], 1, oblk, opats[2]), # stage 6: iDFT in y
                                PrunedIPRDFT(nlist[1], 1, oblk, opats[1]),   # stage 7: iPRDFT in z
                                InfoNt(i)
                            ]]),

       apply := (nt, C, cnt) -> let(prdft1d := C[1],
                                    pdft1d := C[2],
                                    iopconv := C[3],
                                    ipdft1d := C[4],
                                    iprdft1d := C[5],
                                    i := cnt[6].params[1],
                                    nlist := nt.params[1],
                                    n1 := nlist[1],
                                    nfreq := nlist[1]/2+1,
                                    n2 := nlist[2],
                                    n3 := nlist[3],
                                    oblk := nt.params[3],
                                    opats := nt.params[4],
                                    iblk := nt.params[5],
                                    ipats := nt.params[6],
                                    ns1 := iblk * Length(ipats[1]),
                                    ns2 := iblk * Length(ipats[2]),
                                    ns3 := iblk * Length(ipats[3]),
                                    nd1 := oblk * Length(opats[1]),
                                    nd2 := oblk * Length(opats[2]),
                                    nd3 := oblk * Length(opats[3]),
                                    stage1 := L(2*nfreq*ns3*ns2, ns3) * Tensor(I(ns2), Tensor(L(2*nfreq, 2) * prdft1d, I(ns3))) * Tensor(L(ns2*ns1, ns2), I(ns3)),
                                    stage2 := Tensor(I(ns3), Tensor(RC(pdft1d), I(nfreq))),
                                    pp := Tensor(L(ns3*n2*nfreq, n2*nfreq), I(2)) * Tensor(I(ns3), L(2*nfreq*n2, nfreq)),
                                    ppi := Tensor(I(nd3), L(2*nfreq*n2, 2*n2)) * Tensor(L(nd3*n2*nfreq, nd3), I(2)),
                                    stage543 := ppi * IDirSum(i, RC(iopconv)) * pp,
                                    stage76 := Tensor(L(nd2*nd1, nd1), I(nd3)) * Grp(Tensor((Tensor(I(nd2), iprdft1d * L(2*nfreq, nfreq)) *
                                        Tensor(RC(ipdft1d), I(nfreq))), I(nd3)) * L(2*nfreq*nd3*n2, 2*nfreq*n2)),
                                    conv3dr := stage76 * stage543 * stage2 * stage1,
                                    conv3dr
                            ),
    ),

    ## X dimension last (convolution)
    ## 5-step, ZYX ====================================================
    IOPrunedMDRConv_3D_5step_zyx_freqdata := rec(
       forTransposition := false,
       applicable :=  (self, nt) >> not nt.hasTags() and Length(nt.params[1]) = 3 and nt.params[7],
       children := nt -> let(nlist := nt.params[1],
                            diag := nt.params[2],
                            oblk := nt.params[3],
                            opats := nt.params[4],
                            iblk := nt.params[5],
                            ipats := nt.params[6],
                            nfreq := nlist[1]/2+1,
                            i := Ind(nfreq*nlist[2]),
                            hfunc := Cond(ObjId(diag) = Lambda,
                                let(j := Ind(nlist[3]),
                                    Lambda(j, cxpack(diag.at(2*(j + i*nlist[3])), diag.at(2*(j + i*nlist[3])+1)))
                                ),
                                ObjId(diag) = fUnk,
                                fUnk(TComplex, nlist[3]),
                                let(list := nt.params[2].list,  # here we assume FList(TReal, [...])
                                    clist := List([1..Length(list)/2], i->Cplx(list[2*i-1], list[2*i])),
                                    fc := FList(TComplex, clist),
                                    gf := fTensor(fBase(i), fId(nlist[3])),
                                    fCompose(fc, gf)
                                )
                            ),
                            [[ PrunedPRDFT(nlist[1], -1, iblk, ipats[1]),  # stage 1: PRDFT z
                                PrunedDFT(nlist[2], -1, iblk, ipats[2]),    # stage 2: DFT y
                                IOPrunedConv(nlist[3], hfunc, oblk, opats[3], iblk, ipats[3], true), # stage 3+4+5: complex conv in x
                                PrunedIDFT(nlist[2], 1, oblk, opats[2]), # stage 6: iDFT in y
                                PrunedIPRDFT(nlist[1], 1, oblk, opats[1]),   # stage 7: iPRDFT in z
                                InfoNt(i)
                            ]]),

       apply := (nt, C, cnt) -> let(prdft1d := Grp(C[1]),
                                    pdft1d := Grp(C[2]),
                                    iopconv := Grp(C[3]),
                                    ipdft1d := Grp(C[4]),
                                    iprdft1d := Grp(C[5]),
                                    i := cnt[6].params[1],
                                    nlist := nt.params[1],
                                    n1 := nlist[1],
                                    nfreq := nlist[1]/2+1,
                                    n2 := nlist[2],
                                    n3 := nlist[3],
                                    oblk := nt.params[3],
                                    opats := nt.params[4],
                                    iblk := nt.params[5],
                                    ipats := nt.params[6],
                                    ns1 := iblk * Length(ipats[1]),
                                    ns2 := iblk * Length(ipats[2]),
                                    ns3 := iblk * Length(ipats[3]),
                                    nd1 := oblk * Length(opats[1]),
                                    nd2 := oblk * Length(opats[2]),
                                    nd3 := oblk * Length(opats[3]),
                                    stage1 := Tensor(I(nfreq), L(2*ns2*ns3, ns2*ns3)) * Tensor(prdft1d, I(ns2*ns3)),
                                    stage2 := RC(Tensor(I(nfreq), pdft1d, I(ns3))),
                                    stage543c := IDirSum(i, iopconv),
                                    stage543 := RC(stage543c),
                                    stage6 := RC(Tensor(I(nfreq), ipdft1d, I(nd3))),
                                    stage7 := Tensor(iprdft1d, I(nd2*nd3)) * Tensor(I(nfreq), L(2*nd2*nd3, 2)),
                                    conv3dr := stage7 * stage6 * stage543 * stage2 * stage1,
                                    conv3dr
                            ),
    ),
    ## 5-step, YZX ====================================================
    IOPrunedMDRConv_3D_5step_yzx_freqdata := rec(
       forTransposition := false,
       applicable :=  (self, nt) >> false, #not nt.hasTags() and Length(nt.params[1]) = 3 and nt.params[7], Breaks!
       children := nt -> let(nlist := nt.params[1],
                            diag := nt.params[2],
                            oblk := nt.params[3],
                            opats := nt.params[4],
                            iblk := nt.params[5],
                            ipats := nt.params[6],
                            nfreq := nlist[1]/2+1,
                            i := Ind(nfreq*nlist[2]),
                            hfunc := Cond(ObjId(diag) = Lambda,
                                let(j := Ind(nlist[3]),
                                    Lambda(j, cxpack(diag.at(2*(j + i*nlist[3])), diag.at(2*(j + i*nlist[3])+1)))
                                ),
                                ObjId(diag) = fUnk,
                                fUnk(TComplex, nlist[3]),
                                let(list := nt.params[2].list,  # here we assume FList(TReal, [...])
                                    clist := List([1..Length(list)/2], i->Cplx(list[2*i-1], list[2*i])),
                                    fc := FList(TComplex, clist),
                                    gf := fTensor(fBase(i), fId(nlist[3])),
                                    fCompose(fc, gf)
                                )
                            ),
                            [[ PrunedPRDFT(nlist[2], -1, iblk, ipats[2]),  # stage 1: PRDFT y
                                PrunedDFT(nlist[1], -1, iblk, ipats[1]),    # stage 2: DFT z
                                IOPrunedConv(nlist[3], hfunc, oblk, opats[3], iblk, ipats[3], true), # stage 3+4+5: complex conv in x
                                PrunedIDFT(nlist[1], 1, oblk, opats[1]), # stage 6: iDFT in z
                                PrunedIPRDFT(nlist[2], 1, oblk, opats[2]),   # stage 7: iPRDFT in y
                                InfoNt(i)
                            ]]),

       apply := (nt, C, cnt) -> let(prdft1d := C[1],
                                    pdft1d := C[2],
                                    iopconv := C[3],
                                    ipdft1d := C[4],
                                    iprdft1d := C[5],
                                    i := cnt[6].params[1],
                                    nlist := nt.params[1],
                                    n1 := nlist[1],
                                    nfreq := nlist[2]/2+1,
                                    n2 := nlist[2],
                                    n3 := nlist[3],
                                    oblk := nt.params[3],
                                    opats := nt.params[4],
                                    iblk := nt.params[5],
                                    ipats := nt.params[6],
                                    ns1 := iblk * Length(ipats[1]),
                                    ns2 := iblk * Length(ipats[2]),
                                    ns3 := iblk * Length(ipats[3]),
                                    nd1 := oblk * Length(opats[1]),
                                    nd2 := oblk * Length(opats[2]),
                                    nd3 := oblk * Length(opats[3]),
                                    stage1 := Tensor(I(nfreq*ns2), L(2*ns3, ns3)) * Tensor(I(ns2), prdft1d, I(ns3)),
                                    stage2 := RC(Tensor(pdft1d, I(nfreq*ns3))),
                                    stage543c := IDirSum(i, iopconv),
                                    stage543 := RC(stage543c),
                                    stage6 := RC(Tensor(ipdft1d, I(nfreq*nd3))),
                                    stage7 := Tensor(I(nd2), iprdft1d, I(nd3)) * Tensor(I(nfreq*nd2), L(2*nd3, 2)),
                                    conv3dr := stage7 * stage6 * stage543 * stage2 * stage1,
                                    conv3dr
                            ),
    ),
    ## 3-step, (ZY)X ====================================================
    IOPrunedMDRConv_3D_3step_zyx_freqdata := rec(
       forTransposition := false,
       applicable :=  (self, nt) >> not nt.hasTags() and Length(nt.params[1]) = 3 and nt.params[7],
       children := nt -> let(nlist := nt.params[1],
                            diag := nt.params[2],
                            oblk := nt.params[3],
                            opats := nt.params[4],
                            iblk := nt.params[5],
                            ipats := nt.params[6],
                            nfreq := nlist[1]/2+1,
                            i := Ind(nfreq*nlist[2]),
                            hfunc := Cond(ObjId(diag) = Lambda,
                                let(j := Ind(nlist[3]),
                                    Lambda(j, cxpack(diag.at(2*(j + i*nlist[3])), diag.at(2*(j + i*nlist[3])+1)))
                                ),
                                ObjId(diag) = fUnk,
                                fUnk(TComplex, nlist[3]),
                                let(list := nt.params[2].list,  # here we assume FList(TReal, [...])
                                    clist := List([1..Length(list)/2], i->Cplx(list[2*i-1], list[2*i])),
                                    fc := FList(TComplex, clist),
                                    gf := fTensor(fBase(i), fId(nlist[3])),
                                    fCompose(fc, gf)
                                )
                            ),
                            [[ PrunedPRDFT(nlist[1], -1, iblk, ipats[1]),  # stage 1a: PRDFT z
                                PrunedDFT(nlist[2], -1, iblk, ipats[2]),    # stage 1b: DFT y
                                IOPrunedConv(nlist[3], hfunc, oblk, opats[3], iblk, ipats[3], true), # stage 3+4+5: complex conv in x
                                PrunedIDFT(nlist[2], 1, oblk, opats[2]), # stage 6b: iDFT in y
                                PrunedIPRDFT(nlist[1], 1, oblk, opats[1]),   # stage 6a: iPRDFT in z
                                InfoNt(i)
                            ]]),

       apply := (nt, C, cnt) -> let(prdft1d := Grp(C[1]),
                                    pdft1d := Grp(C[2]),
                                    iopconv := Grp(C[3]),
                                    ipdft1d := Grp(C[4]),
                                    iprdft1d := Grp(C[5]),
                                    i := cnt[6].params[1],
                                    nlist := nt.params[1],
                                    n1 := nlist[1],
                                    nfreq := nlist[1]/2+1,
                                    n2 := nlist[2],
                                    n3 := nlist[3],
                                    oblk := nt.params[3],
                                    opats := nt.params[4],
                                    iblk := nt.params[5],
                                    ipats := nt.params[6],
                                    ns1 := iblk * Length(ipats[1]),
                                    ns2 := iblk * Length(ipats[2]),
                                    ns3 := iblk * Length(ipats[3]),
                                    nd1 := oblk * Length(opats[1]),
                                    nd2 := oblk * Length(opats[2]),
                                    nd3 := oblk * Length(opats[3]),
                                    stage1 := Tensor(I(nfreq), L(2*ns2, ns2)) * Tensor(prdft1d, I(ns2)),
                                    stage2 := RC(Tensor(I(nfreq), pdft1d)),
                                    stage12 := Tensor(I(nfreq * n2), L(2*ns3, ns3)) * Tensor(stage2 * stage1, I(ns3)),
                                    stage543c := IDirSum(i, iopconv),
                                    stage543 := RC(stage543c),
                                    stage6 := RC(Tensor(I(nfreq), ipdft1d)),
                                    stage7 := Tensor(iprdft1d, I(nd2)) * Tensor(I(nfreq), L(2*nd2, 2)),
                                    stage67 := Tensor(stage7 * stage6, I(nd3)) * Tensor(I(nfreq * n2), L(2*nd3, 2)),
                                    conv3dr := stage67 * stage543 * stage12,
                                    conv3dr
                            ),
    ),
    ## 3-step, (YZ)X ====================================================
    IOPrunedMDRConv_3D_3step_yzx_freqdata := rec(
       forTransposition := false,
       applicable :=  (self, nt) >> false, #not nt.hasTags() and Length(nt.params[1]) = 3 and nt.params[7], Breaks!
       children := nt -> let(nlist := nt.params[1],
                            diag := nt.params[2],
                            oblk := nt.params[3],
                            opats := nt.params[4],
                            iblk := nt.params[5],
                            ipats := nt.params[6],
                            nfreq := nlist[1]/2+1,
                            i := Ind(nfreq*nlist[2]),
                            hfunc := Cond(ObjId(diag) = Lambda,
                                let(j := Ind(nlist[3]),
                                    Lambda(j, cxpack(diag.at(2*(j + i*nlist[3])), diag.at(2*(j + i*nlist[3])+1)))
                                ),
                                ObjId(diag) = fUnk,
                                fUnk(TComplex, nlist[3]),
                                let(list := nt.params[2].list,  # here we assume FList(TReal, [...])
                                    clist := List([1..Length(list)/2], i->Cplx(list[2*i-1], list[2*i])),
                                    fc := FList(TComplex, clist),
                                    gf := fTensor(fBase(i), fId(nlist[3])),
                                    fCompose(fc, gf)
                                )
                            ),
                            [[ PrunedPRDFT(nlist[2], -1, iblk, ipats[2]),  # stage 1a: PRDFT y
                                PrunedDFT(nlist[1], -1, iblk, ipats[1]),    # stage 1b: DFT z
                                IOPrunedConv(nlist[3], hfunc, oblk, opats[3], iblk, ipats[3], true), # stage 3+4+5: complex conv in x
                                PrunedIDFT(nlist[1], 1, oblk, opats[1]), # stage 6b: iDFT in z
                                PrunedIPRDFT(nlist[2], 1, oblk, opats[2]),   # stage 6a: iPRDFT in y
                                InfoNt(i)
                            ]]),

       apply := (nt, C, cnt) -> let(prdft1d := C[1],
                                    pdft1d := C[2],
                                    iopconv := C[3],
                                    ipdft1d := C[4],
                                    iprdft1d := C[5],
                                    i := cnt[6].params[1],
                                    nlist := nt.params[1],
                                    n1 := nlist[1],
                                    nfreq := nlist[2]/2+1,
                                    n2 := nlist[2],
                                    n3 := nlist[3],
                                    oblk := nt.params[3],
                                    opats := nt.params[4],
                                    iblk := nt.params[5],
                                    ipats := nt.params[6],
                                    ns1 := iblk * Length(ipats[1]),
                                    ns2 := iblk * Length(ipats[2]),
                                    ns3 := iblk * Length(ipats[3]),
                                    nd1 := oblk * Length(opats[1]),
                                    nd2 := oblk * Length(opats[2]),
                                    nd3 := oblk * Length(opats[3]),
                                    stage1 := Tensor(I(ns1), prdft1d),
                                    stage2 := RC(Tensor(pdft1d, I(nfreq))),
                                    stage12 := Tensor(I(nfreq * n2), L(2*ns3, ns3)) * Tensor(stage2 * stage1, I(ns3)),
                                    stage543c := IDirSum(i, iopconv),
                                    stage543 := RC(stage543c),
                                    stage6 := RC(Tensor(ipdft1d, I(nfreq))),
                                    stage7 := Tensor(I(nd1), iprdft1d),
                                    stage67 := Tensor(stage7 * stage6, I(nd3)) * Tensor(I(nfreq * n2), L(2*nd3, 2)),
                                    conv3dr := stage67 * stage543 * stage12,
                                    conv3dr
                            ),
    ),
#======================

    IOPrunedMDRConv_2D_2trip_xy_freqdata := rec(
       forTransposition := true,
       applicable :=  (self, nt) >> not nt.hasTags() and Length(nt.params[1]) = 2 and nt.params[7],
       children := nt -> let(nlist := nt.params[1],
                            diag := nt.params[2],
                            oblk := nt.params[3],
                            opats := nt.params[4],
                            iblk := nt.params[5],
                            ipats := nt.params[6],
                            nfreq := nlist[2]/2+1,
                            i := Ind(nfreq),
                            hfunc := Cond(ObjId(diag) = Lambda,
                                let(j := Ind(nlist[1]),
                                    pos := i + j*nfreq,
                                    Lambda(j, cxpack(diag.at(2*pos), diag.at(2*pos+1)))
                                ),
                                ObjId(diag) = fUnk,
                                fUnk(TComplex, nlist[1]),
                                let(list := nt.params[1].list,  # here we assume FList(TReal, [...])
                                    clist := List([1..Length(list)/2], i->Cplx(list[2*i-1], list[2*i])),
                                    fc := FList(TComplex, clist),
                                    gf := fTensor(fBase(i), fId(nlist[1])),
                                    fCompose(fc, gf)
                                )
                            ),
                            [[ PrunedPRDFT(nlist[2], -1, iblk, ipats[2]),  # stage 1: PRDFT x
                                IOPrunedConv(nlist[1], hfunc, oblk, opats[1], iblk, ipats[1], true), # stage 2+3+4: complex conv in y
                                PrunedIPRDFT(nlist[2], 1, oblk, opats[1]),   # stage 5: iPRDFT in x
                                InfoNt(i)
                            ]]),

       apply := (nt, C, cnt) -> let(prdft1d := C[1],
                                    iopconv := C[2],
                                    iprdft1d := C[3],
                                    i := cnt[4].params[1],
                                    nlist := nt.params[1],
                                    n1 := nlist[1],
                                    nfreq := nlist[1]/2+1,
                                    n2 := nlist[2],
                                    oblk := nt.params[3],
                                    opats := nt.params[4],
                                    iblk := nt.params[5],
                                    ipats := nt.params[6],
                                    ns1 := iblk * Length(ipats[1]),
                                    ns2 := iblk * Length(ipats[2]),
                                    nd1 := oblk * Length(opats[1]),
                                    nd2 := oblk * Length(opats[2]),

#                                    stage1 := L(2*nfreq*ns3*ns2, ns3) * Tensor(I(ns2), Tensor(L(2*nfreq, 2) * prdft1d, I(ns3))) * Tensor(L(ns2*ns1, ns2), I(ns3)),
#                                    stage2 := Tensor(I(ns3), Tensor(RC(pdft1d), I(nfreq))),
#                                    pp := Tensor(L(ns3*n2*nfreq, n2*nfreq), I(2)) * Tensor(I(ns3), L(2*nfreq*n2, nfreq)),
#                                    ppi := Tensor(I(nd3), L(2*nfreq*n2, 2*n2)) * Tensor(L(nd3*n2*nfreq, nd3), I(2)),
#                                    stage543 := ppi * IDirSum(i, RC(iopconv)) * pp,
#                                    stage76 := Tensor(L(nd2*nd1, nd1), I(nd3)) * Grp(Tensor((Tensor(I(nd2), iprdft1d * L(2*nfreq, nfreq)) *
#                                        Tensor(RC(ipdft1d), I(nfreq))), I(nd3)) * L(2*nfreq*nd3*n2, 2*nfreq*n2)),

                                    stage1 := Tensor(I(ns2), prdft1d),
                                    pp := Tensor(L(ns2*nfreq, nfreq), I(2)),
                                    ppi := Tensor(L(nd2*nfreq, nd2), I(2)),
                                    stage432 := IDirSum(i, RC(iopconv)),
                                    stage5 :=  Tensor(I(nd2), iprdft1d),

                                    conv2dr := Grp(stage5 * ppi) * stage432 * Grp(pp * stage1),
                                    conv2dr
                            ),
    ),
#     IOPrunedMDRConv_2D_2trip_yx_freqdata := rec(
#       forTransposition := true,
#       applicable :=  (self, nt) >> not nt.hasTags() and Length(nt.params[1]) = 2 and nt.params[7],
#       children := nt -> let(nlist := nt.params[1],
#                            diag := nt.params[2],
#                            oblk := nt.params[3],
#                            opats := nt.params[4],
#                            iblk := nt.params[5],
#                            ipats := nt.params[6],
#                            nfreq := nlist[1]/2+1,
#                            i := Ind(nfreq),
#                            hfunc := Cond(ObjId(diag) = Lambda,
#                                let(j := Ind(nlist[2]),
#                                    pos := i + j*nfreq,
#                                    Lambda(j, cxpack(diag.at(2*pos), diag.at(2*pos+1)))
#                                ),
#                                ObjId(diag) = fUnk,
#                                fUnk(TComplex, nlist[2]),
#                                let(list := nt.params[2].list,  # here we assume FList(TReal, [...])
#                                    clist := List([1..Length(list)/2], i->Cplx(list[2*i-1], list[2*i])),
#                                    fc := FList(TComplex, clist),
#                                    gf := fTensor(fBase(i), fId(nlist[2])),
#                                    fCompose(fc, gf)
#                                )
#                            ),
#                            [[ PrunedPRDFT(nlist[1], -1, iblk, ipats[1]),  # stage 1: PRDFT y
#                                IOPrunedConv(nlist[2], hfunc, oblk, opats[2], iblk, ipats[2], true), # stage 2+3+4: complex conv in x
#                                PrunedIPRDFT(nlist[1], 1, oblk, opats[1]),   # stage 5: iPRDFT in y
#                                InfoNt(i)
#                            ]]),
#
#       apply := (nt, C, cnt) -> let(prdft1d := C[1],
#                                    iopconv := C[2],
#                                    iprdft1d := C[3],
#                                    i := cnt[4].params[1],
#                                    nlist := nt.params[1],
#                                    n1 := nlist[1],
#                                    nfreq := nlist[1]/2+1,
#                                    n2 := nlist[2],
#                                    oblk := nt.params[3],
#                                    opats := nt.params[4],
#                                    iblk := nt.params[5],
#                                    ipats := nt.params[6],
#                                    ns1 := iblk * Length(ipats[1]),
#                                    ns2 := iblk * Length(ipats[2]),
#                                    nd1 := oblk * Length(opats[1]),
#                                    nd2 := oblk * Length(opats[2]),
#
##                                    stage1 := L(2*nfreq*ns3*ns2, ns3) * Tensor(I(ns2), Tensor(L(2*nfreq, 2) * prdft1d, I(ns3))) * Tensor(L(ns2*ns1, ns2), I(ns3)),
##                                    stage2 := Tensor(I(ns3), Tensor(RC(pdft1d), I(nfreq))),
##                                    pp := Tensor(L(ns3*n2*nfreq, n2*nfreq), I(2)) * Tensor(I(ns3), L(2*nfreq*n2, nfreq)),
##                                    ppi := Tensor(I(nd3), L(2*nfreq*n2, 2*n2)) * Tensor(L(nd3*n2*nfreq, nd3), I(2)),
##                                    stage543 := ppi * IDirSum(i, RC(iopconv)) * pp,
##                                    stage76 := Tensor(L(nd2*nd1, nd1), I(nd3)) * Grp(Tensor((Tensor(I(nd2), iprdft1d * L(2*nfreq, nfreq)) *
##                                        Tensor(RC(ipdft1d), I(nfreq))), I(nd3)) * L(2*nfreq*nd3*n2, 2*nfreq*n2)),
#
#                                    stage1 := L(2*nfreq*ns3*ns2, ns3) * Tensor(I(ns2), Tensor(L(2*nfreq, 2) * prdft1d, I(ns3))) * Tensor(L(ns2*ns1, ns2), I(ns3)),
#                                    pp := Tensor(L(ns3*n2*nfreq, n2*nfreq), I(2)) * Tensor(I(ns3), L(2*nfreq*n2, nfreq)),
#                                    ppi := Tensor(I(nd3), L(2*nfreq*n2, 2*n2)) * Tensor(L(nd3*n2*nfreq, nd3), I(2)),
#                                    stage432 := ppi * IDirSum(i, RC(iopconv)) * pp,
#                                    stage5 := Tensor(L(nd2*nd1, nd1), I(nd3)) * Grp(Tensor((Tensor(I(nd2), iprdft1d * L(2*nfreq, nfreq)) *
#                                        Tensor(RC(ipdft1d), I(nfreq))), I(nd3)) * L(2*nfreq*nd3*n2, 2*nfreq*n2)),
#
#                                    conv2dr := stage5 * stage432 * stage1,
#                                    conv2dr
#                            ),
#    ),

));


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

NewRulesFor(Circulant, rec(
    Circulant_PRDFT_FDataNT := rec(
    	forTransposition := false,
    	switch           := true,

    	applicable       := nt -> ObjId(nt.params[2]) = FDataNT and ObjId(nt.params[2].nt) = IPRDFT and
                                  nt.params[2].nt.params = [nt.params[1], 1] and nt.params[1] = -nt.params[3],

    	children      := nt -> let(n := nt.params[1],
    	    [[ spiral.transforms.realdft.IPRDFT(n, 1), spiral.transforms.realdft.PRDFT(n, -1) ]]),

        apply := (nt, C, cnt) -> C[1] * RCDiag(FDataOfs(nt.params[2].var, Cols(nt.params[2].nt), 0)) * C[2]
    )
));

#_isFractionP := (n, p) -> IntDouble(p*n)/p = n;
#
#NewRulesFor(TResample, rec(
#    TResample_TGath := rec(
#    	forTransposition := false,
#    	switch           := true,
#
#    	applicable       := nt -> ForAny(Zip2(nt.params[1], nt.params[2]), l-> l[1]<l[2]),
#
#    	children      := nt -> [[TResample(nt.params[1], nt.params[1], nt.params[3]), TGath(_toBox(nt.params[2], nt.params[1]))]],
#
#        apply := (nt, C, cnt) -> C[1] * C[2]
#    ),
#
#    TResample_TScath := rec(
#    	forTransposition := false,
#    	switch           := true,
#
#    	applicable       := nt -> ForAny(Zip2(nt.params[1], nt.params[2]), l-> l[1]>l[2]),
#
#    	children      := nt -> [[TScat(_toBox(nt.params[1], nt.params[2])), TResample(nt.params[2], nt.params[2], nt.params[3])]],
#
#        apply := (nt, C, cnt) -> C[1] * C[2]
#    ),
#
#    TResample_RC := rec(
#    	forTransposition := false,
#    	switch           := true,
#
#    	applicable       := nt -> Length(nt.params[1]) > 1 and Length(nt.params[2]) > 1 and Length(nt.params[3]) > 1
#                                  and nt.params[1][1] = nt.params[2][1],
#
#    	children      := nt -> [ List(Zip2(Zip2(nt.params[1], nt.params[2]), nt.params[3]), e-> ApplyFunc(TResample, List(Flat(e), i->[i]))) ],
#
#        apply := (nt, C, cnt) -> let(_C:= Reversed(C), n := Length(nt.params), Compose(
#            List([1..n], i->let(lens := List(_C, j->j.dims()[1]), Tensor(I(Product(lens{[1..i-1]})), _C[i], I(Product(lens{[i+1..n]})))))))
#    ),
#
#    TResample_I := rec(
#    	forTransposition := false,
#    	switch           := true,
#
#    	applicable       := nt -> Length(nt.params[1]) = 1 and Length(nt.params[2]) = 1 and Length(nt.params[3]) = 1
#                                  and nt.params[1][1] = nt.params[2][1]  and nt.params[3][1] = 0,
#
#    	children      := nt -> [[]],
#
#        apply := (nt, C, cnt) -> I(nt.dims()[1])
#    ),
#
#    TResample_PRDFT_frac := rec(
#    	forTransposition := false,
#        denominator      := 2,
#    	switch           := true,
#
#    	applicable       := (self,nt) >> Length(nt.params[1]) = 1 and Length(nt.params[2]) = 1 and Length(nt.params[3]) = 1
#                                  and nt.params[1][1] = nt.params[2][1] and nt.params[3][1] <> 0 and _isFractionP(nt.params[3][1], self.denominator),
#
#    	children      := nt -> [[ IPRDFT1(nt.params[1][1], -1), PRDFT1(nt.params[1][1], 1) ]],
#
#        apply := (self, nt, C, cnt) >> let(n := nt.params[1][1], nf := n/2+1, shft := IntDouble(nt.params[3][1] * self.denominator),
#            df := diagMul(fConst(nf, 1/n), fCompose(dOmega(self.denominator*n, shft), dLin(nf, 1, 0, TInt))),
#            dm :=RCDiag(RCData(df)),
#            C[1] *dm * C[2])
#    )
#));
#


lpbp := (1,2,3);
rpbp := (1,3,4);

gpbp := Group([lpbp, rpbp]);

lfbp := (1,2,3)(4,5,6);
rfbp := (1,6,7)(4,3,8);

gfbp := Group([lfbp,rfbp]);

bs := Blocks(gfbp, [1..8]);

hom := ActionHomomorphism(gfbp, bs, OnSets);

im := Image(hom);

iso := IsomorphismGroups(im, gfbp);

k := Kernel(im);


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

_ToDec := v -> let(d:=Length(v), Sum(List([1..d], e -> v[e]*2^(d-e))));
_ToBin := (i,d) -> List([1..d], e -> BinAnd(QuoInt(i, 2^(d-e)), 1));

_TB := (i, n) -> GF(2).one * _ToBin(i, n);
_TD := (i) -> _ToDec(IntVecFFE(i));

PrintHex := function(lst, del)
    local i;
    if Length(lst) = 0 then
        return;
    fi;
    Print(HexInt(lst[1]));
    for i in [2..Length(lst)] do
        Print(del, HexInt(lst[i]));
    od;
end;

Declare(B);
Class(B, PermClass, rec(
    def := (n,l) -> Checked(
        # make sure the 1s are inside the matrix.
        ForAll(l, e ->
            IsPosInt(e[1]) and IsPosInt(e[2])
            and e[1] <= Log2Int(n) and e[2] <= Log2Int(n)),
        # make sure 'l' is a list.
        IsList(l),
	# done
	rec()),

    domain := self >> self.params[1],
    range  := self >> self.params[1],

    # return the bit matrix
    bm := meth(self)
        local n, m, i;

        n := Log2Int(self.params[1]);

        # generate matrix of zeros.
        m := List([1..n], e -> List([1..n], ee -> 0));

        # fill in identity
        for i in [1..n] do
            m[i][i] := 1;
        od;

        # xor in individual 1s from 'l'
        for i in self.params[2] do
            m[i[1]][i[2]] := BinXor(m[i[1]][i[2]], 1);
        od;

        return m * GF(2).root;
    end,

    # convert the sparse bit matrix info into a permutation matrix
    toAMat := meth(self)
        local m, n, i, M;

        m := self.bm();
        n := self.params[1];

        # generate full permutation matrix
        M := [];
        for i in [0..(n-1)] do
            Add(M, BasisVec(
                n,
                _TD(m * _TB(i, Log2Int(n)))
            ));
        od;

        return AMatMat(M);
    end,

    transpose := (self) >> self(
        self.params[1],
        self._mklist(
            MatAMat(
                InverseAMat(
                    AMatMat(self.bm())
                )
            )
        )
    ),

    _mklist := function(b)
        local l, i, j;

        Constraint(Dimensions(b)[1] = Dimensions(b)[2]);

        b := b - MatSPL(I(Dimensions(b)[1])) * GF(2).root;

        l := [];
        for i in [1..Length(b)] do
            for j in [1..Length(b)] do
                if b[i][j] = GF(2).root then
                    Add(l, [i,j]);
                fi;
            od;
        od;

        return l;
    end,

    transpose := self >> B(self.params[1], List(self.params[2], e -> [e[2],e[1]])),

    sums := (self) >> Chain(Error("Don't use legacy sumsgen!!"), true),
));

Declare(B2);

Class(B2, PermClass, rec(
    def := (N, E, S) -> Checked(
            Is2Power(N),
            Is2Power(E),
            Is2Power(S),
            E*(S^2) >= N,
            rec( transposed := false)
    ),

    domain := self >> self.params[1],
    range  := self >> self.params[1],

    transpose := self >> CopyFields(
        ApplyFunc(B2, self.params), rec(transposed := not self.transposed)),

    getES := (self) >> Product(self.params{[2..3]}),

    bm := meth(self)
        local n, nes, b, i;

        n := Log2Int(self.params[1]);
        nes := n - Log2Int(self.params[2] * self.params[3]);

        b := MatSPL(I(n)) * GF(2).root;

        if self.transposed then
            for i in [1..nes] do
                b[i+nes][i] := GF(2).root;
            od;
        else
            for i in [1..nes] do
                b[i][i+nes] := GF(2).root;
            od;
        fi;

        return b * GF(2).root;
    end,

    toAMat := meth(self)
        local b, n, i, M;

        b := self.bm();
        n := self.params[1];

        # generate full permutation matrix
        M := [];
        for i in [0..(n-1)] do
            Add(M, BasisVec(
                n,
                _TD(_TB(i, Log2Int(n)) * b)
            ));
        od;

        return AMatMat(M);
    end,
));

Declare(BP);

BPI := (n) -> BP(MatSPL(I(Log2Int(n)))*GF(2).root);
BPL := (n,str) -> BP(MatSPL(Z(Log2Int(n), Log2Int(str)))*GF(2).root);

Class(BP, PermClass, rec(
    # p must be a full rank 2-d square matrix of size no larger than 29x29.
    def := function(arg)
        local n, m;

        n := Length(arg);

        if n = 1 and IsMat(arg[1]) then
            m := arg[1];
            n := Dimensions(m)[1];
        else if n > 0 and IsInt(arg[1]) then

            Constraint(ForAny(arg, e -> e < 2^n));
            m := List(arg, e -> _TB(e, n));
        fi; fi;

        # ints in SPIRAL are 29bits long, the upper bits are used for flags
        Constraint(IsInvertibleMat(AMatMat(m)) and Dimensions(m)[1] < 30);

        # NOTE: this is buggy, because fields below are not exposed through .rChildren
        return rec(
            size := 2^Dimensions(m)[1],
            mat:= m,
            fast := List(m, e -> _TD(e)),
            operations := rec(
                Print := (self) >> Print(self.name, "(", PrintCS(self.fast), ")")
            )
        );
    end,

    # NOTE: this is buggy, because .size is not exposed through .rChildren
    domain := self >> self.size,
    range  := self >> self.size,

#    this method converts 'i' to an array of bits, multiplies by the matrix, and converts back
#
#    lambda := self >> let(
#       d := Dimensions(self.params[1])[1],
#       i := Ind(self.size),
#       Lambda(i, todec(self.params[1] * tobin(i,d)))),

#   alternative method: ands a row of the matrix with the input and uses a parity
#   check to see if the bit is set in the output. shifts the output to the proper place. repeat
    lambda := self >> let(
        f := Length(self.fast),
        i := Ind(self.size),
        Lambda(i, Sum(List([1..f], e -> bin_parity(bin_and(self.fast[e], i))*2^(f-e))))),

    transpose := self >> self.__bases__[1](TransposedMat(self.mat))
));

## -----------------------------------------------------------------------------
# CL(N, STR, [E,S,A,R]) - counters a stride L(N, STR) for cache given by [E,S,A,R]
#
# this returns a compound object, namely, P * B where
#
# P = I(N/(E*S)) x L(E*S, Ceil(E*S/STR))
#
# and B is a permutation specified by a function linear on the bits
# which forces the P*B object to span the sets in the cache after L.
#

CL := function(N, STR, CS)
    local E,S,A,Q,P,l,k,i,j, n;

    E := CS[1];
    S := CS[2];

    # deal with degenerate cases and check params.

    # if cache is larger/equal to transform size, no
    # fancy footwork is necessary.
    if E * S >= N then
        return I(N);
    fi;

    # the cache must be large enough, otherwise this method
    # will not build an adequate perm to counteract the stride.
    if S^2 * E < N then
        Error("CL failure: Cache inadequately sized.");
    fi;

    # build the P portion as a bit matrix
    P := MatSPL(DirectSum(
        I(Log2Int(N/(E*S))),
        Z(Log2Int(E*S), Log2Int(E*S) - Log2Int(Maximum(E*S/ Maximum(2*S/E, STR),1)))
#        Z(Log2Int(E*S), Log2Int(E*S) - Log2Int(Maximum(E*S/STR,1)))
    )) * GF(2).root;

    # build the list of 1's in the B matrix.
    l := [];
    k := Log2Int(Minimum(STR, (N/(E*S))));
    i := Log2Int(N/(E*S)) + 1;
    n := Log2Int(N);

    # paranoia check
    if i-k < 1 then
        Error("Problem!!");
    fi;

    A := Copy(P);

    for j in [0..i-2] do
        A[j+1][j+i] := GF(2).root;
    od;

    Q := (TransposedMat(P) * A) - (MatSPL(I(n))*GF(2).root);

    for j in [1..n] do
        for i in [1..n] do
            if Q[j][i] = GF(2).root then
                Add(l, [j,i]);
            fi;
        od;
    od;

    # rebuild P as a normal perm matrix that corresponds to the
    # bit matrix from earlier.
    j := Maximum(E*S/ Maximum(2*S/E, STR),1);

    P := When(E*S = j or j = 1,
        I(N),
        Tensor(
            I(N/(E*S)),
            L(E*S, j)
        )
    );

    # combine the two and return
    # temp -- remove the B matrix, to remove the xor/and/shifts
    # this means we no longer guarantee spanning of the elements/sets
    # but the expressions should reduce better.
    return Compose(P,B(N, l));
end;

#F fB describes a permutation which can be expressed as a sparse linear
#F     function on the bits.
#F
#F I_2^n -> I_2^N
#F
#F params:
#F  n (input size)
#F  N (output size)
#F  l (list of location of 1s in the bit matrix)
#F
#F additional details:
#F   l defines a list of pairs. each pair describes the position
#F   of a 1 in bit matrix of size Nxn. The output mapping is described
#F   by (I + BM)(index), where BM is the sparse bit matrix generated by the
#F   pairs. the pairs themselves are in SPIRAL parlance, that is, [ROW, COL]
#F   where the first row/col is index 1.
#F
#F NOTE:
#F   in order for composition to work like matrix multiplication, the index
#F   is now applied from the LEFT (eg: (index)(I + BM)) instead of from the
#F   right (eg: (I + BM)(index)).
#F
#F example: fB(8,8, [[1,2],[2,3]])
#F
#F [ 1 1 0
#F   0 1 1
#F   0 0 1 ]
#F
#F
#F
Class(fB, FuncClass, rec(
    # full input definition.
    def := (n,N,l) -> rec(),
    range := self >> self.params[2],
    domain := self >> self.params[1],

    # abbreviated input forms, from most complex to simplest.
    abbrevs := [
        (n, N, l) -> Checked(Is2Power(n), Is2Power(N), IsList(l),
            [n,N,l]),
        (n, l) -> Checked(Is2Power(n), IsList(l),
            [n,n,l]),
        (n) -> Checked(Is2Power(n),
            [n,n,[]])
    ],

    transpose := (self) >> self(
        self.params[2],
        self.params[1],
        self._mklist(
            MatAMat(
                InverseAMat(
                    AMatMat(self.bm())
                )
            )
        )
    ),

    # LAMBDA with index on the LEFT: (index)(I + BM)
    lambdaLEFT := self >> let(
        i := Ind(self.params[1]),
        Lambda(i,
            When(Length(self.params[3]) = 0,
                i,
                ApplyFunc(
                    xor,
                    Concat([i], List(self.params[3], e -> let(
                        a := Log2Int(self.params[1]) - e[1],
                        b := Log2Int(self.params[2]) - e[2],
                        When(b = a,
                            bin_and(i, V(2^a)),
                            When(b > a,
                                shl(bin_and(i, V(2^a)), b - a),
                                shr(bin_and(i, V(2^a)), a - b)
                            )
                        )
                    )))
                )
            )
        )
    ),

    # LAMBDA with index on the RIGHT: (I + BM)(index)
    lambdaRIGHT := self >> let(
        i := Ind(self.params[1]),
        Lambda(i,
            When(Length(self.params[3]) = 0,
                i,
                ApplyFunc(
                    xor,
                    Concat([i], List(self.params[3], e -> let(
                        a := Log2Int(self.params[1]) - e[2],
                        b := Log2Int(self.params[2]) - e[1],
                        When(b = a,
                            bin_and(i, V(2^a)),
                            When(b > a,
                                shl(bin_and(i, V(2^a)), b - a),
                                shr(bin_and(i, V(2^a)), a - b)
                            )
                        )
                    )))
                )
            )
        )
    ),

    lambda := self >> self.lambdaLEFT(),

    # a bit more complicated than the B case since matrix is not necessarily
    # square.
    bm := meth(self)
        local n, N, m, minn, minN, j, i;

        n := Log2Int(self.params[1]);
        N := Log2Int(self.params[2]);

        # since we multiply the index from the right, i * BM,
        # and n -> N, the bit matrix is nxN. (n rows, N cols)

        # generate matrix of zeros.
        m := List([1..n], e -> List([1..N], ee -> 0));

        # fill in identity
        minn := When(N > n, 1, n - N + 1);
        minN := When(n > N, 1, N - n + 1);
        for i in [minn..n] do
            m[i][i-minn+minN] := 1;
        od;

        # xor in individual 1s from 'l'
        for i in self.params[3] do
            m[i[1]][i[2]] := BinXor(m[i[1]][i[2]], 1);
        od;

        return m * GF(2).root;
    end,

    # given a bit matrix, make a list.
    _mklist := function(b)
        local n,N, m, minn, minN, i, j, l;

        ## generate identity...
        #

        n := Dimensions(b)[1];
        N := Dimensions(b)[2];

        # generate matrix of zeros.
        m := List([1..n], e -> List([1..N], ee -> GF(2).root*0));

        # fill in identity
        minn := When(N > n, 1, n - N + 1);
        minN := When(n > N, 1, N - n + 1);
        for i in [minn..n] do
            m[i][i-minn+minN] := GF(2).root;
        od;

        ## subtract out identity.
        b := b - m;

        ## add a list entry for each nonzero elem in b.

        l := [];
        for j in [1..Length(b)] do #rows
            for i in [1..Length(b[1])] do # cols
                if b[j][i] = GF(2).root then
                    Add(l, [j,i]);
                fi;
            od;
        od;

        return l;
    end,
));

Class(fB2, fTensorBase, rec(
    __call__ := meth(arg)
        local self, children, lkup, res, h, es;
        self := arg[1];
        es := arg[2];
        children := Flat(Drop(arg, 2));
        if self.skipOneChild and Length(children)=1 then return children[1]; fi;

        h := self.hash;
        if h<>false then
            lkup := h.objLookup(self, children);
            if lkup[1]<>false then return lkup[1]; fi;
        fi;
        res := WithBases(self, rec(es := es, operations := RewritableObjectOps, _children := children));
        if h<>false then return h.objAdd(res, lkup[2]);
        else return res;
        fi;
    end,

    rightBinary  := self >> FoldR1(self._children, (p,x) -> let(base:=self.__bases__[1], base(x, p))),
    leftBinary := self >> FoldL1(self._children, (p,x) -> let(base:=self.__bases__[1], base(p, x))),

    # this works just like the normal fTensor except that
    # subject to some conditions, the variables responsible for the
    # top-end of the range get XORed with variables responsible
    # for the middle of the range.

    lambda := meth(self)
        local es;

        es := self.es;

        # flatten the fTensors first
        # ensure we always work with 2 children
        if self.numChildren() > 2 then
            return self.leftBinary().lambda();
        fi;

        # the expression is now guaranteed to be a left sided
        # binary tree. we need to tell each node/leaf how it is
        # to be split according to the params available to the fB2.

    end,

    combine_op := (self, jv, split, f, g) >> let(a := Error("fB2.lambda()!"), self),
    transpose := self >> self.__bases__[1](List(self.children(), c->c.transpose()))
));


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

########################################################################
#   rules for A x I, I x A, (A x I)L, (I x A)L
NewRulesFor(TTensorI, rec(
#   loop splitting for A x I
    AxI_LS_L := rec(
        info := "(A_rxs x I_n)(L|I) -> Sum(Sum(SAG))",
        forTransposition := false,
        applicable := nt -> let(P := nt.params,
            P[3] = AVec
            and nt.isTag(1, AVecMemL)
            and IsInt(P[2]/nt.firstTag().v)
            and P[2]/nt.firstTag().v>1
        ),
        children := nt -> [[
            When(nt.numTags() = 1,
                nt.params[1],
                nt.params[1].setTags(nt.params[1].withoutFirstTag())
            )
        ]],
        apply := (nt, C, cnt) -> let(
            v := nt.firstTag().v,
            i := Ind(v),
            m_by_v := nt.params[2]/v,
            j := Ind(m_by_v),
            d := C[1].dims(),
            fr := When(nt.params[4] = AVec,
                fTensor(fId(d[2]), fBase(m_by_v, j), fBase(v, i)),
                fTensor(fBase(m_by_v, j), fBase(v, i), fId(d[2]))
            ),

            ISumLS(j, m_by_v,
                Buf(Scat(fTensor(
                    fId(d[1]), fBase(m_by_v, j), fId(v)
                )))
                * ISum(i, v, Scat(fTensor(
                    fId(d[1]), fBase(v, i))) * C[1] * Gath(fr)
                )
            )
        )

#D        isApplicable := P -> P[3].isVec and Length(P[5]) > 0 and P[5][1].isMemL and IsInt(P[2]/P[5][1].v) and P[2]/P[5][1].v>1,
#D        allChildren := P -> let(pv:=P[5], v:=pv[1].v, d:=P[1].dims(), [[When(Length(pv)=1, P[1], P[1].setpv(Drop(pv, 1)))]]),
#D        rule := (P, C) -> let(
#D                v:=P[5][1].v, i:=Ind(v), m_by_v := P[2]/v, j:=Ind(m_by_v), d:=C[1].dims(),
#D                fr:=When(P[4].isVec, fTensor(fId(d[2]), fBase(m_by_v, j), fBase(v, i)), fTensor(fBase(m_by_v, j), fBase(v, i), fId(d[2]))),
#D                ISumLS(j, m_by_v, Buf(Scat(fTensor(fId(d[1]), fBase(m_by_v, j), fId(v)))) *
#D                    ISum(i, v, Scat(fTensor(fId(d[1]), fBase(v, i))) * C[1] * Gath(fr)))
#D            )
    ),
    AxI_LS_R := rec(
        info := "(I|L)(A_rxs x I_n) -> Sum(Sum(SAG))",
        forTransposition := false,
        applicable := nt -> let(P := nt.params,
            P[4] = AVec
            and nt.isTag(1, AVecMemR)
            and IsInt(P[2]/nt.firstTag().v)
            and P[2]/nt.firstTag().v > 1
        ),
        children := nt -> [[
            When(nt.numTags() = 1,
                nt.params[1],
                nt.params[1].setTags(nt.params[1].withoutFirstTag())
            )
        ]],
        apply := (nt, C, cnt) -> let(
            v := nt.firstTag().v,
            i := Ind(v),
            m_by_v := nt.params[2]/v,
            j := Ind(m_by_v),
            d := C[1].dims(),
            fw := When(nt.params[3] = AVec,
                fTensor(fId(d[1]), fBase(m_by_v, j), fBase(v, i)),
                fTensor(fBase(m_by_v, j), fBase(v, i), fId(d[1]))
            ),

            ISumLS(j, m_by_v,
                ISum(i, v, Scat(fw) * C[1] * Gath(fTensor(fId(d[2]), fBase(v, i))))
                * Buf(Gath(fTensor(
                    fId(d[2]), fBase(m_by_v, j), fId(v)
                )))
            )
        )

#D        isApplicable := P -> P[4].isVec and Length(P[5]) > 0 and P[5][1].isMemR and IsInt(P[2]/P[5][1].v) and P[2]/P[5][1].v>1,
#D        allChildren := P -> let(pv:=P[5], v:=pv[1].v, d:=P[1].dims(), [[When(Length(pv)=1, P[1], P[1].setpv(Drop(pv, 1)))]]),
#D        rule := (P, C) -> let(
#D                v:=P[5][1].v, i:=Ind(v), m_by_v := P[2]/v, j:=Ind(m_by_v), d:=C[1].dims(),
#D                fw:=When(P[3].isVec, fTensor(fId(d[1]), fBase(m_by_v, j), fBase(v, i)), fTensor(fBase(m_by_v, j), fBase(v, i), fId(d[1]))),
#D                ISumLS(j, m_by_v,
#D                    ISum(i, v, Scat(fw) * C[1] * Gath(fTensor(fId(d[2]), fBase(v, i)))) *
#D                            Buf(Gath(fTensor(fId(d[2]), fBase(m_by_v, j), fId(v)))))
#D            )
    ),
    LS_drop := rec(
        info := "drop tag",
        forTransposition := false,
        applicable := nt -> let(P := nt.params,
            (nt.isTag(1, AVecMemL) or nt.isTag(1, AVecMemR))
            and (
                (not IsInt(P[2]/nt.firstTag().v))
                or P[2]/nt.firstTag().v = 1
            )
        ),
        children := nt -> let(P := nt.params,
            [[ TTensorI(P[1], P[2], P[3], P[4]) ]]
        ),
        apply := (nt, C, cnt) -> C[1],

#D        isApplicable := P -> Length(P[5]) > 0 and (P[5][1].isMemL or P[5][1].isMemR) and
#D                            ((not IsInt(P[2]/P[5][1].v)) or P[2]/P[5][1].v=1),
#D        allChildren := P -> [[TTensorI(P[1], P[2], P[3], P[4], [])]],
#D        rule := (P, C) -> C[1],
    )
));

########################################################################
#   TCompose rules
NewRulesFor(TCompose, rec(
    TCompose_LS_L := rec(
        info := "TCompose loop splitting left",
        forTransposition := false,
        applicable := nt -> nt.isTag(1, AVecMemL),
        children := nt -> [ Concat(
            [ nt.params[1][1].setTags(nt.getTags()) ],
            Drop(nt.params[1], 1)
        )],
        apply := (nt, C, cnt) -> Compose(C)
#D        isApplicable := P -> Length(P[2]) > 0 and P[2][1].isMemL,
#D        allChildren := P -> [Concat([P[1][1].setpv(P[2])], Drop(P[1], 1))],
#D        rule := (P, C) -> Compose(C)
    ),
    TCompose_LS_R := rec(
        info := "TCompose loop splitting right",
        forTransposition := false,
        applicable := nt -> nt.isTag(1, AVecMemR),
        children := nt -> [ Concat(
            DropLast(nt.params[1], 1),
            [
                nt.params[1][Length(nt.params[1])].setTags(nt.getTags())
            ]
        )],
        apply := (nt, C, cnt) -> Compose(C)

#D        isApplicable := P -> Length(P[2]) > 0 and P[2][1].isMemR,
#D        allChildren := P -> [Concat(DropLast(P[1], 1), [P[1][Length(P[1])].setpv(P[2])])],
#D        rule := (P, C) -> Compose(C)
    )
));


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

Class(DWT, NonTerminal, DataNonTerminalMixin, rec(
    abbrevs := [
       (n,w) -> Checked(IsPosInt(n), IsEvenInt(n), n>=2, IsList(w), Length(w)=2,
	   [n, toFiltFunc(w[1]), toFiltFunc(w[2])]),
       (n,g,h) -> Checked(IsPosInt(n), IsEvenInt(n), n>=2,
	   [n, toFiltFunc(g), toFiltFunc(h)]),
    ],
    isReal := self >> true,

    setData := meth(self, gdata, hdata)
       self.params[2][1] := gdata; self.params[3][1] := hdata; return self;
    end,

    dims := self >> let(n:=self.params[1], k:=self.params[2][1].domain(),
                        [n, n + k - 2]),

    terminate := self >> let(
	n := self.params[1],
	g := self.params[2],
	h := self.params[3],
	VStack(Filt(n/2,g[1],g[2],2).terminate(), Filt(n/2,h[1],h[2],2).terminate())),

    HashId := self >> [ self.params[1], self.params[2][1].domain() ]
));

KnownCoeffs := filtfunc -> ObjId(filtfunc[1])=FList or
                           (ObjId(filtfunc[1])=FData and IsBound(filtfunc[1].var.value));

RulesFor(DWT, rec(
    #F DWT_Base: (base case)
    #F
    #F Computes DWT by definition
    #F
    DWT_Filt := rec(
	forTransposition := true,
	limit            := -1, # means no limit
	isApplicable     := (self, P) >> When(self.limit < 0, true,
	    P[1] <= self.limit or P[2][1].domain() <= self.limit),

	allChildren := P -> let(n := P[1], g := P[2], h := P[3],
	    [[ Filt(n/2,g[1],g[2],2), Filt(n/2,h[1],h[2],2) ]]),

	rule := (P, C) -> VStack(C[1], C[2])
    ),

    DWT_Base2 := rec(
	isApplicable := P -> true,
	rule := (P, C) -> let(n := P[1], g := P[2][1].tolist(), h := P[3][1].tolist(),
	    diff := Length(h) - Length(g),
	    gg := Cond(diff <= 0, g, Concatenation(g, Replicate(diff,0))),
	    hh := Cond(diff >= 0, h, Concatenation(h, Replicate(-diff,0))),
	    k := Maximum(Length(g), Length(h)),
	    When(n=1, Mat([gg,hh]), L(n,2)*RowTensor(n/2,k-2,Mat([gg,hh]))))
    ),

    DWT_Base4 := rec(
	isApplicable := P -> P[1] mod 4 = 0,
	rule := (P, C) -> let(n := P[1], g := P[2][1].tolist(), h := P[3][1].tolist(),
	    diff := Length(h) - Length(g),
	    gg := Cond(diff <= 0, g, Concatenation(g, Replicate(diff,0))),
	    hh := Cond(diff >= 0, h, Concatenation(h, Replicate(-diff,0))),
	    k := Maximum(Length(g), Length(h)),
	    When(n=1, Mat([gg,hh]), L(n,2)*RowTensor(n/4,k-2,BB(RowTensor(2,k-2,Mat([gg,hh]))))))
    ),
    DWT_Base8 := rec(
	isApplicable := P -> P[1] mod 8 = 0,
	rule := (P, C) -> let(n := P[1], g := P[2][1].tolist(), h := P[3][1].tolist(),
	    diff := Length(h) - Length(g),
	    gg := Cond(diff <= 0, g, Concatenation(g, Replicate(diff,0))),
	    hh := Cond(diff >= 0, h, Concatenation(h, Replicate(-diff,0))),
	    k := Maximum(Length(g), Length(h)),
	    When(n=1, Mat([gg,hh]), L(n,2)*RowTensor(n/8,k-2,BB(RowTensor(4,k-2,Mat([gg,hh]))))))
    ),

    #F DWT_Lifting: lifting steps, does not work yet
    #F
    DWT_Lifting := rec(
        info             := "DWT(n,g,h) -> Lifting steps",
        forTransposition := false,
        isApplicable     := P -> P[1]>2 and P[1] mod 2 =0 and KnownCoeffs(P[2]),
	switch := false,
        allChildren := function ( P )
            local n,g,h,vg,vh, lift_schemes,children,scheme,step,filts,pol;
            n       := P[1];
	    [g, h]  := [List(P[2][1].tolist(), EvalScalar), List(P[3][1].tolist(), EvalScalar)];
	    [vg,vh] := [P[2][2], P[3][2]];

            lift_schemes := Copy(HashLookupWav(HashTableWavelets, [[g,h],[vg,vh]]));
	    children := [];

            for scheme in lift_schemes do
	        filts := [];
	        for step in Drop(scheme,1) do
		    pol := FillZeros(ListPoly(step));
		    Add(filts, Filt(n/2, pol[1], pol[2]));
		od;
                # attach the indicator of the type of the first liftings step
		filts[1].lift := scheme[1];
		Add(children, filts);
            od;
            return children;
        end,

        rule := function ( P, C, Nonterms )
            local n, i, ind, b, l, M, ind0, ind1, lstep;
            n := P[1];
            b := Nonterms[1].lift;
            l:=Length(C);

	    ind0 := fTensor(fBase(2,0), fId(Rows(C[3])));
	    ind1 := fTensor(fBase(2,1), fId(Rows(C[3])));
	    lstep := (f,b) -> When(b=0,
		LStep(Scat(ind0) * f * Gath(ind1)),
		LStep(Scat(ind1) * f * Gath(ind0)));

            M := When(b < 0,
		SUM(Scat(ind0)*C[2]*Gath(ind1), Scat(ind1)*C[1]*Gath(ind0)),
		SUM(Scat(ind0)*C[2]*Gath(ind0), Scat(ind1)*C[1]*Gath(ind1)));

	    b := (b+2) mod 2; # make b positive
	    for i in [1 .. l-2] do
	        M := M * lstep(C[i+2], b);
		b := (b+1) mod 2;
            od;

	    return M.child(1) * Inplace(Compose(Drop(M.children(),1))) * L(n,2);
       end
    )
));