File Coverage

blib/lib/App/Physics/ParticleMotion.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             package App::Physics::ParticleMotion;
2              
3 2     2   9253 use 5.006001;
  2         6  
  2         69  
4 2     2   9 use strict;
  2         4  
  2         59  
5 2     2   10 use warnings;
  2         3  
  2         112  
6              
7             our $VERSION = '1.01';
8              
9 2     2   10 use Carp qw/croak carp/;
  2         3  
  2         141  
10 2     2   1919 use Math::Symbolic qw/:all/;
  2         288172  
  2         706  
11 2     2   24 use Math::Symbolic::Compiler;
  2         4  
  2         70  
12 2     2   2137 use Math::RungeKutta qw//;
  2         11903  
  2         50  
13 2     2   1979 use Math::Project3D;
  2         54228  
  2         70  
14 2     2   778 use Tk qw/DoOneEvent DONT_WAIT MainLoop/;
  0            
  0            
15             use Tk::Cloth;
16             use Time::HiRes qw/time sleep/;
17             use Config::Tiny;
18              
19             # a short little helper
20             sub _def_or ($$) { defined( $_[0] ) ? $_[0] : $_[1] }
21              
22             sub new {
23             my $proto = shift;
24             my $class = ref($proto)||$proto;
25              
26             my $self = {
27             run => 0,
28             config => Config::Tiny->new(),
29             };
30             bless $self => $class;
31              
32             return $self;
33             }
34              
35              
36             sub config {
37             my $self = shift;
38             my $name = shift;
39              
40             return $self->{config} if not defined $name;
41            
42             if (ref($name) eq 'Config::Tiny') {
43             $self->{config} = $name;
44             return $name;
45             }
46              
47             eval {$self->{config} = Config::Tiny->read($name)};
48             croak "Could not read file '$name': $@." if ($@);
49              
50             return $self->{config};
51             }
52              
53             sub run {
54             my $app = shift;
55             die "Cannot run app more than once." if $app->{run};
56             $app->{run} = 1;
57             my $config = $app->config();
58            
59             # Coordinate and velocity variable names
60             my @coords = qw( x y z );
61             my @speeds = qw( vx vy vz );
62            
63             # Extract particle options from configuration file
64             my @particles = map { $config->{$_} }
65             sort { substr( $a, 1 ) <=> substr( $b, 1 ) }
66             grep { /^p\d+$/ }
67             keys %$config;
68            
69             # Default colors for particles
70             $_->{color} = defined( $_->{color} ) ? $_->{color} : '#FFFFFF'
71             foreach @particles;
72             $_->{colort} = defined( $_->{colort} ) ? $_->{colort} : '#000000'
73             foreach @particles;
74            
75             # General configuration options
76             my $gen_conf = $config->{_};
77             my $dimensions = $gen_conf->{dimensions} || 3;
78             $app->{dimensions} = $dimensions;
79              
80             my $particles = scalar(@particles);
81             $app->{particles} = \@particles;
82             my $free = $dimensions * $particles;
83             $app->{free} = $free;
84             my $axiscolor = $gen_conf->{axiscolor} || '#000000';
85             $app->{axiscolor} = $axiscolor;
86             my $displayscale = $gen_conf->{zoom} || 20;
87             $app->{displayscale} = $displayscale;
88             my $time_warp = $gen_conf->{timewarp} || 1;
89             $app->{time_warp} = $time_warp;
90             my $epsilon = $gen_conf->{epsilon} || 0.00000000001;
91             $app->{epsilon} = $epsilon;
92             my $trace = $gen_conf->{trace};
93             $app->{trace} = $trace;
94             my $out_file = $gen_conf->{output_file};
95             $app->{out_file} = $out_file;
96            
97             # Projection plane (see Math::Project3D for details!)
98             my $plane_basis_vector = [
99             _def_or( $gen_conf->{plane_base_x}, 0 ),
100             _def_or( $gen_conf->{plane_base_y}, 0 ),
101             _def_or( $gen_conf->{plane_base_z}, 0 ),
102             ];
103             my $plane_direction1 = [
104             _def_or( $gen_conf->{plane_vec1_x}, 0.371391 ),
105             _def_or( $gen_conf->{plane_vec1_y}, 0.928477 ),
106             _def_or( $gen_conf->{plane_vec1_z}, 0 ),
107             ];
108             my $plane_direction2 = [
109             _def_or( $gen_conf->{plane_vec2_x}, 0.371391 ),
110             _def_or( $gen_conf->{plane_vec2_y}, 0 ),
111             _def_or( $gen_conf->{plane_vec2_z}, 0.928477 ),
112             ];
113            
114             # Generate Math::Symbolic function objects from the functions
115             # specified as differential equations of the particle's motion
116             my @functions;
117             foreach my $p (@particles) {
118             foreach ( 0 .. $dimensions - 1 ) {
119             my $str = $p->{"func".$coords[$_]};
120             my $temp;
121             eval { $temp = Math::Symbolic->parse_from_string($str) };
122             if ($@ or not defined $temp) {
123             require Data::Dumper;
124             my $part = Data::Dumper->Dump($p);
125             croak "Could not parse function '$str' for particle\n$part";
126             }
127             push @functions, $temp;
128             }
129             }
130              
131             # Get starting values for the particles
132             my @startx = map {
133             my $p = $_;
134             ( map { $p->{ $coords[$_] } } ( 0 .. $dimensions - 1 ) )
135             } @particles;
136             my @startv = map {
137             my $p = $_;
138             ( map { $p->{ $speeds[$_] } } ( 0 .. $dimensions - 1 ) )
139             } @particles;
140            
141             my %const = %{ $config->{constants} };
142            
143             # Generate variable names from the number of particles and the
144             # coordinate and velocity names specified above.
145             my @vars;
146             foreach my $pno ( 1 .. $particles ) {
147             push @vars, $coords[ $_ - 1 ] . $pno foreach 1 .. $dimensions;
148             }
149             foreach my $pno ( 1 .. $particles ) {
150             push @vars, $speeds[ $_ - 1 ] . $pno foreach 1 .. $dimensions;
151             }
152            
153             my %vars = map { ( $_, undef ) } @vars;
154            
155             # Insert constants into the functions and make sure the
156             # remaining variables are all particle coordinates or velocities
157             # Compile functions to Perl code for speed.
158             # (Evaluating the trees every time would be a couple orders of
159             # magnitude slower!)
160             foreach my $coord ( 0 .. $free - 1 ) {
161             $functions[$coord]->implement(
162             map { ( $_ => Math::Symbolic::Constant->new( $const{$_} ) ) }
163             keys %const
164             );
165            
166             my @sig = $functions[$coord]->signature();
167             if ( grep { not exists $vars{$_} and not $_ eq 't' } @sig ) {
168             croak("Invalid function '" . $functions[$coord] . "'");
169             }
170            
171             my ( $sub, $leftover ) =
172             Math::Symbolic::Compiler->compile_to_sub( $functions[$coord],
173             [ 't', @vars ] );
174             croak("Could not resolve all derivatives in function $functions[$coord].")
175             if not defined $sub
176             or ( defined $leftover and ref($leftover) eq 'ARRAY' and @$leftover );
177            
178            
179             $functions[$coord] = $sub;
180             }
181             $app->{functions} = \@functions;
182            
183             # Define the projection used for displaying 3D-motion on a 2D display.
184             my $proj = Math::Project3D->new(
185             plane_basis_vector => $plane_basis_vector,
186             plane_direction1 => $plane_direction1,
187             plane_direction2 => $plane_direction2
188             );
189             $app->{proj} = $proj;
190            
191             # plane_direction1 => [ 0.371391, 0.928477, 0 ],
192             # plane_direction2 => [ 0.371391, 0, 0.928477 ],
193            
194             # Open output file if defined in configuration
195             my $out_filehandle;
196             if ( defined $out_file and not $out_file eq '' ) {
197             open $out_filehandle, '>', $out_file
198             or croak "Cannot write to output file '$out_file': $!";
199             }
200             $app->{out_filehandle} = $out_filehandle;
201            
202             # Starting coordinates and velocities. The @y vector will be acted
203             # upon by the runge-kutta integrator.
204             # It will always contain the coords in the first half and the velocities
205             # in the second.
206             my @y = ( @startx, @startv );
207             $app->{y} = \@y;
208            
209             $app->{started} = 0;
210            
211             # Tk setup
212             my $top = MainWindow->new();
213             $app->{top} = $top;
214            
215             my $button = $top->Button(
216             -height => 1,
217             -text => 'Run Simulation',
218             -command => sub {
219             # Only start if we haven't done so before
220             _draw($app) if $app->{started} == 0;
221             }
222             );
223             $button->pack( -fill => 'x', -expand => 0, -side => 'top' );
224            
225             my $cloth = $top->Scrolled('Cloth', -scrollbars => 'se');
226             #my $cloth = $top->Cloth;
227             $cloth->configure( -scrollregion => [-10000, -10000, 10000, 10000] );
228             $cloth->configure( -height => 600, -width => 800 );
229             $cloth->pack( -fill => 'both', -expand => 1, -side => 'top' );
230             $app->{cloth} = $cloth;
231            
232             # Math::Project3D is meant to project arbitrary functions. If we
233             # want to project discrete data, we just use the current contents
234             # of three variables as functions.
235             $proj->new_function( 'x,y,z', '$x', '$y', '$z' );
236             $app->{axis} = [];
237            
238             # Run the sub for the first time.
239             _draw_axis($app);
240            
241             # Print starting coordinates to file if required
242             if ( defined $out_file ) {
243             foreach my $p_no ( 0 .. $#particles ) {
244             my @proj = @y[ $dimensions * $p_no .. $dimensions * ( $p_no + 1 ) - 1 ];
245             push @proj, (0) x ( 3 - scalar(@proj) ) if @proj != 3;
246             print( $out_filehandle ( $p_no + 1 ) . " 0 @proj\n" );
247             }
248             }
249            
250             # Run the TK MainLoop
251             MainLoop;
252             }
253              
254              
255             # main routine that doesn't end.
256             sub _draw {
257             my $app = shift;
258             $app->{started} = 0;
259            
260             my $trace = $app->{trace};
261             my $cloth = $app->{cloth};
262             my $epsilon = $app->{epsilon};
263             my @functions = @{$app->{functions}};
264             my @particles = @{$app->{particles}};
265             my $particles = scalar @particles;
266             my $dimensions = $app->{dimensions};
267             my $out_file = $app->{out_file};
268             my $out_filehandle = $app->{out_filehandle};
269             my $displayscale = $app->{displayscale};
270             my $time_warp = $app->{time_warp};
271             my @y = @{$app->{y}};
272             my $proj = $app->{proj};
273              
274             # Starting time and time steps. $dt will be adjusted by the integrator
275             my $t = 0;
276             my $dt = 0.1;
277              
278             # @prevlines holds line objects from the previous iterations.
279             my @prevlines = ();
280              
281             # Previous values for line drawing
282             my @prev_x = ();
283             my @prev_y = ();
284              
285             # Start time of the simulation for speed adjustment on fast systems
286             my $timeref = time();
287              
288             # main loop
289             while (1) {
290              
291             # Delete old lines if we don't want to keep traces.
292             # Change their color otherwise
293             if ( not $trace ) {
294             $cloth->delete($_) foreach @prevlines;
295             @prevlines = ();
296             }
297             else {
298             $_->configure( -state => 'disabled' ) foreach @prevlines;
299              
300             # Won't have to keep the objects around for modification:
301             @prevlines = ();
302             }
303              
304             # Integrate the next step. (I'm open for speed improvements!)
305             ( $t, $dt, @y ) = Math::RungeKutta::rk4_auto(
306             \@y,
307             sub {
308             my $t = shift;
309             my @dydt = @_[ @_ / 2 .. $#_ ];
310             foreach ( 0 .. $#functions ) {
311             push @dydt, $functions[$_]->( $t, @_ );
312             }
313             return @dydt;
314             },
315             $t,
316             $dt,
317             $epsilon
318             );
319              
320             # Project and draw
321             foreach my $p_no ( 0 .. $particles - 1 ) {
322             my @proj =
323             @y[ $dimensions * $p_no .. $dimensions * ( $p_no + 1 ) - 1 ];
324             push @proj, (0) x ( 3 - scalar(@proj) ) if @proj != 3;
325              
326             # File output
327             print( $out_filehandle ( $p_no + 1 ) . " $t @proj\n" )
328             if defined $out_file;
329              
330             my ( $x, $y ) = $proj->project(@proj);
331              
332             if ( defined $prev_x[$p_no] ) {
333             my $coords =
334             [ _transform( $app, $prev_x[$p_no], $prev_y[$p_no], $x, $y ) ];
335             @$coords = map { int $_ } @$coords;
336             $coords->[2] -= 1 while abs( $coords->[0] - $coords->[2] ) < 1;
337             $coords->[3] -= 1 while abs( $coords->[1] - $coords->[3] ) < 1;
338              
339             my $line = $cloth->Line(
340             -coords => $coords,
341             -fill => $particles[$p_no]{color},
342             -disabledfill => $particles[$p_no]{colort},
343             );
344             push @prevlines, $line; # if not $trace;
345             }
346             $prev_x[$p_no] = $x;
347             $prev_y[$p_no] = $y;
348             }
349              
350             # Speed control.
351             DoOneEvent(DONT_WAIT);
352             my $endtime = $timeref + $t / $time_warp;
353             while ( $endtime > time() ) {
354             sleep(0.01);
355             DoOneEvent(DONT_WAIT);
356             }
357             }
358             }
359              
360              
361              
362              
363              
364             # transform calculates the window coordinates from the
365             # relative coordinates of the projected plane
366             sub _transform {
367             my $app = shift;
368             my $cloth = $app->{cloth};
369             my $displayscale = $app->{displayscale};
370             my $y_max_half = $cloth->cget('-height') / 2;
371             my $x_max_half = $cloth->cget('-width') / 2;
372             my @res;
373             while (@_) {
374             push @res, $x_max_half + shift(@_) * $displayscale,
375             $y_max_half - shift(@_) * $displayscale;
376             }
377             return @res;
378             }
379              
380             # draw_axis draws the n axis' as Tk::Cloth::Line objects.
381             sub _draw_axis {
382             my $app = shift;
383             my $axis = $app->{axis};
384             my $proj = $app->{proj};
385             my $dimensions = $app->{dimensions};
386             my $axiscolor = $app->{axiscolor};
387             my $cloth = $app->{cloth};
388             my $displayscale = $app->{displayscale};
389             my $max = 20000;
390             $_->delete() foreach @$axis;
391             foreach my $dim ( 0 .. $dimensions - 1 ) {
392             my ( $x1, $y1 ) =
393             $proj->project( map { $_ == $dim ? -$max / $displayscale : 0 }
394             0 .. 2 );
395             my ( $x2, $y2 ) =
396             $proj->project( map { $_ == $dim ? $max / $displayscale : 0 }
397             0 .. 2 );
398             push @$axis,
399             $cloth->Line(
400             -coords => [ _transform( $app, $x1, $y1, $x2, $y2 ) ],
401             -fill => $axiscolor,
402             );
403             }
404             }
405              
406              
407             1;
408             __END__