Left-multiplies the given axes of the wf tensor by the given gate matrix.

    Compare with :py:func:`targeted_einsum`. The semantics of these two functions should be
    identical, except this uses ``np.tensordot`` instead of ``np.einsum``.

    :param gate: What to left-multiply the target tensor by.
    :param wf: A tensor to carefully broadcast a left-multiply over.
    :param wf_target_inds: Which axes of the target are being operated on.
    :returns: The output tensor.

def targeted_tensordot(gate: np.ndarray,
                       wf: np.ndarray,
                       wf_target_inds: Sequence[int]
                       ) -> np.ndarray:
    """Left-multiplies the given axes of the wf tensor by the given gate matrix.

    Compare with :py:func:`targeted_einsum`. The semantics of these two functions should be
    identical, except this uses ``np.tensordot`` instead of ``np.einsum``.

    :param gate: What to left-multiply the target tensor by.
    :param wf: A tensor to carefully broadcast a left-multiply over.
    :param wf_target_inds: Which axes of the target are being operated on.
    :returns: The output tensor.
    """
    gate_n_qubits = gate.ndim // 2
    n_qubits = wf.ndim

    # the indices we want to sum over are the final half
    gate_inds = np.arange(gate_n_qubits, 2 * gate_n_qubits)
    assert len(wf_target_inds) == len(gate_inds), (wf_target_inds, gate_inds)
    wf = np.tensordot(gate, wf, (gate_inds, wf_target_inds))

    # tensordot dumps "output" indices into 0, 1, .. gate_n_qubits
    # we need to move them to the right place.

    # First create a list of all the "unaffected" indices which is everything but the
    # first `gate_n_qubits`
    axes_ordering = list(range(gate_n_qubits, n_qubits))

    # We want to "insert" the affected indices into the right place. This means
    # we have to be extra careful about calling list.insert in the correct order.
    # Namely, we have to insert low target indices first.
    where_td_put_them = np.arange(gate_n_qubits)
    sorty = np.argsort(wf_target_inds)
    where_td_put_them = where_td_put_them[sorty]
    sorted_targets = np.asarray(wf_target_inds)[sorty]
    # now that everything is sorted, we can do the insertion.
    for target_ind, from_ind in zip(sorted_targets, where_td_put_them):
        axes_ordering.insert(target_ind, from_ind)

    # A quick call to transpose gives us the right thing.
    return wf.transpose(axes_ordering)