=head1 NAME

iPE::gHMM - The generalized Hidden Markov Model structure for iParameterEstimation.

=head1 DESCRIPTION

A main container class for all models in iParameterEstimation.  Handles all types of input for the gHMM, and outputs via the models it contains.

=cut

package iPE::gHMM;

use iPE;
use iPE::Globals;
use iPE::Comments;
use iPE::XML::Wrappers;
use iPE::Util::Output;
use iPE::Model::ISO;
use iPE::Options;
use iPE::State;
use iPE::State::AltSplice;
use iPE::State::PseudoState;
use iPE::State::Transitions;
use iPE::Model::Emission;
use iPE::Model::Duration;
use iPE::Annotation::NullDefinition;

use base ("iPE::XML::DocObject");
use strict;

=head1 FUNCTIONS

=over 8

=item new(filename)

Creates a new gHMM object based on the file passed.  If the file ends in .xml, the file is interpreted as an xml parameter template.  Some initial checking of the file is performed for validity, and then each of the objects described in the parameter template file are constructed.  Some additional checking is done there as well.

=cut
sub new {
    my ($class) = shift;
    my $this = $class->SUPER::new(@_);

	# contains all the model objects for a gHMM
    $this->{author_} = "";
    $this->{date_} = "";

    $this->{initISOs_} = undef;
    $this->{initModels_} = {};
    $this->{boundInitModel_} = undef;
    $this->{states_} = {};
    $this->{altsplice_states_} = {};

    $this->{transISOs_} = undef;
    $this->{transitions_} = {};
    $this->{transRootElement_} = undef; 
       # the XML element containing the transitions
    $this->{transElements_} = {}; 
        # hash of hashes to transition elements in the document

    $this->{durations_} = [];

    my $g = new iPE::Globals();
    for my $type (@{$g->seqtypes}) {
        $this->{$type."Emissions_"} = [];
    }

	# items not necessarily bound to a gHMM, but included 
    # in the parameter template file.
    $this->{nullDefinitions_} = [];

    $this->{total_seq_len_} = 0;
        #the amount of sequence that's been estimated on so far.

    return $this;
}

sub init {
    my ($this) = @_;
    for my $state (values %{$this->states}) { $state->init; }

    #create the comments node
    my $commentsElement = $this->createSubElement("comments", $this->element);
    $this->{comments_} = new iPE::Comments("comments", {}, "", 
        $commentsElement);
    $this->{comments_}->addComment(
        "Generated with iParameterEstimation v$iPE::VERSION $iPE::VERSION_DATE by Bob Zimmermann");
    $this->{comments_}->addComment(
        "Parameter template authored by ".$this->author." on ".$this->date);
    $this->{comments_}->addComment("");

    $this->_calc_inits;

    $this->_checkObsolete();
}

# check for obsolete idioms in the gHMM file:
#
# 1. Use of <conservation_models>, <est_models>, etc. tags.  All models should
#    be under the <sequence_models> umbrella
# 2. 

sub _checkObsolete {
    my ($this) = @_;

    my $g = new iPE::Globals();

    my @errors;
    my %emitted_errors;
    my ($models_tag, $data_attr, $bntree) = (0,1,2);
    $errors[$models_tag] = 
"* All string_models should be under the <sequence_models> tag.  Other tags,
   such as conservation_models, est_models are no longer used\n";
    $errors[$data_attr] = 
"* All data attributes for string_models are now under the more expressive
   setting=value format, for example \"order=5\"\n";
    $errors[$bntree] = 
"* BNTREE* models are now proper iPE models, rather than hacked zoe_models.
   all models with a BNTREE* zoe_model are to the models attribute\n";

    my $last_error = 
"\nUnrecoverable errors from obsolete gHMM file.
You may correct the gHMM by using the script convert_old_gHMM_to_new_format.\n";

    my $emit_error = sub {
        my ($error_id, $error_ary, $emitted_hash) = @_;

        my $first_error = 
__PACKAGE__.": It appears as though ".$this->filename."
is an obsolete gHMM file.  Here are the obsoleted features in your gHMM:\n\n";

        Warn($first_error) unless(scalar(keys(%$emitted_hash)));
        Warn($error_ary->[$error_id]) unless($emitted_hash->{$error_id});
        $emitted_hash->{$error_id} = 1;
    };

    for my $child ($this->element->childNodes) {
        $emit_error->($models_tag, \@errors, \%emitted_errors) 
          if($child->nodeName =~ m/_models$/ && 
                $child->nodeName ne "sequence_models");
    }
    
    for my $type (@{$g->seqtypes}) {
        for my $emis (@{$this->getEmissions($type)}) {
            $emit_error->($data_attr, \@errors, \%emitted_errors)
                if($emis->data =~ m/\S/ && $emis->data !~ m/=/);
            $emit_error->($bntree, \@errors, \%emitted_errors)
                if(defined($emis->zModel) && $emis->zModel =~ m/BNTREE/);
        }
    }

    die($last_error) if(scalar(keys(%emitted_errors)));
}

=item author

Return the author of the parameter template.

=cut
sub author       { return shift->{author_}; }
=item date

Return the date of creation indicated in the parameter template.

=cut
sub date         { return shift->{date_};   }

=item comments

Return the Comments object contained in the gHMM.

=cut
sub comments        { return shift->{comments_} }

=item initISOs, transISOs

Return the iPE::Model::ISO levels object which contains the ISO levels of the initial and transition probabilities.

=cut
sub initISOs     { return shift->{initISOs_}; }
sub transISOs    { return shift->{transISOs_}; }

sub boundInitModel { return shift->{boundInitModel_} }
sub initModels { return shift->{initModels_} }

=item nullDefinitions ()

Returns a reference to an array of all the NullDefinition objects created in the gHMM.

=cut
sub nullDefinitions { shift->{nullDefinitions_} }

=item states ()

Return the state hash, keyed by the name of the state.

=cut
sub states       { return shift->{states_}; }

=item altspliceStates ()

Return the altsplice state hash, keyed by the name of the altsplice state.

=cut
sub altspliceStates    { return shift->{altsplice_states_} }

=item state (name)

Return the state with the passed name, if none exists, undef is returned.

=cut
sub state        { return $_[0]->{states_}->{$_[1]} }

=item conversion ()

Return the gtf conversion for zoe.

=cut
sub conversion  { shift->{conversion_}  }

=item getEmissions (type)

Return the array reference to the emissions type that is passed.

=cut
sub getEmissions {
    my ($this, $type) = @_;

    my $ret = $this->{$type."Emissions_"};

    die __PACKAGE__.": Unknown emssion type $type."  unless(defined $ret);

    return $ret;
}

=item dnaEmissions (), consEmissions (), estEmissions (), malignEmissions (), etc.

Accessor functions for the array of dna and conservation sequence emission models.

=cut
sub AUTOLOAD {
  my $this = shift;
  my $fname = our $AUTOLOAD;
  return if $fname =~ /::DESTROY$/;
  $fname =~ s/.*:://;
  if($fname =~ /Emissions/) {
    return $this->{$fname."_"};
  }
  die "Cannot call $fname on object of class ".ref($this)."\n";
}

sub durations    { return shift->{durations_}; }

=item countNullRegion (region)

The gHMM, being the holder of all references to models, is responsible for counting the null region, to ensure that each model gets counted only once.

=cut
sub countNullRegion {
    my ($this, $region) = @_;

    msg("Counting null region (".  $region->start.", ".$region->end.")\n");
    my $emisArr = $this->getEmissions($region->seq->type);
    for my $emis (@$emisArr) {
        next if($emis->nullParams ||
                $emis->type == iPE::Model::Emission::FIXED_MODEL);
        if($emis->samplingRate != 1 && rand() > $emis->samplingRate) {
            msg("Skipping ".$emis->name." because of sampling rate.\n");
        }
        if($emis->nullModel) {
            $emis->countNullRegion($region);
        }
    }
}

=item finalizeCounts ()

Takes all of the models and requests that they take all the observed ambiguity codes and include them in the counts for the finalized counts.

=cut
sub finalizeCounts {
    my ($this) = @_;
    my $g = new iPE::Globals();
    for my $type (@{$g->seqtypes}) {
        my $emisArr = $this->getEmissions($type);
        for my $emis (@$emisArr) {
            next if($emis->nullParams ||
                    $emis->type == iPE::Model::Emission::FIXED_MODEL);
            $emis->finalizeCounts;
        }
    }
}

=item smooth ()

Smooths all the distributions in the gHMM.

=cut
sub smooth {
    my ($this) = @_;

    # smooth durations
    for my $dur (@{$this->durations}) {
        $dur->smooth;
    }

    my $g = new iPE::Globals();
    # smooth emissions
    for my $type (@{$g->seqtypes}) {
        my $emisArr = $this->getEmissions($type);
        for my $emis (@$emisArr) {
            next if(!$emis->countable);
            $emis->smooth;
        }
    }
}

=item normalize ()

Normalizes all models after they have been counted.

=cut
sub normalize {
    my ($this) = @_;

    # normalize transitions
    for my $stateName (keys %{$this->states}) {
        $this->states->{$stateName}->transitions->normalize;
    }

    # normalize durations
    for my $dur (@{$this->durations}) {
        $dur->normalize;
    }

    my $g = new iPE::Globals();
    # normalize emissions
    for my $type (@{$g->seqtypes}) {
        my $emisArr = $this->getEmissions($type);
        for my $emis (@$emisArr) {
            next if(!$emis->countable);
            $emis->normalize;
        }
    }
}

=item score ()

Scores all models after having been normalized.

=cut
sub score {
    my ($this) = @_;

    for my $dur (@{$this->durations}) {
        $dur->score;
    }

    my $g = new iPE::Globals();
    for my $type (@{$g->seqtypes}) {
        my $emisArr = $this->getEmissions($type);
        for my $emis (@$emisArr) {
            next if($emis->nullParams ||
                    $emis->type == iPE::Model::Emission::FIXED_MODEL);
            $emis->score;
        }
    }
}

=item optimize ()

Optimize the tuple counts against the global N-SCAN BNTree.  Note that this is fairly monolithic since the author did not reimpliment the EM optimization algorithm.

=cut
sub optimize {
    my ($this) = @_;

    for my $emis(@{$this->getEmissions("malign")}) {
        $emis->optimize();
    }
}

=item outputPrepare(out, mode)

Prepare all the states and models contained in the gHMM for outputting.  out is an iPE::Util::Output object and mode is one of "count", "prob", or "score", indicating what kind of parameters will be output.

=cut
sub outputPrepare {
    my ($this, $out, $mode) = @_;

    for my $state (sort keys %{$this->states}) {
        $this->states->{$state}->outputPrepare($out, $mode);
    }

    for my $state (values %{$this->states}) {
        $state->transitions->outputPrepare($out, $mode);

        #set the XML element associated with this transition for the XML doc.
        #TODO: again, this is bad form.  Transitions should probably get their 
        #      own element.
        for my $dest (@{$state->transitions->dests}) {
            # this would mean it has a fixed transition.
            next if (!defined($this->{transElements_}->{$state->name}->{$dest}));
            my $transString = "";
            for my $level (@{$state->transitions->ISOs->levels}) {
                $transString .= $out->floatf(
                    $state->transitions->counts->{$dest}->{$level})." ";
            }
            $this->{transElements_}->{$state->name}->{$dest}->setAttribute(
                "values", $transString);
        }
    }

    for my $dur (@{$this->durations}) {
        $dur->outputPrepare($out, $mode);
    }

    my $g = new iPE::Globals();
    for my $emisType (@{$g->seqtypes}) {
        my $emisArr = $this->getEmissions($emisType) ;
        for my $emis(@$emisArr) {
            if($emis->type != iPE::Model::Emission::FIXED_MODEL &&
               $emis->type != iPE::Model::Emission::FIXED_SUBMODEL ) {
                $emis->outputPrepare($out, $mode);
            }
        }
    }
}

=item outputZoe(out, mode)

Output the gHMM, estimated or not, to the iPE::Util::Output object passed to the function.  The mode is one of "count", "prob", or "score" depending on the mode of output.  This is outputted in Zoe format.

=cut
sub outputZoe {
    my ($this, $out, $mode) = @_;
    my $state_count = $this->_pseudostate_count 
        + scalar(keys %{$this->states});
    my $trans_count = 0;
    for my $state (keys %{$this->states}) {
        $trans_count += $this->states->{$state}->transitions->nTotDests;

        Warn("state $state has no duration model\n")
            if not defined $this->states->{$state}->durModel;
    }

    #output the header
    $out->print ("zHMM name=iPE_gHMM\n");
    $out->print ("states=".$state_count."\n");
    $out->print ("transitions=".$trans_count."\n");
    $out->print ("durations=".scalar(@{$this->durations})."\n");
    $out->print ("seq_models=".scalar(@{$this->dnaEmissions})."\n");
    if(scalar(@{$this->colaEmissions})) {
        $out->print ("conseq_models=".scalar(@{$this->colaEmissions})."\n");
    }
    else {
        $out->print ("conseq_models=".scalar(@{$this->consEmissions})."\n");
    }
    $out->print ("arrayseq_models=".scalar(@{$this->arrayEmissions})."\n")
        if(scalar(@{$this->arrayEmissions}));
    $out->print ("tileseq_models=".scalar(@{$this->tileEmissions})."\n")
        if(scalar(@{$this->tileEmissions}));
    $out->print ("repseq_models=".scalar(@{$this->repEmissions})."\n")
        if(scalar(@{$this->repEmissions}));
    $out->print ("estseq_models=".scalar(@{$this->estEmissions})."\n")
        if(scalar(@{$this->estEmissions}));
    $out->print ("estseq_models=".scalar(@{$this->asestEmissions})."\n")
        if(scalar(@{$this->asestEmissions}));
    $out->print ("phylo_models=".scalar(@{$this->malignEmissions})."\n")
        if(scalar(@{$this->malignEmissions}));

    $out->print ("\n");

    my $g = new iPE::Globals();
    #output the comments
    if(!$g->options->zoeCommentsAtEnd) {
        $out->print($this->comments->getZoeComments);
    }

    $out->print ("\n");

    $out->print ("<STATES>\n");
    if(defined($this->initISOs)) {
        $out->print ("ISO ".$this->initISOs->nLevels." levels:\n");
        $out->print ($_."\t") for(@{$this->initISOs->levels});
        $out->print ("\n");
    }

    for my $state (sort keys %{$this->states}) {
        $this->states->{$state}->outputZoe($out, $mode);
    }

    $out->print ("\n");

    $out->print ("<STATE_TRANSITIONS>\n");
    $out->print ("ISO ".$this->transISOs->nLevels." levels:\n");
    $out->print ($_."\t") for(@{$this->transISOs->levels});
    $out->print ("\n\n");

    for my $state (sort keys %{$this->states}) {
        $this->states->{$state}->transitions->outputZoe($out, $mode);
    }

    $out->print ("\n");

    $out->print ("<STATE_DURATIONS>\n");
    for my $dur (@{$this->durations}) {
        $dur->outputZoe($out, $mode);
    }

    $out->print ("\n");

    $out->print ("<SEQUENCE_MODELS>\n");
    for my $emis (@{$this->dnaEmissions}) {
        $emis->outputZoe($out, $mode);
    }

    $out->print ("\n");

    for my $type (@{$g->seqtypes}) {
        next if($type eq "dna"); # the tag says "SEQUENCE", so do this above
        if(scalar(@{$this->getEmissions($type)})) {
          $out->print("<".$g->zoe_seqtype($type)."_MODELS>\n");
          for my $emis(@{$this->getEmissions($type)}) {
              $emis->outputZoe($out, $mode);
          }
        }
    }

    $out->print ("\n");
    $out->print ("\n");
    $out->print ("<GTF_CONVERSION>\n\n");

    $out->print ($this->conversion);

    if($g->options->zoeCommentsAtEnd) {
        $out->print("\n");
        $out->print($this->comments->getZoeComments);
    }
}

#private functions

sub _pseudostate_count {
    my ($this) = @_;
    my $count = 0;

    for my $state (keys %{$this->states}) {
        $count += scalar(@{$this->states->{$state}->pseudoStates});
    }

    return $count;
}

sub handle_subelement {
    my $this = shift;
    my ($tag, $att, $data, $element) = @_;

    # here, we find the root element of the transition node if it hasn't
    # already been found.  This is done here so that we can add to the 
    # transition root element later on in the code
    # TODO: really, transitions should get their own elements, but
    #       i didn't want to compromise the stability of the code for the time
    #       being.  if necessary, rewrite transition code.
    if(!defined($this->{transRootElement})) {
        for my $element ($this->element->childNodes) {
            $this->{transRootElement_} = $element
                if($element->nodeName =~ /^trans_model$/);
        }
        $this->{transRootElement_} = $this->createSubElement("trans_model", 
            $this->element) if(!defined($this->{transRootElement_}));
    }
 
    #pass the attributes of the XML element to the model object to be created
    #and maybe the parser itself there is potential for submodels, allowing
    #for recursive parsing.
    for($tag) {
        if(/^author$/)                      { $this->_set_author($data)        }
        elsif(/^date$/)                     { $this->_set_date($data)          }
        elsif(/^states$/)                   { $this->handle_children($element) }
        elsif(/^altsplice_state$/)          { $this->_add_altsplice_state(@_)  }
        elsif(/^state$/)                    { $this->_add_state(@_)            }
        elsif(/^pseudostate$/)              { $this->_add_pseudostate($att)    }
        elsif(/^zoe_gtf_conversion$/)       { $this->_set_conversion($data)    }
        elsif(/^null_region_definitions$/)  { $this->handle_children($element) }
        elsif(/^null_region_definition$/)   { $this->_add_null_region(@_)      }
        elsif(/^init_model$/)               { $this->_init_inits($att); 
                                              $this->handle_children($element) }
        elsif(/^init_prob$/)                { $this->_add_init($att)           }
        elsif(/^trans_model$/)              { $this->_set_trans(@_);          
                                              $this->handle_children($element) }
        elsif(/^transition$/) {}# do nothing -- these are programmatically added
        elsif(/^fixed_transition$/)         { $this->_fix_trans($att)          }
        elsif(/^pseudo_transitions$/)       { $this->handle_children($element) }
        elsif(/^pseudo_transition$/)        { $this->_add_pseudo_trans($att)   }
        elsif(/^state_durations$/)          { $this->handle_children($element) }
        elsif(/^duration_model$/)           { $this->_add_duration(@_) }
        elsif(/\S+_models$/)                { $this->handle_children($element) }
        elsif(/^(fixed_|default_)?string_model$/)             
                                            { $this->_add_string_model(@_)     }
        else                                { Warn("Unrecognized tag: $tag\n") }
    }
}

sub _add_null_region {
    my $this = shift;
    push @{$this->{nullDefinitions_}}, new iPE::Annotation::NullDefinition(@_);
}

sub _add_state {
    my $this = shift;
    my ($tag, $att) = @_;
    my $state = new iPE::State(@_);

    die "State ".$state->name." defined twice.\n" 
        if (defined ($this->states->{$state->name}));

    $this->states->{$state->name} = $state;

    for my $dest (@{$state->transitions->dests}) {
        my $transElement = $this->createSubElement(
            "transition", $this->{transRootElement_});   
        $transElement->setAttribute("from", $state->name);
        $transElement->setAttribute("to", $dest);
        $this->{transElements_}->{$state->name}->{$dest} = $transElement;
    }
}

sub _add_altsplice_state {
    my $this = shift;
    my ($tag, $att) = @_;
    my $classname = iPE::State::AltSplice::getClassname($att->{altsplice_type});
    my $state = $classname->new(@_);

    die "Altsplice State ".$state->name." defined twice.\n" 
        if (defined ($this->states->{$state->name}));

    $this->states->{$state->name} = $state;
    $this->altspliceStates->{$state->name} = $state;

    for my $dest (@{$state->transitions->dests}) {
        my $transElement = $this->createSubElement(
            "transition", $this->{transRootElement_});   
        $transElement->setAttribute("from", $state->name);
        $transElement->setAttribute("to", $dest);
        $this->{transElements_}->{$state->name}->{$dest} = $transElement;
    }
}

sub _add_pseudostate {
    my ($this, $att) = @_;

    die "Pseudostate $att->{name} does not have a valid parent state: ".
        $att->{actual_state}."\n" 
        unless defined $this->{states_}{$att->{actual_state}};

    my $pseudoState = new iPE::State::PseudoState($att);

    $this->states->{$att->{actual_state}}->addPseudoState($pseudoState);
}

sub _set_conversion {
    my ($this, $data) = @_;

    $this->{conversion_} = $data;
}

sub _init_inits {
    my ($this, $att) = @_;

    die "Redefinition of initial model in parameter template\n" 
        if defined $this->{initISOs_};

    $this->{initISOs_} = new iPE::Model::ISO({ISOs => $att->{isochores}});
    for my $state (keys %{$this->states}) {
        $this->states->{$state}->initInits($this->initISOs);
    }
}

sub _add_init {
    my ($this, $att) = @_;

    if(!defined($att->{probs}) || $att->{probs} !~ m/\d/) {
        if(defined $this->{boundInitModel_}) {
            die "Multiple initial models with no probabilities listed.\n";
        }
        $this->{boundInitModel_} = $att->{name};
    }
    else {
        $this->{initModels_}->{$att->{name}} = [ split ' ', $att->{probs} ];
    }
}

sub _calc_inits {
    my ($this) = @_;

    return if (!defined $this->initISOs);

    my @totals;
    push @totals, 0 for (@{$this->initISOs->levels});
    for my $initModelName (keys %{$this->initModels}) {
        $this->_calc_init($initModelName, $this->initModels->{$initModelName});
        for(my $i = 0; $i < $this->initISOs->nLevels; $i++) {
            $totals[$i] += $this->initModels->{$initModelName}->[$i];
        }
    }

    if(defined($this->boundInitModel)) {
        my @initProbs;
        for(my $i = 0; $i < $this->initISOs->nLevels; $i++) {
            $initProbs[$i] = (1-$totals[$i]);
        }
        $this->_calc_init($this->boundInitModel, \@initProbs);
    }
}

sub _calc_init {
    my ($this, $initModelName, $initProbs) = @_;

    my @states;
    for my $stateName (keys (%{$this->states})) {
        if(defined($this->states->{$stateName}->initModelName) &&
                $this->states->{$stateName}->initModelName eq $initModelName) {
            push @states, $this->states->{$stateName};
        }
    }
    if (scalar(@states) == 0) {
        die (__PACKAGE__.
            ": No state found for initial probability $initModelName.\n");
    }

    my @adjProbs;
    for(my $i = 0; $i < $this->initISOs->nLevels; $i++) {
        $adjProbs[$i] = $initProbs->[$i]/scalar(@states);
    }
    for my $state(@states) {
        $state->setInitials(@adjProbs);
    }
}

sub _set_trans {
    my ($this, $tag, $att, $data, $element) = @_;

    die "Redefinition of transition model in parameter template\n"
        if defined $this->{transISOs_};

    $this->{transISOs_} = new iPE::Model::ISO({ISOs => $att->{isochores}});
    for my $state (keys %{$this->states}) {
        $this->states->{$state}->transitions->setISOs(
            $this->transISOs, $att->{pseudocounts});
    }
}

sub _fix_trans {
    my ($this, $att) = @_;

    die "No state $att->{from} defined for fixed_transition ".
        "to $att->{to}.\n"
        if(!defined $this->states->{$att->{from}});

    $this->states->{$att->{from}}->transitions->fixTransition($att->{to},
        split (' ', $att->{values}));

    # remove the now-redundant transition.
    my $element = $this->{transElements_}->{$att->{from}}->{$att->{to}};
    $this->{transRootElement_}->removeChild($element) if(defined($element));
}

sub _add_pseudo_trans {
    my ($this, $att) = @_;

    die "Pseudo transition for undefined state(s) ".$att->{source}." and ".
        $att->{dest}."\n" if(not defined $this->states->{$att->{source}} ||
                             not defined $this->states->{$att->{dest}});
    
    $this->states->{$att->{source}}->transitions->addPseudoTransition($att);
}

sub _add_duration {
    my $this = shift;
    my ($tag, $att) = @_;

    my $region = $att->{region};
    my $duration = new iPE::Model::Duration(@_);
    
    push (@{$this->{durations_}}, $duration);
    for my $state_name (keys %{$this->states}) {
        my $state = $this->states->{$state_name};
        if($state->durRegionName eq $duration->region) {
            $state->setDurModel($duration);
        }
    }
}

sub _add_string_model {
    my $this = shift;
    my ($tag, $att, $data) = @_;

    my $emis_aref = $this->getEmissions($att->{source});
    my $classname = iPE::Model::Emission::getClassname($att->{model});
    my $emis = $classname->new (@_);
    push (@$emis_aref, $emis);

    for my $state (split (" ", $att->{states})) {
        die "Bad state for emission ".$att->{name}.": \"".$state."\"\n"
            if not defined $this->states->{$state};
        $this->states->{$state}->addModel($emis);
    }
}

sub _set_author { chomp ($_[1]); $_[0]->{author_} = $_[1]; }
sub _set_date { chomp ($_[1]); $_[0]->{date_} = $_[1]; }

=back

=head1 SEE ALSO

L<iPE>

=head1 AUTHOR

Bob Zimmermann (rpz@cs.wustl.edu)

=cut

1;
