File: atomic_orbitals.py

package info (click to toggle)
python-pyvista 0.46.4-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 176,968 kB
  • sloc: python: 94,346; sh: 216; makefile: 70
file content (269 lines) | stat: -rw-r--r-- 8,011 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
"""
.. _atomic_orbitals_example:

Plot Atomic Orbitals
--------------------
Visualize the wave functions (orbitals) of the hydrogen atom.

"""

# %%
# Import
# ~~~~~~
# Import the applicable libraries.
#
# .. note::
#    This example is modeled off of `Matplotlib: Hydrogen Wave Function
#    <http://staff.ustc.edu.cn/~zqj/posts/Hydrogen-Wavefunction/>`_.
#
#    This example requires `sympy <https://www.sympy.org/>`_. Install it with:
#
#    .. code-block:: bash
#
#       pip install sympy
from __future__ import annotations

import numpy as np

import pyvista as pv
from pyvista import examples

# %%
# Generate the Dataset
# ~~~~~~~~~~~~~~~~~~~~
# Generate the dataset by evaluating the analytic hydrogen wave function from
# ``sympy``.
#
# .. math::
#    \begin{equation}
#        \psi_{n\ell m}(r,\theta,\phi)
#        =
#        \sqrt{
#            \left(\frac{2}{na_0}\right)^3\, \frac{(n-\ell-1)!}{2n[(n+\ell)!]}
#        }
#        e^{-r / na_0}
#        \left(\frac{2r}{na_0}\right)^\ell
#        L_{n-\ell-1}^{2\ell+1} \cdot Y_\ell^m(\theta, \phi)
#    \end{equation}
#
# See `Hydrogen atom <https://en.wikipedia.org/wiki/Hydrogen_atom>`_ for more
# details.
#
# This dataset evaluates this function for the hydrogen orbital
# :math:`3d_{xy}`, with the following quantum numbers:
#
# * Principal quantum number: ``n=3``
# * Azimuthal quantum number: ``l=2``
# * Magnetic quantum number: ``m=-2``

grid = examples.load_hydrogen_orbital(3, 2, -2)
grid


# %%
# Plot the Orbital
# ~~~~~~~~~~~~~~~~
# Plot the orbital using :func:`add_volume() <pyvista.Plotter.add_volume>` and
# using the default scalars contained in ``grid``, ``real_wf``. This way we
# can plot more than just the probability of the electron, but also the phase
# of the electron wave function.
#
# .. note::
#    Since the real value of evaluated wave function for this orbital varies
#    between ``[-<value>, <value>]``, we cannot use the default opacity
#    ``opacity='linear'``. Instead, we use ``[1, 0, 1]`` since we would like
#    the opacity to be proportional to the absolute value of the scalars.

# sphinx_gallery_start_ignore
# volume rendering does not work in interactive plots currently
PYVISTA_GALLERY_FORCE_STATIC = True
# sphinx_gallery_end_ignore

pl = pv.Plotter()
vol = pl.add_volume(grid, cmap='magma', opacity=[1, 0, 1])
vol.prop.interpolation_type = 'linear'
pl.camera.zoom(2)
pl.show_axes()
pl.show()


# %%
# Plot the Orbital Contours as an Isosurface
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Generate the contour plot for the orbital by determining when the orbital
# equals 10% the maximum value of the orbital. This effectively captures the
# most likely locations of the electron for this orbital.
#
# Note how we use the absolute value of the scalars when evaluating
# :func:`contour() <pyvista.DataSetFilters.contour>` to capture where the
# positive and negative phases cross ``eval_at``.

eval_at = grid['real_wf'].max() * 0.1
contours = grid.contour(
    [eval_at],
    scalars=np.abs(grid['real_wf']),
    method='marching_cubes',
)
contours = contours.interpolate(grid)
contours.plot(
    smooth_shading=True,
    show_scalar_bar=False,
)


# %%
# Volumetric Plot: Plot the Orbitals using RGBA
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Let's now combine some of the best parts of the two above plots. The
# volumetric plot is great for showing the probability of the "electron cloud"
# orbitals, but the colormap doesn't quite match reality as well as the
# isosurface plot.
#
# For this example we're going to use an RGBA colormap to tightly control the
# way the orbitals are plotted. For this, the opacity will be mapped to the
# probability of the electron being at a location in the grid, which we can do
# by taking the absolute value squared of the orbital's wave function. We can
# set the color of the orbital based on the phase, which we can get simply
# with ``orbital['real_wf'] < 0``.
#
# Let's start with a simple one, the :math:`3p_z` orbital.

# sphinx_gallery_start_ignore
# volume rendering does not work in interactive plots currently
PYVISTA_GALLERY_FORCE_STATIC = True
# sphinx_gallery_end_ignore


def plot_orbital(orbital, cpos='iso', clip_plane='x'):
    """Plot an electron orbital using an RGBA colormap."""
    neg_mask = orbital['real_wf'] < 0
    rgba = np.zeros((orbital.n_points, 4), np.uint8)
    rgba[neg_mask, 0] = 255
    rgba[~neg_mask, 1] = 255

    # normalize opacity
    opac = np.abs(orbital['real_wf']) ** 2
    opac /= opac.max()
    rgba[:, -1] = opac * 255

    orbital['plot_scalars'] = rgba

    pl = pv.Plotter()
    vol = pl.add_volume(
        orbital,
        scalars='plot_scalars',
    )
    vol.prop.interpolation_type = 'linear'
    if clip_plane:
        pl.add_volume_clip_plane(
            vol,
            normal=clip_plane,
            normal_rotation=False,
        )
    pl.camera_position = cpos
    pl.camera.zoom(1.5)
    pl.show_axes()
    return pl.show()


hydro_orbital = examples.load_hydrogen_orbital(3, 1, 0)
plot_orbital(hydro_orbital, clip_plane='-x')


# %%
# Volumetric Plot: :math:`4d_{z^2}` orbital
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

# sphinx_gallery_start_ignore
# volume rendering does not work in interactive plots currently
PYVISTA_GALLERY_FORCE_STATIC = True
# sphinx_gallery_end_ignore
hydro_orbital = examples.load_hydrogen_orbital(4, 2, 0)
plot_orbital(hydro_orbital, clip_plane='-y')


# %%
# Volumetric Plot: :math:`4d_{xz}` orbital
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

# sphinx_gallery_start_ignore
# volume rendering does not work in interactive plots currently
PYVISTA_GALLERY_FORCE_STATIC = True
# sphinx_gallery_end_ignore
hydro_orbital = examples.load_hydrogen_orbital(4, 2, -1)
plot_orbital(hydro_orbital, clip_plane='-y')


# %%
# Plot an Orbital Using a Density Plot
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# We can also plot atomic orbitals using a 3D density plot. For this, we will
# use :func:`numpy.random.choice` to sample all the points of our
# :class:`pyvista.ImageData` based on the probability of the electron being
# at that coordinate.

# Seed rng for reproducibility
rng = np.random.default_rng(seed=0)

# Generate the orbital and sample based on the square of the probability of an
# electron being within a particular volume of space.
hydro_orbital = examples.load_hydrogen_orbital(4, 2, 0, zoom_fac=0.5)
prob = np.abs(hydro_orbital['real_wf']) ** 2
prob /= prob.sum()
indices = rng.choice(hydro_orbital.n_points, 10000, p=prob)

# add a small amount of noise to these coordinates to remove the "grid like"
# structure present in the underlying ImageData
points = hydro_orbital.points[indices]
points += rng.random(points.shape) - 0.5

# Create a point cloud and add the phase as the active scalars
point_cloud = pv.PolyData(points)
point_cloud['phase'] = hydro_orbital['real_wf'][indices] < 0

# Turn the point cloud into individual spheres. We do this so we can improve
# the plot by enabling surface space ambient occlusion (SSAO)
dplot = point_cloud.glyph(
    geom=pv.Sphere(theta_resolution=8, phi_resolution=8),
    scale=False,
    orient=False,
)

# be sure to enable SSAO here. This makes the "points" that are deeper within
# the density plot darker.
pl = pv.Plotter()
pl.add_mesh(
    dplot,
    smooth_shading=True,
    show_scalar_bar=False,
    cmap=['red', 'green'],
    ambient=0.2,
)
pl.enable_ssao(radius=10)
pl.enable_anti_aliasing()
pl.camera.zoom(2)
pl.background_color = 'w'
pl.show()


# %%
# Density Plot - Gaussian Points Representation
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Finally, let's plot the same data using the "Gaussian points" representation.


# sphinx_gallery_start_ignore
# volume rendering does not work in interactive plots currently
PYVISTA_GALLERY_FORCE_STATIC = True
# sphinx_gallery_end_ignore

point_cloud.plot(
    style='points_gaussian',
    render_points_as_spheres=False,
    point_size=3,
    emissive=True,
    background='k',
    show_scalar_bar=False,
    cpos='xz',
    zoom=2,
)