# Copyright 2018 The Cirq Developers
#
# 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
#
#     https://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.

"""XmonSimulator for the Google's Xmon class quantum computers.

The simulator can be used to run all of a Circuit or to step through the
simulation Moment by Moment. The simulator requires that all gates used in
the circuit are either an XmonGate or are CompositeGate which can be
decomposed into XmonGates. Measurement gates must all have unique string keys.

A simple example:
    circuit = Circuit([Moment([X(q1), X(q2)]), Moment([CZ(q1, q2)])])
    sim = XmonSimulator()
    results = sim.run(circuit)

Note that there are two types of methods for the simulator.  "Run" methods
mimic what the quantum hardware provides, and, for example, do not give
access to the wave function.  "Simulate" methods give access to the wave
function, i.e. one can retrieve the final wave function from the simulation
via.
    final_state = sim.simulate(circuit).final_state
"""

import math
import collections
from typing import Dict, Iterable, Iterator, List, Set, Union, cast
from typing import Tuple  # pylint: disable=unused-import

import numpy as np

from cirq import ops
from cirq.circuits import Circuit
from cirq.circuits.drop_empty_moments import DropEmptyMoments
from cirq.extension import Extensions
from cirq.google import xmon_gates
from cirq.google import xmon_gate_ext
from cirq.google.convert_to_xmon_gates import ConvertToXmonGates
from cirq.google.sim import xmon_stepper
from cirq.schedules import Schedule
from cirq.study import ParamResolver, Sweep, Sweepable, TrialResult

class XmonOptions:
    """XmonOptions for the XmonSimulator.

    Attributes:
        num_prefix_qubits: Sharding of the wave function is performed over 2
            raised to this value number of qubits.
        min_qubits_before_shard: Sharding will be done only for this number
            of qubits or more. The default is 18.
        use_processes: Whether or not to use processes instead of threads.
            Processes can improve the performance slightly (varies by machine
            but on the order of 10 percent faster).  However this varies
            significantly by architecture, and processes should not be used
            for interactive use on Windows.
    """

    def __init__(self,
                 num_shards: int=None,
                 min_qubits_before_shard: int=18,
                 use_processes: bool=False) -> None:
        """XmonSimulator options constructor.

        Args:
            num_shards: sharding will be done for the greatest value of a
                power of two less than this value. If None, the default will
                be used which is the smallest power of two less than or equal
                to the number of CPUs.
            min_qubits_before_shard: Sharding will be done only for this number
                of qubits or more. The default is 18.
            use_processes: Whether or not to use processes instead of threads.
                Processes can improve the performance slightly (varies by
                machine but on the order of 10 percent faster).  However this
                varies significantly by architecture, and processes should not
                be used for interactive python use on Windows.
        """
        assert num_shards is None or num_shards > 0, (
            "Num_shards cannot be less than 1.")
        if num_shards is None:
            self.num_prefix_qubits = None
        else:
            self.num_prefix_qubits = int(math.log(num_shards, 2))

        assert min_qubits_before_shard >= 0, (
            'Min_qubit_before_shard must be positive.')
        self.min_qubits_before_shard = min_qubits_before_shard
        self.use_processes = use_processes

class XmonSimulateTrialResult:
    """Results of a simulation of the XmonSimulator.

    Unlike TrialResult these results contain the final state (wave function)
    of the system.

    Attributes:
        params: A ParamResolver of settings used for this result.
        measurements: A dictionary from measurement gate key to measurement
            results. Measurement results are a numpy ndarray of actual boolean
            measurement results (ordered by the qubits acted on by the
            measurement gate.)
        final_state: The final state (wave function) of the system after the
            trial finishes.
    """

    def __init__(self,
                 params: ParamResolver,
                 measurements: Dict[str, np.ndarray],
                 final_state: np.ndarray) -> None:
        self.params = params
        self.measurements = measurements
        self.final_state = final_state

    def __repr__(self):
        return ('XmonSimulateTrialResult(params={!r}, '
                'measurements={!r}, '
                'final_state={!r})').format(self.params,
                                            self.measurements,
                                            self.final_state)

    def __str__(self):
        def bitstring(vals):
            return ''.join('1' if v else '0' for v in vals)

        results = sorted(
            [(key, bitstring(val)) for key, val in self.measurements.items()])
        return ' '.join(
            ['{}={}'.format(key, val) for key, val in results])

class XmonSimulator:
    """XmonSimulator for Xmon class quantum circuits.

    This simulator has different methods for different types of simulations.
    For simulations that mimic the quantum hardware, the run methods are
    provided:
        run
        run_sweep
    These methods do not return or give access to the full wave function.

    To get access to the wave function during a simulation, including being
    able to set the wave function, the simulate methods are provided:
        simulate
        simulate_sweep
        simulate_moment_steps (for stepping through a circuit moment by moment)
    """

    def __init__(self, options: XmonOptions = None) -> None:
        """Construct a XmonSimulator.

        Args:
            options: XmonOptions configuring the simulation.
        """
        self.options = options or XmonOptions()

    def run(
        self,
        circuit: Circuit,
        param_resolver: ParamResolver = ParamResolver({}),
        repetitions: int = 1,
        qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT,
        extensions: Extensions = None,
    ) -> TrialResult:
        """Runs the entire supplied Circuit, mimicking the quantum hardware.

        If one wants access to the wave function (both setting and getting),
        the "simulate" methods should be used.

        The initial state of the  run methods is the all zeros state in the
        computational basis.

        Args:
            circuit: The circuit to simulate.
            param_resolver: Parameters to run with the program.
            repetitions: The number of repetitions to simulate.
            qubit_order: Determines the canonical ordering of the qubits used to
                define the order of amplitudes in the wave function.
            extensions: Extensions that will be applied while trying to
                decompose the circuit's gates into XmonGates. If None, this
                uses the default of xmon_gate_ext.

        Returns:
            TrialResult for a run.
        """
        return self.run_sweep(circuit, [param_resolver], repetitions,
                              qubit_order, extensions or xmon_gate_ext)[0]

    def run_sweep(
            self,
            program: Union[Circuit, Schedule],
            params: Sweepable = ParamResolver({}),
            repetitions: int = 1,
            qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT,
            extensions: Extensions = None
    ) -> List[TrialResult]:
        """Runs the entire supplied Circuit, mimicking the quantum hardware.

        If one wants access to the wave function (both setting and getting),
        the "simulate" methods should be used.

        The initial state of the  run methods is the all zeros state in the
        computational basis.

        Args:
            program: The circuit or schedule to simulate.
            params: Parameters to run with the program.
            repetitions: The number of repetitions to simulate.
            qubit_order: Determines the canonical ordering of the qubits used to
                define the order of amplitudes in the wave function.
            extensions: Extensions that will be applied while trying to
                decompose the circuit's gates into XmonGates. If None, this
                uses the default of xmon_gate_ext.

        Returns:
            TrialResult list for this run; one for each possible parameter
            resolver.
        """
        circuit = (
            program if isinstance(program, Circuit) else program.to_circuit())
        param_resolvers = self._to_resolvers(params or ParamResolver({}))

        trial_results = []  # type: List[TrialResult]
        qubit_order = ops.QubitOrder.as_qubit_order(qubit_order)
        for param_resolver in param_resolvers:
            xmon_circuit, keys = self._to_xmon_circuit(
                    circuit,
                    param_resolver,
                    extensions or xmon_gate_ext)
            measurements = {
                k: [] for k in keys}  # type: Dict[str, List[np.ndarray]]
            for _ in range(repetitions):
                all_step_results = _simulator_iterator(
                    xmon_circuit,
                    self.options,
                    qubit_order,
                    initial_state=0)
                for step_result in all_step_results:
                    for k, v in step_result.measurements.items():
                        measurements[k].append(np.array(v, dtype=bool))
            trial_results.append(TrialResult(
                params=param_resolver,
                repetitions=repetitions,
                measurements={k: np.array(v) for k, v in measurements.items()}
            ))
        return trial_results

    def simulate(
        self,
        circuit: Circuit,
        param_resolver: ParamResolver = ParamResolver({}),
        qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT,
        initial_state: Union[int, np.ndarray] = 0,
        extensions: Extensions = None,
    ) -> XmonSimulateTrialResult:
        """Simulates the entire supplied Circuit.

        This method returns the final wave function.

        Args:
            circuit: The circuit to simulate.
            param_resolver: Parameters to run with the program.
            qubit_order: Determines the canonical ordering of the qubits used to
                define the order of amplitudes in the wave function.
            initial_state: If an int, the state is set to the computational
                basis state corresponding to this state.
                Otherwise  if this is a np.ndarray it is the full initial
                state. In this case it must be the correct size, be normalized
                (an L2 norm of 1), and be safely castable to a np.complex64.
            extensions: Extensions that will be applied while trying to
                decompose the circuit's gates into XmonGates. If None, this
                uses the default of xmon_gate_ext.

        Returns:
            XmonSimulateTrialResults for the simulation. Includes the final
            wave function.
        """
        return self.simulate_sweep(circuit, [param_resolver], qubit_order,
                                   initial_state,
                                   extensions or xmon_gate_ext)[0]

    def simulate_sweep(
        self,
        program: Union[Circuit, Schedule],
        params: Sweepable = ParamResolver({}),
        qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT,
        initial_state: Union[int, np.ndarray] = 0,
        extensions: Extensions = None
    ) -> List[XmonSimulateTrialResult]:
        """Simulates the entire supplied Circuit.

        Args:
            program: The circuit or schedule to simulate.
            params: Parameters to run with the program.
            repetitions: The number of repetitions to simulate.
            qubit_order: Determines the canonical ordering of the qubits used to
                define the order of amplitudes in the wave function.
            initial_state: If an int, the state is set to the computational
                basis state corresponding to this state.
                Otherwise if this is a np.ndarray it is the full initial state.
                In this case it must be the correct size, be normalized (an L2
                norm of 1), and be safely castable to a np.complex64.
            extensions: Extensions that will be applied while trying to
                decompose the circuit's gates into XmonGates. If None, this
                uses the default of xmon_gate_ext.

        Returns:
            List of XmonSimulatorTrialResults for this run, one for each
            possible parameter resolver.
        """
        circuit = (
            program if isinstance(program, Circuit) else program.to_circuit())
        param_resolvers = self._to_resolvers(params or ParamResolver({}))

        trial_results = []  # type: List[XmonSimulateTrialResult]
        qubit_order = ops.QubitOrder.as_qubit_order(qubit_order)
        for param_resolver in param_resolvers:
            xmon_circuit, _ = self._to_xmon_circuit(
                circuit,
                param_resolver,
                extensions or xmon_gate_ext)
            measurements = {}  # type: Dict[str, np.ndarray]
            all_step_results = _simulator_iterator(
                xmon_circuit,
                self.options,
                qubit_order,
                initial_state)
            step_result = None
            for step_result in all_step_results:
                for k, v in step_result.measurements.items():
                    measurements[k] = np.array(v, dtype=bool)
            if step_result:
                final_state = step_result.state()
            else:
                # Empty circuit, so final state should be initial state.
                num_qubits = len(qubit_order.order_for(circuit.qubits()))
                final_state = xmon_stepper.decode_initial_state(initial_state,
                                                                num_qubits)
            trial_results.append(XmonSimulateTrialResult(
                params=param_resolver,
                measurements=measurements,
                final_state=final_state))
        return trial_results

    def simulate_moment_steps(
            self,
            program: Circuit,
            options: 'XmonOptions' = None,
            qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT,
            initial_state: Union[int, np.ndarray]=0,
            param_resolver: ParamResolver = None,
            extensions: Extensions = None) -> Iterator['XmonStepResult']:
        """Returns an iterator of XmonStepResults for each moment simulated.

        Args:
            program: The Circuit to simulate.
            options: XmonOptions configuring the simulation.
            qubit_order: Determines the canonical ordering of the qubits used to
                define the order of amplitudes in the wave function.
            initial_state: If an int, the state is set to the computational
                basis state corresponding to this state.
                Otherwise if this is a np.ndarray it is the full initial state.
                In this case it must be the correct size, be normalized (an L2
                norm of 1), and be safely castable to a np.complex64.
            param_resolver: A ParamResolver for determining values of
                Symbols.
            extensions: Extensions that will be applied while trying to
                decompose the circuit's gates into XmonGates. If None, this
                uses the default of xmon_gate_ext.

        Returns:
            SimulatorIterator that steps through the simulation, simulating
            each moment and returning a XmonStepResult for each moment.
        """
        param_resolver = param_resolver or ParamResolver({})
        xmon_circuit, _ = self._to_xmon_circuit(program,
                                                param_resolver,
                                                extensions or xmon_gate_ext)
        return _simulator_iterator(xmon_circuit,
                                   options or XmonOptions(),
                                   qubit_order,
                                   initial_state)

    def _to_resolvers(self, sweepable: Sweepable) -> List[ParamResolver]:
        if isinstance(sweepable, ParamResolver):
            return [sweepable]
        elif isinstance(sweepable, Sweep):
            return list(sweepable)
        elif isinstance(sweepable, collections.Iterable):
            iterable = cast(collections.Iterable, sweepable)
            return list(iterable) if isinstance(next(iter(iterable)),
                                                ParamResolver) else sum(
                [list(s) for s in iterable], [])
        raise TypeError('Unexpected Sweepable type')

    def _to_xmon_circuit(self, circuit: Circuit,
                         param_resolver: ParamResolver,
                         extensions: Extensions = None
                         ) -> Tuple[Circuit, Set[str]]:
        # TODO: Use one optimization pass.
        xmon_circuit = self._to_circuit_with_parameters_resolved(
                circuit, param_resolver)
        ConvertToXmonGates(extensions).optimize_circuit(xmon_circuit)
        DropEmptyMoments().optimize_circuit(xmon_circuit)
        keys = find_measurement_keys(xmon_circuit)
        return xmon_circuit, keys

    def _to_circuit_with_parameters_resolved(self, circuit: Circuit,
                                             param_resolver: ParamResolver
                                             ) -> Circuit:
        resolved_circuit = Circuit()
        for moment in circuit.moments:
            resolved_circuit.append(
                    self._to_operations_with_parameters_resolved(
                        moment.operations, param_resolver))
        return resolved_circuit

    def _to_operations_with_parameters_resolved(
            self,
            operations: Iterable[ops.Operation],
            param_resolver: ParamResolver
            ) -> List[ops.Operation]:
        resolved_operations = []
        for op in operations:
            gate, qubits = op.gate, op.qubits
            if (isinstance(gate, ops.ParameterizableGate) and
                    gate.is_parameterized()):
                gate = gate.with_parameters_resolved_by(param_resolver)
            resolved_op = ops.Operation(gate, qubits)
            resolved_operations.append(resolved_op)
        return resolved_operations

def _simulator_iterator(
        circuit: Circuit,
        options: 'XmonOptions' = XmonOptions(),
        qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT,
        initial_state: Union[int, np.ndarray]=0
) -> Iterator['XmonStepResult']:
    """Iterator over XmonStepResult from Moments of a Circuit.

    This should rarely be instantiated directly, instead prefer to create an
    XmonSimulator and use methods on that object to get an iterator.

    Args:
        circuit: The circuit to simulate. Must contain only xmon gates with no
            unresolved parameters.
        options: XmonOptions configuring the simulation.
        qubit_order: Determines the canonical ordering of the qubits used to
            define the order of amplitudes in the wave function.
        initial_state: If this is an int, the state is set to the computational
            basis state corresponding to the integer. Note that
            the low bit of the integer corresponds to the value of the first
            qubit as determined by the basis argument.

            If this is a np.ndarray it is the full initial state.
            In this case it must be the correct size, be normalized (an L2
            norm of 1), and be safely castable to a np.complex64.

    Yields:
        StepResults from simulating a Moment of the Circuit.

    Raises:
        TypeError: if the circuit contains gates that are not XmonGates or
            composite gates made of XmonGates.
    """
    qubits = ops.QubitOrder.as_qubit_order(qubit_order).order_for(
        circuit.qubits())
    qubit_map = {q: i for i, q in enumerate(reversed(qubits))}
    if isinstance(initial_state, np.ndarray):
        initial_state = initial_state.astype(dtype=np.complex64,
                                             casting='safe')

    with xmon_stepper.Stepper(
            num_qubits=len(qubits),
            num_prefix_qubits=options.num_prefix_qubits,
            initial_state=initial_state,
            min_qubits_before_shard=options.min_qubits_before_shard,
            use_processes=options.use_processes
    ) as stepper:
        for moment in circuit.moments:
            measurements = collections.defaultdict(
                list)  # type: Dict[str, List[bool]]
            phase_map = {}  # type: Dict[Tuple[int, ...], float]
            for op in moment.operations:
                gate = op.gate
                if isinstance(gate, xmon_gates.ExpZGate):
                    index = qubit_map[op.qubits[0]]
                    phase_map[(index,)] = cast(float, gate.half_turns)
                elif isinstance(gate, xmon_gates.Exp11Gate):
                    index0 = qubit_map[op.qubits[0]]
                    index1 = qubit_map[op.qubits[1]]
                    phase_map[(index0, index1)] = cast(float, gate.half_turns)
                elif isinstance(gate, xmon_gates.ExpWGate):
                    index = qubit_map[op.qubits[0]]
                    stepper.simulate_w(
                        index=index,
                        half_turns=gate.half_turns,
                        axis_half_turns=gate.axis_half_turns)
                elif isinstance(gate, xmon_gates.XmonMeasurementGate):
                    invert_mask = gate.invert_mask or len(op.qubits) * (False,)
                    for qubit, invert in zip(op.qubits, invert_mask):
                        index = qubit_map[qubit]
                        result = stepper.simulate_measurement(index)
                        if invert:
                            result = not result
                        measurements[cast(str, gate.key)].append(result)
                else:
                    raise TypeError('{!r} is not supported by the '
                                    'xmon simulator.'.format(gate))
            stepper.simulate_phases(phase_map)
            yield XmonStepResult(stepper, qubit_map, measurements)

def find_measurement_keys(circuit: Circuit) -> Set[str]:
    keys = set()  # type: Set[str]
    for moment in circuit.moments:
        for op in moment.operations:
            if isinstance(op.gate, xmon_gates.XmonMeasurementGate):
                key = cast(str, op.gate.key)
                if key in keys:
                    raise ValueError('Repeated Measurement key {}'.format(key))
                keys.add(key)
    return keys

class XmonStepResult:
    """Results of a step of the simulator.

    Attributes:
        qubit_map: A map from the Qubits in the Circuit to the the index
            of this qubit for a canonical ordering. This canonical ordering is
            used to define the state (see the state() method).
        measurements: A dictionary from measurement gate key to measurement
            results, ordered by the qubits that the measurement operates on.
    """

    def __init__(
            self,
            stepper: xmon_stepper.Stepper,
            qubit_map: Dict,
            measurements: Dict[str, List[bool]]) -> None:
        self.qubit_map = qubit_map or {}
        self.measurements = measurements or collections.defaultdict(list)
        self._stepper = stepper

    def state(self) -> np.ndarray:
        """Return the state (wave function) at this point in the computation.

        The state is returned in the computational basis with these basis
        states defined by the qubit_map. In particular the value in the
        qubit_map is the index of the qubit, and these are translated into
        binary vectors where the last qubit is the 1s bit of the index, the
        second-to-last is the 2s bit of the index, and so forth (i.e. big
        endian ordering).

        Example:
             qubit_map: {QubitA: 0, QubitB: 1, QubitC: 2}
             Then the returned vector will have indices mapped to qubit basis
             states like the following table
               |   | QubitA | QubitB | QubitC |
               +---+--------+--------+--------+
               | 0 |   0    |   0    |   0    |
               | 1 |   0    |   0    |   1    |
               | 2 |   0    |   1    |   0    |
               | 3 |   0    |   1    |   1    |
               | 4 |   1    |   0    |   0    |
               | 5 |   1    |   0    |   1    |
               | 6 |   1    |   1    |   0    |
               | 7 |   1    |   1    |   1    |
               +---+--------+--------+--------+
        """
        return self._stepper.current_state

    def set_state(self, state: Union[int, np.ndarray]):
        """Updates the state of the simulator to the given new state.

        Args:
            state: If this is an int, then this is the state to reset
            the stepper to, expressed as an integer of the computational basis.
            Integer to bitwise indices is little endian. Otherwise if this is
            a np.ndarray this must be the correct size and have dtype of
            np.complex64.

        Raises:
            ValueError if the state is incorrectly sized or not of the correct
            dtype.
        """
        self._stepper.reset_state(state)


#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @File  : dueling_brain.py
# @Author: zixiao
# @Date  : 2019-04-01
# @Desc  :
import os

import numpy as np
import torch
import torch.nn as nn
from torch.autograd import Variable

from dueling.memory import Memory
from dueling.model import DuelingCNN as CNN

save_q_eval_path = 'dueling/save_pkl/q_eval.pkl'
save_q_next_path = 'dueling/save_pkl/q_next.pkl'

USE_CUDA = torch.cuda.is_available()
if torch.cuda.is_available():
    dtype = torch.cuda.FloatTensor
else:
    dtype = torch.FloatTensor

class AbsErrorLoss(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, q_target, q_eval):
        return torch.sum(torch.abs(q_target - q_target), dim=1)

class Loss(nn.Module):
    def __init__(self):
        super().__init__()
        self.loss = nn.MSELoss(reduce=False, reduction='none')

    def forward(self, IS_weights, q_target, q_eval):
        x = self.loss(q_target, q_eval)
        return torch.mean(IS_weights * x)

class Variable(Variable):
    def __init__(self, data, *args, **kwargs):
        if USE_CUDA:
            data = data.cuda()
        super(Variable, self).__init__(data, *args, **kwargs)

class Brain:
    def __init__(self,
                 memory_size,
                 input_args,
                 num_actions,
                 shape,
                 learning_rate,
                 reward_decay,
                 e_greedy,
                 e_greedy_increment,
                 e_greedy_start,
                 batch_size,
                 replace_target_iter):
        self.q_eval = CNN(in_channels=input_args, num_action=num_actions).type(dtype)
        self.q_next = CNN(in_channels=input_args, num_action=num_actions).type(dtype)
        if os.path.isfile(save_q_eval_path):
            print('load q_eval ...')
            self.q_eval.load_state_dict(torch.load(save_q_eval_path))
        if os.path.isfile(save_q_next_path):
            print('load q_next ...')
            self.q_next.load_state_dict(torch.load(save_q_next_path))
        self.memory = Memory(size=memory_size, frame_len=input_args, w=shape[0], h=shape[1])
        self.channels = input_args
        self.num_action = num_actions
        self.learning_rate = learning_rate
        self.gamma = reward_decay
        self.batch_size = batch_size
        self.replace_target_iter = replace_target_iter
        self.epsilon_max = e_greedy
        self.epsilon_increment = e_greedy_increment
        self.epsilon = e_greedy_start
        self.learn_step_count = 0

        self.op = torch.optim.Adam(self.q_eval.parameters(), lr=learning_rate)  # optimize all cnn parameters
        self.loss_func = Loss()
        self.abs_errors_func = AbsErrorLoss()
        # self.loss_func = nn.CrossEntropyLoss()
        self.save_step = 10000
        self.learn_step = 0

    def choose_action(self, obs):
        if np.random.uniform() < self.epsilon and obs.shape[0] == self.channels:
            obs = torch.FloatTensor(obs)
            obs = obs.unsqueeze(0).type(dtype)
            out = self.q_eval(obs / 255.0)
            return np.argmax(out.detach().cpu()).item()
        return np.random.randint(0, self.num_action - 1)

    def store_transition(self, action, reward, obs_):
        self.memory.store_transition(action, reward, obs_)

    def learn(self):
        if self.learn_step_count == self.replace_target_iter:
            self.learn_step_count = 0
            self.q_next.load_state_dict(self.q_eval.state_dict())
        if self.learn_step == self.save_step:
            torch.save(self.q_eval.state_dict(), save_q_eval_path)
            torch.save(self.q_next.state_dict(), save_q_next_path)
            self.learn_step = 0
        batch_leaf_index, IS_weights, batch_obs, batch_action, batch_reward, batch_obs_ = self.memory.get_memory(
            self.batch_size)
        batch_obs = Variable(torch.from_numpy(batch_obs).type(dtype))
        batch_obs_ = Variable(torch.from_numpy(batch_obs_).type(dtype))
        IS_weights = Variable(torch.from_numpy(IS_weights).type(dtype))
        q_next = self.q_next(batch_obs_ / 255.0)
        q_eval = self.q_eval(batch_obs / 255.0)
        reward_batch = torch.from_numpy(batch_reward)
        if USE_CUDA:
            # act_batch = act_batch.cuda()
            reward_batch = reward_batch.cuda()

        q_target = q_eval.clone().detach()
        batch_index = np.arange(self.batch_size)
        q_target[batch_index, batch_action] = reward_batch.float() + self.gamma * torch.max(q_next, dim=1)[0]
        loss = self.loss_func(IS_weights, q_eval, q_target)
        abs_error_loss = self.abs_errors_func(q_target, q_eval)
        self.memory.batch_update(batch_leaf_index, abs_error_loss.detach().cpu())
        self.op.zero_grad()
        loss.backward()
        self.op.step()

        self.epsilon = self.epsilon + self.epsilon_increment if self.epsilon < self.epsilon_max else self.epsilon_max
        self.learn_step_count += 1
        self.learn_step += 1

    def store_start_frame(self, obs):
        self.memory.store_frame(obs)

    def get_last_memory(self):
        return self.memory.get_last_frame()

    def double_learn(self):
        # print('\r', self.learn_step_count, end='')
        if self.learn_step_count == self.replace_target_iter:
            self.learn_step_count = 0
            self.q_next.load_state_dict(self.q_eval.state_dict())
        if self.learn_step == self.save_step:
            torch.save(self.q_eval.state_dict(), save_q_eval_path)
            torch.save(self.q_next.state_dict(), save_q_next_path)
            self.learn_step = 0
        batch_leaf_index, IS_weights, batch_obs, batch_action, batch_reward, batch_obs_ = self.memory.get_memory(
            self.batch_size)
        batch_obs = Variable(torch.from_numpy(batch_obs).type(dtype))
        batch_obs_ = Variable(torch.from_numpy(batch_obs_).type(dtype))
        IS_weights = Variable(torch.from_numpy(IS_weights).type(dtype))
        q_next = self.q_next(batch_obs_ / 255.0)
        q_eval_next = self.q_eval(batch_obs_ / 255.0)
        q_eval = self.q_eval(batch_obs / 255.0)
        reward_batch = torch.from_numpy(batch_reward)
        if USE_CUDA:
            # act_batch = act_batch.cuda()
            reward_batch = reward_batch.cuda()

        q_target = q_eval.clone().detach()
        batch_index = np.arange(self.batch_size)
        max_act_q_eval_next = torch.argmax(q_eval_next, dim=1)
        # print(max_act_q_eval_next)
        select_q_next = q_next[batch_index, max_act_q_eval_next]
        q_target[batch_index, batch_action] = reward_batch.float() + self.gamma * select_q_next
        loss = self.loss_func(IS_weights, q_eval, q_target)
        abs_error_loss = self.abs_errors_func(q_target, q_eval)
        self.memory.batch_update(batch_leaf_index, abs_error_loss.detach().cpu())
        self.op.zero_grad()
        loss.backward()
        self.op.step()

        self.epsilon = self.epsilon + self.epsilon_increment if self.epsilon < self.epsilon_max else self.epsilon_max
        self.learn_step_count += 1
        self.learn_step += 1


import sys
import fileinput
import argparse
import time
import itertools
import pickle
import random
import codecs
from collections import defaultdict
from sklearn import svm
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction import DictVectorizer
from scipy.sparse import coo_matrix, hstack, vstack
import numpy as np

from ClassifierStuff import *
from SentenceModel import *

def findEventTrigger(sentenceData,triggerid):
	for sentenceid, sentence in enumerate(sentenceData):
		if triggerid in sentence.predictedEntityLocs:
			return sentenceid,sentence.predictedEntityLocs[triggerid]
	raise RuntimeError('Unable to find location of event trigger ID ('+triggerid+') in sentences')

def findArgumentTrigger(sentenceData,triggerid):
	for sentenceid, sentence in enumerate(sentenceData):
		if triggerid in sentence.knownEntityLocs:
			return sentenceid,sentence.knownEntityLocs[triggerid]
	raise RuntimeError('Unable to find location of argument trigger ID ('+triggerid+') in sentences')

def findTrigger(sentenceData,triggerid):
	for sentenceid, sentence in enumerate(sentenceData):
		if triggerid in sentence.predictedEntityLocs:
			return sentenceid,sentence.predictedEntityLocs[triggerid]
		if triggerid in sentence.knownEntityLocs:
			return sentenceid,sentence.knownEntityLocs[triggerid]
	raise RuntimeError('Unable to find location of trigger ID ('+triggerid+') in sentences')

def generateModificationExamples(sentenceAndEventData,targetModifications={}):

	examples = []
	classes = []

	# Let's first try to learn the event triggers (ignore this for trigger-less events)
	for filename in sentenceAndEventData:

		(sentenceData,relations,modifiers) = sentenceAndEventData[filename]

		positiveCases = {}
		for modifierid,(modifiertype,entityid) in modifiers.iteritems():
			if not entityid[0] == 'E':
				print "WARNING: Found modifier on non event denotations"
				print modifierid,modifiertype,entityid
				continue

			#sentenceid,locs = findEventTrigger(entityid)
			#positiveCases.add((sentenceid,locs))
			if modifiertype in targetModifications:
				positiveCases[entityid] = targetModifications[modifiertype]

		positiveCasesProcessed = []
		# Now we go through all sentences and create examples for all possible token combinations
		# Then check if any are already marked as positive and add to the appropriate list of examples
		for sentenceid,sentence in enumerate(sentenceData):
			tokenCount = len(sentence.tokens)
			for eventid in sentence.predictedEntityLocs:
				locs = sentence.predictedEntityLocs[eventid]
				type = sentence.predictedEntityTypes[eventid]

				example = Example(filename, sentenceData, arg1_sentenceid=sentenceid, arg1_locs=locs)
				examples.append(example)

				exampleClass = 0

				key = eventid
				if key in positiveCases:
					exampleClass = positiveCases[key]
					positiveCasesProcessed.append(key)
					#print "WOOOOO!!!! Positive added"
				classes.append(exampleClass)

			#print filename
		for key in positiveCases:
			if not key in positiveCasesProcessed:
				raise RuntimeError( 'WARNING: Unprocessed modification found: %s in file: %s' % (str(key), filename) )

	#for c,e in zip(classes,examples):
	#	print c,e
	#sys.exit(0)

	return classes, examples

def createModificationClassifier(sentenceAndEventData,targetModifications,parameters=None):
	classes,examples = generateModificationExamples(sentenceAndEventData,targetModifications)
	assert min(classes) == 0, "Expecting negative cases in modification examples"
	assert len(classes) > 0, "Expecting greater than zero classes & examples"

	if max(classes) == 0:
		print "WARNING: No modifications found. Are you sure that all events are triggerless?"
		return None,None,None,None

	vectors,vectorizer,featureSelector = buildVectorizer(classes,examples,parameters)
	classifier = buildClassifierFromVectors(classes,vectors,parameters)
	data = (classes,examples,vectors)
	return data,vectorizer,featureSelector,classifier

def saveCOOMatrixToFile(matrix,filename):
	with open(filename,'w') as f:
		f.write("%%sparse\t%d\t%d\n" % (matrix.shape[0],matrix.shape[1]))
		for row,col,data in zip(matrix.row,matrix.col,matrix.data):
			line = "%d\t%d\t%f" % (row,col,data)
			f.write(line + "\n")

def loadCOOMatrixFromFile(filename):
	rows,cols = -1,-1
	data = []
	matrix = None
	with open(filename,'r') as f:
		header = True
		for line in f:
			if header:
				_,r,c = line.split("\t")
				rows,cols = int(r),int(c)
				header = False
			else:
				x,y,value = line.split("\t")
				x,y,value = int(x),int(y),float(value)
				data.append((x,y,value))

	xs = [ x for x,y,value in data ]
	ys = [ y for x,y,value in data ]
	vals = [ value for x,y,value in data ]

	matrix = coo_matrix((vals,(xs,ys)),shape=(rows,cols))
	return matrix

def loadNumpyArrayFromFile(filename):
	return np.loadtxt(filename,comments='%')

def loadMatrixFromFile(filename):
	with open(filename,'r') as f:
		header = f.readline().strip().split("\t")
	type,dim1,dim2 = header
	#dim1,dim2 = int(dim1),int(dim2)
	print filename, type
	if type == "%sparse":
		return loadCOOMatrixFromFile(filename)
	elif type == "%dense":
		return loadNumpyArrayFromFile(filename)
	else:
		raise RuntimeError("Unknown type of matrix: %s" % type)

# It's the main bit. Yay!
if __name__ == "__main__":
	parser = argparse.ArgumentParser(description='VERSE Modification Extraction tool')

	parser.add_argument('--trainingFile', required=True, type=str, help='Parsed-text file containing the training data')
	parser.add_argument('--testingFile', required=True, type=str, help='Parsed-text file containing the test data to predict modifications for')
	parser.add_argument('--modificationDescriptions', required=True, type=str, help='Description file containing list of modification types to predict')
	parser.add_argument('--mergeWithExisting', action='store_true',  help='Whether to keep existing modifications in testingFile')
	parser.add_argument('--parameters', type=str, help='Parameters to use for feature construction, selection and classification')
	parser.add_argument('--outFile', type=str, help='Output filename for data with predicted modifications')
	args = parser.parse_args()

	parameters = {}
	if args.parameters:
		for arg in args.parameters.split(';'):
			name,value = arg.strip().split(":")
			parameters[name.strip()] = value.strip()

	sentenceRange = 0
	if "sentenceRange" in parameters:
		sentenceRange = int(parameters["sentenceRange"])

	trainFilename = args.trainingFile
	with open(trainFilename, 'r') as f:
		trainingSentenceAndEventData = pickle.load(f)
	print "Loaded " + trainFilename

	targetModifications = []
	with open(args.modificationDescriptions,'r') as f:
		for line in f:
			targetModifications.append(line.strip())

	targetModificationsToIDs = { event:i+1 for i,event in enumerate(targetModifications) }

	print "-"*30
	for i,targetModification in enumerate(targetModifications):
		print i, targetModification
	print "-"*30

	trigVec,trigFS,trigClf = None,None,None
	_,trigVec,trigFS,trigClf = createModificationClassifier(trainingSentenceAndEventData,targetModificationsToIDs,parameters)

	with open(args.testingFile, 'r') as f:
		testingSentenceAndEventData = pickle.load(f)
	print "Loaded " + args.testingFile

	if not args.mergeWithExisting:
		print "Blanking test data..."
		for filename in testingSentenceAndEventData:
			(sentenceData,relations,modifications) = testingSentenceAndEventData[filename]
			modifications = {}
			testingSentenceAndEventData[filename] = (sentenceData,relations,modifications)

	potentialEvents = {}

	print "generateTriggerExamples..."
	_,tExamples = generateModificationExamples(testingSentenceAndEventData,targetModificationsToIDs)

	assert len(tExamples) > 0, "Expecting greater than zero examples from the test data"

	print "vectorize, trim and predict..."
	tVectors = trigVec.vectorize(tExamples)
	if trigFS is None:
		tPredictions = trigClf.predict(tVectors)
	else:
		tTrimmed = trigFS.transform(tVectors)
		tPredictions = trigClf.predict(tTrimmed)

	predictedModificationID = 1

	potentialEventsArgs = defaultdict(list)

	print tPredictions
	print "insert modification predictions into sentence data..."
	for p,example in zip(tPredictions,tExamples):
		if p != 0:
			modType = targetModifications[p-1]

			sentenceFilename = example.filename
			sentenceID,locs = example.arguments[0]

			sentence = testingSentenceAndEventData[sentenceFilename][0][sentenceID]
			modifications = testingSentenceAndEventData[sentenceFilename][2]

			entityID = None
			for tmpID,tmpLocs in sentence.predictedEntityLocs.iteritems():
				if tmpLocs == locs:
					entityID = tmpID
					break

			assert not entityID is None

			triggerIDTxt = "M%d" % predictedModificationID
			predictedModificationID += 1

			print "ADDING", triggerIDTxt, modType, entityID
			modifications[triggerIDTxt] = (modType,entityID)

	with open(args.outFile, 'w') as f:
		pickle.dump(testingSentenceAndEventData,f)

	print "Complete."


import copy
import numpy as np
from Classes.TransectData import adjusted_ensemble_duration
from Classes.TransectData import TransectData
from Classes.QComp import QComp
from Classes.MatSonTek import MatSonTek
from MiscLibs.common_functions import cart2pol, sind, pol2cart, rad2azdeg

class MovingBedTests(object):
    """Stores and processes moving-bed tests.

    Attributes
    ----------
    type: str
        Loop or Stationary
    transect: TransectData
        Object of TransectData
    duration_sec: float
        Duration of test, in secs
    percent_invalid_bt: float
        Percent of invalid bottom track
    compass_diff_deg: float
        Difference in heading for out and back of loop
    flow_dir: float
        Mean flow direction from loop test
    mb_dir: float
        Moving bed or closure error direction
    dist_us_m: float
        Distance moved upstream, in m
    flow_spd_mps: float
        Magnitude of water velocity, in mps
    mb_spd_mps: float
        Magnitude of moving=bed velocity, in mps
    percent_mb: float
        Potential error due to moving bed, in percent
    moving_bed: str
        Moving-bed determined ("Yes" or "No")
    user_valid: bool
        Boolean to allow user to determine if test should be considered a valid test (True or False)
    test_quality: str
        Quality of test, 'Valid' 'Warnings' 'Errors'
    use_2_correct: bool
        Use this test to correct discharge (True or False)
    selected: bool
        Selected as valid moving-bed test to use for correction or determine moving-bed condition
    messages: list
        List of strings for warning and error messages based on data processing
    near_bed_speed_mps: float
        Mean near-bed water speed for test, in mps
    stationary_us_track: np.array(float)
        Upstream component of the bottom track referenced ship track
    stationary_cs_track: np.array(float)
        Cross=stream component of the bottom track referenced ship track
    stationary_mb_vel: np.array(float)
        Moving-bed velocity by ensemble, m/s
    ref: str
        Identifies reference used to compute moving bed
    bt_percent_mb: float
        Percent moving-bed using only BT
    bt_dist_us_m: float
        Distance upstream using only BT
    bt_mb_dir: float
        Moving-bed direction using only BT
    bt_mb_spd_mps: float
        Moving-bed speed using only BT
    bt_flow_spd_mps: float
        Corrected flow speed using only BT
    gps_percent_mb: float
        Percent moving-bed using BT and GPS
    gps_dist_us_m: float
        Distance upstream using BT and GPS
    gps_mb_dir: float
        Moving-bed direction using BT and GPS
    gps_mb_spd_mps: float
        Moving-bed speed using BT and GPS
    gps_flow_spd_mps: float
        Corrected flow speed using BT and GPS
    """

    def __init__(self):
        """Initialize class and instance variables."""

        self.type = None  # Loop or Stationary
        self.transect = None  # Object of TransectData
        self.duration_sec = np.nan  # Duration of test in secs
        self.percent_invalid_bt = np.nan  # Percent of invalid bottom track
        self.compass_diff_deg = np.nan  # Difference in heading for out and back of loop
        self.flow_dir = np.nan  # Mean flow direction from loop test
        self.mb_dir = np.nan  # Moving bed or closure error direction
        self.dist_us_m = np.nan  # Distance moved upstream in m
        self.flow_spd_mps = np.nan  # Magnitude of water velocity in mps
        self.mb_spd_mps = np.nan  # Magnitude of moving=bed velocity in mps
        self.percent_mb = np.nan  # Potential error due to moving bed in percent
        self.moving_bed = np.nan  # Moving-bed determined 'Yes' 'No'
        self.user_valid = True  # Logical to allow user to determine if test should be considered a valid test
        self.test_quality = None  # Quality of test 'Valid' 'Warnings' 'Errors'
        self.use_2_correct = None  # Use this test to correct discharge
        self.selected = None  # Selected valid moving-bed test to use for correction or determine moving-bed condition
        self.messages = None  # Cell array of warning and error messages based on data processing
        self.near_bed_speed_mps = np.nan  # Mean near-bed water speed for test in mps
        self.stationary_us_track = np.array([])  # Upstream component of the bottom track referenced ship track
        self.stationary_cs_track = np.array([])  # Cross=stream component of the bottom track referenced ship track
        self.stationary_mb_vel = np.array([])  # Moving-bed velocity by ensemble
        self.ref = 'BT'
        self.bt_percent_mb = np.nan
        self.bt_dist_us_m = np.nan
        self.bt_mb_dir = np.nan
        self.bt_mb_spd_mps = np.nan
        self.bt_flow_spd_mps = np.nan
        self.gps_percent_mb = np.nan
        self.gps_dist_us_m = np.nan
        self.gps_mb_dir = np.nan
        self.gps_mb_spd_mps = np.nan
        self.gps_flow_spd_mps = np.nan

    def populate_data(self, source, file=None, test_type=None):
        """Process and store moving-bed test data.

        Parameters
        ----------
        source: str
            Manufacturer of ADCP, SonTek or TRDI
        file: TransectData or str
            Object of TransectData for TRDI and str of filename for SonTek
        test_type: str
            Type of moving-bed test (Loop or Stationary)
        """

        if source == 'TRDI':
            self.mb_trdi(file, test_type)
        else:
            self.mb_sontek(file, test_type)

        self.process_mb_test(source)

    def process_mb_test(self, source):

        # Convert to earth coordinates and set the navigation reference to BT
        # for both boat and water data
        self.transect.boat_vel.bt_vel.apply_interpolation(transect=self.transect, interpolation_method='Linear')
        self.transect.change_coord_sys(new_coord_sys='Earth')
        self.transect.change_nav_reference(update=True, new_nav_ref='BT')

        # Adjust data for default manufacturer specific handling of invalid data
        delta_t = adjusted_ensemble_duration(self.transect, 'mbt')

        if self.type == 'Loop':
            if source == 'TRDI':
                self.loop_test(delta_t)
            else:
                self.loop_test()
        elif self.type == 'Stationary':
            self.stationary_test()
        else:
            raise ValueError('Invalid moving-bed test identifier specified.')

    @staticmethod
    def qrev_mat_in(meas_struct):
        """Processes the Matlab data structure to obtain a list of TransectData objects containing transect
           data from the Matlab data structure.

       Parameters
       ----------
       meas_struct: mat_struct
           Matlab data structure obtained from sio.loadmat

       Returns
       -------
       mb_tests: list
           List of MovingBedTests objects
       """

        mb_tests = []
        if hasattr(meas_struct, 'mbTests'):
            try:
                # If there are multiple test the Matlab structure will be an array
                if type(meas_struct.mbTests) == np.ndarray:
                    for test in meas_struct.mbTests:
                        temp = MovingBedTests()
                        temp.populate_from_qrev_mat(test)
                        mb_tests.append(temp)
                # If only one test, that test is not stored in an array
                else:
                    temp = MovingBedTests()
                    temp.populate_from_qrev_mat(meas_struct.mbTests)
                    mb_tests.append(temp)
            except (TypeError, AttributeError):
                pass
        return mb_tests

    def populate_from_qrev_mat(self, mat_data):
        """Populates the object using data from previously saved QRev Matlab file.

        Parameters
        ----------
        mat_data: mat_struct
           Matlab data structure obtained from sio.loadmat
        """

        self.type = mat_data.type
        self.transect = TransectData()
        self.transect.populate_from_qrev_mat(mat_data.transect)
        self.duration_sec = mat_data.duration_sec
        self.percent_invalid_bt = mat_data.percentInvalidBT

        # Handle situation for one or more tests
        if type(mat_data.compassDiff_deg) is float:
            self.compass_diff_deg = mat_data.compassDiff_deg
        else:
            self.compass_diff_deg = self.make_list(mat_data.compassDiff_deg)

        # Handle situation for one or more tests
        if type(mat_data.flowDir_deg) is float:
            self.flow_dir = mat_data.flowDir_deg
        else:
            self.flow_dir = self.make_list(mat_data.flowDir_deg)

        # Handle situation for one or more tests
        if type(mat_data.mbDir_deg) is float:
            self.mb_dir = mat_data.mbDir_deg
        else:
            self.mb_dir = self.make_list(mat_data.mbDir_deg)

        self.dist_us_m = mat_data.distUS_m
        self.flow_spd_mps = mat_data.flowSpd_mps
        self.mb_spd_mps = mat_data.mbSpd_mps
        self.percent_mb = mat_data.percentMB
        self.moving_bed = mat_data.movingBed
        self.user_valid = bool(mat_data.userValid)
        self.test_quality = mat_data.testQuality
        self.use_2_correct = bool(mat_data.use2Correct)
        self.selected = bool(mat_data.selected)

        # Handle situation for one or more messages
        if type(mat_data.messages) == np.ndarray:
            self.messages = mat_data.messages.tolist()
        else:
            self.messages = [mat_data.messages]

        # Handle situation for one or more tests
        if type(mat_data.nearBedSpeed_mps) is np.ndarray:
            self.near_bed_speed_mps = np.nan
        else:
            self.near_bed_speed_mps = mat_data.nearBedSpeed_mps

        self.stationary_us_track = mat_data.stationaryUSTrack
        self.stationary_cs_track = mat_data.stationaryCSTrack
        self.stationary_mb_vel = mat_data.stationaryMBVel

        # Feature that can use GPS for moving-bed tests
        if hasattr(mat_data, 'bt_percent_mb'):
            self.bt_percent_mb = mat_data.bt_percent_mb
            self.bt_dist_us_m = mat_data.bt_dist_us_m
            self.bt_mb_dir = mat_data.bt_mb_dir
            self.bt_mb_spd_mps = mat_data.bt_mb_spd_mps
            self.bt_flow_spd_mps = mat_data.bt_flow_spd_mps
            self.gps_percent_mb = mat_data.gps_percent_mb
            self.gps_dist_us_m = mat_data.gps_dist_us_m
            self.gps_mb_dir = mat_data.gps_mb_dir
            self.gps_mb_spd_mps = mat_data.gps_mb_spd_mps
        else:
            self.bt_percent_mb = self.percent_mb
            self.bt_dist_us_m = self.dist_us_m
            self.bt_mb_dir = self.mb_dir
            self.bt_mb_spd_mps = self.mb_spd_mps
            self.bt_flow_spd_mps = self.flow_spd_mps
            self.compute_mb_gps()

    @staticmethod
    def make_list(array_in):
        """Method to make list from several special cases that can occur in the Matlab data.

        Parameters
        ----------
        array_in: np.ndarray
            Input that needs to be convert to a list
        """

        # This traps messages with the associated codes
        if array_in.size > 3:
            list_out = array_in.tolist()
        else:
            # Create a list of lists
            temp = array_in.tolist()
            if len(temp) > 0:
                internal_list = []
                for item in temp:
                    internal_list.append(item)
                list_out = [internal_list]
            else:
                list_out = np.nan
        return list_out

    def mb_trdi(self, transect, test_type):
        """Function to create object properties for TRDI moving-bed tests

        Parameters
        ----------
        transect: TransectData
            Object of TransectData
        test_type: str
            Type of moving-bed test."""

        self.transect = transect
        self.user_valid = True
        self.type = test_type

    def mb_sontek(self, file_name, test_type):
        """Function to create object properties for SonTek moving-bed tests

        Parameters
        ----------
        file_name: str
            Name of moving-bed test data file
        test_type: str
            Type of moving-bed test."""
        self.type = test_type

        # Read Matlab file for moving-bed test
        rsdata = MatSonTek(file_name)

        # Create transect objects for each discharge transect
        self.transect = TransectData()
        self.transect.sontek(rsdata, file_name)

    def loop_test(self, ens_duration=None, ref='BT'):
        """Process loop moving bed test.

        Parameters
        ----------
        ens_duration: np.array(float)
            Duration of each ensemble, in sec
        ref: str
            Reference used to compare distance moved
        """

        # Assign data from transect to local variables
        self.transect.boat_interpolations(update=False, target='BT', method='Linear')
        self.transect.boat_interpolations(update=False, target='GPS', method='Linear')
        trans_data = copy.deepcopy(self.transect)
        in_transect_idx = trans_data.in_transect_idx
        n_ensembles = len(in_transect_idx)
        bt_valid = trans_data.boat_vel.bt_vel.valid_data[0, in_transect_idx]

        # Set variables to defaults
        self.messages = []
        vel_criteria = 0.012

        # Check that there is some valid BT data
        if np.nansum(bt_valid) > 1:
            wt_u = trans_data.w_vel.u_processed_mps[:, in_transect_idx]
            wt_v = trans_data.w_vel.v_processed_mps[:, in_transect_idx]
            if ens_duration is None:
                ens_duration = trans_data.date_time.ens_duration_sec[in_transect_idx]

            bt_u = trans_data.boat_vel.bt_vel.u_processed_mps[in_transect_idx]
            bt_v = trans_data.boat_vel.bt_vel.v_processed_mps[in_transect_idx]
            bin_size = trans_data.depths.bt_depths.depth_cell_size_m[:, in_transect_idx]

            # Compute closure distance and direction
            bt_x = np.nancumsum(bt_u * ens_duration)
            bt_y = np.nancumsum(bt_v * ens_duration)
            direct, self.bt_dist_us_m = cart2pol(bt_x[-1], bt_y[-1])
            self.bt_mb_dir = rad2azdeg(direct)

            # Compute duration of test
            self.duration_sec = np.nansum(ens_duration)

            # Compute the moving-bed velocity
            self.bt_mb_spd_mps = self.bt_dist_us_m / self.duration_sec

            # Compute discharge weighted mean velocity components for the
            # purposed of computing the mean flow direction
            xprod = QComp.cross_product(transect=trans_data)
            q = QComp.discharge_middle_cells(xprod, trans_data, ens_duration)
            wght = np.abs(q)
            se = np.nansum(np.nansum(wt_u * wght)) / np.nansum(np.nansum(wght))
            sn = np.nansum(np.nansum(wt_v * wght)) / np.nansum(np.nansum(wght))
            direct, flow_speed_q = cart2pol(se, sn)

            # Compute flow speed and direction
            self.flow_dir = rad2azdeg(direct)

            # Compute the area weighted mean velocity components for the
            # purposed of computing the mean flow speed. Area weighting is used for flow speed instead of
            # discharge so that the flow speed is not included in the weighting used to compute the mean flow speed.
            wght_area = np.multiply(np.multiply(np.sqrt(bt_u ** 2 + bt_v ** 2), bin_size), ens_duration)
            idx = np.where(np.isnan(wt_u) == False)
            se = np.nansum(np.nansum(wt_u[idx] * wght_area[idx])) / np.nansum(np.nansum(wght_area[idx]))
            sn = np.nansum(np.nansum(wt_v[idx] * wght_area[idx])) / np.nansum(np.nansum(wght_area[idx]))
            dir_a, self.bt_flow_spd_mps = cart2pol(se, sn)
            self.bt_flow_spd_mps = self.bt_flow_spd_mps + self.bt_mb_spd_mps

            # Compute potential error in BT referenced discharge
            self.bt_percent_mb = (self.bt_mb_spd_mps / self.bt_flow_spd_mps) * 100

            # Compute test with GPS
            self.compute_mb_gps()

            # Store selected test characteristics
            if ref == 'BT':
                self.mb_spd_mps = self.bt_mb_spd_mps
                self.dist_us_m = self.bt_dist_us_m
                self.percent_mb = self.bt_percent_mb
                self.mb_dir = self.bt_mb_dir
                self.flow_spd_mps = self.bt_flow_spd_mps
            elif not np.isnan(self.gps_percent_mb):
                self.mb_spd_mps = self.gps_mb_spd_mps
                self.dist_us_m = self.gps_dist_us_m
                self.percent_mb = self.gps_percent_mb
                self.mb_dir = self.gps_mb_dir
                self.flow_spd_mps = self.bt_flow_spd_mps

            # Assess invalid bottom track
            # Compute percent invalid bottom track
            self.percent_invalid_bt = (np.nansum(bt_valid == False) / len(bt_valid)) * 100

            # Determine if more than 9 consecutive seconds of invalid BT occurred
            consect_bt_time = np.zeros(n_ensembles)
            for n in range(1, n_ensembles):
                if bt_valid[n]:
                    consect_bt_time[n] = 0
                else:
                    consect_bt_time[n] = consect_bt_time[n - 1] + ens_duration[n]

            max_consect_bt_time = np.nanmax(consect_bt_time)

            # Evaluate compass calibration based on flow direction

            # Find apex of loop adapted from
            # http://www.mathworks.de/matlabcentral/newsreader/view_thread/164048
            loop_out = np.array([bt_x[0], bt_y[0], 0])
            loop_return = np.array([bt_x[-1], bt_y[-1], 0])

            distance = np.zeros(n_ensembles)
            for n in range(n_ensembles):
                p = np.array([bt_x[n], bt_y[n], 0])
                distance[n] = np.linalg.norm(np.cross(loop_return - loop_out, p - loop_out))  \
                    / np.linalg.norm(loop_return - loop_out)

            dmg_idx = np.where(distance == np.nanmax(distance))[0][0]

            # Compute flow direction on outgoing part of loop
            u_out = wt_u[:, :dmg_idx + 1]
            v_out = wt_v[:, :dmg_idx + 1]
            wght = np.abs(q[:, :dmg_idx+1])
            se = np.nansum(u_out * wght) / np.nansum(wght)
            sn = np.nansum(v_out * wght) / np.nansum(wght)
            direct, _ = cart2pol(se, sn)
            flow_dir1 = rad2azdeg(direct)

            # Compute unweighted flow direction in each cell
            direct, _ = cart2pol(u_out, v_out)
            flow_dir_cell = rad2azdeg(direct)

            # Compute difference from mean and correct to +/- 180
            v_dir_corr = flow_dir_cell - flow_dir1
            v_dir_idx = v_dir_corr > 180
            v_dir_corr[v_dir_idx] = 360-v_dir_corr[v_dir_idx]
            v_dir_idx = v_dir_corr < -180
            v_dir_corr[v_dir_idx] = 360 + v_dir_corr[v_dir_idx]

            # Number of invalid weights
            idx2 = np.where(np.isnan(wght) == False)
            nwght = len(idx2[0])

            # Compute 95% uncertainty using weighted standard deviation
            uncert1 = 2. * np.sqrt(np.nansum(np.nansum(wght * v_dir_corr**2))
                                   / (((nwght - 1) * np.nansum(np.nansum(wght))) / nwght)) / np.sqrt(nwght)

            # Compute flow direction on returning part of loop
            u_ret = wt_u[:, dmg_idx + 1:]
            v_ret = wt_v[:, dmg_idx + 1:]
            wght = np.abs(q[:, dmg_idx+1:])
            se = np.nansum(u_ret * wght) / np.nansum(wght)
            sn = np.nansum(v_ret * wght) / np.nansum(wght)
            direct, _ = cart2pol(se, sn)
            flow_dir2 = rad2azdeg(direct)

            # Compute unweighted flow direction in each cell
            direct, _ = cart2pol(u_ret, v_ret)
            flow_dir_cell = rad2azdeg(direct)

            # Compute difference from mean and correct to +/- 180
            v_dir_corr = flow_dir_cell - flow_dir2
            v_dir_idx = v_dir_corr > 180
            v_dir_corr[v_dir_idx] = 360 - v_dir_corr[v_dir_idx]
            v_dir_idx = v_dir_corr < -180
            v_dir_corr[v_dir_idx] = 360 + v_dir_corr[v_dir_idx]

            # Number of valid weights
            idx2 = np.where(np.isnan(wght) == False)
            nwght = len(idx2[0])

            # Compute 95% uncertainty using weighted standard deviation
            uncert2 = 2.*np.sqrt(np.nansum(np.nansum(wght * v_dir_corr**2))
                                 / (((nwght-1)*np.nansum(np.nansum(wght))) / nwght)) / np.sqrt(nwght)

            # Compute and report difference in flow direction
            diff_dir = np.abs(flow_dir1 - flow_dir2)
            if diff_dir > 180:
                diff_dir = diff_dir - 360
            self.compass_diff_deg = diff_dir
            uncert = uncert1 + uncert2

            # Compute potential compass error
            idx = np.where(np.isnan(bt_x) == False)
            if len(idx[0]) > 0:
                idx = idx[0][-1]
            width = np.sqrt((bt_x[dmg_idx] - bt_x[idx] / 2) ** 2 + (bt_y[dmg_idx] - bt_y[idx] / 2) ** 2)
            compass_error = (2 * width * sind(diff_dir / 2) * 100) / (self.duration_sec * self.flow_spd_mps)

            # Initialize message counter
            self.test_quality = 'Good'

            # Low water velocity
            if self.flow_spd_mps < 0.25:
                self.messages.append('WARNING: The water velocity is less than recommended minimum for '
                                     + 'this test and could cause the loop method to be inaccurate. '
                                     + 'CONSIDER USING A STATIONARY TEST TO CHECK MOVING-BED CONDITIONS')
                self.test_quality = 'Warnings'

            # Percent invalid bottom track
            if self.percent_invalid_bt > 20:
                self.messages.append('ERROR: Percent invalid bottom track exceeds 20 percent. '
                                     + 'THE LOOP IS NOT ACCURATE. TRY A STATIONARY MOVING-BED TEST.')
                self.test_quality = 'Errors'
            elif self.percent_invalid_bt > 5:
                self.messages.append('WARNING: Percent invalid bottom track exceeds 5 percent. '
                                     + 'Loop may not be accurate. PLEASE REVIEW DATA.')
                self.test_quality = 'Warnings'

            # More than 9 consecutive seconds of invalid BT
            if max_consect_bt_time > 9:
                self.messages.append('ERROR: Bottom track is invalid for more than 9 consecutive seconds.'
                                     + 'THE LOOP IS NOT ACCURATE. TRY A STATIONARY MOVING-BED TEST.')
                self.test_quality = 'Errors'

            if np.abs(compass_error) > 5 and np.abs(diff_dir) > 3 and np.abs(diff_dir) > uncert:
                self.messages.append('ERROR: Difference in flow direction between out and back sections of '
                                     + 'loop could result in a 5 percent or greater error in final discharge. '
                                     + 'REPEAT LOOP AFTER COMPASS CAL. OR USE A STATIONARY MOVING-BED TEST.')
                self.test_quality = 'Errors'

        else:
            self.messages.append('ERROR: Loop has no valid bottom track data. '
                                 + 'REPEAT OR USE A STATIONARY MOVING-BED TEST.')
            self.test_quality = 'Errors'

        # If loop is valid then evaluate moving-bed condition
        if self.test_quality != 'Errors':

            # Check minimum moving-bed velocity criteria
            if self.mb_spd_mps > vel_criteria:
                # Check that closure error is in upstream direction
                if 135 < np.abs(self.flow_dir - self.mb_dir) < 225:
                    # Check if moving-bed is greater than 1% of the mean flow speed
                    if self.percent_mb > 1:
                        self.messages.append('Loop Indicates a Moving Bed -- Use GPS as reference. If GPS is '
                                             + 'unavailable or invalid use the loop method to correct the '
                                             + 'final discharge.')
                        self.moving_bed = 'Yes'
                    else:
                        self.messages.append('Moving Bed Velocity < 1% of Mean Velocity -- No Correction Recommended')
                        self.moving_bed = 'No'
                else:
                    self.messages.append('ERROR: Loop closure error not in upstream direction. '
                                         + 'REPEAT LOOP or USE STATIONARY TEST')
                    self.test_quality = 'Errors'
                    self.moving_bed = 'Unknown'
            else:
                self.messages.append('Moving-bed velocity < Minimum moving-bed velocity criteria '
                                     + '-- No correction recommended')
                self.moving_bed = 'No'

            # Notify of differences in results of test between BT and GPS
            if not np.isnan(self.gps_percent_mb):
                if np.abs(self.bt_percent_mb - self.gps_percent_mb) > 2:
                    self.messages.append('WARNING - Bottom track and GPS results differ by more than 2%.')
                    self.test_quality = 'Warnings'

                if np.logical_xor(self.bt_percent_mb >= 1,  self.gps_percent_mb >= 1):
                    self.messages.append('WARNING - Bottom track and GPS results do not agree.')
                    self.test_quality = 'Warnings'

        else:
            self.messages.append('ERROR: Due to ERRORS noted above this loop is NOT VALID. '
                                 + 'Please consider suggestions.')
            self.moving_bed = 'Unknown'

    def stationary_test(self, ref='BT'):
        """Processed the stationary moving-bed tests.
        """

        # Assign data from transect to local variables
        trans_data = copy.deepcopy(self.transect)
        in_transect_idx = trans_data.in_transect_idx
        bt_valid = trans_data.boat_vel.bt_vel.valid_data[0, in_transect_idx]

        # Check to see that there is valid bottom track data
        self.messages = []
        if np.nansum(bt_valid) > 0:
            # Assign data to local variables
            wt_u = trans_data.w_vel.u_processed_mps[:, in_transect_idx]
            wt_v = trans_data.w_vel.v_processed_mps[:, in_transect_idx]
            ens_duration = trans_data.date_time.ens_duration_sec[in_transect_idx]
            bt_u = trans_data.boat_vel.bt_vel.u_processed_mps[in_transect_idx]
            bt_v = trans_data.boat_vel.bt_vel.v_processed_mps[in_transect_idx]

            # Use only data with valid bottom track
            valid_bt = trans_data.boat_vel.bt_vel.valid_data[0, in_transect_idx]
            wt_u[:, valid_bt == False] = np.nan
            wt_v[:, valid_bt == False] = np.nan
            bt_u[valid_bt == False] = np.nan
            bt_v[valid_bt == False] = np.nan

            u_water = np.nanmean(wt_u)
            v_water = np.nanmean(wt_v)
            self.flow_dir = np.arctan2(u_water, v_water) * 180 / np.pi
            if self.flow_dir < 0:
                self.flow_dir = self.flow_dir + 360

            bin_depth = trans_data.depths.bt_depths.depth_cell_depth_m[:, in_transect_idx]
            trans_select = getattr(trans_data.depths, trans_data.depths.selected)
            depth_ens = trans_select.depth_processed_m[in_transect_idx]

            nb_u, nb_v, unit_nbu, unit_nbv = self.near_bed_velocity(wt_u, wt_v, depth_ens, bin_depth)

            # Compute bottom track parallel to water velocity
            unit_nb_vel = np.vstack([unit_nbu, unit_nbv])
            bt_vel = np.vstack([bt_u, bt_v])
            bt_vel_up_strm = -1 * np.sum(bt_vel * unit_nb_vel, 0)
            bt_up_strm_dist = bt_vel_up_strm * ens_duration
            bt_up_strm_dist_cum = np.nancumsum(bt_up_strm_dist)
            self.bt_dist_us_m = bt_up_strm_dist_cum[-1]

            # Compute bottom track perpendicular to water velocity
            nb_vel_ang, _ = cart2pol(unit_nbu, unit_nbv)
            nb_vel_unit_cs1, nb_vel_unit_cs2 = pol2cart(nb_vel_ang + np.pi / 2, 1)
            nb_vel_unit_cs = np.vstack([nb_vel_unit_cs1, nb_vel_unit_cs2])
            bt_vel_cs = np.sum(bt_vel * nb_vel_unit_cs, 0)
            bt_cs_strm_dist = bt_vel_cs * ens_duration
            bt_cs_strm_dist_cum = np.nancumsum(bt_cs_strm_dist)

            # Compute cumulative mean moving bed velocity
            valid_bt_vel_up_strm = np.isnan(bt_vel_up_strm) == False

            mb_vel = np.nancumsum(bt_vel_up_strm) / np.nancumsum(valid_bt_vel_up_strm)

            # Compute the average ensemble velocities corrected for moving bed
            if mb_vel[-1] > 0:
                u_corrected = np.add(wt_u, (unit_nb_vel[0, :]) * bt_vel_up_strm)
                v_corrected = np.add(wt_v, (unit_nb_vel[1, :]) * bt_vel_up_strm)
            else:
                u_corrected = wt_u
                v_corrected = wt_v

            # Compute the mean of the ensemble magnitudes

            # Mean is computed using magnitudes because if a Streampro with no compass is the data source the change
            # in direction could be either real change in water direction or an uncompensated turn of the floating
            # platform. This approach is the best compromise when there is no compass or the compass is unreliable,
            # which is often why the stationary method is used. A weighted average is used to account for the possible
            # change in cell size within and ensemble for the RiverRay and RiverPro.

            mag = np.sqrt(u_corrected**2 + v_corrected**2)
            depth_cell_size = trans_data.depths.bt_depths.depth_cell_size_m[:, in_transect_idx]
            depth_cell_size[np.isnan(mag)] = np.nan
            mag_w = mag * depth_cell_size
            self.bt_flow_spd_mps = np.nansum(mag_w) / np.nansum(depth_cell_size)
            self.bt_mb_spd_mps = mb_vel[-1]
            self.bt_percent_mb = (self.bt_mb_spd_mps / self.bt_flow_spd_mps) * 100
            if self.bt_percent_mb < 0:
                self.bt_percent_mb = 0

            # Compute percent invalid bottom track
            self.percent_invalid_bt = (np.nansum(bt_valid == False) / len(bt_valid)) * 100
            self.duration_sec = np.nansum(ens_duration)

            # Compute test using GPS
            self.compute_mb_gps()

            # Store selected test characteristics
            if ref == 'BT':
                self.mb_spd_mps = self.bt_mb_spd_mps
                self.dist_us_m = self.bt_dist_us_m
                self.percent_mb = self.bt_percent_mb
                self.mb_dir = self.bt_mb_dir
                self.flow_spd_mps = self.bt_flow_spd_mps
            elif not np.isnan(self.gps_percent_mb):
                self.mb_spd_mps = self.gps_mb_spd_mps
                self.dist_us_m = self.gps_dist_us_m
                self.percent_mb = self.gps_percent_mb
                self.mb_dir = self.gps_mb_dir
                self.flow_spd_mps = self.bt_flow_spd_mps

            self.near_bed_speed_mps = np.sqrt(np.nanmean(nb_u)**2 + np.nanmean(nb_v)**2)
            self.stationary_us_track = bt_up_strm_dist_cum
            self.stationary_cs_track = bt_cs_strm_dist_cum
            self.stationary_mb_vel = mb_vel

            # Quality check
            self.test_quality = 'Good'
            # Check duration
            if self.duration_sec < 300:
                self.messages.append('WARNING - Duration of stationary test is less than 5 minutes')
                self.test_quality = 'Warnings'

            # Check validity of mean moving-bed velocity
            if self.duration_sec > 60:
                mb_vel_std = np.nanstd(mb_vel[-30:], ddof=1)
                cov = mb_vel_std / mb_vel[-1]
                if cov > 0.25 and mb_vel_std > 0.03:
                    self.messages.append('WARNING - Moving-bed velocity may not be consistent. '
                                         + 'Average maybe inaccurate.')
                    self.test_quality = 'Warnings'

            # Check percentage of invalid BT data
            if np.nansum(ens_duration[valid_bt_vel_up_strm]) <= 120:

                self.messages.append('ERROR - Total duration of valid BT data is insufficient for a valid test.')
                self.test_quality = 'Errors'
                self.moving_bed = 'Unknown'
            elif self.percent_invalid_bt > 10:
                self.messages.append('WARNING - Number of ensembles with invalid bottom track exceeds 10%')
                self.test_quality = 'Warnings'

            # Determine if the test indicates a moving bed
            if self.test_quality != 'Errors':
                if self.percent_mb >= 1:
                    self.moving_bed = 'Yes'
                else:
                    self.moving_bed = 'No'

            # Notify of differences in results of test between BT and GPS
            if not np.isnan(self.gps_percent_mb):
                if np.abs(self.bt_percent_mb - self.gps_percent_mb) > 2:
                    self.messages.append('WARNING - Bottom track and GPS results differ by more than 2%.')
                    self.test_quality = 'Warnings'

                if np.logical_xor(self.bt_percent_mb >= 1,  self.gps_percent_mb >= 1):
                    self.messages.append('WARNING - Bottom track and GPS results do not agree.')
                    self.test_quality = 'Warnings'

        else:
            self.messages.append('ERROR - Stationary moving-bed test has no valid bottom track data.')
            self.test_quality = 'Errors'
            self.moving_bed = 'Unknown'
            self.duration_sec = np.nansum(trans_data.date_time.ens_duration_sec[in_transect_idx])
            self.percent_invalid_bt = 100

    def compute_mb_gps(self):
        """Computes moving-bed data using GPS.
        """
        if np.isnan(self.flow_dir):
            u_water = np.nanmean(self.transect.w_vel.u_processed_mps[:, self.transect.in_transect_idx])
            v_water = np.nanmean(self.transect.w_vel.v_processed_mps[:, self.transect.in_transect_idx])
            self.flow_dir = np.arctan2(u_water, v_water) * 180 / np.pi
            if self.flow_dir < 0:
                self.flow_dir = self.flow_dir + 360

        gps_bt = None
        # Use GGA data if available and VTG is GGA is not available
        if self.transect.boat_vel.gga_vel is not None:
            gps_bt = TransectData.compute_gps_bt(self.transect, gps_ref='gga_vel')
        elif self.transect.boat_vel.vtg_vel is not None:
            gps_bt = TransectData.compute_gps_bt(self.transect, gps_ref='vtg_vel')
        if gps_bt is not None and len(gps_bt) > 0:
            self.gps_dist_us_m = gps_bt['mag']
            self.gps_mb_dir = gps_bt['dir']
            self.gps_mb_spd_mps = self.gps_dist_us_m / self.duration_sec
            self.gps_flow_spd_mps = self.bt_flow_spd_mps - self.bt_mb_spd_mps + self.gps_mb_spd_mps
            self.gps_percent_mb = (self.gps_mb_spd_mps / self.gps_flow_spd_mps) * 100

    def magvar_change(self, magvar, old_magvar):
        """Adjust moving-bed test for change in magvar.

        Parameters
        ----------
        magvar: float
            New magvar
        old_magvar: float
            Existing magvar
        """

        if self.transect.sensors.heading_deg.selected == 'internal':
            magvar_change = magvar - old_magvar
            self.bt_mb_dir = self.bt_mb_dir + magvar_change
            self.flow_dir = self.flow_dir + magvar_change

            # Recompute moving-bed tests with GPS and set results using existing reference
            self.compute_mb_gps()
            self.change_ref(self.ref)

    def h_offset_change(self, h_offset, old_h_offset):
        """Adjust moving-bed test for change in h_offset for external compass.

        Parameters
        ----------
        h_offset: float
            New h_offset
        old_h_offset: float
            Existing h_offset
        """

        if self.transect.sensors.heading_deg.selected == 'external':
            h_offset_change = h_offset - old_h_offset
            self.bt_mb_dir = self.bt_mb_dir + h_offset_change
            self.flow_dir = self.flow_dir + h_offset_change

            # Recompute moving-bed tests with GPS and set results using existing reference
            self.compute_mb_gps()
            self.change_ref(self.ref)

    def change_ref(self, ref):
        """Change moving-bed test fixed reference.

        Parameters
        ----------
        ref: str
            Defines specified reference (BT or GPS)
        """

        if ref == 'BT':
            self.mb_spd_mps = self.bt_mb_spd_mps
            self.dist_us_m = self.bt_dist_us_m
            self.percent_mb = self.bt_percent_mb
            self.mb_dir = self.bt_mb_dir
            self.flow_spd_mps = self.bt_flow_spd_mps
            self.ref = 'BT'
            check_mb = True
            if self.test_quality != 'Errors':
                if self.type == 'Loop':
                    if self.mb_spd_mps <= 0.012:
                        check_mb = False
                        self.moving_bed = 'No'
                    else:
                        if 135 < np.abs(self.flow_dir - self.mb_dir) < 225:
                            check_mb = True
                        else:
                            check_mb = False
                            self.moving_bed = 'Unknown'
                if check_mb:
                    if self.percent_mb > 1:
                        self.moving_bed = 'Yes'
                    else:
                        self.moving_bed = 'No'
            else:
                self.moving_bed = 'Unknown'
        elif ref == 'GPS':
            self.mb_spd_mps = self.gps_mb_spd_mps
            self.dist_us_m = self.gps_dist_us_m
            self.percent_mb = self.gps_percent_mb
            self.mb_dir = self.gps_mb_dir
            self.flow_spd_mps = self.gps_flow_spd_mps
            self.ref = 'GPS'
            check_mb = True
            if self.test_quality != 'Errors':
                if self.type == 'Loop':
                    if self.mb_spd_mps <= 0.012:
                        check_mb = False
                        self.moving_bed = 'No'
                    else:
                        if 135 < np.abs(self.flow_dir - self.mb_dir) < 225:
                            check_mb = True
                        else:
                            check_mb = False
                            self.messages.append('ERROR: GPS Loop closure error not in upstream direction. '
                                                 + 'REPEAT LOOP or USE STATIONARY TEST')
                            self.moving_bed = 'Unknown'
                if check_mb:
                    if self.percent_mb > 1:
                        self.moving_bed = 'Yes'
                    else:
                        self.moving_bed = 'No'
            else:
                self.moving_bed = 'Unknown'

    @staticmethod
    def near_bed_velocity(u, v, depth, bin_depth):
        """Compute near bed velocities.

        Parameters
        ----------
        u: np.array(float)
            u water velocity component
        v: np.array(float)
            v water velocity component
        depth: np.array(float)
            Water depth for each ensemble
        bin_depth: np.array(float)
            Depth to centerline of each bin

        Returns
        -------
        nb_u: np.array(float)
            u near-bed velocity component
        nb_v: np.array(float)
            v near-bed velocity component
        unit_nbu: np.array(float)
            u component of the near-bed unit vector
        unit_nbv: np.array(float)
            v component of the near-bed unit vector
        """

        # Compute z near bed as 10% of depth
        z_near_bed = depth * 0.1

        # Initialize variables
        n_ensembles = u.shape[1]
        nb_u = np.tile(np.nan, n_ensembles)
        nb_v = np.tile(np.nan, n_ensembles)
        unit_nbu = np.tile(np.nan, n_ensembles)
        unit_nbv = np.tile(np.nan, n_ensembles)
        z_depth = np.tile(np.nan, n_ensembles)
        u_mean = np.tile(np.nan, n_ensembles)
        v_mean = np.tile(np.nan, n_ensembles)
        speed_near_bed = np.tile(np.nan, n_ensembles)

        # Compute near bed velocity for each ensemble
        for n in range(n_ensembles):
            idx = np.where(np.isnan(u[:, n]) == False)
            if len(idx[-1]) > 0:
                if len(idx[-1]) > 0:
                    idx = idx[-1][-2::]
                else:
                    idx = idx[-1][-1]
                # Compute near-bed velocity
                z_depth[n] = depth[n] - np.nanmean(bin_depth[idx, n], 0)
                u_mean[n] = np.nanmean(u[idx, n], 0)
                v_mean[n] = np.nanmean(v[idx, n], 0)
                nb_u[n] = (u_mean[n] / z_depth[n] ** (1. / 6.)) * (z_near_bed[n] ** (1. / 6.))
                nb_v[n] = (v_mean[n] / z_depth[n] ** (1. / 6.)) * (z_near_bed[n] ** (1. / 6.))
                speed_near_bed[n] = np.sqrt(nb_u[n] ** 2 + nb_v[n] ** 2)
                unit_nbu[n] = nb_u[n] / speed_near_bed[n]
                unit_nbv[n] = nb_v[n] / speed_near_bed[n]

        return nb_u, nb_v, unit_nbu, unit_nbv

    @staticmethod
    def auto_use_2_correct(moving_bed_tests, boat_ref=None):
        """Apply logic to determine which moving-bed tests should be used
        for correcting bottom track referenced discharges with moving-bed conditions.

        Parameters
        ----------
        moving_bed_tests: list
            List of MovingBedTests objects.
        boat_ref: str
            Boat velocity reference.

        Returns
        -------
        moving_bed_tests: list
            List of MovingBedTests objects.
        """

        if len(moving_bed_tests) != 0:
            # Initialize variables
            lidx_user = []
            lidx_no_errors = []
            test_type = []
            lidx_stationary = []
            lidx_loop = []
            flow_speed = []
            for test in moving_bed_tests:
                test.use_2_correct = False
                test.selected = False
                # Valid test according to user
                lidx_user.append(test.user_valid == True)
                # Valid test according to quality assessment
                lidx_no_errors.append(test.test_quality != 'Errors')
                # Identify type of test
                test_type.append(test.type)
                lidx_stationary.append(test.type == 'Stationary')
                lidx_loop.append(test.type == 'Loop')
                flow_speed.append(test.flow_spd_mps)

            # Combine
            lidx_valid_loop = np.all(np.vstack((lidx_user, lidx_no_errors, lidx_loop)), 0)
            lidx_valid_stationary = np.all(np.vstack((lidx_user, lidx_no_errors, lidx_stationary)), 0)

            # Check flow speed
            lidx_flow_speed = np.array(flow_speed) > 0.25

            # Determine if there are valid loop tests
            # This is the code in matlab but I don't think it is correct. I the valid loop should also have a valid
            # flow speed, if not then a stationary test, if available could be used.
            lidx_loops_2_select = np.all(np.vstack((lidx_flow_speed, lidx_valid_loop)), 0)
            if np.any(lidx_loops_2_select):
                # Select last loop
                idx_select = np.where(lidx_loops_2_select)[0][-1]
                test_select = moving_bed_tests[idx_select]
                test_select.selected = True

                if test_select.moving_bed == 'Yes':
                    test_select.use_2_correct = True

            # If there are no valid loop look for valid stationary tests
            elif np.any(lidx_valid_stationary):
                moving_bed = []
                for n, lidx in enumerate(lidx_valid_stationary):
                    if lidx:
                        moving_bed_tests[n].selected = True
                        # Determine if any stationary test resulted in a moving bed
                        if moving_bed_tests[n].moving_bed == 'Yes':
                            moving_bed.append(True)
                        else:
                            moving_bed.append(False)
                # If any stationary test shows a moving-bed use all valid stationary test to correct BT discharge
                if any(moving_bed) > 0:
                    for n, test in enumerate(moving_bed_tests):
                        if lidx_valid_stationary[n]:
                            test.use_2_correct = True

            # If the flow speed is too low but there are not valid stationary tests use the last loop test.
            elif np.any(lidx_valid_loop):
                # Select last loop
                idx_select = np.where(lidx_valid_loop)[0][-1]
                moving_bed_tests[idx_select].selected = True
                if moving_bed_tests[idx_select].moving_bed == 'Yes':
                    moving_bed_tests[idx_select].use_2_correct = True

            # If the navigation reference for discharge computations is set
            # GPS then none of test should be used for correction. The
            # selected test should be used to determine if there is a valid
            # moving-bed and a moving-bed condition.
            if boat_ref is None:
                ref = 'BT'
            else:
                ref = boat_ref

            if ref != 'BT':
                for test in moving_bed_tests:
                    test.use_2_correct = False
        return moving_bed_tests