File Coverage

blib/lib/Math/PlanePath/VogelFloret.pm
Criterion Covered Total %
statement 76 124 61.2
branch 10 32 31.2
condition 14 26 53.8
subroutine 20 28 71.4
pod 11 11 100.0
total 131 221 59.2


line stmt bran cond sub pod time code
1             # Copyright 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 Kevin Ryde
2              
3             # This file is part of Math-PlanePath.
4             #
5             # Math-PlanePath is free software; you can redistribute it and/or modify
6             # it under the terms of the GNU General Public License as published by the
7             # Free Software Foundation; either version 3, or (at your option) any later
8             # version.
9             #
10             # Math-PlanePath is distributed in the hope that it will be useful, but
11             # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12             # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13             # for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with Math-PlanePath. If not, see .
17              
18              
19             # n_start=>0 to include N=0 at the origin, but that not a documented feature
20             # yet.
21              
22             # http://algorithmicbotany.org/papers/#abop
23             #
24             # http://www.sciencedirect.com/science/article/pii/0025556479900804
25             # http://dx.doi.org/10.1016/0025-5564(79)90080-4 Helmut Vogel, "A Better Way
26             # to Construct the Sunflower Head", Volume 44, Issues 3-4, June 1979, Pages
27             # 179-189
28              
29             # http://artemis.wszib.edu.pl/~sloot/2_1.html
30             #
31             # http://www.csse.monash.edu.au/publications/2003/tr-2003-149-full.pdf
32             # on 3D surfaces of revolution or some such maybe
33             # 14 Mbytes (or preview with google)
34              
35             # Count of Zeckendorf bits plotted on Vogel floret.
36             # Zeckendorf/Fibbinary with N bits makes radial spokes. cf FibbinaryBitCount
37             # http://www.ms.unimelb.edu.au/~segerman/papers/sunflower_spiral_fibonacci_metric.pdf
38             # private copy ?
39              
40             # closest two for phi are 1 and 4
41             # n=1 r=sqrt(1) = 1
42             # t=1/phi^2 = 0.381 around
43             # x=-.72 y=.68
44             # n=4 r=sqrt(4) = 2
45             # t=4/phi^2 = 1.527 = .527 around
46             # x=-1.97 y=-.337
47             # diff angle=4/phi^2 - 1/phi^2 = 3/phi^2 = 3*(2-phi) = 1.14 = .14
48             # diff dx=1.25 dy=1.017 hypot=1.61
49             # dang = 2*PI()*(5-3*phi)
50             # y = sin()
51             # x = sin(2*PI()*(5-3*phi))
52              
53             # Continued fraction
54             # 1
55             # x = k + ------
56             # k + 1
57             # ------
58             # k + 1
59             # ---
60             # k + ...
61             #
62             # x = k + 1/x
63             # (x-k/2)^2 = 1 + (k^2)/4
64             #
65             # k + sqrt(4+k^2)
66             # x = ---------------
67             # 2
68             #
69             # k x
70             # 1 (1+sqrt(5)) / 2
71             # 2 1 + sqrt(2)
72             # 3 (3+sqrt(13)) / 2
73             # 4 2 + sqrt(5)
74             # 5 (5 + sqrt(29)) / 2
75             # 6 3 + sqrt(10)
76             # 2e e + sqrt(1+e^2) even
77              
78              
79              
80             package Math::PlanePath::VogelFloret;
81 2     2   1774 use 5.004;
  2         9  
82 2     2   12 use strict;
  2         14  
  2         60  
83 2     2   11 use Carp 'croak';
  2         5  
  2         96  
84 2     2   891 use Math::Libm 'hypot';
  2         6863  
  2         148  
85              
86 2     2   24 use vars '$VERSION', '@ISA';
  2         4  
  2         101  
87             $VERSION = 128;
88 2     2   698 use Math::PlanePath;
  2         4  
  2         93  
89             @ISA = ('Math::PlanePath');
90              
91             use Math::PlanePath::Base::Generic
92 2     2   15 'is_infinite';
  2         3  
  2         88  
93 2     2   944 use Math::PlanePath::SacksSpiral;
  2         5  
  2         84  
94              
95             # uncomment this to run the ### lines
96             #use Smart::Comments '###';
97              
98 2     2   14 use constant figure => 'circle';
  2         4  
  2         117  
99              
100 2     2   11 use constant 1.02; # for leading underscore
  2         30  
  2         64  
101 2     2   10 use constant _PHI => (1 + sqrt(5)) / 2;
  2         5  
  2         110  
102 2     2   10 use constant _TWO_PI => 4*atan2(1,0);
  2         4  
  2         180  
103              
104             # not documented yet ...
105 2         305 use constant rotation_types =>
106             { phi => { rotation_factor => 2 - _PHI(),
107             radius_factor => 0.624239116809924,
108             # closest_Ns => [ 1,4 ],
109             # continued_frac => [ 1,1,1,1,1,... ],
110             },
111             sqrt2 => { rotation_factor => sqrt(2)-1,
112             radius_factor => 0.679984167849259,
113             # closest_Ns => [ 3,8 ],
114             # continued_frac => [ 2,2,2,2,2,... ],
115             },
116             sqrt3 => { rotation_factor => sqrt(3)-1,
117             radius_factor => 0.755560810248419,
118             # closest_Ns => [ 3,7 ],
119             # continued_frac => [ 1,2,1,2,1,2,1,2,... ],
120             },
121             sqrt5 => { rotation_factor => sqrt(5)-2,
122             radius_factor => 0.853488207169303,
123             # closest_Ns => [ 4,8 ],
124             # continued_frac => [ 4,4,4,4,4,4,... ],
125             },
126 2     2   14 };
  2         4  
127              
128 2         469 use constant parameter_info_array =>
129             [
130             {
131             name => 'rotation_type',
132             type => 'enum',
133             display => 'Rotation Type',
134             share_key => 'vogel_rotation_type',
135             choices => ['phi', 'sqrt2', 'sqrt3', 'sqrt5', 'custom'],
136             default => 'phi',
137             },
138             {
139             name => 'rotation_factor',
140             type => 'float',
141             type_hint => 'expression',
142             display => 'Rotation Factor',
143             description => 'Rotation factor. If you have Math::Symbolic then this can be an expression like pi+2*e-phi (constants phi,e,gam,pi), otherwise it should be a plain number.',
144             default => - (1 + sqrt(5)) / 2,
145             default_expression => '-phi',
146             width => 10,
147             when_name => 'rotation_type',
148             when_value => 'custom',
149             },
150             { name => 'radius_factor',
151             display => 'Radius Factor',
152             description => 'Radius factor, spreading points out to make them non-overlapping. 0 means the default factor.',
153             type => 'float',
154             minimum => 0,
155             maximum => 999,
156             page_increment => 1,
157             step_increment => .1,
158             decimals => 2,
159             default => 1,
160             when_name => 'rotation_type',
161             when_value => 'custom',
162             },
163 2     2   14 ];
  2         4  
164              
165             sub x_negative_at_n {
166 0     0 1 0 my ($self) = @_;
167 0         0 return int(.25 / $self->{'rotation_factor'}) + 1;
168             }
169             sub y_negative_at_n {
170 0     0 1 0 my ($self) = @_;
171 0         0 return int(.5 / $self->{'rotation_factor'}) + 1;
172             }
173             sub sumabsxy_minimum {
174 0     0 1 0 my ($self) = @_;
175 0         0 my ($x,$y) = $self->n_to_xy($self->n_start);
176 0         0 return abs($x)+abs($y);
177             }
178             sub rsquared_minimum {
179 0     0 1 0 my ($self) = @_;
180             # starting N=1 at R=radius_factor*sqrt(1), theta=something
181 0         0 return $self->{'radius_factor'} ** 2;
182             }
183 2     2   14 use constant gcdxy_maximum => 0;
  2         15  
  2         250  
184              
185             sub turn_any_left { # always left if rot<=0.5
186 0     0 1 0 my ($self) = @_;
187 0         0 return ($self->{'rotation_factor'} <= 0.5);
188             }
189             sub turn_any_right { # always left if rot<=0.5
190 0     0 1 0 my ($self) = @_;
191 0         0 return ($self->{'rotation_factor'} > 0.5);
192             }
193 2     2   15 use constant turn_any_straight => 0; # never straight
  2         4  
  2         2025  
194              
195              
196             #------------------------------------------------------------------------------
197              
198             sub new {
199 11     11 1 1666 my $self = shift->SUPER::new (@_);
200             ### $self
201              
202 11   100     58 my $rotation_type = ($self->{'rotation_type'} ||= 'phi');
203 11   33     32 my $defaults = rotation_types()->{$rotation_type}
204             || croak 'Unrecognised rotation_type: "',$rotation_type,'"';
205              
206             $self->{'radius_factor'} ||= ($self->{'rotation_factor'}
207             ? 1.0
208 11 100 66     43 : $defaults->{'radius_factor'});
209 11   66     42 $self->{'rotation_factor'} ||= $defaults->{'rotation_factor'};
210 11         27 return $self;
211             }
212              
213             # R=radius_factor*sqrt($n)
214             # R^2 = radius_factor^2 * $n
215             # avoids sqrt and sin/cos in the main n_to_xy()
216             #
217             sub n_to_rsquared {
218 8     8 1 127 my ($self, $n) = @_;
219             ### VogelFloret RSquared: $i, $seq->{'planepath_object'}
220              
221 8 50       22 if ($n < 0) { return undef; }
  0         0  
222 8         408 my $rf = $self->{'radius_factor'};
223 8         15 $rf *= $rf; # squared
224              
225             # don't round BigInt*flonum if radius_factor is not an integer, promote to
226             # BigFloat instead
227 8 100 66     36 if (ref $n && $n->isa('Math::BigInt') && $rf != int($rf)) {
      100        
228 1         1030 require Math::BigFloat;
229 1         26899 $n = Math::BigFloat->new($n);
230             }
231 8         981 return $n * $rf;
232             }
233              
234              
235             sub n_to_xy {
236 0     0 1 0 my ($self, $n) = @_;
237 0 0       0 if ($n < 0) { return; }
  0         0  
238              
239 0         0 my $two_pi = _TWO_PI();
240              
241 0 0       0 if (ref $n) {
242 0 0       0 if ($n->isa('Math::BigInt')) {
243 0         0 $n = Math::PlanePath::SacksSpiral::_bigfloat()->new($n);
244             }
245 0 0       0 if ($n->isa('Math::BigRat')) {
246 0         0 $n = $n->as_float;
247             }
248 0 0       0 if ($n->isa('Math::BigFloat')) {
249 0         0 $two_pi = 2 * Math::BigFloat->bpi;
250             }
251             }
252              
253 0         0 my $r = sqrt($n) * $self->{'radius_factor'};
254              
255             # take the frac part of 1==circle and then convert to radians, so as not
256             # to lose precision in an fmod(...,2*pi)
257             #
258 0         0 my $theta = $n * $self->{'rotation_factor'}; # 1==full circle
259 0         0 $theta = $two_pi * ($theta - int($theta)); # radians 0 to 2*pi
260 0         0 return ($r * cos($theta),
261             $r * sin($theta));
262              
263             # cylindrical_to_cartesian() is only perl code, so may as well sin/cos
264             # here directly
265             # return (Math::Trig::cylindrical_to_cartesian($r, $theta, 0))[0,1];
266             }
267              
268             sub xy_to_n {
269 0     0 1 0 my ($self, $x, $y) = @_;
270              
271             # Slack approach just trying all the N values between r-.5 and r+.5.
272             #
273             # r = sqrt(n)*FACTOR
274             # n = (r/FACTOR)^2
275             #
276             # The target N satisfies N = K * phi + epsilon for integer K. What's an
277             # easy way to find the first integer N >= (r-.5)**2 satisfying -small <= N
278             # mod .318 <= +small ?
279             #
280 0         0 my $r = sqrt($x*$x + $y*$y); # hypot
281 0         0 my $factor = $self->{'radius_factor'};
282 0         0 my $n_lo = int( (($r-.6)/$factor)**2 );
283 0 0       0 if ($n_lo < 0) { $n_lo = 0; }
  0         0  
284 0         0 my $n_hi = int( (($r+.6)/$factor)**2 + 1 );
285             #### $r
286             #### xy: "$x,$y"
287             #### $n_lo
288             #### $n_hi
289              
290 0 0 0     0 if (is_infinite($n_lo) || is_infinite($n_hi)) {
291             ### infinite range, r inf or too big
292 0         0 return undef;
293             }
294              
295             # for(;;) loop since "reverse $n_lo..$n_hi" limited to IV range
296 0         0 for (my $n = $n_hi; $n >= $n_lo; $n--) {
297 0         0 my ($nx, $ny) = $self->n_to_xy($n);
298             ### hypot: "$n ".hypot($nx-$x,$ny-$y)
299 0 0       0 if (hypot($nx-$x,$ny-$y) <= 0.5) {
300             #### found: $n
301 0         0 return $n;
302             }
303             }
304 0         0 return undef;
305              
306             # my $theta_frac = Math::PlanePath::MultipleRings::_xy_to_angle_frac($x,$y);
307             # ### assert: 0 <= $frac && $frac < 1
308             #
309             # # seeking integer k where (k+theta)*PHIPHI == $r*$r == $n or nearby
310             # my $k = $r*$r / (PHI*PHI) - $theta;
311             #
312             # ### $x
313             # ### $y
314             # ### $r
315             # ### $theta
316             # ### $k
317             #
318             # foreach my $ki (POSIX::floor($k), POSIX::ceil($k)) {
319             # my $n = int (($ki+$theta)*PHI*PHI + 0.5);
320             #
321             # # look for within 0.5 radius
322             # my ($nx, $ny) = $self->n_to_xy($n);
323             # ### $ki
324             # ### n frac: ($ki+$theta)*PHI*PHI
325             # ### $n
326             # ### hypot: hypot($nx-$x,$ny-$y)
327             # if (hypot($nx-$x,$ny-$y) <= 0.5) {
328             # return $n;
329             # }
330             # }
331             # return;
332             }
333              
334             # max corner at R
335             # R+0.5 = sqrt(N) * radius_factor
336             # sqrt(N) = (R+0.5)/rfactor
337             # N = (R+0.5)^2 / rfactor^2
338             # = (R^2 + R + 1/4) / rfactor^2
339             # <= (X^2+Y^2 + X+Y + 1/4) / rfactor^2
340             # <= (X(X+1) + Y(Y+1) + 1) / rfactor^2
341             #
342             # min corner at R
343             # R-0.5 = sqrt(N) * radius_factor
344             # sqrt(N) = (R-0.5)/rfactor
345             # N = (R-0.5)^2 / rfactor^2
346             # = (R^2 - R + 1/4) / rfactor^2
347             # >= (X^2+Y^2 - (X+Y)) / rfactor^2 because x+y >= r
348             # = (X(X-1) + Y(Y-1)) / rfactor^2
349              
350             # not exact
351             sub rect_to_n_range {
352 1     1 1 5 my $self = shift;
353             ### VogelFloret rect_to_n_range(): @_
354 1         4 my ($n_lo, $n_hi) = Math::PlanePath::SacksSpiral->rect_to_n_range(@_);
355              
356 1         2 my $rf = $self->{'radius_factor'};
357 1         2 $rf *= $rf; # squared
358              
359             # avoid BigInt/flonum if radius_factor is not an integer, promote to
360             # BigFloat instead
361 1 50       5 if ($rf == int($rf)) {
362 0         0 $n_hi += $rf-1; # division round upwards
363             } else {
364 1 50 33     3 if (ref $n_lo && $n_lo->isa('Math::BigInt')) {
365 0         0 require Math::BigFloat;
366 0         0 $n_lo = Math::BigFloat->new($n_lo);
367             }
368 1 50 33     3 if (ref $n_hi && $n_lo->isa('Math::BigInt')) {
369 0         0 require Math::BigFloat;
370 0         0 $n_hi = Math::BigFloat->new($n_hi);
371             }
372             }
373              
374 1         3 $n_lo = int($n_lo / $rf);
375 1 50       2 if ($n_lo < 1) { $n_lo = 1; }
  1         2  
376              
377 1         10 $n_hi = _ceil($n_hi / $rf);
378              
379 1         5 return ($n_lo, $n_hi);
380             }
381              
382             sub _ceil {
383 1     1   3 my ($x) = @_;
384 1         2 my $int = int($x);
385 1 50       10 return ($x > $int ? $int+1 : $int);
386             }
387              
388             1;
389             __END__