File Coverage

blib/lib/Math/Symbolic/Custom/Equation.pm
Criterion Covered Total %
statement 273 405 67.4
branch 113 210 53.8
condition 37 75 49.3
subroutine 20 28 71.4
pod 14 14 100.0
total 457 732 62.4


line stmt bran cond sub pod time code
1             package Math::Symbolic::Custom::Equation;
2              
3 2     2   272392 use 5.006;
  2         15  
4 2     2   13 use strict;
  2         4  
  2         75  
5 2     2   11 use warnings;
  2         6  
  2         119  
6 2     2   23 use Carp;
  2         5  
  2         261  
7              
8             =pod
9              
10             =encoding utf8
11              
12             =head1 NAME
13              
14             Math::Symbolic::Custom::Equation - Work with equations of Math::Symbolic expressions
15              
16             =head1 VERSION
17              
18             Version 0.2
19              
20             =cut
21              
22             our $VERSION = '0.2';
23              
24             our $EQ_PH = 'EQ';
25              
26 2     2   669 use Math::Symbolic 0.613 qw(:all);
  2         181617  
  2         623  
27 2     2   2297 use Math::Symbolic::Custom::Collect 0.32;
  2         42449  
  2         22  
28 2     2   2051 use Math::Symbolic::Custom::Factor 0.13;
  2         27398  
  2         20  
29              
30             =head1 DESCRIPTION
31              
32             This class implements methods for equating two Math::Symbolic expressions, and performing various operations on that equation.
33              
34             =head1 EXAMPLE
35              
36             use strict;
37             use Math::Symbolic 0.613 qw(:all);
38             use Math::Symbolic::Custom::Equation 0.2;
39             use Math::Symbolic::Custom::Polynomial 0.3;
40             use Math::Symbolic::Custom::CollectSimplify 0.2;
41             Math::Symbolic::Custom::CollectSimplify->register();
42              
43             # Solve the simultaneous equations:-
44             # x - 2*y = 7
45             # x^2 + 4*y^2 = 37
46             my $eq1 = Math::Symbolic::Custom::Equation->new('x - 2*y = 7');
47             my $eq2 = Math::Symbolic::Custom::Equation->new('x^2 + 4*y^2 = 37');
48              
49             print "Solve the simultaneous equations:-\n\n";
50             print "\t[1]\t", $eq1->to_string(), "\n";
51             print "\t[2]\t", $eq2->to_string(), "\n\n";
52              
53             # Make x the subject of eq. 1
54             my $eq1_x = $eq1->isolate('x');
55             die "Cannot isolate 'x' in " . $eq1->to_string() . "\n" unless defined $eq1_x;
56             print "Make x the subject of [1]: ", $eq1_x->to_string(), "\n\n";
57             my $x_expr = $eq1_x->RHS();
58              
59             # Substitute into eq. 2, re-arrange to make RHS = 0, and simplify
60             my $eq3 = $eq2->implement('x' => $x_expr)->simplify();
61             print "Substitute into [2]: ", $eq3->to_string(), "\n\n";
62              
63             # Re-arrange it to equal 0
64             my $eq3_2 = $eq3->to_zero()->simplify();
65             print "Rearrange to equal zero: ", $eq3_2->to_string(), "\n\n";
66              
67             # we have an expression for y, solve it
68             my ($var, $coeffs, $disc, $roots) = $eq3_2->LHS()->test_polynomial();
69             die "Cannot solve quadratic!\n" unless defined($var) && ($var eq 'y');
70              
71             my $y_1 = $roots->[0];
72             my $y_2 = $roots->[1];
73             print "The solutions for y are: ($y_1, $y_2)\n\n";
74              
75             # put these solutions into the expression for x in terms of y to get x values
76             my $x_1 = $eq1_x->implement('y' => $y_1)->simplify()->RHS();
77             my $x_2 = $eq1_x->implement('y' => $y_2)->simplify()->RHS();
78             print "The solutions for x given y are: (x = $x_1 when y = $y_1) and (x = $x_2 when y = $y_2)\n\n";
79              
80             # Check that these solutions hold for the original equations
81             print "Check: ";
82             if ( $eq1->holds({'x' => $x_1, 'y' => $y_1}) && $eq2->holds({'x' => $x_1, 'y' => $y_1}) ) {
83             print "Solution (x = $x_1, y = $y_1) holds for [1] and [2]\n";
84             }
85             print "Check: ";
86             if ( $eq1->holds({'x' => $x_2, 'y' => $y_2}) && $eq2->holds({'x' => $x_2, 'y' => $y_2}) ) {
87             print "Solution (x = $x_2, y = $y_2) holds for [1] and [2]\n";
88             }
89              
90             =head1 METHODS
91              
92             =head2 Constructor new
93              
94             Expects the left hand side and right hand side of the desired equation as parameters. These can be Math::Symbolic expressions,
95             or strings which will be parsed into Math::Symbolic expressions using the parser. Another option is to pass one parameter, an
96             equation string, from which the left hand side and the right hand side of the equation will be extracted.
97              
98             # specify LHS and RHS separately
99             my $eq1 = Math::Symbolic::Custom::Equation->new('y', '2*x + 4');
100            
101             # pass it an equation
102             my $eq2 = Math::Symbolic::Custom::Equation->new('y = 2*x + 4');
103              
104             =cut
105              
106             sub new {
107 39     39 1 387924 my ($proto, $LHS, $RHS) = @_;
108 39   66     238 my $class = ref($proto) || $proto;
109 39         89 my $self;
110              
111 39 50 66     274 if ( ref($proto) && !defined($LHS) ) {
112             # copy constructor
113 0         0 $self->{LHS} = $proto->LHS()->new();
114 0         0 $self->{RHS} = $proto->RHS()->new();
115             }
116             else {
117             # might have been passed an equation in string form
118 39 50 66     266 if ( !defined($RHS) && (ref($LHS) eq "") && ($LHS =~ /=/) ) {
      66        
119 14         77 ($LHS, $RHS) = split(/=/, $LHS);
120             }
121              
122 39 100       268 $LHS = Math::Symbolic::parse_from_string($LHS) if ref($LHS) !~ /^Math::Symbolic/;
123 39 100       244365 $RHS = Math::Symbolic::parse_from_string($RHS) if ref($RHS) !~ /^Math::Symbolic/;
124              
125 39         211424 $self = { LHS => $LHS, RHS => $RHS };
126             }
127              
128 39         305 bless $self, $class;
129             }
130              
131             =head2 Method LHS
132              
133             With no parameter, will return the left-hand side of the equation.
134             With a parameter, will set the left-hand side of the equation, and return it.
135              
136             =cut
137              
138             sub LHS {
139 153     153 1 327 my $self = shift;
140 153         422 my $t = shift;
141 153 50       447 if ( defined $t ) {
142 0 0       0 $t = Math::Symbolic::parse_from_string($t) if ref($t) !~ /^Math::Symbolic/;
143 0 0       0 if ( defined $t ) {
144 0         0 $self->{LHS} = $t;
145             }
146             else {
147 0         0 carp "LHS(): not setting undefined LHS";
148             }
149             }
150 153         616 return $self->{LHS};
151             }
152              
153             =head2 Method RHS
154              
155             With no parameter, will return the right-hand side of the equation.
156             With a parameter, will set the right-hand side of the equation, and return it.
157              
158             =cut
159              
160             sub RHS {
161 131     131 1 278 my $self = shift;
162 131         247 my $t = shift;
163 131 50       378 if ( defined $t ) {
164 0 0       0 $t = Math::Symbolic::parse_from_string($t) if ref($t) !~ /^Math::Symbolic/;
165 0 0       0 if ( defined $t ) {
166 0         0 $self->{RHS} = $t;
167             }
168             else {
169 0         0 carp "RHS(): not setting undefined RHS";
170             }
171             }
172 131         337 return $self->{RHS};
173             }
174              
175             =head2 Method to_string
176              
177             Takes no parameter. Will return the equation in string form, e.g. "LHS = RHS".
178              
179             =cut
180              
181             sub to_string {
182 68     68 1 1172 my $self = shift;
183              
184 68         270 my $LHS = $self->LHS();
185 68         269 my $RHS = $self->RHS();
186              
187 68 50 33     395 unless ( defined($LHS) && defined($RHS) ) {
188 0         0 carp "display(): equation not properly set up, needs both sides.";
189 0         0 return q{};
190             }
191              
192 68         633 return "$LHS = $RHS";
193             }
194              
195             =head2 Method holds
196              
197             Tests to see if the equation is true for given variable values, passed as a hash reference.
198             This calls L's value() method with the passed values on the expressions for the left-hand side
199             and right-hand side and compares the two results.
200              
201             An optional second argument is a threshold used to set the accuracy of the numerical comparison (set
202             by default to 1e-11).
203              
204             my $eq = Math::Symbolic::Custom::Equation->new('y', '2*x + 4');
205              
206             if ( $eq->holds({'x' => 2, 'y' => 8}) ) {
207             print "'", $eq->to_string(), "' holds for x = 2 and y = 8.\n";
208             # 'y = (2 * x) + 4' holds for x = 2 and y = 8.
209             }
210              
211             =cut
212              
213             sub holds {
214 33     33 1 23377 my $self = shift;
215 33         85 my $vals = shift;
216 33         76 my $epsilon = shift;
217 33 100       140 $epsilon = 1e-11 unless defined $epsilon;
218              
219 33         146 my $LHS = $self->LHS();
220 33         179 my $RHS = $self->RHS();
221              
222 33 50 33     214 unless ( defined($LHS) && defined($RHS) ) {
223 0         0 carp "holds(): equation not properly set up, needs both sides.";
224 0         0 return undef;
225             }
226              
227             # try hard to force down to a number
228 33         85 my $LHS_val = $LHS->value(%{$vals});
  33         233  
229 33 50       3889 if ( ref($LHS_val) =~ /Math::Symbolic::Operator/ ) {
230 0         0 $LHS_val = $LHS_val->simplify();
231             }
232 33 50       126 if ( ref($LHS_val) =~ /Math::Symbolic::Constant/ ) {
233 0         0 $LHS_val = $LHS_val->value();
234             }
235              
236 33         66 my $RHS_val = $RHS->value(%{$vals});
  33         160  
237 33 50       16222 if ( ref($RHS_val) =~ /Math::Symbolic::Operator/ ) {
238 0         0 $RHS_val = $RHS_val->simplify();
239             }
240 33 50       133 if ( ref($RHS_val) =~ /Math::Symbolic::Constant/ ) {
241 0         0 $RHS_val = $RHS_val->value();
242             }
243              
244 33 50 33     222 unless ( defined($LHS_val) && defined($RHS_val) ) {
245 0         0 carp "holds(): some problem setting values for equation. Perhaps a variable is missing or there is a typo.";
246 0         0 return undef;
247             }
248              
249 33         269 return abs($LHS_val - $RHS_val) < $epsilon;
250             }
251              
252             =head2 Method simplify
253              
254             Takes no parameters. Calls Math::Symbolic's simplify() (or whichever simplify() is currently
255             registered) on both sides of the equation. If successful returns a new (simplifed) equation object, otherwise undef.
256              
257             =cut
258              
259             sub simplify {
260 0     0 1 0 my $self = shift;
261            
262 0         0 my $LHS = $self->LHS()->new();
263 0         0 my $RHS = $self->RHS()->new();
264              
265 0 0 0     0 unless ( defined($LHS) && defined($RHS) ) {
266 0         0 carp "simplify(): equation not properly set up, needs both sides.";
267 0         0 return undef;
268             }
269              
270 0         0 my $new_LHS = $LHS->simplify();
271 0         0 my $new_RHS = $RHS->simplify();
272              
273 0 0 0     0 if ( defined($new_LHS) && defined($new_RHS) ) {
274 0         0 return $self->new($new_LHS, $new_RHS);
275             }
276            
277 0         0 return undef; # simplify failed
278             }
279              
280             =head2 Method implement
281              
282             Calls Math::Symbolic's implement() on both sides of the equation. This can be used to substitute a specified variable with another
283             Math::Symbolic expression (see the example above). If successful returns a new equation object, otherwise undef.
284              
285             =cut
286              
287             sub implement {
288 0     0 1 0 my $self = shift;
289 0         0 my %to_implement = @_;
290            
291 0         0 my $LHS = $self->LHS()->new();
292 0         0 my $RHS = $self->RHS()->new();
293              
294 0 0 0     0 unless ( defined($LHS) && defined($RHS) ) {
295 0         0 carp "implement(): equation not properly set up, needs both sides.";
296 0         0 return undef;
297             }
298              
299 0         0 my $new_LHS = $LHS->implement(%to_implement);
300 0         0 my $new_RHS = $RHS->implement(%to_implement);
301              
302 0 0 0     0 if ( defined($new_LHS) && defined($new_RHS) ) {
303 0         0 return $self->new($new_LHS, $new_RHS);
304             }
305            
306 0         0 return undef;
307             }
308              
309             sub _transform {
310 0     0   0 my $self = shift;
311 0         0 my $t1 = shift;
312              
313 0         0 my $LHS = $self->LHS()->new();
314 0         0 my $RHS = $self->RHS()->new();
315              
316 0 0 0     0 unless ( defined($LHS) && defined($RHS) ) {
317 0         0 carp "transform(): equation not properly set up, needs both sides.";
318 0         0 return 0;
319             }
320              
321 0 0       0 $t1 = Math::Symbolic::parse_from_string($t1) if ref($t1) !~ /^Math::Symbolic/;
322              
323 0 0       0 unless ( defined $t1 ) {
324 0         0 carp "transform(): passed expression is not a valid Math::Symbolic expression.";
325 0         0 return 0;
326             }
327              
328 0         0 my @vars = $t1->explicit_signature();
329 0         0 my @got_eq = grep { $_ eq $EQ_PH } @vars;
  0         0  
330 0 0       0 if ( scalar(@got_eq) == 0 ) {
331 0         0 carp "transform(): not found equation placeholder variable $EQ_PH in passed expression.";
332             }
333              
334 0         0 my $t2 = $t1->new();
335              
336 0         0 my $new_LHS = $t1->implement($EQ_PH => $LHS);
337 0         0 my $new_RHS = $t2->implement($EQ_PH => $RHS);
338              
339 0 0 0     0 if ( defined($new_LHS) && defined($new_RHS) ) {
340 0         0 return $self->new($new_LHS, $new_RHS);
341             }
342            
343 0         0 return undef;
344             }
345              
346             =head2 Method add
347              
348             Takes one parameter, which can be another equation object, or a Math::Symbolic expression (or a text string which can parse to a
349             Math::Symbolic expression). If passed an equation then it will perform equation addition, or if passed an expression it will add
350             the passed expression to both sides of the equation. Returns a new equation object.
351              
352             =cut
353              
354             sub add {
355 0     0 1 0 my $self = shift;
356 0         0 my $t = shift;
357              
358 0 0       0 if ( ref($t) eq ref($self) ) {
359              
360 0         0 my $LHS1 = $self->LHS()->new();
361 0         0 my $RHS1 = $self->RHS()->new();
362 0         0 my $LHS2 = $t->LHS()->new();
363 0         0 my $RHS2 = $t->RHS()->new();
364 0         0 my $LHS3 = Math::Symbolic::Operator->new('+', $LHS1, $LHS2);
365 0         0 my $RHS3 = Math::Symbolic::Operator->new('+', $RHS1, $RHS2);
366 0         0 return $self->new($LHS3, $RHS3);
367             }
368             else {
369              
370 0 0       0 $t = Math::Symbolic::parse_from_string($t) if ref($t) !~ /^Math::Symbolic/;
371              
372 0         0 my $operation = Math::Symbolic::Operator->new('+', Math::Symbolic::Variable->new($EQ_PH), $t);
373              
374 0         0 return $self->_transform($operation);
375             }
376             }
377              
378             =head2 Method subtract
379              
380             Takes one parameter, which can be another equation object, or a Math::Symbolic expression (or a text string which can parse to a
381             Math::Symbolic expression). If passed an equation then it will perform equation subtraction, or if passed an expression it will subtract
382             the passed expression to from sides of the equation. Returns a new equation object.
383              
384             =cut
385              
386             sub subtract {
387 0     0 1 0 my $self = shift;
388 0         0 my $t = shift;
389              
390 0 0       0 if ( ref($t) eq ref($self) ) {
391              
392 0         0 my $LHS1 = $self->LHS()->new();
393 0         0 my $RHS1 = $self->RHS()->new();
394 0         0 my $LHS2 = $t->LHS()->new();
395 0         0 my $RHS2 = $t->RHS()->new();
396 0         0 my $LHS3 = Math::Symbolic::Operator->new('-', $LHS1, $LHS2);
397 0         0 my $RHS3 = Math::Symbolic::Operator->new('-', $RHS1, $RHS2);
398 0         0 return $self->new($LHS3, $RHS3);
399             }
400             else {
401              
402 0 0       0 $t = Math::Symbolic::parse_from_string($t) if ref($t) !~ /^Math::Symbolic/;
403              
404 0         0 my $operation = Math::Symbolic::Operator->new('-', Math::Symbolic::Variable->new($EQ_PH), $t);
405              
406 0         0 return $self->_transform($operation);
407             }
408             }
409              
410             =head2 Method multiply
411              
412             Takes one parameter, a Math::Symbolic expression or a text string which can parse to a Math::Symbolic
413             expression.
414              
415             Multiplies the passed expression with both sides of the equation and returns a new equation object.
416              
417             =cut
418              
419             sub multiply {
420 0     0 1 0 my $self = shift;
421 0         0 my $t = shift;
422              
423 0 0       0 $t = Math::Symbolic::parse_from_string($t) if ref($t) !~ /^Math::Symbolic/;
424              
425 0         0 my $operation = Math::Symbolic::Operator->new('*', $t, Math::Symbolic::Variable->new($EQ_PH));
426              
427 0         0 return $self->_transform($operation);
428             }
429              
430             =head2 Method divide
431              
432             Takes one parameter, a Math::Symbolic expression or a text string which can parse to a Math::Symbolic
433             expression.
434              
435             Divides both sides of the equation by the passed expression and returns a new equation object.
436              
437             =cut
438              
439             sub divide {
440 0     0 1 0 my $self = shift;
441 0         0 my $t = shift;
442              
443 0 0       0 $t = Math::Symbolic::parse_from_string($t) if ref($t) !~ /^Math::Symbolic/;
444              
445 0         0 my $operation = Math::Symbolic::Operator->new('/', Math::Symbolic::Variable->new($EQ_PH), $t);
446              
447 0         0 return $self->_transform($operation);
448             }
449              
450             =head2 Method to_zero
451              
452             Takes no parameters. Re-arranges the equation to equate to zero, by subracting the right-hand side from both sides.
453             Returns a new equation object.
454              
455             my $eq = Math::Symbolic::Custom::Equation->new('3*x^3 - 2*x^2 + 5*x - 10 = 5*x + 8');
456             $eq->to_zero();
457             print $eq->to_string(), "\n"; # ((3 * (x ^ 3)) - 18) - (2 * (x ^ 2)) = 0
458              
459             =cut
460              
461             sub to_zero {
462 0     0 1 0 my $self = shift;
463              
464 0         0 my $LHS = $self->LHS()->new();
465 0         0 my $RHS = $self->RHS()->new();
466              
467 0 0 0     0 unless ( defined($LHS) && defined($RHS) ) {
468 0         0 carp "transform(): equation not properly set up, needs both sides.";
469 0         0 return;
470             }
471              
472 0         0 my $new_LHS = Math::Symbolic::Operator->new('-', $LHS, $RHS);
473              
474 0         0 return $self->new($new_LHS, '0');
475             }
476              
477             =head2 Method explicit_signature
478              
479             Takes no parameters. Calls Math::Symbolic's explicit_signature() on both sides of the
480             equation and returns the de-duped results, effectively returning a list of variables used
481             in the equation.
482              
483             my $eq = Math::Symbolic::Custom::Equation->new('y', '2*x + 4');
484             my @vars = $eq->explicit_signature();
485             print "Vars: ('", join("', '", sort {$a cmp $b } @vars), "')\n"; # Vars: ('x', 'y')
486              
487             =cut
488              
489             sub explicit_signature {
490 23     23 1 78 my $self = shift;
491            
492 23         119 my $LHS = $self->LHS();
493 23         100 my $RHS = $self->RHS();
494              
495 23 50 33     152 unless ( defined($LHS) && defined($RHS) ) {
496 0         0 carp "explicit_signature(): equation not properly set up, needs both sides.";
497 0         0 return ();
498             }
499            
500 23         38 my %vars;
501 23         130 my @LHS_vars = $LHS->explicit_signature();
502 23         1549 my @RHS_vars = $RHS->explicit_signature();
503 23         1929 $vars{$_} = 1 for (@LHS_vars, @RHS_vars);
504            
505 23         93 return keys %vars;
506             }
507              
508             =head2 Method isolate
509              
510             Takes a Math::Symbolic::Variable, or a string which parses to a Math::Symbolic::Variable, as a
511             parameter. This method attempts to re-arrange the equation to make that variable the subject of
512             the equation, returning new equation object(s). It will return undef if it doesn't succeed.
513              
514             When called in a scalar context, it will return the first (simplest) result it can find. When called
515             in a list context it will return all the results it can find.
516              
517             my $eq = Math::Symbolic::Custom::Equation->new('v^2 = u^2 + 2*a*s');
518             my $hit = $eq->isolate('u');
519             print "Result 1: ", $hit->to_string(), "\n\n";
520             # Result 1: u = ((v ^ 2) - ((2 * a) * s)) ^ (1 / 2)
521              
522             my @hits = $eq->isolate('u');
523             foreach my $hit (@hits) {
524             print "Result 2: ", $hit->to_string(), "\t";
525             }
526             # Result 2: u = ((v ^ 2) - ((2 * a) * s)) ^ (1 / 2)
527             # Result 2: u = -1 * (((v ^ 2) - ((2 * a) * s)) ^ (1 / 2))
528            
529             Warning: this is very different to how it worked in the previous version of the module, and it probably
530             has a way to go yet.
531              
532             =cut
533              
534             sub isolate {
535 22     22 1 16137 my ($self, $expr) = @_;
536              
537 22         58 my $autodetected = 0;
538              
539 22 50       81 if ( not defined $expr ) {
540             # try to autodetect
541 0         0 my @v = $self->explicit_signature();
542 0 0       0 if (scalar(@v) == 1) {
543 0         0 $expr = $v[0];
544 0         0 $autodetected = 1;
545             }
546             else {
547 0         0 carp "isolate: not passed a variable and cannot autodetect. (Variables in equation: ['" .
548             join("', '", @v) . "'])";
549 0 0       0 return wantarray ? () : undef;
550             }
551             }
552              
553 22 50       173 $expr = Math::Symbolic::parse_from_string($expr)
554             if ref($expr) !~ /^Math::Symbolic/;
555            
556             # ensure we've been passed a variable
557 22 50       102248 if ( ref($expr) ne 'Math::Symbolic::Variable' ) {
558 0         0 carp "isolate: not passed a variable.";
559 0 0       0 return wantarray ? () : undef;
560             }
561              
562 22 50       111 if ( not $autodetected ) {
563             # ensure it's a var in the equation
564 22         115 my @v = $self->explicit_signature();
565 22         69 my @r = grep { $expr->{name} eq $_ } @v;
  58         166  
566            
567 22 50       132 if ( scalar(@r) == 0 ) {
568             carp "isolate: not passed a variable that is present in the equation. (Was passed: '" .
569 0         0 $expr->{name} . "'. Variables in equation: ['" . join("', '", @v) . "'])";
570 0 0       0 return wantarray ? () : undef;
571             }
572             }
573            
574 22         52 my @matches;
575              
576             # is it already in the correct form?
577 22 100       111 if ( $expr->is_identical( $self->LHS() ) ) {
578 3 50       379 if ( wantarray ) {
579 3         13 push @matches, [$self->new($self->LHS(), $self->RHS()), 0];
580             }
581             else {
582 0         0 return $self->new($self->LHS(), $self->RHS());
583             }
584             }
585            
586             # init search
587 22         1349 my %nodes_todo;
588             my %nodes_done;
589 22         85 my $node_key = $self->to_string();
590 22         4257 $nodes_todo{$node_key} = { LHS => $self->{LHS}, RHS => $self->{RHS}, level => 0, operation => 'None', previous => 'None', plevel => 'None' };
591            
592             # process the list
593             # FIXME: must be a better way to limit the loop
594 22         99 NODE_LOOP: foreach my $i (1..200) {
595            
596 570         4447 my @todo = sort { $nodes_todo{$a}{level} <=> $nodes_todo{$b}{level} } keys %nodes_todo;
  8527         23113  
597 570 100       2666 last NODE_LOOP if scalar(@todo) == 0;
598 548         1338 my $next = $todo[0]; # get an unexpanded entry
599            
600             # "expand" the node to get other candidate nodes
601             # step 1: Collect
602 548         2657 my %step1_nodes = _expand_collect($next, $nodes_todo{$next});
603              
604             # step 2: Factor
605 548         2833 my %step2_nodes = _expand_factor($next, $nodes_todo{$next});
606            
607             # step 3: Unwind operator
608 548         3525 my %step3_nodes = _expand_operator($next, $nodes_todo{$next});
609            
610 548         2273 foreach my $hash (\%step1_nodes, \%step2_nodes, \%step3_nodes) {
611 1644         3331 foreach my $new_node (keys %{$hash}) {
  1644         4521  
612            
613 1790         6436 $hash->{$new_node}{level} = $i;
614              
615 1790 100 100     9467 if ( !exists($nodes_done{$new_node}) &&
616             !exists($nodes_todo{$new_node}) ) {
617              
618 526 100 100     3745 if ( ($hash->{$new_node}{operation} !~ /Factor/) && ($hash->{$new_node}{operation} !~ /Collect/) ) {
619             # check if we have sucessfully isolated
620 332         654 my ($subject, $object);
621 332 100       2293 if ( $expr->is_identical($hash->{$new_node}{LHS}) ) {
622 19         1961 $subject = $hash->{$new_node}{LHS};
623 19         83 $object = $hash->{$new_node}{RHS};
624             }
625            
626 332 100       15362 if ( $expr->is_identical($hash->{$new_node}{RHS}) ) {
627 17         1587 $subject = $hash->{$new_node}{RHS};
628 17         45 $object = $hash->{$new_node}{LHS};
629             }
630            
631 332 100       11631 if ( defined $object ) {
632 36         167 my @v = $object->explicit_signature();
633 36         6330 my @r = grep { $expr->{name} eq $_ } @v;
  79         250  
634 36 100       149 if ( scalar(@r) == 0 ) {
635             # succesfully isolated, add it to matches
636 21         140 push @matches, [$self->new($subject, $object), $hash->{$new_node}{level}];
637             }
638             }
639             }
640            
641 526         2020 $nodes_todo{$new_node} = $hash->{$new_node};
642             }
643             }
644             }
645              
646 548 50       1807 unless ( wantarray ) {
647 0 0       0 if ( scalar(@matches) ) {
648             # return least "complex"
649 0         0 my @sorted = sort { $a->_complexity() <=> $b->_complexity() } map { $_->[0] } @matches;
  0         0  
  0         0  
650 0         0 return $sorted[0];
651             }
652             }
653            
654             # move this node to the done pile
655 548         1812 $nodes_done{$next} = $nodes_todo{$next};
656 548         13690 delete $nodes_todo{$next};
657             }
658              
659 22 50       85 if ( scalar(@matches) ) {
660 22 50       104 if ( wantarray ) {
661              
662 22         62 my @reduced = sort { $a->_complexity() <=> $b->_complexity() } map { $_->[0] } @matches;
  2         13  
  24         113  
663 22         4979 return @reduced;
664             }
665             else {
666 0         0 my @sorted = sort { $a->_complexity() <=> $b->_complexity() } map { $_->[0] } @matches;
  0         0  
  0         0  
667 0         0 return $sorted[0];
668             }
669             }
670              
671 0 0       0 return wantarray ? () : undef;
672             }
673              
674             sub _expand_collect {
675 548     548   1744 my ($node_name, $node) = @_;
676              
677 548         1193 my %new_nodes;
678              
679 548         4571 my $LHS = $node->{LHS}->to_collected();
680 548         1699520 my $RHS = $node->{RHS}->to_collected();
681            
682 548 100       1109079 if ( $LHS->to_string() ne $node->{LHS}->to_string() ) {
683 97         25239 my $new_node = "$LHS = " . $node->{RHS};
684 97         23821 $new_nodes{$new_node} = { LHS => $LHS, RHS => $node->{RHS}, previous => $node_name, plevel => $node->{level}, operation => 'Collect LHS' };
685             }
686            
687 548 100       118277 if ( $RHS->to_string() ne $node->{RHS}->to_string() ) {
688 127         26543 my $new_node = $node->{LHS} . " = $RHS";
689 127         29026 $new_nodes{$new_node} = { LHS => $node->{LHS}, RHS => $RHS, previous => $node_name, plevel => $node->{level}, operation => 'Collect RHS' };
690             }
691            
692 548 100 100     80596 if ( ($LHS->to_string() ne $node->{LHS}->to_string()) &&
693             ($RHS->to_string() ne $node->{RHS}->to_string()) ) {
694 35         13167 my $new_node = "$LHS = $RHS";
695 35         6967 $new_nodes{$new_node} = { LHS => $LHS, RHS => $RHS, previous => $node_name, plevel => $node->{level}, operation => 'Collect LHS & RHS' };
696             }
697              
698 548         141941 return %new_nodes;
699             }
700              
701             sub _expand_factor {
702 548     548   1683 my ($node_name, $node) = @_;
703              
704 548         1191 my %new_nodes;
705              
706 548         4642 my $LHS = $node->{LHS}->to_factored();
707 548         30805138 my $RHS = $node->{RHS}->to_factored();
708            
709 548 100       28161184 if ( $LHS->to_string() ne $node->{LHS}->to_string() ) {
710 116         35951 my $new_node = "$LHS = " . $node->{RHS};
711 116         29196 $new_nodes{$new_node} = { LHS => $LHS, RHS => $node->{RHS}, previous => $node_name, plevel => $node->{level}, operation => 'Factor LHS' };
712             }
713            
714 548 100       119655 if ( $RHS->to_string() ne $node->{RHS}->to_string() ) {
715 134         33377 my $new_node = $node->{LHS} . " = $RHS";
716 134         30623 $new_nodes{$new_node} = { LHS => $node->{LHS}, RHS => $RHS, previous => $node_name, plevel => $node->{level}, operation => 'Factor RHS' };
717             }
718            
719 548 100 100     86028 if ( ($LHS->to_string() ne $node->{LHS}->to_string()) &&
720             ($RHS->to_string() ne $node->{RHS}->to_string()) ) {
721 41         16313 my $new_node = "$LHS = $RHS";
722 41         8197 $new_nodes{$new_node} = { LHS => $LHS, RHS => $RHS, previous => $node_name, plevel => $node->{level}, operation => 'Factor LHS & RHS' };
723             }
724              
725 548         139107 return %new_nodes;
726             }
727              
728             sub _expand_operator {
729 548     548   1847 my ($node_name, $node) = @_;
730            
731 548         1588 my %new_nodes;
732             my $t;
733            
734 548         1944 $t = $node->{LHS};
735            
736 548 100       2101 if ( $t->term_type() == T_OPERATOR ) {
737              
738 439 100       3624 if ( $t->type() == B_DIVISION ) {
    100          
    100          
    100          
    50          
739 153         1755 my $new_LHS = $t->op1();
740 153         1784 my $new_RHS = Math::Symbolic::Operator->new('*', $node->{RHS}, $t->op2() )->to_collected();
741 153         278071 my $eq_str = "$new_LHS = $new_RHS";
742 153 50       32087 if ( (!exists $new_nodes{$eq_str}) ) {
743 153         1602 $new_nodes{$eq_str} = { LHS => $new_LHS, RHS => $new_RHS, previous => $node_name, plevel => $node->{level}, operation => 'LHS unwind division' };
744             }
745             }
746             elsif ( $t->type() == B_DIFFERENCE ) {
747 86         1573 my $new_LHS = $t->op1();
748 86         896 my $new_RHS = Math::Symbolic::Operator->new('+', $t->op2(), $node->{RHS})->to_collected();
749 86         242929 my $eq_str = "$new_LHS = $new_RHS";
750 86 50       20280 if ( (!exists $new_nodes{$eq_str}) ) {
751 86         851 $new_nodes{$eq_str} = { LHS => $new_LHS, RHS => $new_RHS, previous => $node_name, plevel => $node->{level}, operation => 'LHS unwind subtraction' };
752             }
753             }
754             elsif ( $t->type() == B_PRODUCT ) {
755 148 50 66     3532 unless ( ($t->op2()->term_type() == T_CONSTANT) && ($t->op2()->value() == 0) ) {
756 148         2467 my $new_LHS = $t->op1();
757 148         1573 my $new_RHS = Math::Symbolic::Operator->new('/', $node->{RHS}, $t->op2())->to_collected();
758 148         743564 my $eq_str = "$new_LHS = $new_RHS";
759 148 50       36364 if ( (!exists $new_nodes{$eq_str}) ) {
760 148         1633 $new_nodes{$eq_str} = { LHS => $new_LHS, RHS => $new_RHS, previous => $node_name, plevel => $node->{level}, operation => 'LHS unwind product op2' };
761             }
762             }
763 148 50 66     651 unless ( ($t->op1()->term_type() == T_CONSTANT) && ($t->op1()->value() == 0) ) {
764 148         2419 my $new_LHS = $t->op2();
765 148         1223 my $new_RHS = Math::Symbolic::Operator->new('/', $node->{RHS}, $t->op1())->to_collected();
766 148         722781 my $eq_str = "$new_LHS = $new_RHS";
767 148 50       39567 if ( (!exists $new_nodes{$eq_str}) ) {
768 148         1440 $new_nodes{$eq_str} = { LHS => $new_LHS, RHS => $new_RHS, previous => $node_name, plevel => $node->{level}, operation => 'LHS unwind product op1' };
769             }
770             }
771             }
772             elsif ( $t->type() == B_SUM ) {
773 15         479 my $new_LHS = $t->op1();
774 15         196 my $new_RHS = Math::Symbolic::Operator->new('-', $node->{RHS}, $t->op2())->to_collected();
775 15         79499 my $eq_str = "$new_LHS = $new_RHS";
776 15 50       3386 if ( (!exists $new_nodes{$eq_str}) ) {
777 15         152 $new_nodes{$eq_str} = { LHS => $new_LHS, RHS => $new_RHS, previous => $node_name, plevel => $node->{level}, operation => 'LHS unwind addition op2' };
778             }
779 15         55 $new_LHS = $t->op2();
780 15         171 $new_RHS = Math::Symbolic::Operator->new('-', $node->{RHS}, $t->op1())->to_collected();
781 15         76625 $eq_str = "$new_LHS = $new_RHS";
782 15 50       3284 if ( (!exists $new_nodes{$eq_str}) ) {
783 15         142 $new_nodes{$eq_str} = { LHS => $new_LHS, RHS => $new_RHS, previous => $node_name, plevel => $node->{level}, operation => 'LHS unwind addition op1' };
784             }
785             }
786             elsif ( $t->type() == B_EXP ) {
787              
788             # FIXME test with Math::Symbolic methods
789 37 100 100     1362 if ( ($t->op2()->to_string() eq '0.5') || ($t->op2()->to_string() eq '1 / 2') ) {
    50          
790 33         3676 my $new_LHS = $t->op1();
791 33         385 my $new_RHS = Math::Symbolic::Operator->new('^', $node->{RHS}, Math::Symbolic::Constant->new(2))->to_collected();
792 33         54059 my $eq_str = "$new_LHS = $new_RHS";
793 33 50       12615 if ( (!exists $new_nodes{$eq_str}) ) {
794 33         336 $new_nodes{$eq_str} = { LHS => $new_LHS, RHS => $new_RHS, previous => $node_name, plevel => $node->{level}, operation => 'LHS unwind sqrt' };
795             }
796             }
797             elsif ( $t->op2()->to_string() eq '2' ) {
798 4         185 my $new_LHS = $t->op1();
799 4         40 my $new_RHS1 = Math::Symbolic::Operator->new('^', $node->{RHS}, Math::Symbolic::Constant->new(0.5))->to_collected();
800 4         27552 my $eq_str1 = "$new_LHS = $new_RHS1";
801 4 50       1305 if ( (!exists $new_nodes{$eq_str1}) ) {
802 4         40 $new_nodes{$eq_str1} = { LHS => $new_LHS, RHS => $new_RHS1, previous => $node_name, plevel => $node->{level}, operation => 'LHS unwind sqr +ve' };
803             }
804 4         20 my $new_RHS2 = Math::Symbolic::Operator->new('*', Math::Symbolic::Constant->new(-1), $new_RHS1)->to_collected();
805 4         28671 my $eq_str2 = "$new_LHS = $new_RHS2";
806 4 50       1463 if ( (!exists $new_nodes{$eq_str2}) ) {
807 4         40 $new_nodes{$eq_str2} = { LHS => $new_LHS, RHS => $new_RHS2, previous => $node_name, plevel => $node->{level}, operation => 'LHS unwind sqr -ve' };
808             }
809             }
810             }
811             }
812            
813 548         2581 $t = $node->{RHS};
814            
815 548 100       1914 if ( $t->term_type() == T_OPERATOR ) {
816              
817 393 100       2753 if ( $t->type() == B_DIVISION ) {
    100          
    100          
    100          
    50          
818 101         1172 my $new_RHS = $t->op1();
819 101         1064 my $new_LHS = Math::Symbolic::Operator->new('*', $node->{LHS}, $t->op2() )->to_collected();
820 101         299251 my $eq_str = "$new_LHS = $new_RHS";
821 101 50       24291 if ( (!exists $new_nodes{$eq_str}) ) {
822 101         1016 $new_nodes{$eq_str} = { LHS => $new_LHS, RHS => $new_RHS, previous => $node_name, plevel => $node->{level}, operation => 'RHS unwind division' };
823             }
824             }
825             elsif ( $t->type() == B_DIFFERENCE ) {
826 31         523 my $new_RHS = $t->op1();
827 31         266 my $new_LHS = Math::Symbolic::Operator->new('+', $t->op2(), $node->{LHS})->to_collected();
828 31         84646 my $eq_str = "$new_LHS = $new_RHS";
829 31 100       7254 if ( (!exists $new_nodes{$eq_str}) ) {
830 29         290 $new_nodes{$eq_str} = { LHS => $new_LHS, RHS => $new_RHS, previous => $node_name, plevel => $node->{level}, operation => 'RHS unwind subtraction' };
831             }
832             }
833             elsif ( $t->type() == B_PRODUCT ) {
834 183 50 66     4282 unless ( ($t->op2()->term_type() == T_CONSTANT) && ($t->op2()->value() == 0) ) {
835 183         3160 my $new_RHS = $t->op1();
836 183         1709 my $new_LHS = Math::Symbolic::Operator->new('/', $node->{LHS}, $t->op2())->to_collected();
837 183         1016801 my $eq_str = "$new_LHS = $new_RHS";
838 183 50       45732 if ( (!exists $new_nodes{$eq_str}) ) {
839 183         1991 $new_nodes{$eq_str} = { LHS => $new_LHS, RHS => $new_RHS, previous => $node_name, plevel => $node->{level}, operation => 'RHS unwind product op2' };
840             }
841             }
842 183 50 66     844 unless ( ($t->op1()->term_type() == T_CONSTANT) && ($t->op1()->value() == 0) ) {
843 183         3053 my $new_RHS = $t->op2();
844 183         2359 my $new_LHS = Math::Symbolic::Operator->new('/', $node->{LHS}, $t->op1())->to_collected();
845 183         1068116 my $eq_str = "$new_LHS = $new_RHS";
846 183 50       44588 if ( (!exists $new_nodes{$eq_str}) ) {
847 183         1919 $new_nodes{$eq_str} = { LHS => $new_LHS, RHS => $new_RHS, previous => $node_name, plevel => $node->{level}, operation => 'RHS unwind product op1' };
848             }
849             }
850             }
851             elsif ( $t->type() == B_SUM ) {
852 53         1682 my $new_RHS = $t->op1();
853 53         525 my $new_LHS = Math::Symbolic::Operator->new('-', $node->{LHS}, $t->op2())->to_collected();
854 53         183115 my $eq_str = "$new_LHS = $new_RHS";
855 53 100       11778 if ( (!exists $new_nodes{$eq_str}) ) {
856 51         443 $new_nodes{$eq_str} = { LHS => $new_LHS, RHS => $new_RHS, previous => $node_name, plevel => $node->{level}, operation => 'RHS unwind addition op2' };
857             }
858 53         201 $new_RHS = $t->op2();
859 53         519 $new_LHS = Math::Symbolic::Operator->new('-', $node->{LHS}, $t->op1())->to_collected();
860 53         174613 $eq_str = "$new_LHS = $new_RHS";
861 53 50       10376 if ( (!exists $new_nodes{$eq_str}) ) {
862 53         538 $new_nodes{$eq_str} = { LHS => $new_LHS, RHS => $new_RHS, previous => $node_name, plevel => $node->{level}, operation => 'RHS unwind addition op1' };
863             }
864             }
865             elsif ( $t->type() == B_EXP ) {
866              
867             # FIXME test with Math::Symbolic methods
868 25 100 66     851 if ( ($t->op2()->to_string() eq '0.5') || ($t->op2()->to_string() eq '1 / 2') ) {
    50          
869 16         1691 my $new_RHS = $t->op1();
870 16         145 my $new_LHS = Math::Symbolic::Operator->new('^', $node->{LHS}, Math::Symbolic::Constant->new(2))->to_collected();
871 16         25013 my $eq_str = "$new_LHS = $new_RHS";
872 16 50       4368 if ( (!exists $new_nodes{$eq_str}) ) {
873 16         134 $new_nodes{$eq_str} = { LHS => $new_LHS, RHS => $new_RHS, previous => $node_name, plevel => $node->{level}, operation => 'RHS unwind sqrt' };
874             }
875             }
876             elsif ( $t->op2()->to_string() eq '2' ) {
877 9         399 my $new_RHS = $t->op1();
878 9         82 my $new_LHS1 = Math::Symbolic::Operator->new('^', $node->{LHS}, Math::Symbolic::Constant->new(0.5))->to_collected();
879 9         71821 my $eq_str1 = "$new_LHS1 = $new_RHS";
880 9 50       3038 if ( (!exists $new_nodes{$eq_str1}) ) {
881 9         97 $new_nodes{$eq_str1} = { LHS => $new_LHS1, RHS => $new_RHS, previous => $node_name, plevel => $node->{level}, operation => 'RHS unwind sqr +ve' };
882             }
883 9         68 my $new_LHS2 = Math::Symbolic::Operator->new('*', Math::Symbolic::Constant->new(-1), $new_LHS1)->to_collected();
884 9         82240 my $eq_str2 = "$new_LHS2 = $new_RHS";
885 9 50       6785 if ( (!exists $new_nodes{$eq_str2}) ) {
886 9         108 $new_nodes{$eq_str2} = { LHS => $new_LHS2, RHS => $new_RHS, previous => $node_name, plevel => $node->{level}, operation => 'RHS unwind sqr -ve' };
887             }
888             }
889             }
890             }
891            
892 548         4754 return %new_nodes;
893             }
894              
895              
896             sub _complexity {
897 4     4   10 my $self = shift;
898              
899 4         16 my $LHS_score = _test_complexity($self->LHS());
900 4         19 my $RHS_score = _test_complexity($self->RHS());
901              
902 4         23 return $LHS_score + $RHS_score;
903             }
904              
905             # Try to achieve a measure of "complexity" of a Math::Symbolic expression.
906             # The greater the score, the higher the "complexity".
907             sub _test_complexity {
908 8     8   18 my ($tree) = @_;
909              
910             # Look at:
911             # 1. the depth of the tree
912             # 2. the number of constants
913             # 3. the number of variable instances (e.g. x * x should count as 2 variables)
914             # 4. the number of operations
915 8         34 my %metrics = ( depth => 0, constants => 0, variables => 0, operations => 0 );
916 8         27 _walk($tree, 0, \%metrics);
917              
918 8         16 my $score = 0;
919             # it should be possible to weight these metrics;
920             # for now all metrics are at weight 1.
921 8         56 $score += $_ for values %metrics;
922              
923 8         44 return $score;
924             }
925              
926             # helper routine to walk the Math::Symbolic expression tree and tot up the metrics.
927             sub _walk {
928 72     72   149 my ($node, $depth, $hr) = @_;
929              
930 72 100       162 $hr->{depth} = $depth if $depth > $hr->{depth};
931              
932 72 100       189 if ($node->term_type() == T_CONSTANT) {
    100          
933 22         102 $hr->{constants}++;
934             } elsif ($node->term_type() == T_VARIABLE) {
935 18         127 $hr->{variables}++;
936             } else {
937 32         182 $hr->{operations}++;
938 32         66 foreach my $child (@{$node->{operands}}) {
  32         85  
939 64         140 _walk($child, $depth + 1, $hr);
940             }
941             }
942             }
943              
944              
945             =head1 SEE ALSO
946              
947             L
948              
949             =head1 AUTHOR
950              
951             Matt Johnson, C<< >>
952              
953             =head1 ACKNOWLEDGEMENTS
954              
955             Steffen Mueller, author of Math::Symbolic
956              
957             =head1 LICENSE AND COPYRIGHT
958              
959             This software is copyright (c) 2025 by Matt Johnson.
960              
961             This is free software; you can redistribute it and/or modify it under
962             the same terms as the Perl 5 programming language system itself.
963              
964             =cut
965              
966             1;
967             __END__