File Coverage

blib/lib/Math/PlanePath/KochCurve.pm
Criterion Covered Total %
statement 198 241 82.1
branch 52 80 65.0
condition 31 52 59.6
subroutine 30 37 81.0
pod 6 6 100.0
total 317 416 76.2


line stmt bran cond sub pod time code
1             # Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 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             # math-image --path=KochCurve --lines --scale=10
20             # math-image --path=KochCurve --all --scale=10
21              
22             # continuous but nowhere differentiable
23             #
24             # Sur une Courbe Continue sans Tangente, Obtenue par une Construction
25             # Géométrique Élémentaire
26             #
27             # http://www.nku.edu/~curtin/grenouille.html
28             # http://www.nku.edu/~curtin/koch_171.jpg
29             #
30             # Cesàro, "Remarques sur la Courbe de von Koch." Atti della
31             # R. Accad. della Scienze Fisiche e Matem. Napoli 12, No. 15, 1-12,
32             # 1905. Reprinted as §228 in Opere scelte, a cura dell'Unione matematica
33             # italiana e col contributo del Consiglio nazionale delle ricerche, Vol. 2:
34             # Geometria, Analisi, Fisica Matematica. Rome: Edizioni Cremonese,
35             # pp. 464-479, 1964.
36             #
37             # Thue-Morse count 1s mod 2 is net direction
38             # Toeplitz first diffs is turn sequence +1 or -1
39             #
40             # J. Ma and J.A. Holdener. When Thue-Morse Meets Koch. In Fractals:
41             # Complex Geometry, Patterns, and Scaling in Nature and Society, volume 13,
42             # pages 191-206, 2005.
43             # http://personal.kenyon.edu/holdenerj/StudentResearch/WhenThueMorsemeetsKochJan222005.pdf
44             #
45             # F.M. Dekking. On the Distribution of Digits In Arithmetic Sequences.
46             # In Seminaire de Theorie des Nombres de Bordeaux, volume 12, 1983, pages
47             # 3201-3212,
48             #
49              
50              
51              
52             package Math::PlanePath::KochCurve;
53 4     4   9656 use 5.004;
  4         22  
54 4     4   25 use strict;
  4         6  
  4         125  
55 4     4   22 use List::Util 'sum','first';
  4         8  
  4         464  
56              
57 4     4   38 use vars '$VERSION', '@ISA';
  4         10  
  4         247  
58             $VERSION = 129;
59 4     4   766 use Math::PlanePath;
  4         9  
  4         234  
60             @ISA = ('Math::PlanePath');
61             *_divrem_mutate = \&Math::PlanePath::_divrem_mutate;
62              
63             use Math::PlanePath::Base::Generic
64 4         222 'is_infinite',
65 4     4   25 'round_nearest';
  4         8  
66             use Math::PlanePath::Base::Digits
67 4         232 'round_down_pow',
68             'round_up_pow',
69             'digit_split_lowtohigh',
70 4     4   511 'digit_join_lowtohigh';
  4         9  
71              
72             # uncomment this to run the ### lines
73             # use Smart::Comments;
74              
75              
76 4     4   23 use constant n_start => 0;
  4         8  
  4         232  
77 4     4   23 use constant class_x_negative => 0;
  4         11  
  4         233  
78 4     4   27 use constant class_y_negative => 0;
  4         8  
  4         182  
79 4     4   21 use constant diffxy_minimum => 0; # X>=Y octant so X-Y>=0
  4         8  
  4         190  
80 4     4   23 use constant dx_minimum => -2;
  4         7  
  4         188  
81 4     4   24 use constant dx_maximum => 2;
  4         8  
  4         197  
82 4     4   22 use constant dy_minimum => -1;
  4         7  
  4         221  
83 4     4   26 use constant dy_maximum => 1;
  4         7  
  4         309  
84             *_UNDOCUMENTED__dxdy_list = \&Math::PlanePath::_UNDOCUMENTED__dxdy_list_six;
85 4     4   32 use constant absdx_minimum => 1; # never vertical
  4         8  
  4         231  
86 4     4   23 use constant dsumxy_minimum => -2; # diagonals
  4         11  
  4         212  
87 4     4   26 use constant dsumxy_maximum => 2;
  4         8  
  4         181  
88 4     4   21 use constant ddiffxy_minimum => -2;
  4         6  
  4         223  
89 4     4   27 use constant ddiffxy_maximum => 2;
  4         8  
  4         223  
90 4     4   25 use constant dir_maximum_dxdy => (1,-1); # South-East
  4         8  
  4         239  
91 4     4   26 use constant turn_any_straight => 0; # never straight
  4         6  
  4         8154  
92              
93              
94             #------------------------------------------------------------------------------
95              
96             sub n_to_xy {
97 1456     1456 1 52706 my ($self, $n) = @_;
98             ### KochCurve n_to_xy(): $n
99              
100             # secret negatives to -.5
101 1456 50       2757 if (2*$n < -1) { return; }
  0         0  
102 1456 50       2657 if (is_infinite($n)) { return ($n,$n); }
  0         0  
103              
104 1456         2466 my $x;
105             my $y;
106             {
107 1456         1850 my $int = int($n);
  1456         2038  
108 1456         1986 $x = 2 * ($n - $int); # usually positive, but n=-0.5 gives x=-0.5
109 1456         1878 $y = $x * 0; # inherit possible bigrat 0
110 1456         1988 $n = $int; # BigFloat int() gives BigInt, use that
111             }
112              
113 1456         1986 my $len = $y+1; # inherit bignum 1
114 1456         2751 foreach my $digit (digit_split_lowtohigh($n,4)) {
115             ### at: "$x,$y digit=$digit"
116              
117 6190 100       11166 if ($digit == 0) {
    100          
    100          
118              
119             } elsif ($digit == 1) {
120 1675         3486 ($x,$y) = (($x-3*$y)/2 + 2*$len, # rotate +60
121             ($x+$y)/2);
122              
123             } elsif ($digit == 2) {
124 1667         3724 ($x,$y) = (($x+3*$y)/2 + 3*$len, # rotate -60
125             ($y-$x)/2 + $len);
126              
127             } else {
128             ### assert: $digit==3
129 1624         2249 $x += 4*$len;
130             }
131 6190         8786 $len *= 3;
132             }
133              
134             ### final: "$x,$y"
135 1456         3478 return ($x,$y);
136             }
137              
138             sub xy_to_n {
139 5645     5645 1 38527 my ($self, $x, $y) = @_;
140             ### KochPeaks xy_to_n(): "$x, $y"
141              
142 5645         10226 $x = round_nearest ($x);
143 5645         10540 $y = round_nearest ($y);
144 5645 100 100     21526 if ($y < 0 || $x < 0 || (($x ^ $y) & 1)) {
      66        
145             ### neg y or parity different ...
146 517         949 return undef;
147             }
148 5128   100     13199 my ($len,$level) = round_down_pow(($x/2)||1, 3);
149             ### $level
150             ### $len
151 5128 50       10068 if (is_infinite($level)) {
152 0         0 return $level;
153             }
154              
155 5128         8136 my $n = 0;
156 5128         8433 foreach (0 .. $level) {
157 16512         20504 $n *= 4;
158             ### at: "level=$level len=$len x=$x,y=$y n=$n"
159 16512 100       25701 if ($x < 3*$len) {
160 8597 100       13266 if ($x < 2*$len) {
161             ### digit 0 ...
162             } else {
163             ### digit 1 ...
164 2434         3228 $x -= 2*$len;
165 2434         4657 ($x,$y) = (($x+3*$y)/2, # rotate -60
166             ($y-$x)/2);
167 2434         3298 $n += 1;
168             }
169             } else {
170 7915         10826 $x -= 4*$len;
171             ### digit 2 or 3 to: "x=$x"
172 7915 100       11617 if ($x < $y) { # before diagonal
173             ### digit 2...
174 3456         4551 $x += $len;
175 3456         4298 $y -= $len;
176 3456         6495 ($x,$y) = (($x-3*$y)/2, # rotate +60
177             ($x+$y)/2);
178 3456         4642 $n += 2;
179             } else {
180             #### digit 3...
181 4459         5585 $n += 3;
182             }
183             }
184 16512         23719 $len /= 3;
185             }
186             ### end at: "x=$x,y=$y n=$n"
187 5128 100 100     11085 if ($x != 0 || $y != 0) {
188 4947         9986 return undef;
189             }
190 181         369 return $n;
191             }
192              
193             # level extends to x= 2*3^level
194             # level = log3(x/2)
195             #
196             # exact
197             sub rect_to_n_range {
198 29     29 1 3451 my ($self, $x1,$y1, $x2,$y2) = @_;
199             ### KochCurve rect_to_n_range(): "$x1,$y1 $x2,$y2"
200              
201 29         70 $x1 = round_nearest ($x1);
202 29         65 $x2 = round_nearest ($x2);
203 29         58 $y1 = round_nearest ($y1);
204 29         60 $y2 = round_nearest ($y2);
205 29 50       63 if ($x1 > $x2) { ($x1,$x2) = ($x2,$x1); }
  0         0  
206 29 50       84 if ($y1 > $y2) { ($y1,$y2) = ($y2,$y1); }
  0         0  
207              
208 29 50 33     159 if ($x2 < 0 || $y2 < 0
      33        
209             || 3*$y1 > $x2 ) { # above line Y=X/3
210 0         0 return (1,0);
211             }
212              
213             # \
214             # \
215             # * \
216             # / \ \
217             # o-+-* *-+-e \
218             # 0 3 6
219             #
220             # 3*Y+X/2 - (Y!=0)
221             #
222             # /
223             # *-+-*
224             # \
225             # * *
226             # / \ /
227             # o-+-* *-+-*
228             # 0 3 6 X/2
229             #
230 29         90 my ($len, $level) = round_down_pow ($x2/2, 3);
231 29         78 return _rect_to_n_range_rot ($len, $level, 0, $x1,$y1, $x2,$y2);
232              
233              
234              
235             # (undef, my $level) = round_down_pow ($x2/2, 3);
236             # ### $level
237             # return (0, 4**($level+1)-1);
238             }
239              
240              
241             my @dir6_to_dx = (2, 1,-1,-2, -1, 1);
242             my @dir6_to_dy = (0, 1, 1, 0, -1,-1);
243             my @max_digit_to_rot = (1, -2, 1, 0);
244             my @min_digit_to_rot = (0, 1, -2, 1);
245             my @max_digit_to_offset = (-1, -1, -1, 2);
246              
247             sub _rect_to_n_range_rot {
248 29     29   61 my ($initial_len, $level_max, $initial_rot, $x1,$y1, $x2,$y2) = @_;
249             ### KochCurve _rect_to_n_range_rot(): "$x1,$y1 $x2,$y2 len=$initial_len level=$level_max rot=$initial_rot"
250              
251 29         45 my ($rot, $len, $x, $y);
252             my $overlap = sub {
253             ### overlap: "$x,$y len=$len rot=$rot"
254              
255 296 100   296   500 if ($len == 1) {
256 144   100     984 return ($x >= $x1 && $x <= $x2
257             && $y >= $y1 && $y <= $y2);
258             }
259 296         201 my $len = $len / 3;
260              
261 296 100       241 if ($rot < 3) {
262 121 100       200 if ($rot == 0) {
    100          
263             # *
264             # / \
265             # o-+-* *-+-.
266 57   100     340 return ($y <= $y2 # bottom before end
267             && $y+$len >= $y1
268             && $x <= $x2
269             && $x+6*$len > $x1); # right before end, exclusive
270             } elsif ($rot == 1) {
271             # .
272             # /
273             # *-+-*
274             # \
275             # * +-----
276             # / |x1,y2
277             # o
278 50   100     216 return ($x <= $x2 # left before end
279             && $y+3*$len > $y1 # top after start, exclusive
280             && $y-$x <= $y2-$x1); # diag before corner
281             } else {
282             # . |x1,y1
283             # \ +-----
284             # *
285             # /
286             # *-+-*
287             # \
288             # o
289 14   100     58 return ($y <= $y2 # bottom before end
290             && $x-3*$len <=$x2 # left before end
291             && $y+$x >= $y1+$x1); # diag after corner
292             }
293             } else {
294 175 100       56 if ($rot == 3) {
    100          
295             # .-+-* *-+-o
296             # \ /
297             # *
298 2   0     8 return ($y >= $y1 # top after start
299             && $y-$len <= $y2 # bottom before end
300             && $x >= $x1 # right after start
301             && $x-6*$len < $x2); # left before end, exclusive
302             } elsif ($rot == 4) {
303             # x2,y1| o
304             # -----+ /
305             # *
306             # \
307             # *-+-*
308             # /
309             # .
310 1   33     8 return ($x >= $x1 # right after start
311             && $y-3*$len < $y2 # bottom before end, exclusive
312             && $y-$x >= $y1-$x2); # diag after corner
313             } else {
314             # o
315             # \
316             # *-+-*
317             # /
318             # *
319             # -----+ \
320             # x2,y2| .
321 172   100     184 return ($y >= $y1 # top after start
322             && $x+3*$len >= $x1 # right after start
323             && $y+$x <= $y2+$x2); # diag before corner
324             }
325             }
326 29         150 };
327              
328 29         56 my $zero = 0*$x1*$x2*$y1*$y2;
329 29         52 my @lens = ($initial_len);
330 29         45 my $n_hi;
331 29         47 $rot = $initial_rot;
332 29         36 $len = $initial_len;
333 29         41 $x = $zero;
334 29         46 $y = $zero;
335 29         45 my @digits = (4);
336              
337 29         45 for (;;) {
338 170         246 my $digit = --$digits[-1];
339             ### max at: "digits=".join(',',@digits)." xy=$x,$y len=$len"
340              
341 170 100       291 if ($digit < 0) {
342 2         3 pop @digits;
343 2 50       6 if (! @digits) {
344             ### nothing found to level_max ...
345 0         0 return (1, 0);
346             }
347             ### end of digits, backtrack ...
348 2         4 $len = $lens[$#digits];
349 2         3 next;
350             }
351              
352 168         229 my $offset = $max_digit_to_offset[$digit];
353 168         226 $rot = ($rot - $max_digit_to_rot[$digit]) % 6;
354 168         248 $x += $dir6_to_dx[$rot] * $offset * $len;
355 168         217 $y += $dir6_to_dy[$rot] * $offset * $len;
356              
357             ### $offset
358             ### $rot
359              
360 168 100       245 if (&$overlap()) {
361 64 100       132 if ($#digits >= $level_max) {
362             ### yes overlap, found n_hi ...
363             ### digits: join(',',@digits)
364             ### n_hi: _digit_join_hightolow (\@digits, 4, $zero)
365 29         98 $n_hi = _digit_join_hightolow (\@digits, 4, $zero);
366 29         66 last;
367             }
368             ### yes overlap, descend ...
369 35         58 push @digits, 4;
370 35   66     107 $len = ($lens[$#digits] ||= $len/3);
371             } else {
372             ### no overlap, next digit ...
373             }
374             }
375              
376 29         44 $rot = $initial_rot;
377 29         55 $x = $zero;
378 29         35 $y = $zero;
379 29         41 $len = $initial_len;
380 29         54 @digits = (-1);
381              
382 29         37 for (;;) {
383 129         190 my $digit = ++$digits[-1];
384             ### min at: "digits=".join(',',@digits)." xy=$x,$y len=$len"
385              
386 129 100       220 if ($digit > 3) {
387 1         2 pop @digits;
388 1 50       4 if (! @digits) {
389             ### oops, n_lo not found to level_max ...
390 0         0 return (1, 0);
391             }
392             ### end of digits, backtrack ...
393 1         2 $len = $lens[$#digits];
394 1         2 next;
395             }
396              
397             ### $digit
398             ### rot increment: $min_digit_to_rot[$digit]
399 128         179 $rot = ($rot + $min_digit_to_rot[$digit]) % 6;
400              
401 128 100       174 if (&$overlap()) {
402 63 100       126 if ($#digits >= $level_max) {
403             ### yes overlap, found n_lo ...
404             ### digits: join(',',@digits)
405             ### n_lo: _digit_join_hightolow (\@digits, 4, $zero)
406 29         55 return (_digit_join_hightolow (\@digits, 4, $zero),
407             $n_hi);
408             }
409             ### yes overlap, descend ...
410 34         55 push @digits, -1;
411 34   33     72 $len = ($lens[$#digits] ||= $len/3);
412              
413             } else {
414             ### no overlap, next digit ...
415 65         90 $x += $dir6_to_dx[$rot] * $len;
416 65         107 $y += $dir6_to_dy[$rot] * $len;
417             }
418             }
419             }
420              
421             # $aref->[0] high digit
422             sub _digit_join_hightolow {
423 58     58   111 my ($aref, $radix, $zero) = @_;
424 58         114 my @lowtohigh = reverse @$aref;
425 58         127 return digit_join_lowtohigh(\@lowtohigh, $radix, $zero);
426             }
427              
428              
429             my @digit_to_dir = (0, 1, -1, 0);
430             my @digit_to_nextturn = (1, # digit=1 (with +1 for "next" N)
431             -2, # digit=2
432             1); # digit=3
433             sub n_to_dxdy {
434 20     20 1 116 my ($self, $n) = @_;
435             ### n_to_dxdy(): $n
436              
437 20 50       35 if ($n < 0) {
438 0         0 return; # first direction at N=0
439             }
440 20 50       40 if (is_infinite($n)) {
441 0         0 return ($n,$n);
442             }
443              
444 20         39 my $int = int($n);
445 20         27 $n -= $int;
446 20         41 my @ndigits = digit_split_lowtohigh($int,4);
447              
448 20         44 my $dir6 = sum(0, map {$digit_to_dir[$_]} @ndigits) % 6;
  144         243  
449 20         35 my $dx = $dir6_to_dx[$dir6];
450 20         29 my $dy = $dir6_to_dy[$dir6];
451              
452 20 100       36 if ($n) {
453             # fraction part
454              
455             # lowest non-3 digit, or zero if all 3s (0 above high digit)
456 12     15   57 $dir6 += $digit_to_nextturn[ first {$_!=3} @ndigits, 0 ];
  15         31  
457 12         31 $dir6 %= 6;
458 12         35 $dx += $n*($dir6_to_dx[$dir6] - $dx);
459 12         27 $dy += $n*($dir6_to_dy[$dir6] - $dy);
460             }
461 20         53 return ($dx, $dy);
462             }
463              
464             sub _UNTESTED__n_to_dir6 {
465 0     0     my ($self, $n) = @_;
466 0 0         if ($n < 0) {
467 0           return undef; # first direction at N=0
468             }
469 0 0         if (is_infinite($n)) {
470 0           return ($n,$n);
471             }
472 0   0       return (sum (map {$digit_to_dir[$_]} digit_split_lowtohigh($n,4))
473             || 0) # if empty
474             % 6;
475             }
476              
477             my @n_to_turn6 = (undef,
478             1, # +60 degrees
479             -2, # -120 degrees
480             1); # +60 degrees
481             sub _UNTESTED__n_to_turn6 {
482 0     0     my ($self, $n) = @_;
483 0 0         if (is_infinite($n)) {
484 0           return undef;
485             }
486 0           while ($n) {
487 0           my $digit = _divrem_mutate($n,4);
488 0 0         if ($digit) {
489             # lowest non-zero digit
490 0           return $n_to_turn6[$digit];
491             }
492             }
493 0           return 0;
494             }
495             sub _UNTESTED__n_to_turn_LSR {
496 0     0     my ($self, $n) = @_;
497 0   0       my $turn6 = $self->_UNTESTED__n_to_turn6($n) || return undef;
498 0 0         return ($turn6 > 0 ? 1 : -1);
499             }
500             sub _UNTESTED__n_to_turn_left {
501 0     0     my ($self, $n) = @_;
502 0   0       my $turn6 = $self->_UNTESTED__n_to_turn6($n) || return undef;
503 0 0         return ($turn6 > 0 ? 1 : 0);
504             }
505             sub _UNTESTED__n_to_turn_right {
506 0     0     my ($self, $n) = @_;
507 0   0       my $turn6 = $self->_UNTESTED__n_to_turn6($n) || return undef;
508 0 0         return ($turn6 < 0 ? 1 : 0);
509             }
510              
511             #------------------------------------------------------------------------------
512             # levels
513              
514             sub level_to_n_range {
515 0     0 1   my ($self, $level) = @_;
516 0           return (0, 4**$level);
517             }
518             sub n_to_level {
519 0     0 1   my ($self, $n) = @_;
520 0 0         if ($n < 0) { return undef; }
  0            
521 0 0         if (is_infinite($n)) { return $n; }
  0            
522 0           $n = round_nearest($n);
523 0           my ($pow, $exp) = round_up_pow ($n, 4);
524 0           return $exp;
525             }
526              
527              
528             #------------------------------------------------------------------------------
529             1;
530             __END__