# Modreduce
#
# Author: Howard Cheng
#
# License: CC BY-SA (http://creativecommons.org/licenses/by-sa/4.0/)
#
# The software is provided "AS IS" and there is no warranty of any kind.
#
# Implements the modular algorithm in
#
# Cheng, H. and Labahn, G. "Modular Computation for Matrices of Ore 
#   Polynomials." Computer Algebra 2006: Latest Advances in Symbolic 
#   Algorithms, pages 43-66, 2007. 
#
# Input:
#
#    F    : a matrix of skew polynomials over Z[t][Z;sigma,delta], where the
#           powers of Z are assumed to be on the right
#    Z    : the indeterminate
#    sigma: the automorphism, implemented as a function: (Z[t], t) -> Z[t]
#    delta: the derivation, implemented as a function: (Z[t], t) -> Z[t]
#    t    : the variable in the domain Z[t]
#
# Output:
#
#    M and R: where
#
#          M . F = R . Z^omega;
#
#    - the number of nonzero rows in R = rank of F;
#    - the rows in M corresponding to the zero rows in R form a basis for
#      the left nullspace of F
#
#   omega:
#          the final order achieved (always (mN+1) \cdot \vec e)
#   mu:
#          the final degree constraints achieved
#

Modreduce := proc(F :: Matrix, Z :: name, sigma, delta, t :: name)
  local M, R, m, n, N, omega, mu, J, 
        M_p, R_p, omega_p, mu_p, J_p, bound,
        smu, smu_p, tau, p, prod, i, j, unchanged, q, newM, newR, T, kappa,
        row;

  option `Copyright (c) 2005 by Howard Cheng`;

  # replace mod with symmetric mod
  `mod` := mods;

  # Initialization
  m, n := LinearAlgebra:-Dimensions(F);
  N := max(seq(seq(degree(F[i,j],Z),i=1..m),j=1..n));
  T, kappa := ComputeTKappa(F, Z, sigma, delta, t);

  userinfo(3, Modreduce, `T = `, T);
  userinfo(3, Modreduce, `kappa = `, kappa);
  
  # compute bound for early termination

  bound := ((m*N+1)*n)*kappa*(T+1);

  tau := 0;
  prod := 1;
  p := 30000;   # We start with a relatively large number
  while prod < bound do
    tau := tau+1;
    p := nextprime(p);
    prod := prod * p;
  od;

  userinfo(3, Modreduce, `bound = `, bound);
  userinfo(3, Modreduce, `tau = `, tau);

  # initialize before entering loop
  p := 30000;
  unchanged := 0;
  omega := Vector[row](n);
  mu := Vector[row](m);
  J := [];
  q := 1;                 # modulus for what has been reconstructed so far  

  # now do the loop
  while true do
    p := nextprime(p);

    userinfo(3, Modreduce, `Processing p = `, p);

    # check for unlucky primes
    if irem(lcoeff(sigma(t, t), t), p) = 0 then
      userinfo(3, Modreduce, `  unlucky: hc(sigma(t)) = 0 mod p`);
      next;
    fi;

    M_p, R_p, omega_p, mu_p, J_p := ReduceZpt(F, m, n, p, N, T, Z, 
                                              sigma, delta, t);

    if q > 1 and not LinearAlgebra:-Equal(omega, omega_p) then
      error `omega is not the same for different primes!  omega = `, omega,
            `omega_p = `, omega_p;
    fi;

    # now check for unlucky homomorphisms
    if (q = 1) or
       (LinearAlgebra:-Equal(mu, mu_p) and LinearAlgebra:-Equal(J, J_p)) then
      # seems to be lucky
      userinfo(3, Modreduce, `  seems to be lucky`);
      
      if q = 1 then
        mu, J, M, R, omega := mu_p, J_p, M_p, R_p, omega_p;
      else
        newM := MatrixCRA(M, M_p, q, p, Z);
        newR := MatrixCRA(R, R_p, q, p, Z);
        M_p, R_p := 0, 0;

        # check for early termination
        if LinearAlgebra:-Equal(M, newM) and LinearAlgebra:-Equal(R, newR) then
          unchanged := unchanged + 1;
          newM, newR := 0, 0;
        else
          unchanged := 0;
          M, R := newM, newR;
        fi;
        if unchanged >= tau then
          userinfo(3, Modreduce, `  early termination`);
          break;
        fi;
      fi;
      q := q*p;
    else
      # one of the is unlucky, figure out which one
      userinfo(3, Modreduce, `  unlucky prime(s) detected`);
      smu := add(mu[i], i=1..m);
      smu_p := add(mu_p[i], i=1..m);
      if smu_p < smu or 
         (smu_p = smu and (LexSmaller(J, J_p) or Closer(mu, mu_p, m))) then
        userinfo(3, Modreduce, `  p = `, p, ` is unlucky`);
      else
        userinfo(3, Modreduce, `  previous primes are unlucky`);
        mu, J, M, R, q := mu_p, J_p, M_p, R_p, p;
        unchanged := 0;
      fi;
    fi;
  od;

  for row from 1 to m do
    LinearAlgebra:-RowOperation(R, row, 1/Z^omega[row], inplace=true)
  od;
  LinearAlgebra:-Map(expand, R);
  return M, R, omega, mu;
end:

#
# ReduceZpt
#
# Computes the order basis and residual over Zp by modular computation.
# This function only takes care of the parts for detecting unlucky
# homomorphisms and for reconstructing the results.
#
# Input:
#
#    F    : a matrix of skew polynomials over Z[t][Z;sigma,delta], where the
#           powers of Z are assumed to be on the right
#    m, n : dimensions of matrix F
#    p    : the prime p
#    N    : degree(F, Z)
#    T    : a bound on the degree(Z^l * F, t) for all 0 <= l <= mN+1
#    Z    : the indeterminate
#    sigma: the automorphism, implemented as a function: (Z[t], t) -> Z[t]
#    delta: the derivation, implemented as a function: (Z[t], t) -> Z[t]
#    t    : the variable in the domain Z[t]
#
# Output:
#
#    M and R: where
#
#          M . F = R . Z^omega;
#
#    - the number of nonzero rows in R = rank of F;
#    - the rows in M corresponding to the zero rows in R form a basis for
#      the left nullspace of F
#
#   omega:
#          the final order achieved (always (mN+1) \cdot \vec e)
#   mu:
#          the final degree constraints achieved
#   J:
#          the set of columns used in elimination
#

ReduceZpt := proc(F :: Matrix, m, n, p, N, T, Z :: name, sigma, delta, 
                  t :: name)
  local M, R, newM, newR, KM, unchanged, alpha, omega, mu, J, q, deg, 
        M_a, R_a, omega_a, mu_a, J_a, i, j, smu, smu_a, Fc, Fv, zv;

  option `Copyright (c) 2005 by Howard Cheng`;

  # first we allocate a matrix to minimize memory management:
  #   (mN+1)n + m rows:
  #     - start with m rows, and each of the (mN+1)n steps adds at most one
  #       row
  #   ((m+1)N+2)n columns for K:
  #     - need to go to order (mN+1), but we had N+1 coefficients to start
  #   (mN+1)n + m columns for M
  KM := LinearAlgebra:-Modular:-Create(p, (m*N+1)*n+m, 
                                       ((m+1)*N+2)*n + (m*N+1)*n+m,
                                       integer[kernelopts(wordsize)/8]);

  # initialize before entering loop
  unchanged := 0;
  omega := Vector[row](n);
  mu := Vector[row](m);
  J := [];
  deg := 0;                 # degree of what has been reconstructed so far  
  q := 1;
  zv := Vector[row](n);

  # Fc is a matrix of the shifts of the rows of F so we don't have to
  # recompute this over different evaluation points.  Fv stores the number
  Fc := [seq(Matrix((m*N+1)*n+m, ((m+1)*N+2)*n), i=1..m)];
  for i to m do
    for j to ((m+1)*N+2)*n do
      Fc[i][1,j] := coeff(F[i, modp(j-1, n)+1], Z, iquo(j-1, n));
    od;
  od;

  Fv := Vector[row](m, 1);

  # now do the loop: try all evaluation points until we are done
  for alpha from 0 to p-1 do
    userinfo(4, Modreduce, `Processing alpha = `, alpha);

    M_a, R_a, omega_a, mu_a, J_a := ReduceZp('Fc', Fv, KM, m, n, p, alpha, N, Z, 
                                             sigma, delta, t);
    if LinearAlgebra:-Equal(omega_a, zv) then
      # unlucky, just do the next one
      next;
    fi;

    if deg > 0 and not LinearAlgebra:-Equal(omega, omega_a) then
      print(`omega = `, omega);
      print(`omega_a = `, omega_a);
      error `omega is not the same for different points!  omega = `, omega,
            `omega_a = `, omega_a;
    fi;

    # now check for unlucky homomorphisms
    if (deg = 0) or
       (LinearAlgebra:-Equal(mu, mu_a) and LinearAlgebra:-Equal(J, J_a)) then
      # seems to be lucky
      userinfo(4, Modreduce, `  seems to be lucky`);
      
      if deg = 0 then
        mu, J, M, R, omega := mu_a, J_a, M_a, R_a, omega_a;
      else
        newM := MatrixInterp(M, M_a, q, alpha, Z, t, p);
        newR := MatrixInterp(R, R_a, q, alpha, Z, t, p);
        M_a, R_a := 0, 0;

        # check for early termination
        if LinearAlgebra:-Equal(M, newM) and LinearAlgebra:-Equal(R, newR) then
          unchanged := unchanged + 1;
          newM, newR := 0, 0; 
        else
          unchanged := 0;
          M, R := newM, newR;
        fi;
        if unchanged >= T then
          userinfo(4, Modreduce, `  early termination`);
          return M, R, omega, mu, J;
        fi;
      fi;
      deg := deg+1;
      q := q * (t-alpha) mod p;
    else
      # one of the is unlucky, figure out which one
      userinfo(4, Modreduce, `  unlucky evaluation(s) detected`);
      smu := add(mu[i], i=1..m);
      smu_a := add(mu_a[i], i=1..m);

      if smu_a < smu or 
         (smu_a = smu and (LexSmaller(J, J_a) or Closer(mu, mu_a, m))) then
        userinfo(4, Modreduce, `  alpha = `, alpha, ` is unlucky`);
      else
        userinfo(4, Modreduce, `  previous points are unlucky`);
        mu, J, M, R, deg, q := mu_a, J_a, M_a, R_a, 1, t-alpha;
        unchanged := 0;
      fi;
    fi;
  od;

  # we have tried all evaluation points
  error `not enough lucky evaluation points mod p = `, p;
end:

#
# ReduceZp
#
# Computes the order basis and residual over Zp by performing Gaussian
# elimination on the striped Krylov matrix that is built incrementally.
#
# Input:
#
#    Fc   : a list of matrices, each containing the shifts of the coefficients
#           of a stripe of F(Z)
#    Fv   : a vector giving the number of rows of Fc computed in each stripe
#    KM   : an ((mN+1)n+m) x (((m+1)N+2)n + (mN+1)n+m) matrix for storing the 
#           striped Krylov matrix and augmented transformation matrix
#    m, n : dimensions of matrix F
#    p    : the prime p
#    alpha: the evaluation point
#    N    : degree(F, Z)
#    Z    : the indeterminate
#    sigma: the automorphism, implemented as a function: (Z[t], t) -> Z[t]
#    delta: the derivation, implemented as a function: (Z[t], t) -> Z[t]
#    t    : the variable in the domain Z[t]
#
# Output:
#
#    M and R: where
#
#          M . F = R . Z^omega;
#
#    - the number of nonzero rows in R = rank of F;
#    - the rows in M corresponding to the zero rows in R form a basis for
#      the left nullspace of F
#
#   omega:
#          the final order achieved (always (mN+1) \cdot \vec e)
#   mu:
#          the final degree constraints achieved
#   J:
#          the set of columns used in elimination
#    

ReduceZp := proc(Fc :: name, Fv :: Vector, KM :: Matrix, m, n, p, alpha, N, 
                 Z :: name, sigma, delta, t :: name)
  local i, j, k, l, pi, omega, mu, J, rsize, csize, AddRow, 
        M, R, rank, found, mult, inv, det, rowindex;

  # add the next row of stripe i to K, and advance the row
  AddRow := proc(i)
    local j;

    # see if the row we need is already computed, it should only be
    # off by 1.
    if Fv[i] < mu[i]+1 then
      for j from csize to n+1 by -1 do
        Fc[i][mu[i]+1, j] := modp(sigma(Fc[i][mu[i],j-n], t) + 
                                  delta(Fc[i][mu[i],j], t), p);
      od;
      for j to n do
        Fc[i][mu[i]+1, j] := modp(delta(Fc[i][mu[i],j], t), p);
      od;
      Fv[i] := Fv[i]+1;
    fi;

    rsize := rsize+1;
    rowindex[i][mu[i]] := rsize;
    for j to csize do
      KM[rsize, j] := modp(subs(t=alpha, Fc[i][mu[i]+1,j]), p);
    od;
    for j to (m*N+1)*n+m do
      KM[rsize, j+csize] := 0;
    od;
    KM[rsize, rsize+csize] := 1;
  end:

  J := { NULL };
  mu := Vector[row](m);
  omega := Vector[row](n);
  rsize := 0;                    # number of rows currently in use in K
  csize := ((m+1)*N+2)*n;        # number of cols currently in use in K
  det := 1;
  rank := 0;

  # indexed by i and l, gives the index of the row in K containing the row
  # for the i-th stripe of Z^l * F.  Allocated for efficiency.
  rowindex := [seq(array(0..(m*N+1)*n+m), i=1..m)];

  # Initialize KM
  for i to m do
    AddRow(i);
  od;
  # now start eliminating each column
  for j to (m*N+1)*n do
    # look for pivot in the current rows
    found := false;
    for l to m do
      if KM[rowindex[l][mu[l]], j] <> 0 and 
         (not found or mu[l] < mu[pi]) then
        pi := l;
        found := true;
      fi;
    od;

    # if column is linearly dependent, do nothing
    if not found then
      userinfo(4, Modreduce, `  column `, j, ` dependent`);
      next;
    fi;

#    userinfo(4, Modreduce, `  column `, j, ` pivot `, pi);	
    # swap the pivot row
    if rowindex[pi][mu[pi]] <> rank+1 then
      # look for a row which is in rank+1 position
      found := false;
      for i to m while not found do
        for k from 0 to mu[i] while not found do
          if rowindex[i][k] = rank+1 then
            found := true;
            det := modp(-det, p);
            LinearAlgebra:-Modular:-Swap(p, KM, rowindex[i][k], 
                                            KM, rowindex[pi][mu[pi]]);
            LinearAlgebra:-Modular:-Swap(p, KM, 1..-1, 
                                            rowindex[i][k]+csize, 
                                            KM, 1..-1, 
                                            rowindex[pi][mu[pi]]+csize);
            rowindex[i][k] := rowindex[pi][mu[pi]];
            rowindex[pi][mu[pi]] := rank+1;
          fi;
        od;
      od;
      if not found then
        error `cannot find a row to swap`;
      fi;
    fi;
    
    # now add the next row, and eliminate it using previous pivots
    mu[pi] := mu[pi]+1;
    AddRow(pi);
    for i from pi+1 to m do
      if type(mu[i]+1, odd) then
        det := modp(-det, p);
      fi;
    od;
    for i to rank do
      found := false;
      for k from i to j-1 while not found do
        if KM[i,k] <> 0 then
          found := true;
          if KM[rsize,k] = 0 then
            next;
          fi;
          mult := modp(-KM[rsize,k]/KM[i,k], p);
          LinearAlgebra:-Modular:-AddMultiple(p, mult, 
                                              KM, rsize, k..rsize+csize,
                                              KM, i, k..rsize+csize, 
                                              KM, rsize, k..rsize+csize,
                                              'sparse');
        elif KM[rsize,k] <> 0 then
          # pivot rows are used out of sequence.  This doesn't necessarily
          # mean that we have an unlucky evaluation point and we may be
          # able to fix it.  But since this doesn't happen often we will
          # just return from here with some values that's guarantee to be
          # unlucky (because mu = 0)
          userinfo(4, Modreduce, `   Pivot rows out of sequence.`);
          return Matrix(m, m), Matrix(m, n), Vector[row](n), 
                 Vector[row](m), Vector[row](1);
        fi;
      od;
    od;

    # eliminate the remaining rows using pivot row
    det := modp(det * KM[rowindex[pi][mu[pi]-1], j], p);
    inv := modp(-KM[rowindex[pi][mu[pi]-1], j]^(-1), p);
    for i from rank+2 to rsize do
      mult := modp(inv * KM[i,j], p);
      LinearAlgebra:-Modular:-AddMultiple(p, mult, KM, i, j..rsize+csize,
                                          KM, rank+1, j..rsize+csize, 
                                          KM, i, j..rsize+csize,
                                          'sparse');
    od;

    rank := rank+1;
    J := J union {j};
    omega[modp(j-1, n)+1] := omega[modp(j-1, n)+1] + 1;
  od;

  # normalize answer to determinant
  for i to m do
    k := rowindex[i][mu[i]];
    mult := modp(det / KM[k, k+csize], p);
    LinearAlgebra:-Modular:-Multiply(p, mult, KM, k, KM, k);
  od;

  # now we extract M and R
  M := Matrix(m, m,
              (i,j) -> add(KM[rowindex[i][mu[i]], 
                              rowindex[j][k]+csize]*Z^k, 
                           k=0..mu[j]));
  R := Matrix(m, n, 
              (i,j) -> add(KM[rowindex[i][mu[i]], j+k*n]*Z^k, 
                           k=0..csize/n-1));
  return M, R, omega, mu, Vector[row](sort([op(J)]));
end:


#
# ComputeTKappa
#
# Compute the bounds T and kappa as defined in the paper.
#

ComputeTKappa := proc(F :: Matrix, Z :: name, sigma, delta, t :: name)
  local m, n, N, T, kappa, F2, l, i, j;

  option `Copyright (c) 2005 by Howard Cheng`;

  m, n := LinearAlgebra:-Dimensions(F);
  N := max(seq(seq(degree(F[i,j],Z),i=1..m),j=1..n));
  F2 := copy(F);

  T, kappa := 0, 0;

  T := max(T, seq(seq(degree(collect(F2[i,j], t), t), i=1..m), j=1..n));
  kappa := max(kappa, seq(seq(norm(expand(F2[i,j]), infinity), i=1..m), 
                          j=1..n));
  for l from 1 to m*N+1 do
    LinearAlgebra:-Map(f -> MultOreZ(f, Z, t, sigma, delta, m*N+1), F2);
    T := max(T, seq(seq(degree(collect(F2[i,j], t), t), i=1..m), j=1..n));
    kappa := max(kappa, seq(seq(norm(expand(F2[i,j]), infinity), i=1..m), 
                            j=1..n));
  od;
  return T, kappa;
end:

#
# MultOreZ
#
# Multiply an Ore polynomial f by Z (as defined by sigma and delta), and 
# truncate results to Z^(order-1).
# It is assumed that f is collected in Z already.
#

MultOreZ := proc(f, Z, t, sigma, delta, order)
  local r, i;

  option `Copyright (c) 2005 by Howard Cheng`;

  r := collect(add(sigma(coeff(f, Z, i),t)*Z^(i+1) + 
                   delta(coeff(f, Z, i),t)*Z^i, 
                   i=0..min(degree(f, Z), order-1)), Z);

  if degree(r, Z) >= order then
    return r - lcoeff(r, Z)*Z^order;
  else
    return r;
  fi;
end:

#
# LexSmaller
#
# Determine if one vector is lexicographically smaller than another.
# It is assumed that the two vectors have the same dimensions.
#

LexSmaller := proc(v1, v2)
  local i;

  for i to LinearAlgebra:-Dimension(v1) do
    if v1[i] < v2[i] then
      return true;
    elif v1[i] > v2[i] then
      return false;
    fi;
  od;
  return false;
end:

#
# Closer
#
# Determine if the first vector is lexicographically closer to a "normal"
# path then the second one.  Assumes that the two vectors have the same
# sum of components but different.
#

Closer := proc(v1, v2, m)
  local i, j, d1, d2, w;

  option `Copyright (c) 2005 by Howard Cheng`;

  w := Vector[row](m);
  j := 1;
  while true do
    # this loop should return eventually if the two vectors are
    # different
    d1 := add(max(0,w[i]-v1[i]),i=1..m);
    d2 := add(max(0,w[i]-v2[i]),i=1..m);
    if d1 < d2 then
      return true;
    elif d1 > d2 then
      return false;
    fi;
    w[j] := w[j]+1;
    j := modp(j, m) + 1;
  od;
end:

#
# MatrixCRA
#
# Given a matrix M1 correct mod m1 (product of primes) and a new image Mp
# mod p, return M such that M = M1 mod m1 and M = Mp mod p
#

MatrixCRA := proc(M1 :: Matrix, Mp :: Matrix, m1, p, Z)
  option `Copyright (c) 2005 by Howard Cheng`;

  return Matrix(LinearAlgebra:-Dimensions(M1), 
                (i,j) -> collect(chrem([expand(M1[i,j]), expand(Mp[i,j])], 
                                       [m1, p]), Z));
end:

#
# MatrixInterp
#
# Given a matrix M1 correct mod m1 (product of (t-alpha)'s) and a new image Ma
# mod (x-alpha), return M such that M = M1 mod m1 and M = Ma mod (t-alpha).
# Everything is computed mod p.
#

MatrixInterp := proc(M1 :: Matrix, Ma :: Matrix, m1, alpha, Z :: name, 
                     t :: name, p)
  local inv;
  option `Copyright (c) 2005 by Howard Cheng`;

  inv := modp(expand(subs(t=alpha, m1)^(-1)*m1), p);
  return Matrix(LinearAlgebra:-Dimensions(M1),
                (i,j) -> PolyCRA(M1[i,j], Ma[i,j], inv, alpha, Z, t, p));
end:

#
# PolyCRA
#
# Chinese remaindering/interpolation on each coefficient of 
# the polynomials p1 and p2 (in Z)
#

PolyCRA := proc(p1, p2, inv, alpha, Z :: name, t :: name, p)
  return modp(collect(modp(p1 + (p2 - subs(t=alpha,p1))* inv, p), Z, expand), p);
end:
