File Coverage

blib/lib/App/ErrorCalculator.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package App::ErrorCalculator;
2              
3 2     2   10545 use strict;
  2         5  
  2         79  
4 2     2   12 use warnings;
  2         4  
  2         130  
5              
6             our $VERSION = '1.02';
7              
8 2     2   2085 use Math::Symbolic ();
  2         326093  
  2         87  
9 2     2   2301 use Math::SymbolicX::Error;
  2         94710  
  2         93  
10 2     2   1855 use Math::SymbolicX::NoSimplification;
  2         989  
  2         112  
11 2     2   1952 use Spreadsheet::Read ();
  2         62109  
  2         387  
12              
13 2     2   20 use Number::WithError;
  2         3  
  2         210  
14 2     2   1094 use Glib qw/TRUE FALSE/;
  0            
  0            
15             use Gtk2 '-init';
16             use Gtk2::Ex::Dialogs ( destroy_with_parent => TRUE,
17             modal => TRUE,
18             no_separator => FALSE );
19              
20             sub _delete_event
21             {
22             # If you return FALSE in the "delete_event" signal handler,
23             # GTK will emit the "destroy" signal. Returning TRUE means
24             # you don't want the window to be destroyed.
25             # This is useful for popping up 'are you sure you want to quit?'
26             # type dialogs.
27             #print "delete event occurred\n";
28              
29             # Change TRUE to FALSE and the main window will be destroyed with
30             # a "delete_event".
31             return FALSE;
32             }
33              
34             my $window = Gtk2::Window->new('toplevel');
35             $window->set_border_width(10);
36              
37             # When the window is given the "delete_event" signal (this is given
38             # by the window manager, usually by the "close" option, or on the
39             # titlebar), we ask it to call the delete_event () functio
40             # as defined above. No data is passed to the callback function.
41             $window->signal_connect(delete_event => \&_delete_event);
42              
43             # Here we connect the "destroy" event to a signal handler.
44             # This event occurs when we call Gtk2::Widget::destroy on the window,
45             # or if we return FALSE in the "delete_event" callback. Perl supports
46             # anonymous subs, so we can use one of them for one line callbacks.
47             $window->signal_connect(destroy => sub { Gtk2->main_quit; });
48              
49             my $table = Gtk2::Table->new(5, 4, FALSE);
50             $window->add($table);
51              
52             # Labels
53             my $l = Gtk2::Label->new('Function:');
54             $table->attach_defaults(
55             $l, 0, 1, # left/right
56             1, 2, # top/bottom
57             );
58             $l->show;
59             $l = Gtk2::Label->new('Input:');
60             $table->attach_defaults(
61             $l, 0, 1, # left/right
62             2, 3, # top/bottom
63             );
64             $l->show;
65             $l = Gtk2::Label->new('Output:');
66             $table->attach_defaults(
67             $l, 0, 1, # left/right
68             3, 4, # top/bottom
69             );
70             $l->show;
71              
72             # feedback labels
73             my $funclabel = Gtk2::Label->new('Valid Function ');
74             $table->attach_defaults(
75             $funclabel, 3, 4, # left/right
76             1, 2, # top/bottom
77             );
78             $funclabel->show;
79             my $inlabel = Gtk2::Label->new('Invalid Data File');
80             $table->attach_defaults(
81             $inlabel, 3, 4, # left/right
82             2, 3, # top/bottom
83             );
84             $inlabel->show;
85             my $outlabel = Gtk2::Label->new('Invalid Output File');
86             $table->attach_defaults(
87             $outlabel, 3, 4, # left/right
88             3, 4, # top/bottom
89             );
90             $outlabel->show;
91              
92             # Entries
93             my $funcentry = Gtk2::Entry->new;
94             $table->attach_defaults(
95             $funcentry, 1, 2, # left/right
96             1, 2, # top/bottom
97             );
98             $funcentry->signal_connect(
99             activate => \&_validate_func,
100             );
101             $funcentry->signal_connect(
102             changed => \&_validate_func,
103             );
104             $funcentry->set_text('f = a * x^2');
105             $funcentry->show;
106              
107             my $inentry = Gtk2::Entry->new;
108             $table->attach_defaults(
109             $inentry, 1, 2, # left/right
110             2, 3, # top/bottom
111             );
112             $inentry->signal_connect(
113             activate => \&_read_file,
114             );
115             $inentry->show;
116              
117             my $outentry = Gtk2::Entry->new;
118             $table->attach_defaults(
119             $outentry, 1, 2, # left/right
120             3, 4, # top/bottom
121             );
122             $outentry->show;
123              
124             # buttons
125             my $valbutton = Gtk2::Button->new('Validate');
126             $table->attach_defaults(
127             $valbutton, 2, 3, # left/right
128             1, 2, # top/bottom
129             );
130             $valbutton->signal_connect( clicked => \&_validate_func );
131             $valbutton->show;
132              
133             my $inbutton = Gtk2::Button->new('Select File');
134             $table->attach_defaults(
135             $inbutton, 2, 3, # left/right
136             2, 3, # top/bottom
137             );
138             $inbutton->signal_connect(
139             clicked => sub {
140             _run_fileselection(
141             'Select input file', $inentry,
142             sub {
143             _read_file();
144             },
145             );
146             },
147             );
148             $inbutton->show;
149              
150             my $outbutton = Gtk2::Button->new('Select File');
151             $table->attach_defaults(
152             $outbutton, 2, 3, # left/right
153             3, 4, # top/bottom
154             );
155             $outbutton->signal_connect(
156             clicked => sub {
157             my $t = $outentry->get_text;
158             _run_fileselection(
159             'Select output file', $outentry,
160             sub {
161             my $text = shift;
162             if ( -e $text ) {
163             my $r = ask Gtk2::Ex::Dialogs::Question( "File exists. Overwrite?" );
164             $outentry->set_text($t), return if not $r;
165             $outlabel->set_text('Valid Output File ');
166             }
167             else {
168             $outlabel->set_text('Valid Output File ');
169             }
170             }
171             );
172             },
173             );
174             $outbutton->show;
175              
176             my $runbutton = Gtk2::Button->new('Run Calculation');
177             $table->attach_defaults(
178             $runbutton, 0, 4, # left/right
179             4, 5, # top/bottom
180             );
181             $runbutton->signal_connect( clicked => \&_run_calculation );
182             $runbutton->show;
183              
184             $table->set_col_spacings(10);
185             $table->set_row_spacings(10);
186              
187             $table->show;
188              
189             sub run {
190             $window->show;
191             Gtk2->main;
192             }
193              
194             sub _run_fileselection {
195             my $title = shift;
196             my $entry = shift;
197             my $callback = shift;
198             my $fsel = Gtk2::FileSelection->new($title);
199             $fsel->set_filename($entry->get_text);
200             $fsel->ok_button->signal_connect(
201             "clicked",
202             sub {
203             $entry->set_text($fsel->get_filename);
204             $callback->($fsel->get_filename) if defined $callback;
205             $fsel->destroy
206             },
207             $fsel
208             );
209             $fsel->cancel_button->signal_connect(
210             "clicked",
211             sub { $fsel->destroy },
212             $fsel
213             );
214             $fsel->show;
215              
216             }
217              
218             sub _parse_function {
219             my $f = shift;
220             my ($name, $body) = split /\s*=\s*/, $f, 2;
221             return() if (not defined $name or $name =~ /^\s*$/ or not defined $body);
222             my $nobj;
223             eval { $nobj = Math::Symbolic::Variable->new($name) };
224             return() if not defined $nobj or not defined $nobj->name or $@;
225             my $func;
226             eval { $func = $Math::Symbolic::Parser->parse($body) };
227             return() if not defined $func or $@;
228             my $var = $nobj->name;
229             # function must not be recursive
230             return() if grep {$var eq $_} $func->signature;
231             $func = $func->apply_derivatives()->simplify();
232             return($nobj, $func);
233             }
234              
235             my ($name, $body);
236             sub _validate_func {
237             my $f = $funcentry->get_text;
238             ($name, $body) = _parse_function($f);
239             if (not defined $name) {
240             $funclabel->set_text('Invalid Function');
241             }
242             else {
243             $funclabel->set_text('Valid Function ');
244             }
245            
246             }
247              
248             my $data;
249             sub _read_file {
250             my $file = $inentry->get_text();
251             if (not -e $file) {
252             $inlabel->set_text('Invalid Data File');
253             $data = undef;
254             }
255             my $ref = Spreadsheet::Read::ReadData($file);
256             if (not defined $ref) {
257             $inlabel->set_text('Invalid Data File');
258             $data = undef;
259             }
260             else {
261             $inlabel->set_text('Valid Data File ');
262             $data = $ref;
263             }
264             }
265              
266             sub _run_calculation {
267             my $func = $body;
268              
269             if (not $funclabel->get_text() eq 'Valid Function ') {
270             new_and_run
271             Gtk2::Ex::Dialogs::ErrorMsg( text => "You should give me a valid formula first." );
272             return();
273             }
274            
275             if (not $inlabel->get_text() eq 'Valid Data File ') {
276             new_and_run
277             Gtk2::Ex::Dialogs::ErrorMsg( text => "You need to select a valid input data file first." );
278             return();
279             }
280            
281             if (not $outlabel->get_text() eq 'Valid Output File ') {
282             new_and_run
283             Gtk2::Ex::Dialogs::ErrorMsg( text => "You need to select a valid output data file first." );
284             return();
285             }
286            
287             my $sym = $name->name;
288             my $csv = $data->[1];
289             my $cell = $csv->{cell};
290             my @vars = $func->signature;
291             my %vars = map {($_ => undef)} @vars;
292              
293             my %errors;
294            
295             foreach my $col (1..$csv->{maxcol}) {
296             my $name = $cell->[$col][1];
297             if ($name =~ /^([a-zA-Z]\w*)_(\d+)$/) {
298             # looks like an error
299             my $var = $1;
300             my $id = $2;
301             if (exists $vars{$var}) {
302             $errors{$var}[$id] = $col;
303             }
304             next;
305             }
306             next if not exists $vars{$name};
307             next if defined $vars{$name};
308             $vars{$name} = $col;
309             }
310              
311             my @undefined = grep {not defined $vars{$_}} keys %vars;
312             if (@undefined) {
313             new_and_run
314             Gtk2::Ex::Dialogs::ErrorMsg( text => "The data file does not include columns for the following variables:\n" . join("\n", sort @undefined) );
315             return();
316             }
317              
318             my $maxrow = 0;
319             foreach my $col (values %vars) {
320             my $this = @{$cell->[$col]};
321             $maxrow = $this if $this > $maxrow;
322             }
323              
324             $maxrow--;
325            
326             if ($maxrow < 2) {
327             new_and_run
328             Gtk2::Ex::Dialogs::ErrorMsg( text => "The data file does not have any data!" );
329             return();
330             }
331              
332             my $maxerr = 0;
333             foreach my $k (keys %errors) {
334             $maxerr = @{$errors{$k}} if $maxerr < @{$errors{$k}};
335             }
336             $maxerr--;
337            
338             my @out;
339             foreach my $i (2..$maxrow) {
340             my %v =
341             map {
342             my $n = $_;
343             my $v = $cell->[$vars{$_}][$i];
344             $v = 0 if not defined $v;
345             $v =~ s/,/./g;
346             my @e =
347             map {s/,/./g}
348             map {defined($_) ? $_ : 0}
349             map {
350             my $col = $errors{$n}[$_];
351             defined $col ? $cell->[$col][$i] : 0
352             }
353             1..$maxerr;
354             ($n => Number::WithError->new_big($v, @e))
355             }
356             keys %vars;
357             my $value = $body->value(%v);
358             $value = Number::WithError->new_big($value) if not ref($value) =~ /^Number::WithError/;
359            
360             push @out, $value;
361             }
362              
363             if (open(my $fh, '>', $outentry->get_text())) {
364             print $fh '"' . join('", "', $sym, map {$sym.'_'.$_} 1..$maxerr), '"', "\n";
365            
366             foreach my $row (0..$#out) {
367             my $v = shift @out;
368             my $num = $v->number;
369             my @e = @{$v->error()};
370             print $fh '"' . join('", "', $num, @e), '"', "\n";
371             }
372             }
373             else {
374             new_and_run
375             Gtk2::Ex::Dialogs::ErrorMsg( text => "Could not open output file for writing: $!" );
376             return();
377             }
378             }
379              
380              
381             1;
382              
383             __END__
384              
385             =head1 NAME
386              
387             App::ErrorCalculator - Calculations with Gaussian Error Propagation
388              
389             =head1 SYNOPSIS
390              
391             # You can use the 'errorcalculator' script instead.
392            
393             require App::ErrorCalculator;
394             App::ErrorCalculator->run();
395              
396             # Using the script:
397             # errorcalculator
398              
399             =head1 DESCRIPTION
400              
401             C<errorcalculator> and its implementing Perl module
402             C<App::ErrorCalculator> is a Gtk2 tool that lets you do
403             calculations with automatic error propagation.
404              
405             Start the script, enter a function into the function entry
406             field, select an input file, select an output file and hit
407             the I<Run Calculation> button to have all data in the input
408             field processed according to the function and written to the
409             output file.
410              
411             Functions should consist of a function name followed by an
412             equals sign and a function body. All identifiers
413             (both the function name and all variables in the function body)
414             should start with a letter. They may contain letters, numbers and
415             underscores.
416              
417             The function body may contain any number of constants, variables,
418             operators, functions and parenthesis.
419             The exact syntax can be obtained by reading
420             the manual page for L<Math::Symbolic::Parser>. Arithmetic
421             operators (C<+ - * / ^>) are supported. The caret indicates
422             exponentiation. Trigonometric, inverse
423             trigonometric and hyperbolic functions are implemented
424             (C<sin cos tan cot asin acos atan acot sinh cosh asinh acoth>).
425             C<log> indicates a natural logarithm.
426              
427             Additionally, you may include derivatives in the formula which
428             will be evaluated (analytically) for you. The syntax for this is:
429             C<partial_derivative(a * x + b, x)>. (Would evaluate to C<a>.)
430              
431             In order to allow for errors in constants, the program uses the
432             L<Math::SymbolicX::Error> parser extension: use the
433             C<error(1 +/- 0.2)> function to include constants with
434             associated uncertainties in your formulas.
435              
436             The input files may be of any format recognized by the
437             L<Spreadsheet::Read> module. That means: Excel sheets,
438             OpenOffice (1.0) spreadsheets, CSV (comma separated values)
439             text files, etc.
440              
441             The program reads tabular data from the spreadsheet file.
442             It expects each column to contain the data for one variable
443             in the formula.
444              
445             a, b, c
446             1, 2, 3
447             4, 5, 6
448             7, 8, 9
449              
450             This would assign C<1> to the variable C<a>, C<2> to C<b>
451             and C<3> to C<c> and then evaluate the formula with those
452             values. The result would be written to the first data line
453             of the output file. Then, the data in the next row will be
454             used and so on. If a column is missing data, it is assumed
455             to be zero.
456              
457             Since this is about errors, you can declare any number of
458             errors to the numbers as demonstrated below:
459              
460             a, a_1, a_2, b, b_1
461             1, 0.2, 0.1, 2, 0.3
462             4, 0.3, 0.3, 5, 0.6
463             7, 0.4, 0,1, 8, 0.9
464              
465             Apart from dropping C<c> for brevity, this example input
466             adds columns for the errors of C<a> and C<b>. C<a>
467             has two errors: C<a_1> and C<a_2>. C<b> only has one
468             error C<b_1> which corresponds to the error C<a_1>.
469             When calculating, C<a> will be used as C<1 +/- 0.2 +/- 0.1>
470             in the first calculation and C<b> as C<2 +/- 0.3 +/- 0>.
471             The error propagation is implemented using
472             L<Number::WithError> so that's where you go for details.
473              
474             The output file will be a CSV file similar to the input examples
475             above.
476              
477             =head1 EXAMPLES
478              
479             =head2 Sample input file
480              
481             "a", "a_1", "a_2", "x", "x_1", "x_2"
482             1, "0.1", "1.1", 10, "0.1", "1.1"
483             2, "0.2", "1.2", 11, "0.2", "1.2"
484             3, "0.3", "1.3", 12, "0.3", "1.3"
485             4, "0.4", "1.4", 13, "0.4", "1.4"
486             5, "0.5", "1.5", 14, "0.5", "1.5"
487              
488             =head2 Example function
489              
490             f = a * x^2
491              
492             =head2 Example output file
493              
494             "f", "f_1", "f_2"
495             "1.0e+02", "1.0e+02", "1.0e+02"
496             "2.4e+02", "1.3e+02", "1.3e+02"
497             "4.3e+02", "1.6e+02", "1.6e+02"
498             "6.8e+02", "2.0e+02", "2.0e+02"
499             "9.8e+02", "2.4e+02", "2.4e+02"
500              
501             =head1 SUBROUTINES
502              
503             =head2 run
504              
505             Just load the module with C<require App::ErrorCalculator> and then run
506              
507             App::ErrorCalculator->run;
508              
509             =head1 SEE ALSO
510              
511             New versions of this module can be found on http://steffen-mueller.net or CPAN.
512              
513             L<Math::Symbolic> implements the formula parser, compiler and evaluator.
514             (See also L<Math::Symbolic::Parser> and L<Math::Symbolic::Compiler>.)
515              
516             L<Number::WithError> does the actual error propagation.
517              
518             L<Gtk2> offers the GUI.
519              
520             =head1 AUTHOR
521              
522             Steffen Mueller, E<lt>particles-module at steffen-mueller dot net<gt>
523              
524             =head1 COPYRIGHT AND LICENSE
525              
526             Copyright (C) 2006 by Steffen Mueller
527              
528             This library is free software; you can redistribute it and/or modify
529             it under the same terms as Perl itself, either Perl version 5.6.1 or,
530             at your option, any later version of Perl 5 you may have available.
531              
532             =cut