# Copyright 2019 The Magenta Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Classes for converting between event sequences and models inputs/outputs.

OneHotEncoding is an abstract class for specifying a one-hot encoding, i.e.
how to convert back and forth between an arbitrary event space and integer
indices between 0 and the number of classes.

EventSequenceEncoderDecoder is an abstract class for translating event
_sequences_, i.e. how to convert event sequences to input vectors and output
labels to be fed into a model, and how to convert from output labels back to
events.

Use EventSequenceEncoderDecoder.encode to convert an event sequence to a
tf.train.SequenceExample of inputs and labels. These SequenceExamples are fed
into the model during training and evaluation.

During generation, use EventSequenceEncoderDecoder.get_inputs_batch to convert a
list of event sequences into an inputs batch which can be fed into the model to
predict what the next event should be for each sequence. Then use
EventSequenceEncoderDecoder.extend_event_sequences to extend each of those event
sequences with an event sampled from the softmax output by the model.

OneHotEventSequenceEncoderDecoder is an EventSequenceEncoderDecoder that uses a
OneHotEncoding of individual events. The input vectors are one-hot encodings of
the most recent event. The output labels are one-hot encodings of the next
event.

LookbackEventSequenceEncoderDecoder is an EventSequenceEncoderDecoder that also
uses a OneHotEncoding of individual events. However, its input and output
encodings also consider whether the event sequence is repeating, and the input
encoding includes binary counters for timekeeping.
"""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import abc
import numbers

from magenta.music import constants
import numpy as np
from six.moves import range  # pylint: disable=redefined-builtin
import tensorflow as tf

DEFAULT_STEPS_PER_BAR = constants.DEFAULT_STEPS_PER_BAR
DEFAULT_LOOKBACK_DISTANCES = [DEFAULT_STEPS_PER_BAR, DEFAULT_STEPS_PER_BAR * 2]


class OneHotEncoding(object):
  """An interface for specifying a one-hot encoding of individual events."""
  __metaclass__ = abc.ABCMeta

  @abc.abstractproperty
  def num_classes(self):
    """The number of distinct event encodings.

    Returns:
      An int, the range of ints that can be returned by self.encode_event.
    """
    pass

  @abc.abstractproperty
  def default_event(self):
    """An event value to use as a default.

    Returns:
      The default event value.
    """
    pass

  @abc.abstractmethod
  def encode_event(self, event):
    """Convert from an event value to an encoding integer.

    Args:
      event: An event value to encode.

    Returns:
      An integer representing the encoded event, in range [0, self.num_classes).
    """
    pass

  @abc.abstractmethod
  def decode_event(self, index):
    """Convert from an encoding integer to an event value.

    Args:
      index: The encoding, an integer in the range [0, self.num_classes).

    Returns:
      The decoded event value.
    """
    pass

  def event_to_num_steps(self, unused_event):
    """Returns the number of time steps corresponding to an event value.

    This is used for normalization when computing metrics. Subclasses with
    variable step size should override this method.

    Args:
      unused_event: An event value for which to return the number of steps.

    Returns:
      The number of steps corresponding to the given event value, defaulting to
      one.
    """
    return 1


class EventSequenceEncoderDecoder(object):
  """An abstract class for translating between events and model data.

  When building your dataset, the `encode` method takes in an event sequence
  and returns a SequenceExample of inputs and labels. These SequenceExamples
  are fed into the model during training and evaluation.

  During generation, the `get_inputs_batch` method takes in a list of the
  current event sequences and returns an inputs batch which is fed into the
  model to predict what the next event should be for each sequence. The
  `extend_event_sequences` method takes in the list of event sequences and the
  softmax returned by the model and extends each sequence by one step by
  sampling from the softmax probabilities. This loop (`get_inputs_batch` ->
  inputs batch is fed through the model to get a softmax ->
  `extend_event_sequences`) is repeated until the generated event sequences
  have reached the desired length.

  Properties:
    input_size: The length of the list returned by self.events_to_input.
    num_classes: The range of ints that can be returned by
        self.events_to_label.

  The `input_size`, `num_classes`, `events_to_input`, `events_to_label`, and
  `class_index_to_event` method must be overwritten to be specific to your
  model.
  """

  __metaclass__ = abc.ABCMeta

  @abc.abstractproperty
  def input_size(self):
    """The size of the input vector used by this model.

    Returns:
        An integer, the length of the list returned by self.events_to_input.
    """
    pass

  @abc.abstractproperty
  def num_classes(self):
    """The range of labels used by this model.

    Returns:
        An integer, the range of integers that can be returned by
            self.events_to_label.
    """
    pass

  @abc.abstractproperty
  def default_event_label(self):
    """The class label that represents a default event.

    Returns:
      An int, the class label that represents a default event.
    """
    pass

  @abc.abstractmethod
  def events_to_input(self, events, position):
    """Returns the input vector for the event at the given position.

    Args:
      events: A list-like sequence of events.
      position: An integer event position in the sequence.

    Returns:
      An input vector, a self.input_size length list of floats.
    """
    pass

  @abc.abstractmethod
  def events_to_label(self, events, position):
    """Returns the label for the event at the given position.

    Args:
      events: A list-like sequence of events.
      position: An integer event position in the sequence.

    Returns:
      A label, an integer in the range [0, self.num_classes).
    """
    pass

  @abc.abstractmethod
  def class_index_to_event(self, class_index, events):
    """Returns the event for the given class index.

    This is the reverse process of the self.events_to_label method.

    Args:
      class_index: An integer in the range [0, self.num_classes).
      events: A list-like sequence of events.

    Returns:
      An event value.
    """
    pass

  def labels_to_num_steps(self, labels):
    """Returns the total number of time steps for a sequence of class labels.

    This is used for normalization when computing metrics. Subclasses with
    variable step size should override this method.

    Args:
      labels: A list-like sequence of integers in the range
          [0, self.num_classes).

    Returns:
      The total number of time steps for the label sequence, defaulting to one
      per event.
    """
    return len(labels)

  def encode(self, events):
    """Returns a SequenceExample for the given event sequence.

    Args:
      events: A list-like sequence of events.

    Returns:
      A tf.train.SequenceExample containing inputs and labels.
    """
    inputs = []
    labels = []
    for i in range(len(events) - 1):
      inputs.append(self.events_to_input(events, i))
      labels.append(self.events_to_label(events, i + 1))
    return make_sequence_example(inputs, labels)

  def get_inputs_batch(self, event_sequences, full_length=False):
    """Returns an inputs batch for the given event sequences.

    Args:
      event_sequences: A list of list-like event sequences.
      full_length: If True, the inputs batch will be for the full length of
          each event sequence. If False, the inputs batch will only be for the
          last event of each event sequence. A full-length inputs batch is used
          for the first step of extending the event sequences, since the RNN
          cell state needs to be initialized with the priming sequence. For
          subsequent generation steps, only a last-event inputs batch is used.

    Returns:
      An inputs batch. If `full_length` is True, the shape will be
      [len(event_sequences), len(event_sequences[0]), INPUT_SIZE]. If
      `full_length` is False, the shape will be
      [len(event_sequences), 1, INPUT_SIZE].
    """
    inputs_batch = []
    for events in event_sequences:
      inputs = []
      if full_length:
        for i in range(len(events)):
          inputs.append(self.events_to_input(events, i))
      else:
        inputs.append(self.events_to_input(events, len(events) - 1))
      inputs_batch.append(inputs)
    return inputs_batch

  def extend_event_sequences(self, event_sequences, softmax):
    """Extends the event sequences by sampling the softmax probabilities.

    Args:
      event_sequences: A list of EventSequence objects.
      softmax: A list of softmax probability vectors. The list of softmaxes
          should be the same length as the list of event sequences.

    Returns:
      A Python list of chosen class indices, one for each event sequence.
    """
    chosen_classes = []
    for i in range(len(event_sequences)):
      if not isinstance(softmax[0][0][0], numbers.Number):
        # In this case, softmax is a list of several sub-softmaxes, each
        # potentially with a different size.
        # shape: [[beam_size, event_num, softmax_size]]
        chosen_class = []
        for sub_softmax in softmax:
          num_classes = len(sub_softmax[0][0])
          chosen_class.append(
              np.random.choice(num_classes, p=sub_softmax[i][-1]))
      else:
        # In this case, softmax is just one softmax.
        # shape: [beam_size, event_num, softmax_size]
        num_classes = len(softmax[0][0])
        chosen_class = np.random.choice(num_classes, p=softmax[i][-1])
      event = self.class_index_to_event(chosen_class, event_sequences[i])
      event_sequences[i].append(event)
      chosen_classes.append(chosen_class)
    return chosen_classes

  def evaluate_log_likelihood(self, event_sequences, softmax):
    """Evaluate the log likelihood of multiple event sequences.

    Each event sequence is evaluated from the end. If the size of the
    corresponding softmax vector is 1 less than the number of events, the entire
    event sequence will be evaluated (other than the first event, whose
    distribution is not modeled). If the softmax vector is shorter than this,
    only the events at the end of the sequence will be evaluated.

    Args:
      event_sequences: A list of EventSequence objects.
      softmax: A list of softmax probability vectors. The list of softmaxes
          should be the same length as the list of event sequences.

    Returns:
      A Python list containing the log likelihood of each event sequence.

    Raises:
      ValueError: If one of the event sequences is too long with respect to the
          corresponding softmax vectors.
    """
    all_loglik = []
    for i in range(len(event_sequences)):
      if len(softmax[i]) >= len(event_sequences[i]):
        raise ValueError(
            'event sequence must be longer than softmax vector (%d events but '
            'softmax vector has length %d)' % (len(event_sequences[i]),
                                               len(softmax[i])))
      end_pos = len(event_sequences[i])
      start_pos = end_pos - len(softmax[i])
      loglik = 0.0
      for softmax_pos, position in enumerate(range(start_pos, end_pos)):
        index = self.events_to_label(event_sequences[i], position)
        if isinstance(index, numbers.Number):
          loglik += np.log(softmax[i][softmax_pos][index])
        else:
          for sub_softmax_i in range(len(index)):
            loglik += np.log(
                softmax[i][softmax_pos][sub_softmax_i][index[sub_softmax_i]])
      all_loglik.append(loglik)
    return all_loglik


class OneHotEventSequenceEncoderDecoder(EventSequenceEncoderDecoder):
  """An EventSequenceEncoderDecoder that produces a one-hot encoding."""

  def __init__(self, one_hot_encoding):
    """Initialize a OneHotEventSequenceEncoderDecoder object.

    Args:
      one_hot_encoding: A OneHotEncoding object that transforms events to and
          from integer indices.
    """
    self._one_hot_encoding = one_hot_encoding

  @property
  def input_size(self):
    return self._one_hot_encoding.num_classes

  @property
  def num_classes(self):
    return self._one_hot_encoding.num_classes

  @property
  def default_event_label(self):
    return self._one_hot_encoding.encode_event(
        self._one_hot_encoding.default_event)

  def events_to_input(self, events, position):
    """Returns the input vector for the given position in the event sequence.

    Returns a one-hot vector for the given position in the event sequence, as
    determined by the one hot encoding.

    Args:
      events: A list-like sequence of events.
      position: An integer event position in the event sequence.

    Returns:
      An input vector, a list of floats.
    """
    input_ = [0.0] * self.input_size
    input_[self._one_hot_encoding.encode_event(events[position])] = 1.0
    return input_

  def events_to_label(self, events, position):
    """Returns the label for the given position in the event sequence.

    Returns the zero-based index value for the given position in the event
    sequence, as determined by the one hot encoding.

    Args:
      events: A list-like sequence of events.
      position: An integer event position in the event sequence.

    Returns:
      A label, an integer.
    """
    return self._one_hot_encoding.encode_event(events[position])

  def class_index_to_event(self, class_index, events):
    """Returns the event for the given class index.

    This is the reverse process of the self.events_to_label method.

    Args:
      class_index: An integer in the range [0, self.num_classes).
      events: A list-like sequence of events. This object is not used in this
          implementation.

    Returns:
      An event value.
    """
    return self._one_hot_encoding.decode_event(class_index)

  def labels_to_num_steps(self, labels):
    """Returns the total number of time steps for a sequence of class labels.

    Args:
      labels: A list-like sequence of integers in the range
          [0, self.num_classes).

    Returns:
      The total number of time steps for the label sequence, as determined by
      the one-hot encoding.
    """
    events = []
    for label in labels:
      events.append(self.class_index_to_event(label, events))
    return sum(self._one_hot_encoding.event_to_num_steps(event)
               for event in events)


class OneHotIndexEventSequenceEncoderDecoder(OneHotEventSequenceEncoderDecoder):
  """An EventSequenceEncoderDecoder that produces one-hot indices."""

  @property
  def input_size(self):
    return 1

  @property
  def input_depth(self):
    return self._one_hot_encoding.num_classes

  def events_to_input(self, events, position):
    """Returns the one-hot index for the event at the given position.

    Args:
      events: A list-like sequence of events.
      position: An integer event position in the event sequence.

    Returns:
      An integer input event index.
    """
    return [self._one_hot_encoding.encode_event(events[position])]


class LookbackEventSequenceEncoderDecoder(EventSequenceEncoderDecoder):
  """An EventSequenceEncoderDecoder that encodes repeated events and meter."""

  def __init__(self, one_hot_encoding, lookback_distances=None,
               binary_counter_bits=5):
    """Initializes the LookbackEventSequenceEncoderDecoder.

    Args:
      one_hot_encoding: A OneHotEncoding object that transforms events to and
         from integer indices.
      lookback_distances: A list of step intervals to look back in history to
         encode both the following event and whether the current step is a
         repeat. If None, use default lookback distances.
      binary_counter_bits: The number of input bits to use as a counter for the
         metric position of the next event.
    """
    self._one_hot_encoding = one_hot_encoding
    if lookback_distances is None:
      self._lookback_distances = DEFAULT_LOOKBACK_DISTANCES
    else:
      self._lookback_distances = lookback_distances
    self._binary_counter_bits = binary_counter_bits

  @property
  def input_size(self):
    one_hot_size = self._one_hot_encoding.num_classes
    num_lookbacks = len(self._lookback_distances)
    return (one_hot_size +                  # current event
            num_lookbacks * one_hot_size +  # next event for each lookback
            self._binary_counter_bits +     # binary counters
            num_lookbacks)                  # whether event matches lookbacks

  @property
  def num_classes(self):
    return self._one_hot_encoding.num_classes + len(self._lookback_distances)

  @property
  def default_event_label(self):
    return self._one_hot_encoding.encode_event(
        self._one_hot_encoding.default_event)

  def events_to_input(self, events, position):
    """Returns the input vector for the given position in the event sequence.

    Returns a self.input_size length list of floats. Assuming a one-hot
    encoding with 38 classes, two lookback distances, and five binary counters,
    self.input_size will = 121. Each index represents a different input signal
    to the model.

    Indices [0, 120]:
    [0, 37]: Event of current step.
    [38, 75]: Event of next step for first lookback.
    [76, 113]: Event of next step for second lookback.
    114: 16th note binary counter.
    115: 8th note binary counter.
    116: 4th note binary counter.
    117: Half note binary counter.
    118: Whole note binary counter.
    119: The current step is repeating (first lookback).
    120: The current step is repeating (second lookback).

    Args:
      events: A list-like sequence of events.
      position: An integer position in the event sequence.

    Returns:
      An input vector, an self.input_size length list of floats.
    """
    input_ = [0.0] * self.input_size
    offset = 0

    # Last event.
    index = self._one_hot_encoding.encode_event(events[position])
    input_[index] = 1.0
    offset += self._one_hot_encoding.num_classes

    # Next event if repeating N positions ago.
    for i, lookback_distance in enumerate(self._lookback_distances):
      lookback_position = position - lookback_distance + 1
      if lookback_position < 0:
        event = self._one_hot_encoding.default_event
      else:
        event = events[lookback_position]
      index = self._one_hot_encoding.encode_event(event)
      input_[offset + index] = 1.0
      offset += self._one_hot_encoding.num_classes

    # Binary time counter giving the metric location of the *next* event.
    n = position + 1
    for i in range(self._binary_counter_bits):
      input_[offset] = 1.0 if (n // 2 ** i) % 2 else -1.0
      offset += 1

    # Last event is repeating N bars ago.
    for i, lookback_distance in enumerate(self._lookback_distances):
      lookback_position = position - lookback_distance
      if (lookback_position >= 0 and
          events[position] == events[lookback_position]):
        input_[offset] = 1.0
      offset += 1

    assert offset == self.input_size

    return input_

  def events_to_label(self, events, position):
    """Returns the label for the given position in the event sequence.

    Returns an integer in the range [0, self.num_classes). Indices in the range
    [0, self._one_hot_encoding.num_classes) map to standard events. Indices
    self._one_hot_encoding.num_classes and self._one_hot_encoding.num_classes +
    1 are signals to repeat events from earlier in the sequence. More distant
    repeats are selected first and standard events are selected last.

    Assuming a one-hot encoding with 38 classes and two lookback distances,
    self.num_classes = 40 and the values will be as follows.

    Values [0, 39]:
      [0, 37]: Event of the last step in the event sequence, if not repeating
               any of the lookbacks.
      38: If the last event is repeating the first lookback, if not also
          repeating the second lookback.
      39: If the last event is repeating the second lookback.

    Args:
      events: A list-like sequence of events.
      position: An integer position in the event sequence.

    Returns:
      A label, an integer.
    """
    if (self._lookback_distances and
        position < self._lookback_distances[-1] and
        events[position] == self._one_hot_encoding.default_event):
      return (self._one_hot_encoding.num_classes +
              len(self._lookback_distances) - 1)

    # If last step repeated N bars ago.
    for i, lookback_distance in reversed(
        list(enumerate(self._lookback_distances))):
      lookback_position = position - lookback_distance
      if (lookback_position >= 0 and
          events[position] == events[lookback_position]):
        return self._one_hot_encoding.num_classes + i

    # If last step didn't repeat at one of the lookback positions, use the
    # specific event.
    return self._one_hot_encoding.encode_event(events[position])

  def class_index_to_event(self, class_index, events):
    """Returns the event for the given class index.

    This is the reverse process of the self.events_to_label method.

    Args:
      class_index: An int in the range [0, self.num_classes).
      events: The current event sequence.

    Returns:
      An event value.
    """
    # Repeat N bar ago.
    for i, lookback_distance in reversed(
        list(enumerate(self._lookback_distances))):
      if class_index == self._one_hot_encoding.num_classes + i:
        if len(events) < lookback_distance:
          return self._one_hot_encoding.default_event
        return events[-lookback_distance]

    # Return the event for that class index.
    return self._one_hot_encoding.decode_event(class_index)

  def labels_to_num_steps(self, labels):
    """Returns the total number of time steps for a sequence of class labels.

    This method assumes the event sequence begins with the event corresponding
    to the first label, which is inconsistent with the `encode` method in
    EventSequenceEncoderDecoder that uses the second event as the first label.
    Therefore, if the label sequence includes a lookback to the very first event
    and that event is a different number of time steps than the default event,
    this method will give an incorrect answer.

    Args:
      labels: A list-like sequence of integers in the range
          [0, self.num_classes).

    Returns:
      The total number of time steps for the label sequence, as determined by
      the one-hot encoding.
    """
    events = []
    for label in labels:
      events.append(self.class_index_to_event(label, events))
    return sum(self._one_hot_encoding.event_to_num_steps(event)
               for event in events)


class ConditionalEventSequenceEncoderDecoder(object):
  """An encoder/decoder for conditional event sequences.

  This class is similar to an EventSequenceEncoderDecoder but operates on
  *conditional* event sequences, where there is both a control event sequence
  and a target event sequence. The target sequence consists of events that are
  directly generated by the model, while the control sequence, known in advance,
  affects the inputs provided to the model. The event types of the two sequences
  can be different.

  Model inputs are determined by both control and target sequences, and are
  formed by concatenating the encoded control and target input vectors. Model
  outputs are determined by the target sequence only.

  This implementation assumes that the control event at position `i` is known
  when the target event at position `i` is to be generated.

  Properties:
    input_size: The length of the list returned by self.events_to_input.
    num_classes: The range of ints that can be returned by
        self.events_to_label.
  """

  def __init__(self, control_encoder_decoder, target_encoder_decoder):
    """Initialize a ConditionalEventSequenceEncoderDecoder object.

    Args:
      control_encoder_decoder: The EventSequenceEncoderDecoder to encode/decode
          the control sequence.
      target_encoder_decoder: The EventSequenceEncoderDecoder to encode/decode
          the target sequence.
    """
    self._control_encoder_decoder = control_encoder_decoder
    self._target_encoder_decoder = target_encoder_decoder

  @property
  def input_size(self):
    """The size of the concatenated control and target input vectors.

    Returns:
        An integer, the size of an input vector.
    """
    return (self._control_encoder_decoder.input_size +
            self._target_encoder_decoder.input_size)

  @property
  def num_classes(self):
    """The range of target labels used by this model.

    Returns:
        An integer, the range of integers that can be returned by
            self.events_to_label.
    """
    return self._target_encoder_decoder.num_classes

  @property
  def default_event_label(self):
    """The class label that represents a default target event.

    Returns:
      An integer, the class label that represents a default target event.
    """
    return self._target_encoder_decoder.default_event_label

  def events_to_input(self, control_events, target_events, position):
    """Returns the input vector for the given position in the sequence pair.

    Returns the vector formed by concatenating the input vector for the control
    sequence and the input vector for the target sequence.

    Args:
      control_events: A list-like sequence of control events.
      target_events: A list-like sequence of target events.
      position: An integer event position in the event sequences. When
          predicting the target label at position `i + 1`, the input vector is
          the concatenation of the control input vector at position `i + 1` and
          the target input vector at position `i`.

    Returns:
      An input vector, a list of floats.
    """
    return (
        self._control_encoder_decoder.events_to_input(
            control_events, position + 1) +
        self._target_encoder_decoder.events_to_input(target_events, position))

  def events_to_label(self, target_events, position):
    """Returns the label for the given position in the target event sequence.

    Args:
      target_events: A list-like sequence of target events.
      position: An integer event position in the target event sequence.

    Returns:
      A label, an integer.
    """
    return self._target_encoder_decoder.events_to_label(target_events, position)

  def class_index_to_event(self, class_index, target_events):
    """Returns the event for the given class index.

    This is the reverse process of the self.events_to_label method.

    Args:
      class_index: An integer in the range [0, self.num_classes).
      target_events: A list-like sequence of target events.

    Returns:
      A target event value.
    """
    return self._target_encoder_decoder.class_index_to_event(
        class_index, target_events)

  def labels_to_num_steps(self, labels):
    """Returns the total number of time steps for a sequence of class labels.

    Args:
      labels: A list-like sequence of integers in the range
          [0, self.num_classes).

    Returns:
      The total number of time steps for the label sequence, as determined by
      the target encoder/decoder.
    """
    return self._target_encoder_decoder.labels_to_num_steps(labels)

  def encode(self, control_events, target_events):
    """Returns a SequenceExample for the given event sequence pair.

    Args:
      control_events: A list-like sequence of control events.
      target_events: A list-like sequence of target events, the same length as
          `control_events`.

    Returns:
      A tf.train.SequenceExample containing inputs and labels.

    Raises:
      ValueError: If the control and target event sequences have different
          length.
    """
    if len(control_events) != len(target_events):
      raise ValueError('must have the same number of control and target events '
                       '(%d control events but %d target events)' % (
                           len(control_events), len(target_events)))

    inputs = []
    labels = []
    for i in range(len(target_events) - 1):
      inputs.append(self.events_to_input(control_events, target_events, i))
      labels.append(self.events_to_label(target_events, i + 1))
    return make_sequence_example(inputs, labels)

  def get_inputs_batch(self, control_event_sequences, target_event_sequences,
                       full_length=False):
    """Returns an inputs batch for the given control and target event sequences.

    Args:
      control_event_sequences: A list of list-like control event sequences.
      target_event_sequences: A list of list-like target event sequences, the
          same length as `control_event_sequences`. Each target event sequence
          must be shorter than the corresponding control event sequence.
      full_length: If True, the inputs batch will be for the full length of
          each control/target event sequence pair. If False, the inputs batch
          will only be for the last event of each target event sequence. A full-
          length inputs batch is used for the first step of extending the target
          event sequences, since the RNN cell state needs to be initialized with
          the priming target sequence. For subsequent generation steps, only a
          last-event inputs batch is used.

    Returns:
      An inputs batch. If `full_length` is True, the shape will be
      [len(target_event_sequences), len(target_event_sequences[0]), INPUT_SIZE].
      If `full_length` is False, the shape will be
      [len(target_event_sequences), 1, INPUT_SIZE].

    Raises:
      ValueError: If there are a different number of control and target event
          sequences, or if one of the control event sequences is not shorter
          than the corresponding control event sequence.
    """
    if len(control_event_sequences) != len(target_event_sequences):
      raise ValueError(
          '%d control event sequences but %d target event sequences' %
          (len(control_event_sequences, len(target_event_sequences))))

    inputs_batch = []
    for control_events, target_events in zip(
        control_event_sequences, target_event_sequences):
      if len(control_events) <= len(target_events):
        raise ValueError('control event sequence must be longer than target '
                         'event sequence (%d control events but %d target '
                         'events)' % (len(control_events), len(target_events)))
      inputs = []
      if full_length:
        for i in range(len(target_events)):
          inputs.append(self.events_to_input(control_events, target_events, i))
      else:
        inputs.append(self.events_to_input(
            control_events, target_events, len(target_events) - 1))
      inputs_batch.append(inputs)
    return inputs_batch

  def extend_event_sequences(self, target_event_sequences, softmax):
    """Extends the event sequences by sampling the softmax probabilities.

    Args:
      target_event_sequences: A list of target EventSequence objects.
      softmax: A list of softmax probability vectors. The list of softmaxes
          should be the same length as the list of event sequences.

    Returns:
      A Python list of chosen class indices, one for each target event sequence.
    """
    return self._target_encoder_decoder.extend_event_sequences(
        target_event_sequences, softmax)

  def evaluate_log_likelihood(self, target_event_sequences, softmax):
    """Evaluate the log likelihood of multiple target event sequences.

    Args:
      target_event_sequences: A list of target EventSequence objects.
      softmax: A list of softmax probability vectors. The list of softmaxes
          should be the same length as the list of target event sequences. The
          softmax vectors are assumed to have been generated by a full-length
          inputs batch.

    Returns:
      A Python list containing the log likelihood of each target event sequence.
    """
    return self._target_encoder_decoder.evaluate_log_likelihood(
        target_event_sequences, softmax)


class OptionalEventSequenceEncoder(EventSequenceEncoderDecoder):
  """An encoder that augments a base encoder with a disable flag.

  This encoder encodes event sequences consisting of tuples where the first
  element is a disable flag. When set, the encoding consists of a 1 followed by
  a zero-encoding the size of the base encoder's input. When unset, the encoding
  consists of a 0 followed by the base encoder's encoding.
  """

  def __init__(self, encoder):
    """Initialize an OptionalEventSequenceEncoder object.

    Args:
      encoder: The base EventSequenceEncoderDecoder to use.
    """
    self._encoder = encoder

  @property
  def input_size(self):
    return 1 + self._encoder.input_size

  @property
  def num_classes(self):
    raise NotImplementedError

  @property
  def default_event_label(self):
    raise NotImplementedError

  def events_to_input(self, events, position):
    # The event sequence is a list of tuples where the first element is a
    # disable flag.
    disable, _ = events[position]
    if disable:
      return [1.0] + [0.0] * self._encoder.input_size
    else:
      return [0.0] + self._encoder.events_to_input(
          [event for _, event in events], position)

  def events_to_label(self, events, position):
    raise NotImplementedError

  def class_index_to_event(self, class_index, events):
    raise NotImplementedError


class MultipleEventSequenceEncoder(EventSequenceEncoderDecoder):
  """An encoder that concatenates multiple component encoders.

  This class, largely intended for use with control sequences for conditional
  encoder/decoders, encodes event sequences with multiple encoders and
  concatenates the encodings.

  Despite being an EventSequenceEncoderDecoder this class does not decode.
  """

  def __init__(self, encoders, encode_single_sequence=False):
    """Initialize a MultipleEventSequenceEncoder object.

    Args:
      encoders: A list of component EventSequenceEncoderDecoder objects whose
          output will be concatenated.
      encode_single_sequence: If True, at encoding time all of the encoders will
          be applied to a single event sequence. If False, each event of the
          event sequence should be a tuple with size the same as the number of
          encoders, each of which will be applied to the events in the
          corresponding position in the tuple, i.e. the first encoder will be
          applied to the first element of each event tuple, the second encoder
          will be applied to the second element, etc.
    """
    self._encoders = encoders
    self._encode_single_sequence = encode_single_sequence

  @property
  def input_size(self):
    return sum(encoder.input_size for encoder in self._encoders)

  @property
  def num_classes(self):
    raise NotImplementedError

  @property
  def default_event_label(self):
    raise NotImplementedError

  def events_to_input(self, events, position):
    input_ = []
    if self._encode_single_sequence:
      # Apply all encoders to the event sequence.
      for encoder in self._encoders:
        input_ += encoder.events_to_input(events, position)
    else:
      # The event sequence is a list of tuples. Apply each encoder to the
      # elements in the corresponding tuple position.
      event_sequences = list(zip(*events))
      if len(event_sequences) != len(self._encoders):
        raise ValueError(
            'Event tuple size must be the same as the number of encoders.')
      for encoder, event_sequence in zip(self._encoders, event_sequences):
        input_ += encoder.events_to_input(event_sequence, position)
    return input_

  def events_to_label(self, events, position):
    raise NotImplementedError

  def class_index_to_event(self, class_index, events):
    raise NotImplementedError


def make_sequence_example(inputs, labels):
  """Returns a SequenceExample for the given inputs and labels.

  Args:
    inputs: A list of input vectors. Each input vector is a list of floats.
    labels: A list of ints.

  Returns:
    A tf.train.SequenceExample containing inputs and labels.
  """
  input_features = [
      tf.train.Feature(float_list=tf.train.FloatList(value=input_))
      for input_ in inputs]
  label_features = []
  for label in labels:
    if isinstance(label, numbers.Number):
      label = [label]
    label_features.append(
        tf.train.Feature(int64_list=tf.train.Int64List(value=label)))
  feature_list = {
      'inputs': tf.train.FeatureList(feature=input_features),
      'labels': tf.train.FeatureList(feature=label_features)
  }
  feature_lists = tf.train.FeatureLists(feature_list=feature_list)
  return tf.train.SequenceExample(feature_lists=feature_lists)
