#!/usr/bin/perl -w #--------------------------------------------------------------------- # # eafplot.pl ($Revision: 251 $) # #--------------------------------------------------------------------- # # Copyright (c) 2005-2017 # Manuel Lopez-Ibanez # TeX: \copyright 2005-2017 Manuel L{\'o}pez-Ib{\'a}{\~n}ez # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You can obtain a copy of the GNU General Public License at # http://www.gnu.org/licenses/gpl.html or writing to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 # USA # #--------------------------------------------------------------------- # # IMPORTANT NOTE: Please be aware that the fact that this program is # released as Free Software does not excuse you from scientific # propriety, which obligates you to give appropriate credit! If you # write a scientific paper describing research that made substantive # use of this program, it is your obligation as a scientist to (a) # mention the fashion in which this software was used in the Methods # section; (b) mention the algorithm in the References section. The # appropriate citation is: # # Manuel López-Ibáñez, Luís Paquete, and Thomas Stützle. # Exploratory Analysis of Stochastic Local Search Algorithms in # Biobjective Optimization. In T. Bartz-Beielstein, M. Chiarandini, # L. Paquete, and M. Preuss, editors, Experimental Methods for the # Analysis of Optimization Algorithms, pages 209–222. Springer, # Berlin, Germany, 2010. # doi: 10.1007/978-3-642-02538-9_9 # # Moreover, as a personal note, I would appreciate it if you would # email with citations of papers # referencing this work so I can mention them to my funding agent and # tenure committee. # #--------------------------------------------------------------------- # # Literature: # # [1] Manuel Lopez-Ibanez, Luis Paquete, and Thomas Stutzle. Hybrid # population-based algorithms for the bi-objective quadratic # assignment problem. Journal of Mathematical Modelling and # Algorithms, 5(1):111-137, April 2006. # # [2] Manuel Lopez-Ibanez, Luis Paquete, and Thomas Stutzle. Hybrid # population-based algorithms for the bi-objective quadratic # assignment problem. Journal of Mathematical Modelling and # Algorithms, 5(1):111-137, April 2006. # #--------------------------------------------------------------------- # # TODO: # # * Fail if any subcommand fails. For example, do not even run R if # the call to the eaf program failed. (See execute in eafdiff.pl). # #--------------------------------------------------------------------- use strict; use warnings FATAL => 'all'; use diagnostics; use Carp; use File::Basename; use Getopt::Long qw(:config pass_through);; use File::Temp qw/ tempfile /; my $copyright =' Copyright (C) 2005-2017 Manuel Lopez-Ibanez This is free software. You may redistribute copies of it under the terms of the GNU General Public License . There is NO WARRANTY, to the extent permitted by law. '; my $progname = basename($0); my $version = ' $Revision: 251 $ '; my $Rexe = "R"; requiredprog($Rexe); &usage(1) if (@ARGV == 0); my $colors = "NULL"; my $percentiles; # FIXME: single/multiple plots are a mess. This is how it should work: # --single file1 file2 ... --output plotfile # produces a single plot 'plotfile' from all input files. Labels are input filenames # # --no-single file --output plotfile # produces a single plot 'plotfile' from input file. Labels are percentiles. # # --no-single file1 file2 --output plotfile # error: use --suffix and/or --output-dir. my $flag_single_plot = 0; my $legend; my $output_file; my @output_files; my $flag_verbose = 0; my $flag_eps = 0; my $flag_crop = (which_program("pdfcrop") ne "pdfcrop"); my $label_obj1 = "objective 1"; my $label_obj2 = "objective 2"; my $flag_area = 0; my $flag_ymaximise = 0; my $flag_xmaximise = 0; my $flag_maximise = 0; my $flag_best = 0; my $flag_median = 0; my $flag_worst = 0; GetOptions ('area' => \$flag_area, 'eps' => \$flag_eps, 'crop' => \$flag_crop, 'single' => \$flag_single_plot, 'best|b' => \$flag_best, 'median|m' => \$flag_median, 'worst|w' => \$flag_worst, 'maximise' => \$flag_maximise, 'xmaximise' => \$flag_xmaximise, 'ymaximise' => \$flag_ymaximise, 'output|o=s' => \$output_file, 'legend=s' => \$legend, 'colors=s' => \$colors, 'obj1:s' => \$label_obj1, 'obj2:s' => \$label_obj2, 'verbose|v' => \$flag_verbose); requiredprog("pdfcrop") if ($flag_crop and not $flag_eps); if ($flag_best) { $percentiles .= ", " if (defined($percentiles)); $percentiles .= "0"; } if ($flag_median) { $percentiles .= ", " if (defined($percentiles)); $percentiles .= "50"; } if ($flag_worst) { $percentiles .= ", " if (defined($percentiles)); $percentiles .= "100"; } sub version { print "$progname: version $version\n"; print $copyright; exit (0); } sub usage { my $exitcode = shift; print <<"EOF"; $progname version $version $copyright Usage: $progname [OPTIONS] FILES Plot the best/median/worst attainment surfaces for a set of input points. -h, --help print this summary and exit --version print version number and exit -v, --verbose increase verbosity and keep intermediate files -I, --iqr, --IQR plot IQR (25%-75% percentile) instead of best and worst -b, --best plot best attainment surface -m, --median plot median attainment surface -w, --worst plot worst attainment surface -p, --percentiles=INT[,INT..] plot the given percentile(s) of the EAF --extra=FILE add extra points to the plot --extra-label=STRING label in the legend for the extra points --obj1=STRING label for objective 1 (x-axis) --obj2=STRING label for objective 2 (y-axis) (labels can be R expressions, e.g., --obj1="expression(pi)") --legend=STRING A legend like: '"Algorithm A", "Algorithm B"' --legendpos={top,bottom}{left,right} position of the legend --xlim=REAL,REAL limits of x-axis --ylim=REAL,REAL limits of y-axis --area plot the area dominated by the attainment surfaces instead of lines. --colors=STRING,STRING bounds for color range in area mode (default '"gray","black"') --maximise handle a maximisation problem --xmaximise maximise first objective --ymaximise maximise second objective -o, --output=FILE output file. --[no]single generate a single plot. --eps generate EPS file (default is PDF). --no-eaf just plot the points directly. EOF exit $exitcode; } ## Format commandline. my $commandline = &format_commandline (); my @files = (); my $flag_axislog = ""; my $extra_points = "NULL"; my $extra_labels = "NULL"; my $IQR_flag = 0; my $legendpos = "topright"; my $xlim = "NULL"; my $ylim = "NULL"; my $do_eaf = 1; #FIXME: Move everything to GetOpt above. ## Handle parameters while (@ARGV) { my $argv = shift @ARGV; if ($argv =~ /^--help/ or $argv =~ /^-h/) { &usage(0); } elsif ($argv =~ /^--version/) { &version(); } elsif (# For backwards compatibility $argv =~ /--stoptime/ or $argv =~ /-stoptime/) { print "$progname: maximising y-axis\n"; $flag_ymaximise = 1; } elsif ($argv =~ /^-I/ or $argv =~ /^--iqr/i) { $IQR_flag = 1; $percentiles .= ", " if (defined($percentiles)); $percentiles .= "25, 50, 75"; } elsif ($argv =~ /^-p$/ or $argv =~ /^--percentiles=/ or $argv =~ /^--percentiles$/) { $percentiles .= ", " if (defined($percentiles)); $percentiles .= &get_arg($argv); } elsif ($argv =~ /--no-eaf/i) { $do_eaf = 0; } elsif ($argv =~ /^--extra=/ or $argv =~ /^--extra$/) { my $arg = &get_arg ($argv); if ($arg and -r $arg) { $extra_points = ($extra_points eq "NULL") ? "\"$arg\"" : $extra_points . ", \"$arg\""; } else { die "cannot read file $arg.\n"; } } elsif ($argv =~ /^--extra-label=/ or $argv =~ /^--extra-label$/) { my $arg = &get_arg ($argv); if ($arg) { $extra_labels = ($extra_labels eq "NULL") ? "\"$arg\"" : $extra_labels . ", \"$arg\""; } } elsif ($argv =~ /^--legendpos=/ or $argv =~ /^--legendpos$/) { my $arg = &get_arg ($argv); $legendpos = "$arg" if ($arg); } elsif ($argv =~ /^--ylog/) { $flag_axislog = "y"; } elsif ($argv =~ /^--xlim=/ or $argv =~ /^--xlim$/) { my $arg = &get_arg ($argv); $xlim = "c($arg)" if ($arg); } elsif ($argv =~ /^--ylim=/ or $argv =~ /^--ylim$/) { my $arg = &get_arg ($argv); $ylim = "c($arg)" if ($arg); } elsif ($argv =~ /^--/ or $argv =~ /^-/) { print "$progname: unknown option $argv\n"; &usage(1); } else { push (@files, $argv); } } @files = &unique(@files); die "$progname: error: no input files given\n" unless (@files); if (defined($percentiles) and not $do_eaf) { die "$progname: cannot specify --no-eaf and any EAF option" . " (--best, --median, --worst, --iqr, --percentiles)"; } my $ps2epsfilter = undef; $ps2epsfilter = "ps2eps"; requiredprog ($ps2epsfilter) if ($flag_eps); my $filter = ""; $filter = "|$ps2epsfilter -s b0 -q -l -B -O -P > " if ($flag_eps and defined($ps2epsfilter) and -x $ps2epsfilter); if ($extra_labels eq "NULL") { $extra_labels = $extra_points; } $percentiles = "NULL" unless ($do_eaf); # Default is best, median, worst. unless(defined($percentiles)) { $percentiles = ($flag_single_plot) ? "50" : "0, 50, 100"; } $label_obj1 = parse_expression ($label_obj1); $label_obj2 = parse_expression ($label_obj2); my $num = 0; my $eaffiles = ""; my @titles; my @eaffiles; foreach my $inpfile (@files) { unless (-r $inpfile) { die "$progname: warning: $inpfile: cannot read file.\n"; #next; } elsif (!(-s $inpfile) or `grep -v -e "#\\|^\$" $inpfile | wc --bytes` =~ /\s+0$/o) { die "$progname: warning: $inpfile: empty file.\n"; #next; } $num++; my $basefile; my $inpdir; my $file_eps; chomp($basefile = `basename $inpfile .dat`); chomp($inpdir = `dirname $inpfile`); my $filedata = $inpfile; if ($num < 10) { print "# f$num: $inpfile\n"; } else { print "#f$num: $inpfile\n"; } if ($do_eaf) { if ($IQR_flag) { $file_eps = "$inpdir/att_iqr_${basefile}"; } elsif ($flag_area) { $file_eps = "$inpdir/att_area_${basefile}"; } else { $file_eps = "$inpdir/${basefile}_att"; } } else { $file_eps = "$inpdir/${basefile}"; } $eaffiles = "\"${filedata}\""; push @output_files, $file_eps; push @titles, $inpfile; push @eaffiles, $eaffiles; } # FIXME: We can override only one output file. if (defined($output_file) and $output_file) { # Remove suffix, the R code will add the correct one. $output_file =~ s/(\.pdf|\.eps|\.ps|\.png)$//; $output_files[0] = $output_file; } my $str_outfiles = asRlist(@output_files); my $str_titles = asRlist(@titles); my $str_eaffiles = join(', ', @eaffiles); $legend = $str_titles unless(defined($legend)); if ($flag_maximise) { $flag_xmaximise = 1; $flag_ymaximise = 1; } print "$progname: maximising x-axis\n" if ($flag_xmaximise); print "$progname: maximising y-axis\n" if ($flag_ymaximise); my $Rfile = "$$.R"; open(R, ">$Rfile") or die "$progname: error: can't open $Rfile: $!\n"; print R <<"EOFR"; #!/usr/bin/r --vanilla # # R script to plot attainment surfaces # # This script is executable if you have littler installed. [*] # [*] http://code.google.com/p/littler/ # # Created by $commandline # # $version # library(eaf) filter <- "$filter" file.extra <- list(${extra_points}) extra.legend <- c(${extra_labels}) extra.pch <- c(20, 1:25) extra.col <- c("black") maximise <- as.logical(c($flag_xmaximise, $flag_ymaximise)) legend.txt <- c($legend) legend.pos <- "$legendpos" log <- "$flag_axislog" Xlim <- $xlim Ylim <- $ylim eaf.type <- ifelse(${flag_area}, "area", "point") xlab <- $label_obj1 ylab <- $label_obj2 colors <- c($colors) percentiles <- c($percentiles) percentiles <- if (is.null(percentiles)) percentiles else sort(percentiles) do.eaf <- as.logical($do_eaf) single.plot <- as.logical($flag_single_plot) flag.eps <- as.logical(${flag_eps}) output.files <- c(${str_outfiles}) titles <- c(${str_titles}) eaffiles <- list(${str_eaffiles}) EOFR print R <<'EOFR'; if (eaf.type == "area") { col <- if (!is.null(colors)) colors else c('grey', 'black') # These two are unused for "area" lty <- NULL pch <- NA } else { col <- c("black", "darkgrey", "black", "grey40", "darkgrey") lty <- c("dashed", "solid", "solid", "solid", "dashed") pch <- NA #lty <- c("blank") #pch <- c(20,21,22,23,4,5) } data <- NULL attsurfs <- NULL xlim <- NULL ylim <- NULL if (do.eaf) { data <- lapply (eaffiles, read_datasets) xlim <- range(xlim, sapply(data, function(x) x[, 1])) ylim <- range(ylim, sapply(data, function(x) x[, 2])) } else { attsurfs <- lapply(eaffiles, function(x) { z <- read_datasets(x) z <- split.data.frame(z, z$set) z <- lapply(z, function(y) {y$set <- 1; return(y)}) return(z)}) xlim <- range(xlim, sapply(attsurfs, function(x) do.call("rbind", x)[,1])) ylim <- range(ylim, sapply(attsurfs, function(x) do.call("rbind", x)[,2])) lty <- c("blank") pch <- c(20,21,22,23,4,5) col <- c("black", "darkgrey", "black", "grey40", "darkgrey") } if (is.null (file.extra[[1]])) { extra.points <- NULL } else { extra.points <- list() for (i in 1:length(file.extra)) { tmp <- read.table(file.extra[[i]], na.strings="NA")[,c(1,2)] extra.points[[i]] <- tmp xlim <- range(xlim, tmp[,1], na.rm=T) ylim <- range(ylim, tmp[,2], na.rm=T) } } cat(sprintf("xlim = c(%s, %s)\n", xlim[1], xlim[2])) cat(sprintf("ylim = c(%s, %s)\n", ylim[1], ylim[2])) if (!is.null(Xlim)) xlim <- Xlim if (!is.null(Ylim)) ylim <- Ylim for (k in seq_along(output.files)) { if (single.plot) { output.title <- output.files } else { output.title <- titles[k] } output.file <- output.files[k] if (flag.eps) { output.file <- paste(filter, output.file, ".eps", sep='') postscript(output.file, title = output.title, paper = "special", horizontal=F, onefile=F, width=4.5, height=4.5) } else { output.file <- paste(filter, output.file, ".pdf", sep='') pdf(output.file, title = output.title, width=4.5, height=4.5) } if (single.plot) { eafplot (data, attsurfs = attsurfs, percentiles = percentiles, xlab = xlab, ylab = ylab, las = 0, log = log, type = eaf.type, lty = lty, col = col, pch=pch, cex.pch=0.75, extra.points = extra.points, extra.pch = extra.pch, extra.col = extra.col, xlim = xlim, ylim = ylim, legend.pos = legend.pos, extra.legend = extra.legend, legend.txt = legend.txt, maximise = maximise, main = NULL) } else { eafplot (data[[k]][,1:2], sets = data[[k]][,3], attsurfs = attsurfs[[k]], percentiles = percentiles, xlab = xlab, ylab = ylab, las = 0, log = log, type = eaf.type, lty = lty, col = col, pch=pch, cex.pch=0.75, extra.points = extra.points, extra.pch = extra.pch, extra.col = extra.col, xlim=xlim, ylim=ylim, legend.pos = legend.pos, extra.legend = extra.legend, maximise = maximise, main = NULL) } dev.null <- dev.off() cat ("Plot: ", output.file, "\n", sep='') } EOFR close R; &execute_verbose ("$Rexe --quiet --vanilla --slave <$Rfile"); ($flag_verbose) ? print "$progname: generated R script: $Rfile\n" : unlink($Rfile); if ($flag_crop and not $flag_eps) { my ($tmpfh, $tmpfilename) = tempfile(); foreach my $outfile (@output_files) { execute ("pdfcrop ${outfile}.pdf $tmpfilename"); execute ("mv $tmpfilename ${outfile}.pdf"); } } exit 0; ################################### # helper sub-routines ################################### sub parse_expression { my $label = shift; $label = "\"$label\"" if ($label !~ /^expression\(/); return $label; } sub unique { my %seen =() ; @_ = grep { ! $seen{$_}++ } @_ ; } sub round { my($number) = shift; return int($number + .5 * ($number <=> 0)); } sub get_arg { my ($option, $arg) = split (/=/, $_[0], 2); $arg = shift @ARGV if (not $arg); return $arg; } sub format_commandline { my $cmd = $0 . " "; for (my $i=0, my $j=25; $i < @ARGV; $i++) { if ($i == $j) { $j += 25; $cmd .= "\\\n# "; } if ($ARGV[$i] =~ /\s/) { $cmd .= " \"" . $ARGV[$i] . "\""; } else { $cmd .= " " . $ARGV[$i]; } } return $cmd; } use Env '@PATH'; use Cwd; # FIXME: This should return the empty string if not found. sub which_program { my $program = shift; my $cwd = cwd(); unless ($program =~ m|^.?.?/|) { # If no path was given foreach my $path (@PATH) { if (-e "$path/$program") { $program = "$path/$program"; last; } } # Try also the current directory: $program = "$cwd/$program" if (-e "$cwd/$program"); } return $program; } sub requiredprog { my $cwd = cwd(); foreach my $program (@_) { next if not defined $program; my $pathtoprogram = which_program($program); die "$progname: cannot find required program `$program'," ." either in your PATH=\"". join(':',@PATH) ."\" or in the current directory`$cwd'\n" unless ($pathtoprogram =~ m|^.?.?/|); die "$progname: required program `$program' is not executable\n" unless (-x $pathtoprogram); } } ## See ## http://svn.collab.net/repos/svn/trunk/contrib/hook-scripts/svn-keyword-check.pl ## for a better way to read from a command output sub runcmd { my $command = shift; system ($command); if ($? == -1) { die "error: failed to execute $command: $!\n"; } elsif ($? & 127) { die "child died with signal ". ($? & 127) . ($? & 128) ? "with core dump.\n" : ".\n"; } else { return $? >> 8; } } sub execute { my $command = shift; if ($flag_verbose) { &execute_verbose ("$command"); } else { `$command`; } } sub execute_verbose { unless (@_) { croak "$progname: execute_verbose passed no arguments.\n"; } print "\n@_\n" if ($flag_verbose); my $fh = _pipe(@_); my @output; while (<$fh>) { print; chomp; push(@output, $_); } close($fh); my $result = $?; my $exit = $result >> 8; my $signal = $result & 127; my $cd = $result & 128 ? "with core dump" : ""; if ($signal or $cd) { warn "$progname: pipe from `@_' failed $cd: exit=$exit signal=$signal\n"; } if (wantarray) { return ($result, @output); } else { return $result; } } # Return the filehandle as a glob so we can loop over it elsewhere. sub _pipe { local *SAFE_READ; my $pid = open(SAFE_READ, '-|'); unless (defined $pid) { die "$progname: cannot fork: $!\n"; } unless ($pid) { open(STDERR, ">&STDOUT") or die "$progname: cannot dup STDOUT: $!\n"; exec(@_) or die "$progname: cannot exec `@_': $!\n"; } return *SAFE_READ; } sub asRlist { return "\"". join("\", \"", @_) . "\""; }