"""
======================
MultiTensor Simulation
======================

In this example we show how someone can simulate the signal and the ODF of a
single voxel using a MultiTensor.
"""

import matplotlib.pyplot as plt
import numpy as np

from dipy.core.sphere import disperse_charges, HemiSphere
from dipy.core.gradients import gradient_table
from dipy.data import get_sphere
from dipy.sims.voxel import multi_tensor, multi_tensor_odf
from dipy.viz import window, actor

"""
For the simulation we will need a GradientTable with the b-values and
b-vectors. To create one, we can first create some random points on a
``HemiSphere`` using spherical polar coordinates.
"""

n_pts = 64
theta = np.pi * np.random.rand(n_pts)
phi = 2 * np.pi * np.random.rand(n_pts)
hsph_initial = HemiSphere(theta=theta, phi=phi)

"""
Next, we call ``disperse_charges`` which will iteratively move the points so
that the electrostatic potential energy is minimized.
"""

hsph_updated, potential = disperse_charges(hsph_initial, 5000)

"""
We need two stacks of ``vertices``, one for every shell, and we need two sets
of b-values, one at 1000 $s/mm^2$, and one at 2500 $s/mm^2$, as we discussed
previously.
"""

vertices = hsph_updated.vertices
values = np.ones(vertices.shape[0])

bvecs = np.vstack((vertices, vertices))
bvals = np.hstack((1000 * values, 2500 * values))

"""
We can also add some b0s. Let's add one at the beginning and one at the end.
"""

bvecs = np.insert(bvecs, (0, bvecs.shape[0]), np.array([0, 0, 0]), axis=0)
bvals = np.insert(bvals, (0, bvals.shape[0]), 0)

"""
Let's now create the ``GradientTable``.
"""

gtab = gradient_table(bvals, bvecs)

"""
In ``mevals`` we save the eigenvalues of each tensor.
"""

mevals = np.array([[0.0015, 0.0003, 0.0003],
                   [0.0015, 0.0003, 0.0003]])

"""
In ``angles`` we save in polar coordinates (:math:`\theta, \phi`) the principal
axis of each tensor.
"""

angles = [(0, 0), (60, 0)]

"""
In ``fractions`` we save the percentage of the contribution of each tensor.
"""

fractions = [50, 50]

"""
The function ``multi_tensor`` will return the simulated signal and an array
with the principal axes of the tensors in cartesian coordinates.
"""

signal, sticks = multi_tensor(gtab, mevals, S0=100, angles=angles,
                              fractions=fractions, snr=None)

"""
We can also add Rician noise with a specific SNR.
"""

signal_noisy, sticks = multi_tensor(gtab, mevals, S0=100, angles=angles,
                                    fractions=fractions, snr=20)

plt.plot(signal, label='noiseless')

plt.plot(signal_noisy, label='with noise')
plt.legend()
# plt.show()
plt.savefig('simulated_signal.png')

"""
.. figure:: simulated_signal.png
   :align: center

   **Simulated MultiTensor signal**
"""

"""
For the ODF simulation we will need a sphere. Because we are interested in a
simulation of only a single voxel, we can use a sphere with very high
resolution. We generate that by subdividing the triangles of one of DIPY_'s
cached spheres, which we can read in the following way.
"""

sphere = get_sphere('repulsion724')
sphere = sphere.subdivide(2)

odf = multi_tensor_odf(sphere.vertices, mevals, angles, fractions)

# Enables/disables interactive visualization
interactive = False

scene = window.Scene()

odf_actor = actor.odf_slicer(odf[None, None, None, :], sphere=sphere,
                             colormap='plasma')
odf_actor.RotateX(90)

scene.add(odf_actor)

print('Saving illustration as multi_tensor_simulation')
window.record(scene, out_path='multi_tensor_simulation.png', size=(300, 300))
if interactive:
    window.show(scene)


"""
.. figure:: multi_tensor_simulation.png
   :align: center

   Simulating a MultiTensor ODF.

.. include:: ../links_names.inc

"""
