File Coverage

blib/lib/Math/PlanePath/SierpinskiCurve.pm
Criterion Covered Total %
statement 159 236 67.3
branch 56 102 54.9
condition 27 35 77.1
subroutine 23 38 60.5
pod 18 18 100.0
total 283 429 65.9


line stmt bran cond sub pod time code
1             # Copyright 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             # Sierpinski, "Sur Une Nouvelle Courbe Qui Remplit Toute Une Aire Plaine",
20             # Bull. Acad. Sci. Cracovie Series A, 1912, pages 462-478.
21              
22              
23             package Math::PlanePath::SierpinskiCurve;
24 2     2   9124 use 5.004;
  2         14  
25 2     2   10 use strict;
  2         5  
  2         62  
26 2     2   18 use List::Util 'sum','first';
  2         5  
  2         244  
27             #use List::Util 'min','max';
28             *min = \&Math::PlanePath::_min;
29             *max = \&Math::PlanePath::_max;
30              
31 2     2   13 use vars '$VERSION', '@ISA';
  2         4  
  2         126  
32             $VERSION = 128;
33 2     2   679 use Math::PlanePath;
  2         4  
  2         103  
34             @ISA = ('Math::PlanePath');
35             *_divrem_mutate = \&Math::PlanePath::_divrem_mutate;
36              
37             use Math::PlanePath::Base::Generic
38 2         102 'is_infinite',
39 2     2   14 'round_nearest';
  2         4  
40             use Math::PlanePath::Base::Digits
41 2         131 'round_up_pow',
42             'round_down_pow',
43 2     2   447 'digit_split_lowtohigh';
  2         3  
44              
45             # uncomment this to run the ### lines
46             # use Smart::Comments;
47              
48              
49 2     2   13 use constant n_start => 0;
  2         4  
  2         352  
50              
51             sub x_negative {
52 8     8 1 30 my ($self) = @_;
53 8         32 return ($self->{'arms'} >= 3);
54             }
55             sub y_negative {
56 8     8 1 18 my ($self) = @_;
57 8         34 return ($self->{'arms'} >= 5);
58             }
59              
60 2         456 use constant parameter_info_array =>
61             [
62             { name => 'arms',
63             share_key => 'arms_8',
64             display => 'Arms',
65             type => 'integer',
66             minimum => 1,
67             maximum => 8,
68             default => 1,
69             width => 1,
70             description => 'Arms',
71             },
72              
73             { name => 'straight_spacing',
74             display => 'Straight Spacing',
75             type => 'integer',
76             minimum => 1,
77             default => 1,
78             width => 1,
79             description => 'Spacing of the straight line points.',
80             },
81             { name => 'diagonal_spacing',
82             display => 'Diagonal Spacing',
83             type => 'integer',
84             minimum => 1,
85             default => 1,
86             width => 1,
87             description => 'Spacing of the diagonal points.',
88             },
89 2     2   15 ];
  2         4  
90              
91             # Ntop = (4^level)/2 - 1
92             # Xtop = 3*2^(level-1) - 1
93             # fill = Ntop / (Xtop*(Xtop-1)/2)
94             # -> 2 * ((4^level)/2 - 1) / (3*2^(level-1) - 1)^2
95             # -> 2 * ((4^level)/2) / (3*2^(level-1))^2
96             # = 4^level / (9*4^(level-1)
97             # = 4/9 = 0.444
98              
99             sub x_negative_at_n {
100 0     0 1 0 my ($self) = @_;
101 0 0       0 return $self->arms_count >= 3 ? 2 : undef;
102             }
103             sub y_negative_at_n {
104 0     0 1 0 my ($self) = @_;
105 0 0       0 return $self->arms_count >= 5 ? 4 : undef;
106             }
107              
108             {
109             # Note: shared by Math::PlanePath::SierpinskiCurveStair
110             my @x_minimum = (undef,
111             1, # 1 arm
112             0, # 2 arms
113             ); # more than 2 arm, X goes negative
114             sub x_minimum {
115 0     0 1 0 my ($self) = @_;
116 0         0 return $x_minimum[$self->arms_count];
117             }
118             }
119             {
120             # Note: shared by Math::PlanePath::SierpinskiCurveStair
121             my @sumxy_minimum = (undef,
122             1, # 1 arm, octant and X>=1 so X+Y>=1
123             1, # 2 arms, X>=1 or Y>=1 so X+Y>=1
124             0, # 3 arms, Y>=1 and X>=Y, so X+Y>=0
125             ); # more than 3 arm, Sum goes negative so undef
126             sub sumxy_minimum {
127 0     0 1 0 my ($self) = @_;
128 0         0 return $sumxy_minimum[$self->arms_count];
129             }
130             }
131 2     2   15 use constant sumabsxy_minimum => 1;
  2         3  
  2         207  
132              
133             # Note: shared by Math::PlanePath::SierpinskiCurveStair
134             # Math::PlanePath::AlternatePaper
135             # Math::PlanePath::AlternatePaperMidpoint
136             sub diffxy_minimum {
137 0     0 1 0 my ($self) = @_;
138 0 0       0 return ($self->arms_count == 1
139             ? 1 # octant Y<=X-1 so X-Y>=1
140             : undef); # more than 1 arm, DiffXY goes negative
141             }
142 2     2   14 use constant absdiffxy_minimum => 1; # X=Y never occurs
  2         10  
  2         119  
143 2     2   14 use constant rsquared_minimum => 1; # minimum X=1,Y=0
  2         10  
  2         807  
144              
145             sub dx_minimum {
146 0     0 1 0 my ($self) = @_;
147             return - max($self->{'straight_spacing'},
148 0         0 $self->{'diagonal_spacing'});
149             }
150             *dy_minimum = \&dx_minimum;
151              
152             sub dx_maximum {
153 0     0 1 0 my ($self) = @_;
154             return max($self->{'straight_spacing'},
155 0         0 $self->{'diagonal_spacing'});
156             }
157             *dy_maximum = \&dx_maximum;
158              
159             sub _UNDOCUMENTED__dxdy_list {
160 0     0   0 my ($self) = @_;
161 0         0 my $s = $self->{'straight_spacing'};
162 0         0 my $d = $self->{'diagonal_spacing'};
163 0 0       0 return ($s,0, # E eight scaled
    0          
    0          
    0          
    0          
    0          
    0          
164             ($d ? ( $d, $d) : ()), # NE except s=0
165             ($s ? ( 0, $s) : ()), # N or d=0 skips
166             ($d ? (-$d, $d) : ()), # NW
167             ($s ? (-$s, 0) : ()), # W
168             ($d ? (-$d,-$d) : ()), # SW
169             ($s ? ( 0,-$s) : ()), # S
170             ($d ? ( $d,-$d) : ())); # SE
171              
172             }
173             {
174             my @_UNDOCUMENTED__dxdy_list_at_n = (undef,
175             21, 20, 27, 36,
176             29, 12, 12, 13);
177             sub _UNDOCUMENTED__dxdy_list_at_n {
178 0     0   0 my ($self) = @_;
179 0         0 return $_UNDOCUMENTED__dxdy_list_at_n[$self->{'arms'}];
180             }
181             }
182             sub dsumxy_minimum {
183 0     0 1 0 my ($self) = @_;
184             return - max($self->{'straight_spacing'},
185 0         0 2*$self->{'diagonal_spacing'});
186             }
187             sub dsumxy_maximum {
188 0     0 1 0 my ($self) = @_;
189             return max($self->{'straight_spacing'},
190 0         0 2*$self->{'diagonal_spacing'});
191             }
192             *ddiffxy_minimum = \&dsumxy_minimum;
193             *ddiffxy_maximum = \&dsumxy_maximum;
194              
195 2     2   14 use constant dir_maximum_dxdy => (1,-1); # South-East
  2         4  
  2         113  
196 2     2   13 use constant turn_any_straight => 0; # never straight
  2         4  
  2         3506  
197              
198              
199             #------------------------------------------------------------------------------
200              
201             sub new {
202 256     256 1 19777 my $self = shift->SUPER::new(@_);
203              
204 256   100     1114 $self->{'arms'} = max(1, min(8, $self->{'arms'} || 1));
205 256   100     773 $self->{'straight_spacing'} ||= 1;
206 256   100     721 $self->{'diagonal_spacing'} ||= 1;
207 256         506 return $self;
208             }
209              
210             sub n_to_xy {
211 7496     7496 1 150506 my ($self, $n) = @_;
212             ### SierpinskiCurve n_to_xy(): $n
213              
214 7496 50       14704 if ($n < 0) {
215 0         0 return;
216             }
217 7496 50       14547 if (is_infinite($n)) {
218 0         0 return ($n,$n);
219             }
220              
221 7496         13514 my $int = int($n); # BigFloat int() gives BigInt, use that
222 7496         10501 $n -= $int; # preserve possible BigFloat
223             ### $int
224             ### $n
225              
226 7496         16425 my $arm = _divrem_mutate ($int, $self->{'arms'});
227              
228 7496         11825 my $s = $self->{'straight_spacing'};
229 7496         10401 my $d = $self->{'diagonal_spacing'};
230 7496         11257 my $base = 2*$d+$s;
231 7496         10971 my $x = my $y = ($int * 0); # inherit big 0
232 7496         11249 my $len = $x + $base; # inherit big
233              
234 7496         15105 foreach my $digit (digit_split_lowtohigh($int,4)) {
235             ### at: "$x,$y digit=$digit"
236              
237 13213 100       26871 if ($digit == 0) {
    100          
    100          
238 1601         2437 $x = $n*$d + $x;
239 1601         2222 $y = $n*$d + $y;
240 1601         2194 $n = 0;
241              
242             } elsif ($digit == 1) {
243 5610         11027 ($x,$y) = ($n*$s - $y + $len-$d-$s, # rotate +90
244             $x + $d);
245 5610         7794 $n = 0;
246              
247             } elsif ($digit == 2) {
248             # rotate -90
249 3493         8189 ($x,$y) = ($n*$d + $y + $len-$d,
250             -$n*$d - $x + $len-$d-$s);
251 3493         4939 $n = 0;
252              
253             } else { # digit==3
254 2509         3415 $x += $len;
255             }
256 13213         20366 $len *= 2;
257             }
258              
259             # n=0 or n=33..33
260 7496         11665 $x = $n*$d + $x;
261 7496         10626 $y = $n*$d + $y;
262              
263 7496         10321 $x += 1;
264 7496 100       14605 if ($arm & 1) {
265 3267         5872 ($x,$y) = ($y,$x); # mirror 45
266             }
267 7496 100       12925 if ($arm & 2) {
268 2743         4942 ($x,$y) = (-1-$y,$x); # rotate +90
269             }
270 7496 100       12826 if ($arm & 4) {
271 1807         2350 $x = -1-$x; # rotate 180
272 1807         2760 $y = -1-$y;
273             }
274              
275             # use POSIX 'floor';
276             # $x += floor($x/3);
277             # $y += floor($y/3);
278              
279             # $x += floor(($x-1)/3) + floor(($x-2)/3);
280             # $y += floor(($y-1)/3) + floor(($y-2)/3);
281              
282              
283             ### final: "$x,$y"
284 7496         17267 return ($x,$y);
285             }
286              
287             my @digit_to_dir = (0, -2, 2, 0);
288             my @dir8_to_dx = (1, 1, 0,-1, -1, -1, 0, 1);
289             my @dir8_to_dy = (0, 1, 1, 1, 0, -1, -1,-1);
290             my @digit_to_nextturn = (-1, # after digit=0
291             2, # digit=1
292             -1); # digit=2
293             sub n_to_dxdy {
294 0     0 1 0 my ($self, $n) = @_;
295             ### n_to_dxdy(): $n
296              
297 0 0       0 if ($n < 0) {
298 0         0 return; # first direction at N=0
299             }
300              
301 0         0 my $int = int($n);
302 0         0 $n -= $int;
303              
304 0         0 my $arm = _divrem_mutate($int,$self->{'arms'});
305 0         0 my $lowbit = _divrem_mutate($int,2);
306             ### $lowbit
307             ### $int
308              
309 0 0       0 if (is_infinite($int)) {
310 0         0 return ($int,$int);
311             }
312 0         0 my @ndigits = digit_split_lowtohigh($int,4);
313             ### @ndigits
314              
315 0         0 my $dir8 = sum(0, map {$digit_to_dir[$_]} @ndigits);
  0         0  
316 0 0       0 if ($arm & 1) {
317 0         0 $dir8 = - $dir8; # mirrored on second,fourth,etc arm
318             }
319 0         0 $dir8 += ($arm|1); # NE,NW,SW, or SE
320              
321 0         0 my $turn;
322 0 0 0     0 if ($n || $lowbit) {
323             # next turn
324              
325             # lowest non-3 digit, or zero if all 3s (implicit 0 above high digit)
326 0     0   0 $turn = $digit_to_nextturn[ first {$_!=3} @ndigits, 0 ];
  0         0  
327 0 0       0 if ($arm & 1) {
328 0         0 $turn = - $turn; # mirrored on second,fourth,etc arm
329             }
330             }
331              
332 0 0       0 if ($lowbit) {
333 0         0 $dir8 += $turn;
334             }
335              
336 0         0 my $s = $self->{'straight_spacing'};
337 0         0 my $d = $self->{'diagonal_spacing'};
338              
339 0         0 $dir8 &= 7;
340 0 0       0 my $spacing = ($dir8 & 1 ? $d : $s);
341 0         0 my $dx = $spacing * $dir8_to_dx[$dir8];
342 0         0 my $dy = $spacing * $dir8_to_dy[$dir8];
343              
344 0 0       0 if ($n) {
345 0         0 $dir8 += $turn;
346 0         0 $dir8 &= 7;
347 0 0       0 $spacing = ($dir8 & 1 ? $d : $s);
348 0         0 $dx += $n*($spacing * $dir8_to_dx[$dir8]
349             - $dx);
350 0         0 $dy += $n*($spacing * $dir8_to_dy[$dir8]
351             - $dy);
352             }
353              
354 0         0 return ($dx, $dy);
355             }
356              
357             # 2| . 3 .
358             # 1| 1 . 2
359             # 0| . 0 .
360             # +------
361             # 0 1 2
362             #
363             # 4| . . . 3 . # diagonal_spacing == 3
364             # 3| . . . . 2 4 # mod=2*3+1=7
365             # 2| . . . . . . .
366             # 1| 1 . . . . . . .
367             # 0| . 0 . . . . . . 6
368             # +------------------
369             # 0 1 2 3 4 5 6 7 8
370             #
371             sub _NOTWORKING__xy_is_visited {
372 0     0   0 my ($self, $x, $y) = @_;
373 0         0 $x = round_nearest($x);
374 0         0 $y = round_nearest($y);
375 0         0 my $mod = 2*$self->{'diagonal_spacing'} + $self->{'straight_spacing'};
376 0   0     0 return (_rect_within_arms($x,$y, $x,$y, $self->{'arms'})
377             && ((($x%$mod)+($y%$mod)) & 1));
378             }
379              
380             # x1 * x2 *
381             # +-----*-+y2*
382             # | *| *
383             # | * *
384             # | |* *
385             # | | **
386             # +-------+y1*
387             # ----------------
388             #
389             # arms=5 x1,y2 after X=Y-1 line, so x1 > y2-1, x1 >= y2
390             # ************
391             # x1 * x2
392             # +---*----+y2
393             # | * |
394             # | * |
395             # |* |
396             # * |
397             # *+--------+y1
398             # *
399             #
400             # arms=7 x1,y1 after X=-2-Y line, so x1 > -2-y1
401             # ************
402             # ** +------+
403             # * *| |
404             # * * |
405             # * |* |
406             # * | * |
407             # *y1+--*---+
408             # * x1 *
409             #
410             # _rect_within_arms() returns true if rectangle x1,y1,x2,y2 has some part
411             # within the extent of the $arms set of octants.
412             #
413             sub _rect_within_arms {
414 363     363   622 my ($x1,$y1, $x2,$y2, $arms) = @_;
415 363 100 66     1470 return ($arms <= 4
      66        
416             ? ($y2 >= 0 # y2 top edge must be positive
417             && ($arms <= 2
418             ? ($arms == 1 ? $x2 > $y1 # arms==1 bottom right
419             : $x2 >= 0) # arms==2 right edge
420             : ($arms == 4 # arms==4 anything
421             || $x2 >= -$y2))) # arms==3 top right
422              
423             # arms >= 5
424             : ($y2 >= 0 # y2 top edge positive is good, otherwise check
425             || ($arms <= 6
426             ? ($arms == 5 ? $x1 < $y2 # arms==5 top left
427             : $x1 < 0) # arms==6 left edge
428             : ($arms == 8 # arms==8 anything
429             || $x1 <= -2-$y1)))); # arms==7 bottom left
430             }
431              
432             sub xy_to_n {
433 57896     57896 1 262199 my ($self, $x, $y) = @_;
434             ### SierpinskiCurve xy_to_n(): "$x, $y"
435              
436 57896         102534 $x = round_nearest($x);
437 57896         102690 $y = round_nearest($y);
438              
439 57896         81914 my $arm = 0;
440 57896 100       99795 if ($y < 0) {
441 27207         35467 $arm = 4;
442 27207         35220 $x = -1-$x; # rotate -180
443 27207         35973 $y = -1-$y;
444             }
445 57896 100       94646 if ($x < 0) {
446 28811         36767 $arm += 2;
447 28811         46505 ($x,$y) = ($y, -1-$x); # rotate -90
448             }
449 57896 100       97316 if ($y > $x) { # second octant
450 25619         32358 $arm++;
451 25619         41661 ($x,$y) = ($y,$x); # mirror 45
452             }
453              
454 57896         84127 my $arms = $self->{'arms'};
455 57896 100       96493 if ($arm >= $arms) {
456 24000         41995 return undef;
457             }
458              
459 33896         44339 $x -= 1;
460 33896 100 100     92556 if ($x < 0 || $x < $y) {
461 4200         8449 return undef;
462             }
463             ### x adjust to zero: "$x,$y"
464             ### assert: $x >= 0
465             ### assert: $y >= 0
466              
467 29696         43273 my $s = $self->{'straight_spacing'};
468 29696         39942 my $d = $self->{'diagonal_spacing'};
469 29696         45841 my $base = (2*$d+$s);
470 29696   100     84306 my ($len,$level) = round_down_pow (($x+$y)/$base || 1, 2);
471             ### $level
472             ### $len
473 29696 50       59613 if (is_infinite($level)) {
474 0         0 return $level;
475             }
476              
477             # Xtop = 3*2^(level-1)-1
478             #
479 29696         50305 $len *= 2*$base;
480             ### initial len: $len
481              
482 29696         39711 my $n = 0;
483 29696         52836 foreach (0 .. $level) {
484 35791         47001 $n *= 4;
485             ### at: "loop=$_ len=$len x=$x,y=$y n=$n"
486             ### assert: $x >= 0
487             ### assert: $y >= 0
488              
489 35791         50491 my $len_sub_d = $len - $d;
490 35791 100       59185 if ($x < $len_sub_d) {
491             ### digit 0 or 1...
492 33045 100       55371 if ($x+$y+$s < $len) {
493             ### digit 0 ...
494             } else {
495             ### digit 1 ...
496 6596         11787 ($x,$y) = ($y-$d, $len-$s-$d-$x); # shift then rotate -90
497 6596         8931 $n += 1;
498             }
499             } else {
500 2746         3818 $x -= $len_sub_d;
501             ### digit 2 or 3 to: "x=$x y=$y"
502 2746 100       4292 if ($x < $y) { # before diagonal
503             ### digit 2...
504 1861         3519 ($x,$y) = ($len-$d-$s-$y, $x); # shift y-len then rotate +90
505 1861         2856 $n += 2;
506             } else {
507             #### digit 3...
508 885         1128 $x -= $d;
509 885         1198 $n += 3;
510             }
511 2746 100       5020 if ($x < 0) {
512 376         857 return undef;
513             }
514             }
515 35415         59143 $len /= 2;
516             }
517              
518             ### end at: "x=$x,y=$y n=$n"
519             ### assert: $x >= 0
520             ### assert: $y >= 0
521              
522 29320         38324 $n *= 4;
523 29320 100 100     126992 if ($y == 0 && $x == 0) {
    100 100        
    100 100        
    100 100        
524             ### final digit 0 ...
525             } elsif ($x == $d && $y == $d) {
526             ### final digit 1 ...
527 1767         2615 $n += 1;
528             } elsif ($x == $d+$s && $y == $d) {
529             ### final digit 2 ...
530 1607         2714 $n += 2;
531             } elsif ($x == $base && $y == 0) {
532             ### final digit 3 ...
533 1279         1868 $n += 3;
534             } else {
535 22874         48247 return undef;
536             }
537              
538 6446         13643 return $n*$arms + $arm;
539             }
540              
541             # not exact
542             sub rect_to_n_range {
543 363     363 1 11276 my ($self, $x1,$y1, $x2,$y2) = @_;
544             ### SierpinskiCurve rect_to_n_range(): "$x1,$y1 $x2,$y2"
545              
546 363         720 $x1 = round_nearest ($x1);
547 363         689 $x2 = round_nearest ($x2);
548 363         648 $y1 = round_nearest ($y1);
549 363         628 $y2 = round_nearest ($y2);
550 363 50       693 ($x1,$x2) = ($x2,$x1) if $x1 > $x2;
551 363 100       671 ($y1,$y2) = ($y2,$y1) if $y1 > $y2;
552              
553 363         523 my $arms = $self->{'arms'};
554 363 100       664 unless (_rect_within_arms($x1,$y1, $x2,$y2, $arms)) {
555             ### rect outside octants, for arms: $arms
556 9         28 return (1,0);
557             }
558              
559 354         599 my $max = ($x2 + $y2);
560 354 100       640 if ($arms >= 3) {
561 307         721 _apply_max ($max, -1-$x1 + $y2);
562              
563 307 100       541 if ($arms >= 5) {
564 237         484 _apply_max ($max, -1-$x1 - $y1-1);
565              
566 237 100       463 if ($arms >= 7) {
567 135         228 _apply_max ($max, $x2 - $y1-1);
568             }
569             }
570             }
571              
572             # base=2d+s
573             # level begins at
574             # base*(2^level-1)-s = X+Y ... maybe
575             # base*2^level = X+base
576             # 2^level = (X+base)/base
577             # level = log2((X+base)/base)
578             # then
579             # Nlevel = 4^level-1
580              
581 354         604 my $base = 2 * $self->{'diagonal_spacing'} + $self->{'straight_spacing'};
582 354         1085 my ($power) = round_down_pow (int(($max+$base-2)/$base),
583             2);
584 354         873 return (0, 4*$power*$power * $arms - 1);
585             }
586              
587             sub _apply_max {
588             ### _apply_max(): "$_[0] cf $_[1]"
589 679 100   679   1227 unless ($_[0] > $_[1]) {
590 273         405 $_[0] = $_[1];
591             }
592             }
593              
594             #------------------------------------------------------------------------------
595              
596             sub level_to_n_range {
597 9     9 1 2183 my ($self, $level) = @_;
598 9         41 return (0, 4**$level * $self->{'arms'} - 1);
599             }
600             sub n_to_level {
601 0     0 1   my ($self, $n) = @_;
602 0 0         if ($n < 0) { return undef; }
  0            
603 0 0         if (is_infinite($n)) { return $n; }
  0            
604 0           $n = round_nearest($n);
605 0           _divrem_mutate ($n, $self->{'arms'});
606 0           my ($pow, $exp) = round_up_pow ($n+1, 4);
607 0           return $exp;
608             }
609              
610             #------------------------------------------------------------------------------
611             1;
612             __END__