[system] / trunk / pg / lib / Value / Formula.pm Repository:
ViewVC logotype

View of /trunk/pg/lib/Value/Formula.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 4991 - (download) (as text) (annotate)
Fri Jun 8 02:09:21 2007 UTC (5 years, 11 months ago) by dpvc
File size: 15599 byte(s)
Update new() and make() methods to accept a context as the first
parameter (making it easier to create objects in a given context
without having to resort to a separate call to coerce them to the
given context after the fact).

    1 ###########################################################################
    2 #
    3 #  Implements the Formula class.
    4 #
    5 package Value::Formula;
    6 my $pkg = 'Value::Formula';
    7 
    8 use strict;
    9 our @ISA = qw(Parser Value);
   10 
   11 my $UNDEF = bless {}, "UNDEF"; # used for undefined points
   12 
   13 #
   14 #  Call Parser to make the new Formula
   15 #
   16 sub new {
   17   my $self = shift;
   18   my $f = $self->SUPER::new(@_);
   19   foreach my $id ('open','close') {$f->{$id} = $f->{tree}{$id}}
   20   return $f;
   21 }
   22 
   23 #
   24 #  Create a new Formula with no string
   25 #    (we'll fill in its tree by hand)
   26 #
   27 sub blank {shift->SUPER::new(@_)}
   28 
   29 #
   30 #  with() changes tree element as well
   31 #    as the formula itself.
   32 #
   33 sub with {
   34   my $self = shift; my %hash = @_;
   35   foreach my $id (keys(%hash)) {
   36     $self->{tree}{$id} = $hash{$id};
   37     $self->{$id} = $hash{$id};
   38   }
   39   return $self;
   40 }
   41 
   42 #
   43 #  Get the type from the tree
   44 #
   45 sub typeRef {(shift)->{tree}->typeRef}
   46 sub length {(shift)->{tree}->typeRef->{length}}
   47 
   48 sub isZero {(shift)->{tree}{isZero}}
   49 sub isOne {(shift)->{tree}{isOne}}
   50 
   51 sub isSetOfReals {(shift)->{tree}->isSetOfReals}
   52 sub canBeInUnion {(shift)->{tree}->canBeInUnion}
   53 
   54 ############################################
   55 #
   56 #  Create a BOP from two operands
   57 #
   58 #  Get the context and variables from the left and right operands
   59 #    if they are formulas
   60 #  Make them into Value objects if they aren't already.
   61 #  Convert '+' to union for intervals or unions.
   62 #  Make a new BOP with the two operands.
   63 #  Record the variables.
   64 #  Evaluate the formula if it is constant.
   65 #
   66 sub bop {
   67   my $bop = shift;
   68   my ($self,$l,$r) = Value::checkOpOrder(@_);
   69   my $class = ref($self) || $self;
   70   my $call = $self->context->{method}{$bop};
   71   my $formula = $self->blank($self->context);
   72   my $parser = $formula->{context}{parser};
   73   if (ref($r) eq $class || ref($r) eq $pkg) {
   74     $formula->{context} = $r->{context};
   75     $r = $r->{tree}->copy($formula);
   76   }
   77   if (ref($l) eq $class || ref($l) eq $pkg) {
   78     $formula->{context} = $l->{context};
   79     $l = $l->{tree}->copy($formula);
   80   }
   81   $l = $self->new($l) if (!ref($l) && Value::getType($formula,$l) eq "unknown");
   82   $r = $self->new($r) if (!ref($r) && Value::getType($formula,$r) eq "unknown");
   83   $l = $parser->{Value}->new($formula,$l) unless ref($l) =~ m/^Parser::/;
   84   $r = $parser->{Value}->new($formula,$r) unless ref($r) =~ m/^Parser::/;
   85   $bop = 'U' if $bop eq '+' &&
   86     ($l->type =~ m/Interval|Set|Union/ || $r->type =~ m/Interval|Set|Union/);
   87   $formula->{tree} = $parser->{BOP}->new($formula,$bop,$l,$r);
   88   $formula->{variables} = $formula->{tree}->getVariables;
   89   return $formula;
   90 }
   91 
   92 sub add   {bop('+',@_)}
   93 sub sub   {bop('-',@_)}
   94 sub mult  {bop('*',@_)}
   95 sub div   {bop('/',@_)}
   96 sub power {bop('**',@_)}
   97 sub cross {bop('><',@_)}
   98 
   99 #
  100 #  Make dot work for vector operands
  101 #
  102 sub _dot   {
  103   my ($l,$r,$flag) = @_;
  104   if ($l->promotePrecedence($r)) {return $r->_dot($l,!$flag)}
  105   return bop('.',@_) if ($l->type eq 'Vector' || $l->{isVector}) &&
  106      Value::isValue($r) && ($r->type eq 'Vector' || $r->{isVector});
  107   $l->SUPER::_dot($r,$flag);
  108 }
  109 
  110 sub pdot {'('.(shift->stringify).')'}
  111 
  112 #
  113 #  Call the Parser::Function call function
  114 #
  115 sub call {
  116   my $self = shift; my $name = shift;
  117   Parser::Function->call($name,$self);
  118 }
  119 
  120 ############################################
  121 #
  122 #  Form the negation of a formula
  123 #
  124 sub neg {
  125   my $self = shift;
  126   my $formula = $self->blank($self->context);
  127   $formula->{variables} = $self->{variables};
  128   $formula->{tree} = $formula->{context}{parser}{UOP}->new($formula,'u-',$self->{tree}->copy($formula));
  129   return $formula;
  130 }
  131 
  132 #
  133 #  Form the function atan2 function call on two operands
  134 #
  135 sub atan2 {
  136   my ($self,$l,$r) = Value::checkOpOrder(@_);
  137   Parser::Function->call('atan2',$l,$r);
  138 }
  139 
  140 sub sin  {shift->call('sin',@_)}
  141 sub cos  {shift->call('cos',@_)}
  142 sub abs  {shift->call('abs',@_)}
  143 sub exp  {shift->call('exp',@_)}
  144 sub log  {shift->call('log',@_)}
  145 sub sqrt {shift->call('sqrt',@_)}
  146 
  147 sub twiddle {shift->call('conj',@_)}
  148 
  149 ############################################
  150 #
  151 #  Compare two functions for equality
  152 #
  153 sub compare {
  154   my ($l,$r) = @_; my $self = $l;
  155   $r = $self->Package("Formula")->new($self->context,$r) unless Value::isFormula($r);
  156   Value::Error("Functions from different contexts can't be compared")
  157     unless $l->{context} == $r->{context};
  158 
  159   #
  160   #  Get the test points and evaluate the functions at those points
  161   #
  162   ##  FIXME: Check given points for consistency
  163   my $points  = $l->{test_points} || $l->createRandomPoints(undef,$l->{test_at});
  164   my $lvalues = $l->{test_values} || $l->createPointValues($points,1,1);
  165   my $rvalues = $r->createPointValues($points,0,1,$l->{checkUndefinedPoints});
  166   #
  167   # Note: $l is bigger if $r can't be evaluated at one of the points
  168   return 1 unless $rvalues;
  169 
  170   my ($i, $cmp);
  171 
  172   #
  173   #  Handle adaptive parameters:
  174   #    Get the tolerances, and check each adapted point relative
  175   #    to the ORIGINAL correct answer.  (This will have to be
  176   #    fixed if we ever do adaptive parameters for non-real formulas)
  177   #
  178   if ($l->AdaptParameters($r,$self->{context}->variables->parameters)) {
  179     my $avalues = $l->{test_adapt};
  180     my $tolerance    = $self->getFlag('tolerance',1E-4);
  181     my $isRelative   = $self->getFlag('tolType','relative') eq 'relative';
  182     my $zeroLevel    = $self->getFlag('zeroLevel',1E-14);
  183     my $zeroLevelTol = $self->getFlag('zeroLevelTol',1E-12);
  184     foreach $i (0..scalar(@{$lvalues})-1) {
  185       my $tol = $tolerance;
  186       my ($lv,$rv,$av) = ($lvalues->[$i]->value,$rvalues->[$i]->value,$avalues->[$i]->value);
  187       if ($isRelative) {
  188   if (abs($lv) <= $zeroLevel) {$tol = $zeroLevelTol}
  189                          else {$tol *= abs($lv)}
  190       }
  191       return $rv <=> $av unless abs($rv - $av) < $tol;
  192     }
  193     return 0;
  194   }
  195 
  196   #
  197   #  Look through the two lists of values to see if they are equal.
  198   #  If not, return the comparison of the first unequal value
  199   #    (not good for < and >, but OK for ==).
  200   #
  201   my $domainError = 0;
  202   foreach $i (0..scalar(@{$lvalues})-1) {
  203     if (ref($lvalues->[$i]) eq 'UNDEF' ^ ref($rvalues->[$i]) eq 'UNDEF') {$domainError = 1; next}
  204     $cmp = $lvalues->[$i] <=> $rvalues->[$i];
  205     return $cmp if $cmp;
  206   }
  207   $l->{domainMismatch} = $domainError;  # return this value
  208 }
  209 
  210 #
  211 #  Create the value list from a given set of test points
  212 #
  213 sub createPointValues {
  214   my $self = shift;
  215   my $points = shift || $self->{test_points} || $self->createRandomPoints;
  216   my $showError = shift; my $cacheResults = shift;
  217   my @vars   = $self->{context}->variables->variables;
  218   my @params = $self->{context}->variables->parameters;
  219   my @zeros  = (0) x scalar(@params);
  220   my $f = $self->{f}; $f = $self->{f} = $self->perlFunction(undef,[@vars,@params]) unless $f;
  221   my $checkUndef = scalar(@params) == 0 && (shift || $self->getFlag('checkUndefinedPoints',0));
  222 
  223   my $values = []; my $v;
  224   foreach my $p (@{$points}) {
  225     $v = eval {&$f(@{$p},@zeros)};
  226     if (!defined($v) && !$checkUndef) {
  227       return unless $showError;
  228       Value::Error("Can't evaluate formula on test point (%s)",join(',',@{$p}));
  229     }
  230     push @{$values}, (defined($v)? Value::makeValue($v,context=>$self->context): $UNDEF);
  231   }
  232   if ($cacheResults) {
  233     $self->{test_points} = $points;
  234     $self->{test_values} = $values;
  235   }
  236   return $values;
  237 }
  238 
  239 #
  240 #  Create the adapted value list for the test points
  241 #
  242 sub createAdaptedValues {
  243   my $self = shift;
  244   my $points = shift || $self->{test_points} || $self->createRandomPoints;
  245   my $showError = shift;
  246   my @vars   = $self->{context}->variables->variables;
  247   my @params = $self->{context}->variables->parameters;
  248   my $f = $self->{f}; $f = $self->{f} = $self->perlFunction(undef,[@vars,@params]) unless $f;
  249 
  250   my $values = []; my $v;
  251   my @adapt = @{$self->{parameters}};
  252   foreach my $p (@{$points}) {
  253     $v = eval {&$f(@{$p},@adapt)};
  254     if (!defined($v)) {
  255       return unless $showError;
  256       Value::Error("Can't evaluate formula on test point (%s) with parameters (%s)",
  257        join(',',@{$p}),join(',',@adapt));
  258     }
  259     push @{$values}, Value::makeValue($v,context=>$self->context);
  260   }
  261   $self->{test_adapt} = $values;
  262 }
  263 
  264 #
  265 #  Create a list of random points, making sure that the function
  266 #  is defined at the given points.  Error if we can't find enough.
  267 #
  268 sub createRandomPoints {
  269   my $self = shift; my $context = $self->context;
  270   my ($num_points,$include) = @_; my $cacheResults = !defined($num_points);
  271   $num_points = int($self->getFlag('num_points',5)) unless defined($num_points);
  272   $num_points = 1 if $num_points < 1;
  273 
  274   my @vars   = $context->variables->variables;
  275   my @params = $context->variables->parameters;
  276   my @limits = $self->getVariableLimits(@vars);
  277   my @make   = $self->getVariableTypes(@vars);
  278   my @zeros  = (0) x scalar(@params);
  279   my $f = $self->{f}; $f = $self->{f} = $self->perlFunction(undef,[@vars,@params]) unless $f;
  280   my $seedRandom = $context->flag('random_seed')? 'PGseedRandom' : 'seedRandom';
  281   my $getRandom  = $context->flag('random_seed')? 'PGgetRandom'  : 'getRandom';
  282   my $checkUndef = scalar(@params) == 0 && $self->getFlag('checkUndefinedPoints',0);
  283   my $max_undef  = $self->getFlag('max_undefined',$num_points);
  284 
  285   $self->$seedRandom;
  286   my $points = []; my $values = []; my $num_undef = 0;
  287   if ($include) {
  288     push(@{$points},@{$include});
  289     push(@{$values},@{$self->createPointValues($include,1,$cacheResults,$self->{checkundefinedPoints})});
  290   }
  291   my (@P,@p,$v,$i); my $k = 0;
  292   while (scalar(@{$points}) < $num_points+$num_undef && $k < 10) {
  293     @P = (); $i = 0;
  294     foreach my $limit (@limits) {
  295       @p = (); foreach my $I (@{$limit})
  296         {push @p, $self->Package("Real")->make($context,$self->$getRandom(@{$I}))}
  297       push @P, $make[$i++]->make($context,@p);
  298     }
  299     $v = eval {&$f(@P,@zeros)};
  300     if (!defined($v)) {
  301       if ($checkUndef && $num_undef < $max_undef) {
  302   push @{$points}, [@P];
  303   push @{$values}, $UNDEF;
  304   $num_undef++;
  305       }
  306       $k++;
  307     } else {
  308       push @{$points}, [@P];
  309       push @{$values}, Value::makeValue($v,context=>$context);
  310       $k = 0; # reset count when we find a point
  311     }
  312   }
  313 
  314   if ($k) {
  315     my $error = "Can't generate enough valid points for comparison";
  316     $error .= ':<div style="margin-left:1em">'.($context->{error}{message} || $@).'</div>'
  317       if ($self->getFlag('showTestPointErrors'));
  318     $error =~ s/ (in \S+ )?at line \d+.*//s;
  319     Value::Error($error);
  320   }
  321 
  322   return ($points,$values) unless $cacheResults;
  323   $self->{test_values} = $values;
  324   $self->{test_points} = $points;
  325 }
  326 
  327 #
  328 #  Get the array of variable limits
  329 #
  330 sub getVariableLimits {
  331   my $self = shift;
  332   my $userlimits = $self->{limits};
  333   if (defined($userlimits)) {
  334     $userlimits = [[[-$userlimits,$userlimits]]] unless ref($userlimits) eq 'ARRAY';
  335     $userlimits = [$userlimits] unless ref($userlimits->[0]) eq 'ARRAY';
  336     $userlimits = [$userlimits] if scalar(@_) == 1 && ref($userlimits->[0][0]) ne 'ARRAY';
  337     foreach my $I (@{$userlimits}) {$I = [$I] unless ref($I->[0]) eq 'ARRAY'};
  338   }
  339   $userlimits = [] unless $userlimits; my @limits;
  340   my $default;  $default = $userlimits->[0][0] if defined($userlimits->[0]);
  341   $default = $default || $self->getFlag('limits',[-2,2]);
  342   my $granularity = $self->getFlag('granularity',1000);
  343   my $resolution = $self->getFlag('resolution');
  344   my $i = 0;
  345   foreach my $x (@_) {
  346     my $def = $self->{context}{variables}{$x};
  347     my $limit = $userlimits->[$i++] || $def->{limits} || [];
  348     $limit = [$limit] if defined($limit->[0]) && ref($limit->[0]) ne 'ARRAY';
  349     push(@{$limit},$limit->[0] || $default) while (scalar(@{$limit}) < $def->{type}{length});
  350     pop(@{$limit}) while (scalar(@{$limit}) > $def->{type}{length});
  351     push @limits, $self->addGranularity($limit,$def,$granularity,$resolution);
  352   }
  353   return @limits;
  354 }
  355 
  356 #
  357 #  Add the granularity to the limit intervals
  358 #
  359 sub addGranularity {
  360   my $self = shift; my $limit = shift; my $def = shift;
  361   my $granularity = shift; my $resolution = shift;
  362   $granularity = $def->{granularity} || $granularity;
  363   $resolution = $def->{resolution} || $resolution;
  364   foreach my $I (@{$limit}) {
  365     my ($a,$b,$n) = @{$I}; $b = -$a unless defined $b;
  366     $I = [$a,$b,($n || $resolution || abs($b-$a)/$granularity)];
  367   }
  368   return $limit;
  369 }
  370 
  371 #
  372 #  Get the routines to make the coordinates of the points
  373 #
  374 sub getVariableTypes {
  375   my $self = shift;
  376   my @make;
  377   foreach my $x (@_) {
  378     my $type = $self->{context}{variables}{$x}{type};
  379     if ($type->{name} eq 'Number') {
  380       push @make,($type->{length} == 1)? 'Value::Formula::number': $self->Package("Complex");
  381     } else {
  382       push @make, $self->Package($type->{name});
  383     }
  384   }
  385   return @make;
  386 }
  387 
  388 #
  389 #  Fake object for making reals (rather than use overhead of Value::Real)
  390 #
  391 sub Value::Formula::number::make {shift; shift; shift->value}
  392 
  393 #
  394 #  Find adaptive parameters, if any
  395 #
  396 sub AdaptParameters {
  397   my $l = shift; my $r = shift;
  398   my @params = @_; my $d = scalar(@params);
  399   return 0 if $d == 0; return 0 unless $l->usesOneOf(@params);
  400   $l->Error("Adaptive parameters can only be used for real-valued functions")
  401     unless $l->{tree}->isRealNumber;
  402   #
  403   #  Get coefficient matrix of adaptive parameters
  404   #  and value vector for linear system
  405   #
  406   my ($p,$v) = $l->createRandomPoints($d);
  407   my @P = (0) x $d; my ($f,$F) = ($l->{f},$r->{f});
  408   my @A = (); my @b = ();
  409   foreach my $i (0..$d-1) {
  410     my @a = (); my @p = @{$p->[$i]};
  411     foreach my $j (0..$d-1) {
  412       $P[$j] = 1; push(@a,(&$f(@p,@P)-$v->[$i])->value);
  413       $P[$j] = 0;
  414     }
  415     push @A, [@a]; push @b, [(&$F(@p,@P)-$v->[$i])->value];
  416   }
  417   #
  418   #  Use MatrixReal1.pm to solve system of linear equations
  419   #
  420   my $M = MatrixReal1->new($d,$d); $M->[0] = \@A;
  421   my $B = MatrixReal1->new($d,1);  $B->[0] = \@b;
  422   ($M,$B) = $M->normalize($B);
  423   $M = $M->decompose_LR;
  424   if (($d,$B,$M) = $M->solve_LR($B)) {
  425     if ($d == 0) {
  426       #
  427       #  Get parameter values and recompute the points using them
  428       #
  429       my @a; my $i = 0; my $max = $l->getFlag('max_adapt',1E8);
  430       foreach my $row (@{$B->[0]}) {
  431   if (abs($row->[0]) > $max) {
  432     $max = Value::makeValue($max);
  433     $l->Error("Constant of integration is too large: %s\n(maximum allowed is %s)",
  434         $row->[0]->string,$max->string) if ($params[$i] eq 'C0');
  435     $l->Error("Adaptive constant is too large: %s = %s\n(maximum allowed is %s)",
  436         $params[$i],$row->[0]->string,$max->string);
  437   }
  438   push @a, $row->[0]; $i++;
  439       }
  440       $l->{parameters} = [@a];
  441       $l->createAdaptedValues;
  442       return 1;
  443     }
  444   }
  445   $l->Error("Can't solve for adaptive parameters");
  446 }
  447 
  448 sub usesOneOf {
  449   my $self = shift;
  450   foreach my $x (@_) {return 1 if $self->{variables}{$x}}
  451   return 0;
  452 }
  453 
  454 ##
  455 ##  debugging routine
  456 ##
  457 #sub main::Format {
  458 #  my $v = scalar(@_) > 1? [@_]: shift;
  459 #  $v = [%{$v}] if ref($v) eq 'HASH';
  460 #  return $v unless ref($v) eq 'ARRAY';
  461 #  my @V; foreach my $x (@{$v}) {push @V, main::Format($x)}
  462 #  return '['.join(",",@V).']';
  463 #}
  464 
  465 #
  466 #  Random number generator  (replaced by Value::WeBWorK.pm)
  467 #
  468 sub seedRandom {srand}
  469 sub getRandom {
  470   my $self = shift;
  471   my ($m,$M,$n) = @_; $n = 1 unless $n;
  472   return $m + $n*int(rand()*(int(($M-$m)/$n)+1));
  473 }
  474 
  475 ############################################
  476 #
  477 #  Check if the value of a formula is constant
  478 #    (could use shift->{tree}{isConstant}, but I don't trust it)
  479 #
  480 sub isConstant {
  481   my @vars = (%{shift->{variables}});
  482   return scalar(@vars) == 0;
  483 }
  484 
  485 ############################################
  486 #
  487 #  Provide output formats
  488 #
  489 sub stringify {
  490   my $self = shift;
  491   return $self->TeX if $self->getFlag('StringifyAsTeX');
  492   $self->string;
  493 }
  494 
  495 ###########################################################################
  496 
  497 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9