All pastes #969071 Raw Edit

Stuff

public text v1 · immutable
·published 2008-04-03 13:39 UTC
rendered paste body
# Bruker::Utils package; parsing for Bruker data format
#
# $Id: Utils.pm,v 1.4 2004/11/10 19:39:58 matthewbrett Exp $

package Bruker::Utils;

use Carp;
use File::Basename;
use Math::Matrix 0.4;
require Exporter;
@ISA = qw( Exporter );
@EXPORT = qw( );
@EXPORT_OK = qw(bruker_series bruker_find_dir parse_bruker_params 
		bruker2generic bruker_transposition 
		bruker_text_headers best_of);

use strict;
use vars qw($VERSION );

$VERSION = 0.18;

# Bruker text files to load, with error messages
my($subjfile) = [["subject"],
		 "no subject info - minor problem"];

# Format; file names to try, message if missing, flag=1 if fatal
my(@paramfiles) = (
		   [["%d/imnd", "%d/method"],
		    "input params - transformation matrix will fail", 0],
		   [["%d/acqp"],
		    "output params - expect problems with size, timing etc", 0],
		   [["%d/pdata/%d/reco"], 
		    "reco params - cannot do conversion", 1],
		   [["%d/pdata/%d/d3proc"],
		    "image display params - used if other files are missing", 0],
		  );

# option defaults
my(%text_opt_defs) = (
		 'anon',  1,
		 );


sub bruker_series{
# gets series nos from Bruker directory
    my $pv_dir = shift;
    my(@flist, $f, @seriesnos);
    @flist = <${pv_dir}/[0-9]*>;
    foreach $f(@flist) {
	if (-d $f) {
	    $f =~ /.*?(\d+)$/;
	    @seriesnos = (@seriesnos, $1);
	}
    }
    return @seriesnos;
}    

sub bruker_find_dir{
# does check / find for Bruker directory
    my ($pv_start, $one_multf, $options, @rest) = @_;
    my @searchpath = @rest ? @rest : 
	defined($options->{dbpath}) ? @{$options->{dbpath}} : 
	    ();
    my ($pv_name, $pv_path, $pv_ext, $spath, 
	@flist, $s_ext, $s_targ, @dirlist);
    $pv_start =~ s/\/$//; # strip trailing path sep
    ($pv_name, $pv_path, $pv_ext) = fileparse($pv_start,'\..*');
    if ($pv_path eq './' && $pv_start !~ "^./") { # path not given
	# check each search directory, last first
	@searchpath = ('.', @searchpath);
    } else { 
	@searchpath = $pv_path;
    }
    @searchpath = reverse(@searchpath);

    # add asterisk for search
    $s_ext = $pv_ext eq "" ? "*" : $pv_ext;
  SEARCHLOOP: foreach $spath(@searchpath) {
      $s_targ = "$spath/$pv_name$s_ext";
      print "Searching for $s_targ...\n" if $options->{verbose};
      if ($pv_ext ne "") { # we know what we are looking for
	  if (-d $s_targ) {
	      push(@dirlist, $s_targ);
	      last SEARCHLOOP if $one_multf;
	  }
      } else { # use input as root file name
	@flist = ();
	foreach (glob($s_targ)) {
	  push(@flist, $_) if (-d);
	}
	if (@flist) {
	  @dirlist = (@dirlist, @flist);
	  last SEARCHLOOP if $one_multf;
	}
      }
    }
    return (\@dirlist, \@searchpath);
  }

# get header files info
sub bruker_text_headers{
    my($pv_dir, $seriesno, $recono, $in_opts) = @_;
    my(%options) = %text_opt_defs;
    foreach (keys(%$in_opts)) {
	$options{$_} = $in_opts->{$_};
    }

    my($pfile, $file_loc, $bhstr, $warning, $hdrdata, $ptry, $foundf);
    local $/;

# add subject info if required
    @paramfiles = ($subjfile, @paramfiles) if (!$options{anon});

# initialize file string
    $hdrdata = "";
    $warning = "";

# cycle through the files passed
    PFILE: foreach $pfile(@paramfiles) {
	$foundf = 0;
	PFILETRY: foreach $ptry(@{$pfile->[0]}) { 
	    # do variable substitution
	    $file_loc = sprintf($ptry, $seriesno, $recono);
	    $bhstr = $pv_dir . "/" . $file_loc;
	    if (open(TD,"<$bhstr") ) {
		$foundf = 1;
		last PFILETRY;
	    }
	}
	unless ($foundf) {
	    if ($pfile->[2]) { # abort
		$warning .=  "No $file_loc file; $pfile->[1]; aborting...\n";
		$hdrdata = undef;
		last PFILE;
	    } else {
		$warning .=  "No $file_loc file; $pfile->[1]; continuing...\n";
		next PFILE;
	    }
	}

# Read input files as one long record.
	{
	    local $/;  
	    $hdrdata=$hdrdata . <TD>;
	}
	close TD;
    }

# dos2unix strip
    $hdrdata =~ s/\r//sg if $hdrdata;

    return($hdrdata, $warning);
}

# Parse Bruker parameter file (passed as string) and return as hash
sub parse_bruker_params{
    my($hdrdata) = shift;

    my($i, $j, $k, $l, $record, $fname, $fval, @arr, @arels, %fields);

# strip out vis sections
    $hdrdata =~ s/(^\$\$ \@vis=(.|\n)*?)(^\#\#)/$3/mg;
 
# to records
    foreach $record (split(/\#\#/, $hdrdata)){
	($fname, $fval) = split(/=/,$record);
	if (defined($fname)) {
	    if ($fname =~ s/^\$// ) {
		chomp($fval);
		if ($fval ne "") {
		    if ($fval =~ s/\((.*)\)\n//) {
			@arels = split(/,/, $1);
			if ($fval =~ /^<(.*)>$/) {
			    $fields{$fname} = $1;
			}
			else {
			    # deals with 1D or 2D or 3D arrays
			    @arr = split(/\s+/, $fval);
			    if (@arels==3){ # 3D
				# fills rowwise - is this correct?
				$l=0;
				for ($i=0;$i<$arels[0];$i++){
				    for ($j=0;$j<$arels[1];$j++){
					for ($k=0;$k<$arels[2];$k++){
					    $fields{$fname}[$i][$j][$k]=$arr[$l];
					    $l++;
					}
				    }
				}
			    } elsif (@arels==2) { # 2D
				# fills rowwise - is this correct?
				$k=0;
				for ($i=0;$i<$arels[0];$i++){
				    for ($j=0;$j<$arels[1];$j++){
					$fields{$fname}[$i][$j]=$arr[$k];
					$k++;
				    }
				}
			    } else {
				$fields{$fname} = [@arr];
			    }
			}
		    } else {	# not an array
			$fields{$fname}=$fval;
		    }
		}
	    }
	}
    }
    return %fields;
}

# parses Bruker header into a generic format. When adding a format,
# why not add it here, in case your calculations are useful to the
# rest of us?  The Bruker header is a bit confusing.  Here's some
# terms to start.  Each time a subject gets into the scanner and gets
# scanned, is a 'session'.  Each individual scan, or time series, is a
# 'series'.  A series may be 3d (volume acquisition) or 2d (slice
# acquisition).  For each volume (3d) or slice (2d), there may be more
# than one scan acquired. For example, the T2/PD scans are two images
# (for each slice) acquired during the same scan (=series) I will term
# this as a 'layer', so that a normal strucutural has one 'layer',
# whereas the T2/PD scan has two layers.  These layers are combined in
# the reconstructed data file (i.e interleaved - volume by volume in
# the 3D case, and slice by slice in the 2D case).  The total number
# of images for one volume (slices * layers for 2D, layers for 3D), I
# will term the number of 'images' for that scan.  The reconstruction
# can output images with the read/phase or read/slice, phase/slices
# dimensions transposed (this transposition is _not_ reflected in the
# image dimensions from the RECO file, but it _is_ reflected in the
# dimensions in the d3proc file).  Each image can be differentially
# transposed.  See the paravision documents for more details, and some
# confusing parameter names.

# conversion between Bruker and other datatypes
my(%dataconv) = (
		 '_8BIT_UNSGN_INT', => {'bytes'   => 1,
					'd3no'    => 2, #?
					'min'     => 0,
					'max',    => 255,
					'anatype' => 2,
					'mnctype' =>  '-unsigned -byte'},
		 '_16BIT_SGN_INT' ,  => {'bytes'  => 2,
					 'd3no'    => 3,
					 'min'     => -(2**15),
					 'max',    => (2**15-1),
					 'anatype' => 4,
					 'mnctype' => '-signed -short'},
		 '_32BIT_SGN_INT' , => {'bytes'   => 4,
					'd3no'    => 5,
					'min'     => -(2**31),
					'max',    => (2**31-1),
					'anatype' => 8,
					'mnctype' => '-signed -long'}
		     );

# RECO transpose coding - see Image_Re.pdf
#1. If (RECO_transposition[i] = 0), no transposition is to be done.
#
#2. If (0 < RECO_transposition[i] < ACQ_dim), the two consecutive data
#directions given by RECO_transposition[i] and RECO_transposition[i] +
#1 will be transposed.
#
#3. If (RECO_transposition[i] = ACQ_dim), the first and final data
#directions will be transposed.
#
#Please note that for ACQ_dim = 2 the two values 1 and 2 are both legal
#for RECO_transposition[i] and will have the same effect; the first and
#second dimensions will be transposed.

sub bruker_transpose {
    my($bhdr, $layer) = @_;
    my $code = $bhdr->{'RECO_transposition'}[$layer];
    return [0, 0, 'none'] if ($code == 0); 
    my $is2d = ($bhdr->{ACQ_dim} == 2);
    return [0, 1, 'read/phase'] if ($is2d || $code == 1);
    return [1, 2, 'phase/slice'] if ($code == 2);
    return [0, 2, 'read/slice'];
}

# option defaults
my(%opt_defs) = (
		 'quiet',   1,
		 'verbose', 0,
		 'frecofix',1,
		 'layers',  1
		 );

sub bruker2generic{
    my ($bhdr, $inopts) = @_;
    my %options = %opt_defs;
    foreach (keys(%$inopts)) {
	$options{$_} = $inopts->{$_};
    }

    my $i4 =  Math::Matrix->eye(4);
    my $swaps = $i4->new([-1, 0, 0, 0],
			 [0, -1, 0, 0],
			 [0,  0, 1, 0],
			 [0,  0, 0, 1]);

    my ($key, $code, @t, $tmp, $bruk_trans, @acq_dim);
    my ($start, $ft2mm, $trans, $recoff, $so_far, $rotn, $vx2mm, $radio);
    my (@ftdim, $is2d, @fov, @vox, $vox, $off, $i, $j, $gradmat, @vector);
   
    my %ghdr = ();
    my $i1 = 0;

    # get the dimensionality of the acquisition
    $ghdr{dimensions} = @{$bhdr->{'RECO_size'}};
    if ($ghdr{dimensions} < 2) {
	unless ($options{quiet}) {
	    warn "Do not know how to handle data of less than two dimensions";
	}
    }

    # fill 2D or 3D worth of dimensions etc
    $ghdr{dim} = [@{$bhdr->{'RECO_size'}}];
    $ghdr{fov} = [@{$bhdr->{'RECO_fov'}}];
    
    # voxel dimensions in mm    
    foreach $i (0..($ghdr{dimensions}-1)) {
	$ghdr{vox}[$i] = $ghdr{fov}[$i] * 10 / $ghdr{dim}[$i];
    }

    # slice separation mode, see below
    $ghdr{sl_mode} = $bhdr->{'ACQ_slice_sepn_mode'};

    # test to see if we have an ACQP file
    if (defined($ghdr{sl_mode})) {
	# We have an ACQ file
	if ($ghdr{dimensions} < 3) { 
	    # This is a 2D image
	    # Therefore we still need slice information 
	    $ghdr{dim}[2] = $bhdr->{'NSLICES'};
	    $ghdr{sl_thick} = $bhdr->{'ACQ_slice_thick'};
	    $ghdr{vox}[2]=$bhdr->{'ACQ_slice_thick'};
	    # slices can be equidistant, contiguous, var parallel
	    # or var angle.  The slice separation as a single no
	    # only makes sense in the non Var case
	    if ((($ghdr{sl_mode} eq "Equidistant") ||
		 ($ghdr{sl_mode} eq "Contiguous"))
		&& defined($bhdr->{'ACQ_slice_sepn'})) {
		$ghdr{sl_sepn} = $bhdr->{'ACQ_slice_sepn'}[0];
		$ghdr{vox}[2] = $ghdr{sl_sepn} if ($ghdr{sl_sepn} > 0);
	    }
	} else {		# 3D
	    $ghdr{sl_thick}= $ghdr{vox}[2];
	    $ghdr{sl_sepn} = $ghdr{sl_thick};
	}
	if ($ghdr{sl_sepn}) {
	    $ghdr{sl_gap} = $ghdr{sl_sepn} > 0 ? 
		$ghdr{sl_sepn} - $ghdr{sl_thick} : 0;
	} else { 
	    $ghdr{sl_gap} = $ghdr{sl_thick};
	}

	# NI is the total number of images (=images) acquired in a
	# repetition so, for a 2D volume, and one layer (e.g. EPI),
	# this will be equal to the number of slices.  If there are
	# two layers, NI will be equal to the NSLICES *2. For a
	# 3D volume, NSLICES should I think be 1. 

	$ghdr{image_nr}= $bhdr->{'NI'}; 
	$ghdr{layer_nr} = $ghdr{image_nr} / $bhdr->{'NSLICES'};

	# no of volumes
	$ghdr{dim}[3]=best_of( $bhdr->{'ACQ_nr_completed'}, 1);

	# the TR.  This seems to be an array, with one element
	# for each multiplex step. We'll take the first I guess.
	$ghdr{vox}[3]=$bhdr->{'ACQ_repetition_time'}[0];

	# the slice acquisition order.  Well in fact the order of
	# acquisition of each 2D or 3D image, ie for a 2D scan, with two
	# layers and 10 slices, this array will have 20 elements

	$ghdr{objorder} = [@{$bhdr->{'ACQ_obj_order'}}];

    } else {

	# no ACQ, let's try our best, maybe using the d3proc file
	
	# guess no of layers

	$ghdr{layer_nr} = 1;
	
	# no of images (as for NI from acqp).  These could be different layers, 
	# could be slices, could be both, we don't know without the ACQP file

	$ghdr{image_nr}= @{$bhdr->{'RECO_transposition'}};

	if ($ghdr{dimensions} < 3) { 
        # we don't know how many slices, let's guess that the NI-like parameter
        # above actually refers to no of slices
	    $ghdr{dim}[2] = $ghdr{image_nr};
	}

	# maybe the total number of volumes can be calculated from the d3proc file
	if (!defined($bhdr->{IM_SIZ})) {
	    # we do seem to have a d3proc
	    # the following apparently overcomplex expression is to
	    # get round the situation where the dimensions have been 
	    # transposed, which will be reflected in the d3proc file
	    my($nslices)=$bhdr->{IM_SIX} * $bhdr->{IM_SIY}* $bhdr->{IM_SIZ} 
	    / $ghdr{dim}[0] / $ghdr{dim}[1] 
		* $bhdr->{IM_SIT};
	    $ghdr{dim}[3]=$nslices / $ghdr{dim}[2];
    	} else {
	    # we have to guess at no of volumes
	    $ghdr{dim}[3]=1;
	}
    }
    # set slice FOV if not already set
    if (!defined($ghdr{fov}[2])) {
	if (!defined($ghdr{vox}[2])) {
	    $ghdr{fov}[2] = 1; # no slice volume info
	} else {
	    $ghdr{fov}[2] = $ghdr{vox}[2] * $ghdr{dim}[2] / 10;
	}
    }

    # set use of layers from command line option
    if (!$options{layers}) {
	if ($ghdr{layer_nr} > 1) {
	    unless ($options{quiet}) {
		warn "Reset layer number to 1 from $ghdr{layer_nr}\n";
	    }
	    $ghdr{dim}[$ghdr{dimensions}] *= $ghdr{layer_nr};
	    $ghdr{layer_nr} = 1;
	}
	$ghdr{dim}[3] = $options{timelength};
    }

    # set no of time points from command line option
    if ($options{timelength}) {
	$ghdr{dim}[3] = $options{timelength};
    }

    # transposition.  For each image, and therefore for each slice
    # for a 2D experiment, the image can have its dimensions
    # transposed in the output reconstructed image.  This is dictated
    # by the REC0_transposition parameter, which has one value for
    # each layer for a 3D series, and one value for each layer for
    # each slice for a 2D series.  This transposition is not yet
    # reflected in the FOV and image dimensions we have recorded
    # above.  Calculate the transposed dimensions for each layer and store.
    # The paravision parameter is called RECO_transpose_dim in the docs, but not
    # in the actual reco files ? version change

    my($l, $r, $oimgno);    
    foreach $l (0..$ghdr{layer_nr}-1) {
	$ghdr{transpose}[$l]{code}=$bhdr->{'RECO_transposition'}[$l];
	for ($r=$l+$ghdr{layer_nr};$r<$ghdr{image_nr}-1;$r+=$ghdr{layer_nr}){
	    if ($bhdr->{'RECO_transposition'}[$r] && # maybe we have not enough tranposes
		($ghdr{transpose}[$l]{code}!=$bhdr->{'RECO_transposition'}[$r])) {
		unless ($options{quiet}) {
		    warn ("Different transpositions for the same image in ".
		     "the same volume: confusing, expect ugliness");
		}
	    }
	}

	# work out the transposed dimensions
	$ghdr{transpose}[$l]{dim} = [@{$ghdr{dim}}];
	$ghdr{transpose}[$l]{vox} = [@{$ghdr{vox}}];
	$ghdr{transpose}[$l]{fov} = [@{$ghdr{fov}}];

	$bruk_trans = bruker_transpose($bhdr, $l);

	# may need to transpose dimensions
	swapels(@{$bruk_trans}[0,1],$ghdr{transpose}[$l]{dim});
	swapels(@{$bruk_trans}[0,1],$ghdr{transpose}[$l]{vox});
	swapels(@{$bruk_trans}[0,1],$ghdr{transpose}[$l]{fov});
	$ghdr{transpose}[$l]{descrip} = $bruk_trans->[2];

	# now set the 4x4 transformation matrix
	
	# check that position parameters are defined
	# this code needs generalizing
	if (best_of(
		    $bhdr->{IMND_read_offset},
		    $bhdr->{PVM_ReadOffset}, 
		    $bhdr->{PVM_SPackArrReadOffset}) &&
	    $bhdr->{ACQ_fov}) {

# Compile matrix
	    $is2d = (@{$bhdr->{ACQ_fov}} == 2);
	    $start = $i4->clone();

# transposition
	    $tmp = $start->[$bruk_trans->[0]];
	    $start->[$bruk_trans->[0]] = $start->[$bruk_trans->[1]];
	    $start->[$bruk_trans->[1]] = $tmp;
		
# reco offset
	    $recoff = $i4->clone();
	    $recoff->[0][3] =  $bhdr->{RECO_offset}[0][$l];
	    $recoff->[1][3] =  $bhdr->{RECO_offset}[1][$l];

	    if ($is2d) {
		$recoff->[2][3] = 0;
	    } else {
		$recoff->[2][3] =  $bhdr->{RECO_offset}[2][$l];
	    }

# ft voxels -> mm
	    $ft2mm = $i4->clone();
	    @ftdim = @{$bhdr->{RECO_ft_size}};
	    @acq_dim = @{$bhdr->{ACQ_fov}};
	    if ($is2d) {
		push(@ftdim, $bhdr->{NSLICES});
		push(@acq_dim, $ghdr{fov}[2]);
	    }
	    foreach $i(0..2) {
		$vox = $acq_dim[$i] / $ftdim[$i] * 10;
		$off =  ($ftdim[$i]+1)/2 * -$vox;
		$ft2mm->[$i][$i] = $vox;
		$ft2mm->[$i][3] = $off;
	    }

# translation offset in read/phase/slice
	    $trans = $i4->clone();
	    if (defined($bhdr->{IMND_read_offset})) {
		$trans->[0][3] = $bhdr->{IMND_read_offset}[0];
		$trans->[1][3] = best_of(
					 $bhdr->{IMND_phase1_offset}[0],
					 $bhdr->{ACQ_phase1_offset}[0],
					 0);
		$trans->[2][3] = $bhdr->{IMND_slice_offset};
	    } else {
		$trans->[0][3] = mean(@{
		    best_of($bhdr->{PVM_ReadOffset}, 
			   $bhdr->{PVM_SPackArrReadOffset})
		       });
		$trans->[1][3] = mean(@{
		    best_of($bhdr->{PVM_Phase1Offset}, 
			   $bhdr->{PVM_SPackArrPhase1Offset})
		       });
		$trans->[2][3] = mean(@{
		    best_of($bhdr->{PVM_SliceOffset}, 
			   $bhdr->{PVM_SPackArrSliceOffset})
		       });
	    }

# gradient matrix - is transposed
# There is one (3x3) gradient matrix for each slice - see Para_Cla.pdf in the Paravision documentation
# Quoting:
# ACQ_grad_matrix - is a three dimensional array of doubles to describe the
# orientation of the images to be measured.
#   � The fast dimension has three items to describe gradient vectors for the Gx/
#      Gy/Gz coordinate system. The gradient vectors should also describe a
#      unit sphere with a radius of 1.0.
#   � The medium dimension has also three items to describe the read-, phase-
#      and slice gradient.
#   � The slow dimension is used to describe all slices to be measured.
	    $rotn = $i4->clone();
	    $gradmat = $bhdr->{ACQ_grad_matrix}[0];
	    for $i(0..2){
		for $j(0..2){
		    $rotn->[$i][$j] = $gradmat->[$j][$i];
		}
	    }

# whole thing
	    $vx2mm =  $rotn * $trans * $swaps * $ft2mm * $recoff * $start;

# do report
	    if ($options{showmat}) { # very verbose
		$start->print("Tranposition\n");
		$so_far = mat_so_far("Reco offset",$recoff, $start);
		$so_far = mat_so_far("fft to mm",$ft2mm, $so_far);
		$so_far = mat_so_far("Swap xy",$swaps, $so_far);
		$so_far = mat_so_far("Translations",$trans, $so_far);
		$so_far = mat_so_far("Rotations",$rotn, $so_far);
	    }

# do radiological conversion if nec
	    if ($options{radio}) {
		$radio = $vx2mm->eye(4);
		$radio->[0][0] *= -1;
		$vx2mm = $radio * $vx2mm;
		if ($options{verbose}) {
		    print "Converting matrix to radiological orientation\n";
		}
	    }

# and into generic header
	    $ghdr{mat}[$l] = $vx2mm;

	} else { 
	    if ($options{verbose}) {
		print "Can't find the data for orientation matrix\n" 
		}
	}
    }

    # data type 
    my($datatype);
    if ($bhdr->{'RECO_wordtype'}){
	$datatype = $bhdr->{'RECO_wordtype'};
    } else {
	# try and get datatype from d3proc file
	if ($bhdr->{DATTYPE}) {
	    $datatype = $dataconv{$bhdr->{DATTYPE}}{d3no};
	}
    }
    $ghdr{datatype} = {%{$dataconv{$datatype}}};
    $ghdr{datatype}{name} = $datatype;

    # the size in bytes of the images (potentially) interleaved
    $ghdr{layersize} = 1;
    foreach $i (0..$ghdr{dimensions}-1) {
	$ghdr{layersize} *= $ghdr{dim}[$i];
    }
    $ghdr{layersize} *= $ghdr{datatype}{bytes};

    # endianness of reconstructed data
    $ghdr{"endian"} = ($bhdr->{RECO_byte_order} eq 'littleEndian') ?
	'le':'be';

    # there's an endian problem with FRECO reconstructions
    # They report littleEndian, when in fact they are bigEndian
    # So, if the raw data is bigEndian, and the reco data is littleEndian,
    # then assume this is a FRECO reconstruction, and fix
    if ($options{frecofix} && 
	($bhdr->{'BYTORDA'} =~ /big/) &&
	($ghdr{"endian"} eq "le")) {
	if (!$options{quiet}) {
	    print "Fixing FRECO endian -> bigEndian\n";
	}
	$ghdr{"endian"} = "be";
    }

    # some details
    if ($bhdr->{'ACQ_time'}){
	$bhdr->{'ACQ_time'} =~ /(..:..:..)\s*(\w*\s*\w*\s*\w*)/;
	$ghdr{time_of_scan} = $1;
	$ghdr{date_of_scan} = $2;
    }

    # image type
    $ghdr{iscomplex}=0;
    if ($bhdr->{RECO_image_type}) {
	$ghdr{image_type}=$bhdr->{RECO_image_type};
	$ghdr{image_type}=~s/_IMAGE//; 
	if ($ghdr{image_type}=~ /COMPLEX/) {
	    $ghdr{iscomplex}=1; 
	}
    }

    # entire data block max/min values 
    if ($bhdr->{RECO_maxima}) {
	$ghdr{blockmax} = $ghdr{datatype}{min};
	foreach $i (@{$bhdr->{RECO_maxima}}) {
	    $ghdr{blockmax} = $i if ($i > $ghdr{blockmax});
	}
    } 
    if ($bhdr->{RECO_minima}) {
	$ghdr{blockmin} = $ghdr{datatype}{max};
	foreach $i (@{$bhdr->{RECO_minima}}) {
	    $ghdr{blockmin} = $i if ($i < $ghdr{blockmin});
	}
    } 
    return %ghdr;
}

sub swapels {
    my($elno1, $elno2, $rarr) = @_;
    my($tmp) = $rarr->[$elno1];
    $rarr->[$elno1] = $rarr->[$elno2];
    $rarr->[$elno2] = $tmp;
}

sub mean {
    my($i, $s);
    $s = 0;
    foreach $i(@_) {
	$s += $i;
    }
    return ($s / @_);
}

sub best_of {
    my $a;
    A: while (@_) {
	$a = shift;
	last A if (defined($a));
    };
	return $a;
}

sub mat_so_far {
    my($label, $in, $sofar) = @_;
    $in->print("$label\n");
    $sofar = $in * $sofar;
    $sofar->print("Result so far\n");
    return $sofar;
}
1;