• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pyiron / structuretoolkit / 5786800899

pending completion
5786800899

Pull #48

github-actions

web-flow
Merge 2333913b2 into de8f8fd69
Pull Request #48: Plotly aspectmode

1 of 1 new or added line in 1 file covered. (100.0%)

2219 of 2553 relevant lines covered (86.92%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

16.82
/structuretoolkit/visualize.py
1
# coding: utf-8
2
# Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department
3
# Distributed under the terms of "New BSD License", see the LICENSE file.
4

5
import warnings
1✔
6

7
import numpy as np
1✔
8
from scipy.interpolate import interp1d
1✔
9

10
__author__ = "Joerg Neugebauer, Sudarsan Surendralal"
1✔
11
__copyright__ = (
1✔
12
    "Copyright 2021, Max-Planck-Institut für Eisenforschung GmbH - "
13
    "Computational Materials Design (CM) Department"
14
)
15
__version__ = "1.0"
1✔
16
__maintainer__ = "Sudarsan Surendralal"
1✔
17
__email__ = "surendralal@mpie.de"
1✔
18
__status__ = "production"
1✔
19
__date__ = "Sep 1, 2017"
1✔
20

21

22
def plot3d(
1✔
23
    structure,
24
    mode="NGLview",
25
    show_cell=True,
26
    show_axes=True,
27
    camera="orthographic",
28
    spacefill=True,
29
    particle_size=1.0,
30
    select_atoms=None,
31
    background="white",
32
    color_scheme=None,
33
    colors=None,
34
    scalar_field=None,
35
    scalar_start=None,
36
    scalar_end=None,
37
    scalar_cmap=None,
38
    vector_field=None,
39
    vector_color=None,
40
    magnetic_moments=False,
41
    view_plane=np.array([0, 0, 1]),
42
    distance_from_camera=1.0,
43
    opacity=1.0,
44
):
45
    """
46
    Plot3d relies on NGLView or plotly to visualize atomic structures. Here, we construct a string in the "protein database"
47

48
    The final widget is returned. If it is assigned to a variable, the visualization is suppressed until that
49
    variable is evaluated, and in the meantime more NGL operations can be applied to it to modify the visualization.
50

51
    Args:
52
        mode (str): `NGLView`, `plotly` or `ase`
53
        show_cell (bool): Whether or not to show the frame. (Default is True.)
54
        show_axes (bool): Whether or not to show xyz axes. (Default is True.)
55
        camera (str): 'perspective' or 'orthographic'. (Default is 'perspective'.)
56
        spacefill (bool): Whether to use a space-filling or ball-and-stick representation. (Default is True, use
57
            space-filling atoms.)
58
        particle_size (float): Size of the particles. (Default is 1.)
59
        select_atoms (numpy.ndarray): Indices of atoms to show, either as integers or a boolean array mask.
60
            (Default is None, show all atoms.)
61
        background (str): Background color. (Default is 'white'.)
62
        color_scheme (str): NGLView color scheme to use. (Default is None, color by element.)
63
        colors (numpy.ndarray): A per-atom array of HTML color names or hex color codes to use for atomic colors.
64
            (Default is None, use coloring scheme.)
65
        scalar_field (numpy.ndarray): Color each atom according to the array value (Default is None, use coloring
66
            scheme.)
67
        scalar_start (float): The scalar value to be mapped onto the low end of the color map (lower values are
68
            clipped). (Default is None, use the minimum value in `scalar_field`.)
69
        scalar_end (float): The scalar value to be mapped onto the high end of the color map (higher values are
70
            clipped). (Default is None, use the maximum value in `scalar_field`.)
71
        scalar_cmap (matplotlib.cm): The colormap to use. (Default is None, giving a blue-red divergent map.)
72
        vector_field (numpy.ndarray): Add vectors (3 values) originating at each atom. (Default is None, no
73
            vectors.)
74
        vector_color (numpy.ndarray): Colors for the vectors (only available with vector_field). (Default is None,
75
            vectors are colored by their direction.)
76
        magnetic_moments (bool): Plot magnetic moments as 'scalar_field' or 'vector_field'.
77
        view_plane (numpy.ndarray): A Nx3-array (N = 1,2,3); the first 3d-component of the array specifies
78
            which plane of the system to view (for example, [1, 0, 0], [1, 1, 0] or the [1, 1, 1] planes), the
79
            second 3d-component (if specified, otherwise [1, 0, 0]) gives the horizontal direction, and the third
80
            component (if specified) is the vertical component, which is ignored and calculated internally. The
81
            orthonormality of the orientation is internally ensured, and therefore is not required in the function
82
            call. (Default is np.array([0, 0, 1]), which is view normal to the x-y plane.)
83
        distance_from_camera (float): Distance of the camera from the structure. Higher = farther away.
84
            (Default is 14, which also seems to be the NGLView default value.)
85

86
        Possible NGLView color schemes:
87
          " ", "picking", "random", "uniform", "atomindex", "residueindex",
88
          "chainindex", "modelindex", "sstruc", "element", "resname", "bfactor",
89
          "hydrophobicity", "value", "volume", "occupancy"
90

91
    Returns:
92
        (nglview.NGLWidget): The NGLView widget itself, which can be operated on further or viewed as-is.
93

94
    Warnings:
95
        * Many features only work with space-filling atoms (e.g. coloring by a scalar field).
96
        * The colour interpretation of some hex codes is weird, e.g. 'green'.
97
    """
98
    if mode == "NGLview":
×
99
        return _plot3d(
×
100
            structure=structure,
101
            show_cell=show_cell,
102
            show_axes=show_axes,
103
            camera=camera,
104
            spacefill=spacefill,
105
            particle_size=particle_size,
106
            select_atoms=select_atoms,
107
            background=background,
108
            color_scheme=color_scheme,
109
            colors=colors,
110
            scalar_field=scalar_field,
111
            scalar_start=scalar_start,
112
            scalar_end=scalar_end,
113
            scalar_cmap=scalar_cmap,
114
            vector_field=vector_field,
115
            vector_color=vector_color,
116
            magnetic_moments=magnetic_moments,
117
            view_plane=view_plane,
118
            distance_from_camera=distance_from_camera,
119
        )
120
    elif mode == "plotly":
×
121
        return _plot3d_plotly(
×
122
            structure=structure,
123
            camera=camera,
124
            particle_size=particle_size,
125
            select_atoms=select_atoms,
126
            scalar_field=scalar_field,
127
            view_plane=view_plane,
128
            distance_from_camera=distance_from_camera,
129
            opacity=opacity,
130
        )
131
    elif mode == "ase":
×
132
        return _plot3d_ase(
×
133
            structure=structure,
134
            show_cell=show_cell,
135
            show_axes=show_axes,
136
            camera=camera,
137
            spacefill=spacefill,
138
            particle_size=particle_size,
139
            background=background,
140
            color_scheme=color_scheme,
141
        )
142
    else:
143
        raise ValueError("plot method not recognized")
×
144

145

146
def _plot3d_plotly(
1✔
147
    structure,
148
    scalar_field=None,
149
    select_atoms=None,
150
    particle_size=1.0,
151
    camera="orthographic",
152
    view_plane=np.array([1, 1, 1]),
153
    distance_from_camera=1,
154
    opacity=1,
155
):
156
    """
157
    Make a 3D plot of the atomic structure.
158

159
    Args:
160
        camera (str): 'perspective' or 'orthographic'. (Default is 'perspective'.)
161
        particle_size (float): Size of the particles. (Default is 1.)
162
        scalar_field (numpy.ndarray): Color each atom according to the array value (Default is None, use coloring
163
            scheme.)
164
        view_plane (numpy.ndarray): A Nx3-array (N = 1,2,3); the first 3d-component of the array specifies
165
            which plane of the system to view (for example, [1, 0, 0], [1, 1, 0] or the [1, 1, 1] planes), the
166
            second 3d-component (if specified, otherwise [1, 0, 0]) gives the horizontal direction, and the third
167
            component (if specified) is the vertical component, which is ignored and calculated internally. The
168
            orthonormality of the orientation is internally ensured, and therefore is not required in the function
169
            call. (Default is np.array([0, 0, 1]), which is view normal to the x-y plane.)
170
        distance_from_camera (float): Distance of the camera from the structure. Higher = farther away.
171
            (Default is 14, which also seems to be the NGLView default value.)
172
        opacity (float): opacity
173

174
    Returns:
175
        (plotly.express): The NGLView widget itself, which can be operated on further or viewed as-is.
176

177
    """
178
    try:
×
179
        import plotly.express as px
×
180
    except ModuleNotFoundError:
×
181
        raise ModuleNotFoundError("plotly not installed - use plot3d instead")
×
182
    if select_atoms is None:
×
183
        select_atoms = np.arange(len(structure))
×
184
    elements = structure.get_chemical_symbols()
×
185
    atomic_numbers = structure.get_atomic_numbers()
×
186
    if scalar_field is None:
×
187
        scalar_field = elements
×
188
    fig = px.scatter_3d(
×
189
        x=structure.positions[select_atoms, 0],
190
        y=structure.positions[select_atoms, 1],
191
        z=structure.positions[select_atoms, 2],
192
        color=scalar_field,
193
        opacity=opacity,
194
        size=_atomic_number_to_radius(
195
            atomic_numbers,
196
            scale=particle_size / (0.1 * structure.get_volume() ** (1 / 3)),
197
        ),
198
    )
199
    fig.layout.scene.camera.projection.type = camera
×
200
    rot = _get_orientation(view_plane).T
×
201
    rot[0, :] *= distance_from_camera * 1.25
×
202
    angle = dict(
×
203
        up=dict(x=rot[2, 0], y=rot[2, 1], z=rot[2, 2]),
204
        eye=dict(x=rot[0, 0], y=rot[0, 1], z=rot[0, 2]),
205
    )
206
    fig.update_layout(scene_camera=angle)
×
207
    fig.update_traces(marker=dict(line=dict(width=0.1, color="DarkSlateGrey")))
×
208
    fig.update_scenes(aspectmode="data")
×
209
    return fig
×
210

211

212
def _plot3d(
1✔
213
    structure,
214
    show_cell=True,
215
    show_axes=True,
216
    camera="orthographic",
217
    spacefill=True,
218
    particle_size=1.0,
219
    select_atoms=None,
220
    background="white",
221
    color_scheme=None,
222
    colors=None,
223
    scalar_field=None,
224
    scalar_start=None,
225
    scalar_end=None,
226
    scalar_cmap=None,
227
    vector_field=None,
228
    vector_color=None,
229
    magnetic_moments=False,
230
    view_plane=np.array([0, 0, 1]),
231
    distance_from_camera=1.0,
232
):
233
    """
234
    Plot3d relies on NGLView to visualize atomic structures. Here, we construct a string in the "protein database"
235
    ("pdb") format, then turn it into an NGLView "structure". PDB is a white-space sensitive format, so the
236
    string snippets are carefully formatted.
237

238
    The final widget is returned. If it is assigned to a variable, the visualization is suppressed until that
239
    variable is evaluated, and in the meantime more NGL operations can be applied to it to modify the visualization.
240

241
    Args:
242
        show_cell (bool): Whether or not to show the frame. (Default is True.)
243
        show_axes (bool): Whether or not to show xyz axes. (Default is True.)
244
        camera (str): 'perspective' or 'orthographic'. (Default is 'perspective'.)
245
        spacefill (bool): Whether to use a space-filling or ball-and-stick representation. (Default is True, use
246
            space-filling atoms.)
247
        particle_size (float): Size of the particles. (Default is 1.)
248
        select_atoms (numpy.ndarray): Indices of atoms to show, either as integers or a boolean array mask.
249
            (Default is None, show all atoms.)
250
        background (str): Background color. (Default is 'white'.)
251
        color_scheme (str): NGLView color scheme to use. (Default is None, color by element.)
252
        colors (numpy.ndarray): A per-atom array of HTML color names or hex color codes to use for atomic colors.
253
            (Default is None, use coloring scheme.)
254
        scalar_field (numpy.ndarray): Color each atom according to the array value (Default is None, use coloring
255
            scheme.)
256
        scalar_start (float): The scalar value to be mapped onto the low end of the color map (lower values are
257
            clipped). (Default is None, use the minimum value in `scalar_field`.)
258
        scalar_end (float): The scalar value to be mapped onto the high end of the color map (higher values are
259
            clipped). (Default is None, use the maximum value in `scalar_field`.)
260
        scalar_cmap (matplotlib.cm): The colormap to use. (Default is None, giving a blue-red divergent map.)
261
        vector_field (numpy.ndarray): Add vectors (3 values) originating at each atom. (Default is None, no
262
            vectors.)
263
        vector_color (numpy.ndarray): Colors for the vectors (only available with vector_field). (Default is None,
264
            vectors are colored by their direction.)
265
        magnetic_moments (bool): Plot magnetic moments as 'scalar_field' or 'vector_field'.
266
        view_plane (numpy.ndarray): A Nx3-array (N = 1,2,3); the first 3d-component of the array specifies
267
            which plane of the system to view (for example, [1, 0, 0], [1, 1, 0] or the [1, 1, 1] planes), the
268
            second 3d-component (if specified, otherwise [1, 0, 0]) gives the horizontal direction, and the third
269
            component (if specified) is the vertical component, which is ignored and calculated internally. The
270
            orthonormality of the orientation is internally ensured, and therefore is not required in the function
271
            call. (Default is np.array([0, 0, 1]), which is view normal to the x-y plane.)
272
        distance_from_camera (float): Distance of the camera from the structure. Higher = farther away.
273
            (Default is 14, which also seems to be the NGLView default value.)
274

275
        Possible NGLView color schemes:
276
          " ", "picking", "random", "uniform", "atomindex", "residueindex",
277
          "chainindex", "modelindex", "sstruc", "element", "resname", "bfactor",
278
          "hydrophobicity", "value", "volume", "occupancy"
279

280
    Returns:
281
        (nglview.NGLWidget): The NGLView widget itself, which can be operated on further or viewed as-is.
282

283
    Warnings:
284
        * Many features only work with space-filling atoms (e.g. coloring by a scalar field).
285
        * The colour interpretation of some hex codes is weird, e.g. 'green'.
286
    """
287
    try:  # If the graphical packages are not available, the GUI will not work.
×
288
        import nglview
×
289
    except ImportError:
×
290
        raise ImportError(
×
291
            "The package nglview needs to be installed for the plot3d() function!"
292
        )
293

294
    if (
×
295
        magnetic_moments is True
296
        and np.sum(np.abs(structure.get_initial_magnetic_moments())) > 0
297
    ):
298
        if len(structure.get_initial_magnetic_moments().shape) == 1:
×
299
            scalar_field = structure.get_initial_magnetic_moments()
×
300
        else:
301
            vector_field = structure.get_initial_magnetic_moments()
×
302

303
    elements = structure.get_chemical_symbols()
×
304
    atomic_numbers = structure.get_atomic_numbers()
×
305
    positions = structure.positions
×
306

307
    # If `select_atoms` was given, visualize only a subset of the `parent_basis`
308
    if select_atoms is not None:
×
309
        select_atoms = np.array(select_atoms, dtype=int)
×
310
        elements = elements[select_atoms]
×
311
        atomic_numbers = atomic_numbers[select_atoms]
×
312
        positions = positions[select_atoms]
×
313
        if colors is not None:
×
314
            colors = np.array(colors)
×
315
            colors = colors[select_atoms]
×
316
        if scalar_field is not None:
×
317
            scalar_field = np.array(scalar_field)
×
318
            scalar_field = scalar_field[select_atoms]
×
319
        if vector_field is not None:
×
320
            vector_field = np.array(vector_field)
×
321
            vector_field = vector_field[select_atoms]
×
322
        if vector_color is not None:
×
323
            vector_color = np.array(vector_color)
×
324
            vector_color = vector_color[select_atoms]
×
325

326
    # Write the nglview protein-database-formatted string
327
    struct = nglview.TextStructure(
×
328
        _ngl_write_structure(elements, positions, structure.cell)
329
    )
330

331
    # Parse the string into the displayable widget
332
    view = nglview.NGLWidget(struct)
×
333

334
    if spacefill:
×
335
        # Color by scheme
336
        if color_scheme is not None:
×
337
            if colors is not None:
×
338
                warnings.warn("`color_scheme` is overriding `colors`")
×
339
            if scalar_field is not None:
×
340
                warnings.warn("`color_scheme` is overriding `scalar_field`")
×
341
            view = _add_colorscheme_spacefill(
×
342
                view, elements, atomic_numbers, particle_size, color_scheme
343
            )
344
        # Color by per-atom colors
345
        elif colors is not None:
×
346
            if scalar_field is not None:
×
347
                warnings.warn("`colors` is overriding `scalar_field`")
×
348
            view = _add_custom_color_spacefill(
×
349
                view, atomic_numbers, particle_size, colors
350
            )
351
        # Color by per-atom scalars
352
        elif scalar_field is not None:  # Color by per-atom scalars
×
353
            colors = _scalars_to_hex_colors(
×
354
                scalar_field, scalar_start, scalar_end, scalar_cmap
355
            )
356
            view = _add_custom_color_spacefill(
×
357
                view, atomic_numbers, particle_size, colors
358
            )
359
        # Color by element
360
        else:
361
            view = _add_colorscheme_spacefill(
×
362
                view, elements, atomic_numbers, particle_size
363
            )
364
        view.remove_ball_and_stick()
×
365
    else:
366
        view.add_ball_and_stick()
×
367

368
    if show_cell:
×
369
        if structure.cell is not None:
×
370
            if all(np.max(structure.cell, axis=0) > 1e-2):
×
371
                view.add_unitcell()
×
372

373
    if vector_color is None and vector_field is not None:
×
374
        vector_color = (
×
375
            0.5
376
            * np.array(vector_field)
377
            / np.linalg.norm(vector_field, axis=-1)[:, np.newaxis]
378
            + 0.5
379
        )
380
    elif (
×
381
        vector_field is not None and vector_field is not None
382
    ):  # WARNING: There must be a bug here...
383
        try:
×
384
            if vector_color.shape != np.ones((len(structure), 3)).shape:
×
385
                vector_color = np.outer(
×
386
                    np.ones(len(structure)),
387
                    vector_color / np.linalg.norm(vector_color),
388
                )
389
        except AttributeError:
×
390
            vector_color = np.ones((len(structure), 3)) * vector_color
×
391

392
    if vector_field is not None:
×
393
        for arr, pos, col in zip(vector_field, positions, vector_color):
×
394
            view.shape.add_arrow(list(pos), list(pos + arr), list(col), 0.2)
×
395

396
    if show_axes:  # Add axes
×
397
        axes_origin = -np.ones(3)
×
398
        arrow_radius = 0.1
×
399
        text_size = 1
×
400
        text_color = [0, 0, 0]
×
401
        arrow_names = ["x", "y", "z"]
×
402

403
        for n in [0, 1, 2]:
×
404
            start = list(axes_origin)
×
405
            shift = np.zeros(3)
×
406
            shift[n] = 1
×
407
            end = list(start + shift)
×
408
            color = list(shift)
×
409
            # We cast as list to avoid JSON warnings
410
            view.shape.add_arrow(start, end, color, arrow_radius)
×
411
            view.shape.add_text(end, text_color, text_size, arrow_names[n])
×
412

413
    if camera != "perspective" and camera != "orthographic":
×
414
        warnings.warn(
×
415
            "Only perspective or orthographic is (likely to be) permitted for camera"
416
        )
417

418
    view.camera = camera
×
419
    view.background = background
×
420

421
    orientation = _get_flattened_orientation(
×
422
        view_plane=view_plane, distance_from_camera=distance_from_camera * 14
423
    )
424
    view.control.orient(orientation)
×
425

426
    return view
×
427

428

429
def _plot3d_ase(
1✔
430
    structure,
431
    spacefill=True,
432
    show_cell=True,
433
    camera="perspective",
434
    particle_size=0.5,
435
    background="white",
436
    color_scheme="element",
437
    show_axes=True,
438
):
439
    """
440
    Possible color schemes:
441
      " ", "picking", "random", "uniform", "atomindex", "residueindex",
442
      "chainindex", "modelindex", "sstruc", "element", "resname", "bfactor",
443
      "hydrophobicity", "value", "volume", "occupancy"
444
    Returns:
445
    """
446
    try:  # If the graphical packages are not available, the GUI will not work.
×
447
        import nglview
×
448
    except ImportError:
×
449
        raise ImportError(
×
450
            "The package nglview needs to be installed for the plot3d() function!"
451
        )
452
    # Always visualize the parent basis
453
    view = nglview.show_ase(structure)
×
454
    if spacefill:
×
455
        view.add_spacefill(
×
456
            radius_type="vdw", color_scheme=color_scheme, radius=particle_size
457
        )
458
        # view.add_spacefill(radius=1.0)
459
        view.remove_ball_and_stick()
×
460
    else:
461
        view.add_ball_and_stick()
×
462
    if show_cell:
×
463
        if structure.cell is not None:
×
464
            if all(np.max(structure.cell, axis=0) > 1e-2):
×
465
                view.add_unitcell()
×
466
    if show_axes:
×
467
        view.shape.add_arrow([-2, -2, -2], [2, -2, -2], [1, 0, 0], 0.5)
×
468
        view.shape.add_arrow([-2, -2, -2], [-2, 2, -2], [0, 1, 0], 0.5)
×
469
        view.shape.add_arrow([-2, -2, -2], [-2, -2, 2], [0, 0, 1], 0.5)
×
470
    if camera != "perspective" and camera != "orthographic":
×
471
        print("Only perspective or orthographic is permitted")
×
472
        return None
×
473
    view.camera = camera
×
474
    view.background = background
×
475
    return view
×
476

477

478
def _ngl_write_cell(a1, a2, a3, f1=90, f2=90, f3=90):
1✔
479
    """
480
    Writes a PDB-formatted line to represent the simulation cell.
481

482
    Args:
483
        a1, a2, a3 (float): Lengths of the cell vectors.
484
        f1, f2, f3 (float): Angles between the cell vectors (which angles exactly?) (in degrees).
485

486
    Returns:
487
        (str): The line defining the cell in PDB format.
488
    """
489
    return "CRYST1 {:8.3f} {:8.3f} {:8.3f} {:6.2f} {:6.2f} {:6.2f} P 1\n".format(
×
490
        a1, a2, a3, f1, f2, f3
491
    )
492

493

494
def _ngl_write_atom(
1✔
495
    num,
496
    species,
497
    x,
498
    y,
499
    z,
500
    group=None,
501
    num2=None,
502
    occupancy=1.0,
503
    temperature_factor=0.0,
504
):
505
    """
506
    Writes a PDB-formatted line to represent an atom.
507

508
    Args:
509
        num (int): Atomic index.
510
        species (str): Elemental species.
511
        x, y, z (float): Cartesian coordinates of the atom.
512
        group (str): A...group name? (Default is None, repeat elemental species.)
513
        num2 (int): An "alternate" index. (Don't ask me...) (Default is None, repeat first number.)
514
        occupancy (float): PDB occupancy parameter. (Default is 1.)
515
        temperature_factor (float): PDB temperature factor parameter. (Default is 0.
516

517
    Returns:
518
        (str): The line defining an atom in PDB format
519

520
    Warnings:
521
        * The [PDB docs](https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/tutorials/pdbintro.html) indicate that
522
            the xyz coordinates might need to be in some sort of orthogonal basis. If you have weird behaviour,
523
            this might be a good place to investigate.
524
    """
525
    if group is None:
×
526
        group = species
×
527
    if num2 is None:
×
528
        num2 = num
×
529
    return "ATOM {:>6} {:>4} {:>4} {:>5} {:10.3f} {:7.3f} {:7.3f} {:5.2f} {:5.2f} {:>11} \n".format(
×
530
        num, species, group, num2, x, y, z, occupancy, temperature_factor, species
531
    )
532

533

534
def _ngl_write_structure(elements, positions, cell):
1✔
535
    """
536
    Turns structure information into a NGLView-readable protein-database-formatted string.
537

538
    Args:
539
        elements (numpy.ndarray/list): Element symbol for each atom.
540
        positions (numpy.ndarray/list): Vector of Cartesian atom positions.
541
        cell (numpy.ndarray/list): Simulation cell Bravais matrix.
542

543
    Returns:
544
        (str): The PDB-formatted representation of the structure.
545
    """
546
    from ase.geometry import cell_to_cellpar, cellpar_to_cell
×
547

548
    if cell is None or any(np.max(cell, axis=0) < 1e-2):
×
549
        # Define a dummy cell if it doesn't exist (eg. for clusters)
550
        max_pos = np.max(positions, axis=0) - np.min(positions, axis=0)
×
551
        max_pos[np.abs(max_pos) < 1e-2] = 10
×
552
        cell = np.eye(3) * max_pos
×
553
    cellpar = cell_to_cellpar(cell)
×
554
    exportedcell = cellpar_to_cell(cellpar)
×
555
    rotation = np.linalg.solve(cell, exportedcell)
×
556

557
    pdb_str = _ngl_write_cell(*cellpar)
×
558
    pdb_str += "MODEL     1\n"
×
559

560
    if rotation is not None:
×
561
        positions = np.array(positions).dot(rotation)
×
562

563
    for i, p in enumerate(positions):
×
564
        pdb_str += _ngl_write_atom(i, elements[i], *p)
×
565

566
    pdb_str += "ENDMDL \n"
×
567
    return pdb_str
×
568

569

570
def _atomic_number_to_radius(atomic_number, shift=0.2, slope=0.1, scale=1.0):
1✔
571
    """
572
    Give the atomic radius for plotting, which scales like the root of the atomic number.
573

574
    Args:
575
        atomic_number (int/float): The atomic number.
576
        shift (float): A constant addition to the radius. (Default is 0.2.)
577
        slope (float): A multiplier for the root of the atomic number. (Default is 0.1)
578
        scale (float): How much to rescale the whole thing by.
579

580
    Returns:
581
        (float): The radius. (Not physical, just for visualization!)
582
    """
583
    return (shift + slope * np.sqrt(atomic_number)) * scale
×
584

585

586
def _add_colorscheme_spacefill(
1✔
587
    view, elements, atomic_numbers, particle_size, scheme="element"
588
):
589
    """
590
    Set NGLView spacefill parameters according to a color-scheme.
591

592
    Args:
593
        view (NGLWidget): The widget to work on.
594
        elements (numpy.ndarray/list): Elemental symbols.
595
        atomic_numbers (numpy.ndarray/list): Integer atomic numbers for determining atomic size.
596
        particle_size (float): A scale factor for the atomic size.
597
        scheme (str): The scheme to use. (Default is "element".)
598

599
        Possible NGLView color schemes:
600
          " ", "picking", "random", "uniform", "atomindex", "residueindex",
601
          "chainindex", "modelindex", "sstruc", "element", "resname", "bfactor",
602
          "hydrophobicity", "value", "volume", "occupancy"
603

604
    Returns:
605
        (nglview.NGLWidget): The modified widget.
606
    """
607
    for elem, num in set(list(zip(elements, atomic_numbers))):
×
608
        view.add_spacefill(
×
609
            selection="#" + elem,
610
            radius_type="vdw",
611
            radius=_atomic_number_to_radius(num, scale=particle_size),
612
            color_scheme=scheme,
613
        )
614
    return view
×
615

616

617
def _add_custom_color_spacefill(view, atomic_numbers, particle_size, colors):
1✔
618
    """
619
    Set NGLView spacefill parameters according to per-atom colors.
620

621
    Args:
622
        view (NGLWidget): The widget to work on.
623
        atomic_numbers (numpy.ndarray/list): Integer atomic numbers for determining atomic size.
624
        particle_size (float): A scale factor for the atomic size.
625
        colors (numpy.ndarray/list): A per-atom list of HTML or hex color codes.
626

627
    Returns:
628
        (nglview.NGLWidget): The modified widget.
629
    """
630
    for n, num in enumerate(atomic_numbers):
×
631
        view.add_spacefill(
×
632
            selection=[n],
633
            radius_type="vdw",
634
            radius=_atomic_number_to_radius(num, scale=particle_size),
635
            color=colors[n],
636
        )
637
    return view
×
638

639

640
def _scalars_to_hex_colors(scalar_field, start=None, end=None, cmap=None):
1✔
641
    """
642
    Convert scalar values to hex codes using a colormap.
643

644
    Args:
645
        scalar_field (numpy.ndarray/list): Scalars to convert.
646
        start (float): Scalar value to map to the bottom of the colormap (values below are clipped). (Default is
647
            None, use the minimal scalar value.)
648
        end (float): Scalar value to map to the top of the colormap (values above are clipped).  (Default is
649
            None, use the maximal scalar value.)
650
        cmap (matplotlib.cm): The colormap to use. (Default is None, which gives a blue-red divergent map.)
651

652
    Returns:
653
        (list): The corresponding hex codes for each scalar value passed in.
654
    """
655
    from matplotlib.colors import rgb2hex
×
656

657
    if start is None:
×
658
        start = np.amin(scalar_field)
×
659
    if end is None:
×
660
        end = np.amax(scalar_field)
×
661
    interp = interp1d([start, end], [0, 1])
×
662
    remapped_field = interp(np.clip(scalar_field, start, end))  # Map field onto [0,1]
×
663

664
    if cmap is None:
×
665
        try:
×
666
            from seaborn import diverging_palette
×
667
        except ImportError:
×
668
            print(
×
669
                "The package seaborn needs to be installed for the plot3d() function!"
670
            )
671
        cmap = diverging_palette(245, 15, as_cmap=True)  # A nice blue-red palette
×
672

673
    return [
×
674
        rgb2hex(cmap(scalar)[:3]) for scalar in remapped_field
675
    ]  # The slice gets RGB but leaves alpha
676

677

678
def _get_orientation(view_plane):
1✔
679
    """
680
    A helper method to plot3d, which generates a rotation matrix from the input `view_plane`, and returns a
681
    flattened list of len = 16. This flattened list becomes the input argument to `view.contol.orient`.
682

683
    Args:
684
        view_plane (numpy.ndarray/list): A Nx3-array/list (N = 1,2,3); the first 3d-component of the array
685
            specifies which plane of the system to view (for example, [1, 0, 0], [1, 1, 0] or the [1, 1, 1] planes),
686
            the second 3d-component (if specified, otherwise [1, 0, 0]) gives the horizontal direction, and the
687
            third component (if specified) is the vertical component, which is ignored and calculated internally.
688
            The orthonormality of the orientation is internally ensured, and therefore is not required in the
689
            function call.
690

691
    Returns:
692
        (list): orientation tensor
693
    """
694
    if len(np.array(view_plane).flatten()) % 3 != 0:
1✔
695
        raise ValueError(
×
696
            "The shape of view plane should be (N, 3), where N = 1, 2 or 3. Refer docs for more info."
697
        )
698
    view_plane = np.array(view_plane).reshape(-1, 3)
1✔
699
    rotation_matrix = np.roll(np.eye(3), -1, axis=0)
1✔
700
    rotation_matrix[: len(view_plane)] = view_plane
1✔
701
    rotation_matrix /= np.linalg.norm(rotation_matrix, axis=-1)[:, np.newaxis]
1✔
702
    rotation_matrix[1] -= (
1✔
703
        np.dot(rotation_matrix[0], rotation_matrix[1]) * rotation_matrix[0]
704
    )  # Gran-Schmidt
705
    rotation_matrix[2] = np.cross(
1✔
706
        rotation_matrix[0], rotation_matrix[1]
707
    )  # Specify third axis
708
    if np.isclose(np.linalg.det(rotation_matrix), 0):
1✔
709
        return np.eye(
×
710
            3
711
        )  # view_plane = [0,0,1] is the default view of NGLview, so we do not modify it
712
    return np.roll(
1✔
713
        rotation_matrix / np.linalg.norm(rotation_matrix, axis=-1)[:, np.newaxis],
714
        2,
715
        axis=0,
716
    ).T
717

718

719
def _get_flattened_orientation(view_plane, distance_from_camera):
1✔
720
    """
721
    A helper method to plot3d, which generates a rotation matrix from the input `view_plane`, and returns a
722
    flattened list of len = 16. This flattened list becomes the input argument to `view.contol.orient`.
723

724
    Args:
725
        view_plane (numpy.ndarray/list): A Nx3-array/list (N = 1,2,3); the first 3d-component of the array
726
            specifies which plane of the system to view (for example, [1, 0, 0], [1, 1, 0] or the [1, 1, 1] planes),
727
            the second 3d-component (if specified, otherwise [1, 0, 0]) gives the horizontal direction, and the
728
            third component (if specified) is the vertical component, which is ignored and calculated internally.
729
            The orthonormality of the orientation is internally ensured, and therefore is not required in the
730
            function call.
731
        distance_from_camera (float): Distance of the camera from the structure. Higher = farther away.
732

733
    Returns:
734
        (list): Flattened list of len = 16, which is the input argument to `view.contol.orient`
735
    """
736
    if distance_from_camera <= 0:
1✔
737
        raise ValueError("´distance_from_camera´ must be a positive float!")
×
738
    flattened_orientation = np.eye(4)
1✔
739
    flattened_orientation[:3, :3] = _get_orientation(view_plane)
1✔
740

741
    return (distance_from_camera * flattened_orientation).ravel().tolist()
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc