Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl Monk, Perl Meditation
 
PerlMonks  

Fractal Curves: Short & Fast Codes?

by chunlou (Curate)
on Jun 17, 2003 at 08:51 UTC ( [id://266410]=perlquestion: print w/replies, xml ) Need Help??

chunlou has asked for the wisdom of the Perl Monks concerning the following question:

Came across the following LOGO code one day:
; Koch Snowflake to KochIsland :size :level repeat 3 [KSide :size :level rt 120] end to KSide :size :level if :level = 0 [fd :size stop] KSide :size / 3 :level - 1 lt 60 KSide :size / 3 :level - 1 rt 120 KSide :size / 3 :level - 1 lt 60 KSide :size / 3 :level - 1 end
It draws Koch snowflake. Don't matter if you don't know (or care about) LOGO, I was just impressed with its briefness, as well as its descriptiveness.

So, I thought, just for fun, I could probably do something more or less as succinct in Perl (which, after all, is known for its terseness).

I came up with the following three versions (in the order of attempt):

Closure version:
#! /usr/local/bin/perl -w use strict; use GD; # ----------------------------------------------------------- use constant PI => 3.14159265359; # ----------------------------------------------------------- # turtle as in LOGO's turtle # Usage: $turtle = turtle(); # $turtle->($turn_right_x_degree, $forward_x_points); # $xy = $turtle->(); sub turtle { my ($h, $xy) = (0, [[0],[0]]); # h = heading (0 - north, 90 - +east, etc) return sub { $h = $h + (shift || 0); # accumulative turns in degree my $d = shift || 0; # distance $xy->[0][scalar(@{$xy->[0]})] = $d*sin(PI*$h/180) + $xy->[0][$ +#{@{$xy->[0]}}]; $xy->[1][scalar(@{$xy->[1]})] = $d*cos(PI*$h/180) + $xy->[1][$ +#{@{$xy->[1]}}]; return $xy; }; } # ----------------------------------------------------------- # Koch Snowflake sub koch { my ($turtle, $d, $level) = @_ ; if ($level==0) {$turtle->(0,$d); return 1;} $turtle->( 0,0); koch($turtle,$d/3,$level-1); $turtle->(-60,0); koch($turtle,$d/3,$level-1); $turtle->(120,0); koch($turtle,$d/3,$level-1); $turtle->(-60,0); koch($turtle,$d/3,$level-1); } # ----------------------------------------------------------- my $turtle = turtle(); map {$turtle->(120, 0); koch($turtle, 170, 4);} 0..2; # ----------------------------------------------------------- plotxy($turtle->(), 'koch.jpg'); # ----------------------------------------------------------- # ----------------------------------------------------------- # Minkowski Island sub minkowski { my ($turtle, $d, $level) = @_ ; if ($level==0) {$turtle->(0,$d); return 1;} minkowski($turtle,$d/4,$level-1); $turtle->(-90,0); minkowski($turtle,$d/4,$level-1); $turtle->( 90,0); minkowski($turtle,$d/4,$level-1); $turtle->( 90,0); minkowski($turtle,$d/4,$level-1); minkowski($turtle,$d/4,$level-1); $turtle->(-90,0); minkowski($turtle,$d/4,$level-1); $turtle->(-90,0); minkowski($turtle,$d/4,$level-1); $turtle->( 90,0); minkowski($turtle,$d/4,$level-1); } # ----------------------------------------------------------- $turtle = turtle(); map {$turtle->(90,0); minkowski($turtle, 150, 3);} 0..3; # ----------------------------------------------------------- plotxy($turtle->(), 'minkowski.jpg'); # ----------------------------------------------------------- # ----------------------------------------------------------- # Dragon Curve sub dragon1 { my ($turtle, $d, $level) = @_ ; if ($level==0) {$turtle->(0,$d); return 1;} dragon($turtle,$d*0.707,$level-1); $turtle->(-90,0); dragon1($turtle,$d*0.707,$level-1); } sub dragon { my ($turtle, $d, $level) = @_ ; if ($level==0) {$turtle->(0,$d); return 1;} dragon($turtle,$d*0.707,$level-1); $turtle->(90,0); dragon1($turtle,$d*0.707,$level-1); } # ----------------------------------------------------------- $turtle = turtle(); dragon($turtle, 150, 12); # ----------------------------------------------------------- plotxy($turtle->(), 'dragon.jpg'); # ----------------------------------------------------------- # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Sub for Plotting # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # sub min {(sort {$a<=>$b} @_ )[0];} # ---------------------------------------------------------------- sub max {(sort {$a<=>$b} @_ )[-1];} # ---------------------------------------------------------------- sub plotxy { my $xy = shift; my $file = shift; my $scale = shift || 1; # ------------------------------------------------------- my @x = @{$xy->[0]}; my @y = @{$xy->[1]}; my $x_min = min(@x); my $y_min = min(@y); map {$x[$_]=$scale*($x[$_]-$x_min)} 0..$#x; map {$y[$_]=$scale*($y[$_]-$y_min)} 0..$#y; my $x_width = max(@x) - 0; my $y_width = max(@y) - 0; # ------------------------------------------------------- my $image = new GD::Image($x_width,$y_width); my $white = $image->colorAllocate(255,255,255); my $black = $image->colorAllocate(0,0,0); $image->transparent($white); $image->interlaced('true'); $image->line($x[$_-1],$y[$_-1],$x[$_],$y[$_],$black) for 1..$#x; $image = $image->copyFlipVertical() ; open(IMG, ">$file"); binmode IMG; print IMG $image->jpeg; }

OO version:
# Turtle as in LOGO's Turtle {package turtle; use constant PI => 3.14159265359; sub new {return bless({h=>0,xy=>[[0],[0]]});} sub rt {$_[0]->{h}=$_[0]->{h}+$_[1];} # right turn by x degrees sub fd { # forward by x points my ($h, $xy, $d) = ($_[0]->{h}, $_[0]->{xy}, $_[1]); $xy->[0][scalar(@{$xy->[0]})] = $d*sin(PI*$h/180) + $xy->[0][$ +#{@{$xy->[0]}}]; $xy->[1][scalar(@{$xy->[1]})] = $d*cos(PI*$h/180) + $xy->[1][$ +#{@{$xy->[1]}}]; $_[0]->{xy} = $xy; return $xy; } } # ----------------------------------------------------------- # Koch Snowflake sub koch { my ($turtle, $d, $level) = @_ ; if ($level==0) {$turtle->fd($d); return 1;} $turtle->rt( 0); koch($turtle, $d/3,$level-1); $turtle->rt(-60); koch($turtle, $d/3,$level-1); $turtle->rt(120); koch($turtle, $d/3,$level-1); $turtle->rt(-60); koch($turtle, $d/3,$level-1); } # ----------------------------------------------------------- my $turtle = turtle->new(); map {$turtle->rt(120); koch($turtle, 170, 4);} 0..2; # ----------------------------------------------------------- plotxy($turtle->{xy}, 'koch.jpg'); # ----------------------------------------------------------- # Minkowski Island sub minkowski { my ($turtle, $d, $level) = @_ ; if ($level==0) {$turtle->fd($d); return 1;} minkowski($turtle,$d/4,$level-1); $turtle->rt(-90); minkowski($turtle,$d/4,$level-1); $turtle->rt( 90); minkowski($turtle,$d/4,$level-1); $turtle->rt( 90); minkowski($turtle,$d/4,$level-1); minkowski($turtle,$d/4,$level-1); $turtle->rt(-90); minkowski($turtle,$d/4,$level-1); $turtle->rt(-90); minkowski($turtle,$d/4,$level-1); $turtle->rt( 90); minkowski($turtle,$d/4,$level-1); } # ----------------------------------------------------------- $turtle = turtle->new(); map {$turtle->rt(90); minkowski($turtle, 150, 3);} 0..3; # ----------------------------------------------------------- plotxy($turtle->{xy}, 'minkowski.jpg'); # ----------------------------------------------------------- # ----------------------------------------------------------- # Dragon Curve sub dragon1 { my ($turtle, $d, $level) = @_ ; if ($level==0) {$turtle->fd($d); return 1;} dragon($turtle,$d*0.707,$level-1); $turtle->rt(-90); dragon1($turtle,$d*0.707,$level-1); } sub dragon { my ($turtle, $d, $level) = @_ ; if ($level==0) {$turtle->fd($d); return 1;} dragon($turtle,$d*0.707,$level-1); $turtle->rt(90); dragon1($turtle,$d*0.707,$level-1); } # ----------------------------------------------------------- $turtle = turtle->new(); dragon($turtle, 150, 12); # ----------------------------------------------------------- plotxy($turtle->{xy}, 'dragon.jpg');

"Simple" version:
use constant PI => 3.14159265359; # ----------------------------------------------------------- # Turtle Graph my $h=0; my @x=(0); my @y=(0); sub rt {$h = $h + shift;} sub fd { $x[scalar @x] = $_[0]*sin(PI*$h/180) + $x[$#x]; $y[scalar @y] = $_[0]*cos(PI*$h/180) + $y[$#y]; } sub reset_xy {$h=0; @x=(0); @y=(0);} # ----------------------------------------------------------- # ----------------------------------------------------------- # Koch Snowflake sub koch { my ($d, $level) = @_ ; if ($level==0) {fd($d); return 1;} rt( 0); koch($d/3,$level-1); rt(-60); koch($d/3,$level-1); rt(120); koch($d/3,$level-1); rt(-60); koch($d/3,$level-1); } # ----------------------------------------------------------- map {rt(120); koch(170, 4);} 0..2; # ----------------------------------------------------------- plotxy([\@x,\@y], 'koch.jpg'); # ----------------------------------------------------------- # ----------------------------------------------------------- # Minkowski Island sub minkowski { my ($d, $level) = @_ ; if ($level==0) {fd($d); return 1;} minkowski($d/4,$level-1); rt(-90); minkowski($d/4,$level-1); rt( 90); minkowski($d/4,$level-1); rt( 90); minkowski($d/4,$level-1); minkowski($d/4,$level-1); rt(-90); minkowski($d/4,$level-1); rt(-90); minkowski($d/4,$level-1); rt( 90); minkowski($d/4,$level-1); } # ----------------------------------------------------------- reset_xy(); # if necessary map {rt(90); minkowski(150, 3);} 0..3; # ----------------------------------------------------------- plotxy([\@x,\@y], 'minkowski.jpg'); # ----------------------------------------------------------- # ----------------------------------------------------------- # Dragon Curve sub dragon1 { my ($d, $level) = @_ ; if ($level==0) {fd($d); return 1;} dragon($d*0.707,$level-1); rt(-90); dragon1($d*0.707,$level-1); } sub dragon { my ($d, $level) = @_ ; if ($level==0) {fd($d); return 1;} dragon($d*0.707,$level-1); rt(90); dragon1($d*0.707,$level-1); } # ----------------------------------------------------------- reset_xy(); # if necessary dragon(150, 12); # ----------------------------------------------------------- plotxy([\@x,\@y], 'dragon.jpg');

All three versions (Closure, OO, "Simple") produce Koch, Minkowski and Dragon curves (which you can view here). (Weird I didn't think of "Simple" version first, but last. Must be due to OOP--Object-Oriented Poisoning.)

Short Code?       A long while back, some monks here wrote some codes that generate Mandelbrot set in about 150 characters! So, I guess, some folks here could probably write some pretty terse code to generate those fractal curves, without relying (too much) on external modules (otherwise it would be like going to a high school algebra exam carrying Mathematica with you).

Fast Code?       In case you're interested in the relative speed. The Closure, OO and "Simple" versions took about 2.4, 1.5 and 1.3 seconds to run respectively. Is closure supposed to be slow or is it just my code? Any trick to speed things up?

(O, if you know of Memoize module, I tried it. Crashed on Minkowski curve.)

Thanks.

Replies are listed 'Best First'.
Re: Fractal Curves: Short & Fast Codes?
by Aristotle (Chancellor) on Jun 17, 2003 at 11:49 UTC
    Nice work. To shorten it without obfuscating it, you can remove a lot of repeated calculations. Compare:
    sub minkowski { my ($d, $level) = @_ ; if ($level==0) {fd($d); return 1;} minkowski($d/4,$level-1); rt(-90); minkowski($d/4,$level-1); rt( 90); minkowski($d/4,$level-1); rt( 90); minkowski($d/4,$level-1); minkowski($d/4,$level-1); rt(-90); minkowski($d/4,$level-1); rt(-90); minkowski($d/4,$level-1); rt( 90); minkowski($d/4,$level-1); }
    Now each calculation done once:
    sub minkowski { my ($d, $level) = @_ ; fd($d), return unless $level--; $d /= 4; minkowski($d,$level); rt(-90); minkowski($d,$level); rt( 90); minkowski($d,$level); rt( 90); minkowski($d,$level); minkowski($d,$level); rt(-90); minkowski($d,$level); rt(-90); minkowski($d,$level); rt( 90); minkowski($d,$level); }
    That didn't buy much here because the function name is so long. Then, notice how you often have rt($foo); minkowski(@bar); ? You could shorten that by making the angle an optional parameter to the second function.
    sub minkowski { my ($d, $level, $rt) = @_ ; rt($rt) if $rt; fd($d), return unless $level--; $d /= 4; minkowski($d,$level); minkowski($d,$level, -90); minkowski($d,$level, 90); minkowski($d,$level, 90); minkowski($d,$level); minkowski($d,$level, -90); minkowski($d,$level, -90); minkowski($d,$level, 90); }
    Hmm. We're calling the same function eight times in a row, with only one different parameter. Looks like an unrolled loop, no?
    sub minkowski { my ($d, $level, $rt) = @_; rt($rt) if $rt; fd($d), return unless $level--; $d /= 4; minkowski($d, $level, $_) for 0, -90, 90, 90, 0, -90, -90, 90; }

    Makeshifts last the longest.

Re: Fractal Curves: Short & Fast Codes?
by Willard B. Trophy (Hermit) on Jun 17, 2003 at 13:42 UTC
    of course, if you don't consider using GhostScript to render your fractal as cheating, here's a simple one using the very old technique of string replacement:
    #!/usr/bin/perl -w use strict; use integer; my $depth = 4; my $start = '_/_\_\_/_'; # _ = forward, / = left, \ = right my $fractal = $start; $fractal =~ s/_/$start/og for ( 0 .. $depth ); $fractal =~ s/_/0 0.8 rlineto /g; $fractal =~ s,/,90 rotate ,g; $fractal =~ s,\\,270 rotate ,g; print '%!', "\n", '0.1 setlinewidth 500 100 moveto ', $fractal, 'stroke showpage', "\n";

    Can't remember what this fractal is called, sorry. As it stands, it should render on A4 and Letter paper.

    --
    bowling trophy thieves, die!

      Cool. Didn't think of using PostScript before. That looks like some sort of 1D automata. (If anyone wanna see, view it here, which was flap 90 degree clockwise , just to fit the screen better.)
        It's definitely a fractal curve. It's just so long since I did fractal things, I don't remember what people were calling these them.

        String rewriting is all in The Science of Fractal Images, ed Peitgen & Saupe (Springer-Verlag, 1991, ISBN: 0387966080). Of course, they were using an awkward Pascal-like pseudocode language with no regular expressions ...

        Here's at least part of the famous snowflake. I think I changed three lines:

        #!/usr/bin/perl -w use strict; use integer; my $depth = 4; my $start = '_/_\_/_'; # _ = forward, / = left, \ = right my $fractal = $start; $fractal =~ s/_/$start/og for ( 0 .. $depth ); $fractal =~ s/_/0 0.8 rlineto /g; $fractal =~ s,/,60 rotate ,g; $fractal =~ s,\\,240 rotate ,g; print '%!', "\n", '0.1 setlinewidth 500 100 moveto ', $fractal, 'stroke showpage', "\n";

        Now what would be really cool would be a Perl routine that would parse ASCII-art axioms and production rules, and generate the fractal. My free time is not that copious.

        --
        bowling trophy thieves, die!

Re: Fractal Curves: Short & Fast Codes?
by Foggy Bottoms (Monk) on Jun 17, 2003 at 10:22 UTC
    I'm pretty amazed by what you did - I just love the idea and the code seems just fine. However, being a newbie, I'm not really good at making it work.
    I first installed the GD package I didn't have. But the code still isn't working fine. They are 2 lines which I don't understand :
            $xy->[0][scalar(@{$xy->[0]})] = $d*sin(PI*$h/180) + $xy->[0][$
    +#{@{$xy->[0]}}];
            $xy->[1][scalar(@{$xy->[1]})] = $d*cos(PI*$h/180) + $xy->[1][$
    +#{@{$xy->[1]}}]; 
    
    Could you please explain them ? They appear in the first 2 scripts.
    In the last one, what is plotxy ? It's not defined anywhere... Is it part of GD ? (I tried GD::plotxy but it still didn't work :( ).
    Please let me know what I'm doing wrong - I'm currently using WinNT with ActivePerl.
      No, I defined plotxy() in the code; it isn't part of GD. I only included it in Closure, omitted elsewhere since it's redundant to include it three times in the post.

      As for the two lines...

      If $xy = [[4,5,6],[4,5,6]], scalar(@{$xy->[0]}) = scalar(@{$xy->[0]}) = 3, the number of elements.

      $xy->[0] points to the first array in $xy; $xy->[1] second.

      @{$xy->[0]} dereferences $xy->[0] into an array.

      So, $xy->[0][scalar(@{$xy->[0]})] will be like $xy->[0][3], the new element in the array, yet to be defined, since the index of the last position is 2.

      $d*sin(PI*$h/180) is just some transformation stuff from trigonometry/geometry/calculus.

      $#{@{$xy->[0]}} gives you the index of the last element, which is 2 in this example.

      So, $xy->[0][$#{@{$xy->[0]}}] = $xy->[0][$#{@{$xy->[1]}}] gives you 6.

      (I've noticed you have "...$+#..." in your reply. When you see + in a code in the post, it indicates a continuation from the previous line. When you cut and past codes, those + might be inadvertently included. Remember to remove them.)

      Hence, the two lines basically say,

      new x coordinate = distance (on x axis) from old x coordinate + old x coordinate
      new y coordinate = distance (on y axis) from old y coordinate + old y coordinate

      You can get rid of the "+" by using the "d/l code" link:

      comment on Fractal Curves: Short & Fast Codes? ---> d/l code <---
Re: Fractal Curves: Short & Fast Codes?
by BrowserUk (Patriarch) on Jun 17, 2003 at 20:28 UTC

    There are several things that you could do to improve the performance of your plotting routine.

    sub min {(sort {$a<=>$b} @_ )[0];} sub max {(sort {$a<=>$b} @_ )[-1];}

    Sorting an array in order to find the minimum or maximum is a very expensive way to do it if you are going to discard the results of the sort. Doing it twice, well that just profligate:)

    As you need both min and max of both set of coordinates, it would be better to discover both in a single pass rather than sorting each array twice to get them.

    sub min_max { my( $min, $max ) = ( 1e-308, 1e308 ); for(@_){ $min = $_ if $_ < $min; $max = $_ if $_ > $max; } return ($min, $max); }

    The reason you need the min and max values is to allow you size the bitmap and transform the coordinates so that the figures draw will be nicely centered. The GD package also comes with a sub-package GD::Polyline which includes functions for scaling and offseting a Polyline as well as the ability to pass the entire polyline to the rendering engine in a single pass rather than line by line. This removes the need for a lot of the math at the Perl level, pushing it instead into the rendering engine in compiled C, which ought to improve things a little, though I haven't benchmarked it.

    #! perl -slw use strict; use GD; use GD::Polyline; use Data::Dumper; use constant PI => 3.14159265359; sub Turtle { my( $dir, $polyline, $xy ) = ( 0, new GD::Polyline, [ 0, 0 ] ); return sub { return $polyline unless @_; $dir += shift||0; my $dist = shift||0; $xy->[0] += $dist*sin(PI*$dir/180); $xy->[1] += $dist*cos(PI*$dir/180); $polyline->addPt( @$xy ); return; }; } sub plotxy { my( $polyline, $file, $scale ) = @_; $polyline->scale( $scale, $scale, 0, 0 ) if $scale; my( $centre_x, $centre_y ) = $polyline->centroid; $polyline->offset( int($centre_x+10), 10 + -int(2*$centre_y) ); my( $width, $height ) = ($polyline->bounds)[2,3]; my $image = new GD::Image( $width + 10, $height + 10 ); my $white = $image->colorAllocate(255,255,255); my $black = $image->colorAllocate(0,0,0); $image->transparent($white); $image->interlaced('true'); $image->polyline( $polyline, $black ); open(IMG, ">$file"); binmode IMG; print IMG $image->jpeg; } sub koch { my ($turtle, $d, $level) = @_ ; if ($level==0) {$turtle->(0,$d); return 1;} $turtle->( 0,0); koch($turtle,$d/3,$level-1); $turtle->(-60,0); koch($turtle,$d/3,$level-1); $turtle->(120,0); koch($turtle,$d/3,$level-1); $turtle->(-60,0); koch($turtle,$d/3,$level-1); } my $turtle = Turtle(); map {$turtle->(120, 0); koch($turtle, 170, 4);} 0..2; plotxy($turtle->(), 'koch.jpg', 3); # Minkowski Island sub minkowski { my ($turtle, $d, $level) = @_ ; if ($level==0) {$turtle->(0,$d); return 1;} minkowski($turtle,$d/4,$level-1); $turtle->(-90,0); minkowski($turtle,$d/4,$level-1); $turtle->( 90,0); minkowski($turtle,$d/4,$level-1); $turtle->( 90,0); minkowski($turtle,$d/4,$level-1); minkowski($turtle,$d/4,$level-1); $turtle->(-90,0); minkowski($turtle,$d/4,$level-1); $turtle->(-90,0); minkowski($turtle,$d/4,$level-1); $turtle->( 90,0); minkowski($turtle,$d/4,$level-1); } $turtle = Turtle(); map {$turtle->(90,0); minkowski($turtle, 150, 3);} 0..3; plotxy($turtle->(), 'minkowski.jpg', 2); # Dragon Curve sub dragon1 { my ($turtle, $d, $level) = @_ ; if ($level==0) {$turtle->(0,$d); return 1;} dragon($turtle,$d*0.707,$level-1); $turtle->(-90,0); dragon1($turtle,$d*0.707,$level-1); } sub dragon { my ($turtle, $d, $level) = @_ ; if ($level==0) {$turtle->(0,$d); return 1;} dragon($turtle,$d*0.707,$level-1); $turtle->(90,0); dragon1($turtle,$d*0.707,$level-1); } $turtle = Turtle(); dragon($turtle, 150, 12); plotxy($turtle->(), 'dragon.jpg', 2);

    There is still some potential for improvement here, but I tried to leave the Turtle stuff pretty much as you had it. A couple of things worthy of note. You use map in a void context in several places. Until quite recently, I beleive that this was noticably less efficient that using

    for(@array) { ...};

    I believe that this has improved somewhat in recent versions with map detecting that is was being used in a void context and not bothering to build the return array, but it is still seen as being "bad form":)

    You mentioned having problems with Memoize. I'm not sure what the problems you were having were, but there is nothing to stop you using the same logic yourself. For example, trancsendental functions tend to be quite expensive. With fractals, the angles you are calculating tend to be the same ones again and again. It makes sense in this case to save the results of the calculation and reuse them rather than recalculating them.

    Replacing this (from my code above)

    $xy->[0] += $dist*sin(PI*$dir/180); $xy->[1] += $dist*cos(PI*$dir/180);

    with something like

    my %sin_d; sub sin_d{ my $d=shift; $sin_d{$d} || ( $sin_d{$d} = sin(PI +*$d/180) ); } my %cos_d; sub cos_d{ my $d=shift; $cos_d{$d} || ( $cos_d{$d} = cos(PI +*$d/180) ); } .... $xy->[0] += $dist * sin_d($dir); $xy->[1] += $dist * cos_d($dir); ...

    would probably save a few calls to sin & cos.


    Examine what is said, not who speaks.
    "Efficiency is intelligent laziness." -David Dunham
    "When I'm working on a problem, I never think about beauty. I think only how to solve the problem. But when I have finished, if the solution is not beautiful, I know it is wrong." -Richard Buckminster Fuller


Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://266410]
Approved by Corion
Front-paged by arthas
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others goofing around in the Monastery: (5)
As of 2024-04-19 18:19 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found