From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

package Math::GF;
use strict;
{ our $VERSION = '0.004'; }
use Moo;
use Ouch;
use constant MARGIN => 1.1;
has order => (is => 'ro');
has p => (is => 'ro');
has n => (is => 'ro');
has order_is_prime => (is => 'ro');
has element_class => (is => 'ro');
# The following are used only for extension fields
has sum_table => (is => 'ro');
has prod_table => (is => 'ro');
# neutral element for "+" operation
sub additive_neutral { return $_[0]->e(0) }
# factory method to create "all" elements in the field
sub all {
my $self = shift;
my $eclass = $self->element_class;
my $order = $self->order;
map { $eclass->new(field => $self, v => $_) } 0 .. ($order - 1);
} ## end sub all
# import a handy factory method into caller's package
sub import_builder {
my ($package, $order) = splice @_, 0, 2;
my %args = (@_ && ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
my $field = $package->new(order => $order);
my $builder = sub { return $field->e(@_) };
my $callpkg = caller($args{level} // 0);
my $name = $args{name} // (
$field->order_is_prime
? "GF_$order"
: join('_', 'GF', $field->p, $field->n)
);
no strict 'refs';
*{$callpkg . '::' . $name} = $builder;
return;
} ## end sub import_builder
# factory method to create "e"lements of the field
sub e {
my $self = shift;
my $ec = $self->element_class;
return $ec->new(field => $self, v => $_[0]) unless wantarray;
return map { $ec->new(field => $self, v => $_) } @_;
}
# neutral element for "*" operation
sub multiplicative_neutral { return $_[0]->e(1) }
sub BUILDARGS {
my ($class, %args) = @_;
ouch 500, 'missing order' unless exists $args{order};
my $order = $args{order};
ouch 500, 'undefined order' unless defined $order;
ouch 500, 'order MUST be integer and greater than 1'
unless $order =~ m{\A(?: [2-9] | [1-9]\d+)\z}mxs;
my ($p, $n) = __prime_power_decomposition($order);
ouch 500, 'order MUST be a power of a prime'
unless defined $p;
$args{p} = $p;
$args{n} = $n;
$args{order_is_prime} = ($n == 1);
if ($n == 1) {
$args{order_is_prime} = 1;
$args{element_class} = 'Math::GF::Zn';
delete @args{qw< sum_table prod_table >};
}
else {
$args{order_is_prime} = 0;
$args{element_class} = 'Math::GF::Extension';
@args{qw< sum_table prod_table >} = __tables($order);
}
return {%args};
} ## end sub BUILDARGS
sub __tables {
my $order = shift;
# Get the basic subfield
my ($p, $n) = __prime_power_decomposition($order);
my $Zp = Math::GF->new(order => $p);
my @Zp_all = $Zp->all;
my ($zero, $one) = ($Zp->additive_neutral, $Zp->multiplicative_neutral);
my $pirr = __get_irreducible_polynomial($Zp, $n);
my $polys = __generate_polynomials($Zp, $n);
my %id_for = map {; "$polys->[$_]" => $_ } 0 .. $#$polys;
my (@sum, @prod);
for my $i (0 .. $#$polys) {
my $I = $polys->[$i];
push @sum, \my @ts;
push @prod, \my @tp;
for my $j (0 .. $i) {
my $J = $polys->[$j];
my $sum = ($I + $J);
push @ts, $id_for{"$sum"};
my $prod = ($I * $J) % $pirr;
push @tp, $id_for{"$prod"};
}
}
return (\@sum, \@prod);
}
sub __generate_polynomials {
my ($field, $degree) = @_;
ouch 500, 'irreducible polynomial search only over Zn field'
unless $field->order_is_prime;
my $zero = $field->additive_neutral;
my $one = $field->multiplicative_neutral;
my @coeffs = ($zero) x ($degree + 1); # last one as a flag
my @retval;
while ($coeffs[-1] == $zero) {
push @retval, Math::Polynomial->new(@coeffs);
for (@coeffs) {
$_ = $_ + $one;
last unless $_ == $zero;
}
}
return \@retval;
}
sub __get_irreducible_polynomial {
my ($field, $degree) = @_;
ouch 500, 'irreducible polynomial search only over Zn field'
unless $field->order_is_prime;
my $zero = $field->additive_neutral;
my $one = $field->multiplicative_neutral;
my @coeffs = ($one, (($zero) x ($degree - 1)), $one);
while ($coeffs[-1] == $one) {
my $poly = Math::Polynomial->new(@coeffs);
return $poly if __rabin_irreducibility_test($poly);
for (@coeffs) {
$_ = $_ + $one;
last unless $_ == $zero; # wrapped up
}
}
ouch 500, "no monic irreducibile polynomial!"; # never happens
}
sub __to_poly {
my ($x, $n) = @_;
my @coeffs;
while ($x) {
push @coeffs, $x % $n;
$x = ($x - $coeffs[-1]) / $n;
}
push @coeffs, 0 unless @coeffs;
return Z_poly($n, @coeffs);
}
sub __rabin_irreducibility_test {
my $f = shift;
my $n = $f->degree;
my $one = $f->coeff_one;
my $pone = Math::Polynomial->monomial(0, $one);
my $x = Math::Polynomial->monomial(1, $one);
my $q = $one->n;
my $ps = __prime_divisors_of($n);
for my $pi (@$ps) {
my $ni = $n / $pi;
my $qni = $q**$ni;
my $h = (Math::Polynomial->monomial($qni, $one) - $x) % $f;
my $g = $h->gcd($f, 'mod');
#return if $g != $pone;
return if $g->degree > 1;
} ## end for my $pi (@$ps)
my $t = (Math::Polynomial->monomial($q**$n, $one) - $x) % $f;
return $t->degree == -1;
} ## end sub rabin_irreducibility_test
sub __prime_power_decomposition {
my $x = shift;
return if $x < 2;
return ($x, 1) if $x < 4;
my $p = __prime_divisors_of($x, 'first only please');
return ($x, 1) if $x == $p; # $x is prime
my $e = 0;
while ($x > 1) {
return if $x % $p; # not the only divisor!
$x /= $p;
++$e;
}
return ($p, $e);
} ## end sub __prime_power_decomposition
sub __prime_divisors_of {
my ($n, $first_only) = @_;
my @retval;
return if $n < 2;
for my $p (2, 3) { # handle cases for 2 and 3 first
next if $n % $p;
return $p if $first_only;
push @retval, $p;
$n /= $p until $n % $p;
}
my $p = 5; # tentative divisor, will increase through iterations
my $top = int(sqrt($n) + MARGIN); # top attempt for divisor
my $d = 2; # increase for $p, alternates between 4 and 2
while ($p <= $top) {
if ($n % $p == 0) {
return $p if $first_only;
unshift @retval, $p;
$n /= $p until $n % $p;
$top = int(sqrt($n) + MARGIN);
}
$p += $d;
$d = ($d == 2) ? 4 : 2;
} ## end while ($n > 1)
# exited with $n left as a prime... maybe
return $n if $first_only; # always in this case
push @retval, $n if $n > 1;
return \@retval;
} ## end sub prime_divisors_of
1;