Skip to content

utils

Utility functions to help the conversion functions along.

Most of these functions rely on the NumPy library.

cell_to_cellpar(cell, radians=False)

Returns the cell parameters [a, b, c, alpha, beta, gamma].

Angles are in degrees unless radian=True is used.

Note

Based on ASE code.

Parameters:

Name Type Description Default
cell tuple[Vector3D, Vector3D, Vector3D]

A Cartesian 3x3 cell. This equates to the lattice_vectors attribute.

required
radians bool

Use radians instead of degrees (default) for angles.

False

Returns:

Type Description
list[float]

The unit cell parameters as a list of float values.

Source code in optimade/adapters/structures/utils.py
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
def cell_to_cellpar(
    cell: tuple[Vector3D, Vector3D, Vector3D], radians: bool = False
) -> list[float]:
    """Returns the cell parameters `[a, b, c, alpha, beta, gamma]`.

    Angles are in degrees unless `radian=True` is used.

    Note:
        Based on [ASE code](https://wiki.fysik.dtu.dk/ase/_modules/ase/geometry/cell.html#cell_to_cellpar).

    Parameters:
        cell: A Cartesian 3x3 cell. This equates to the
            [`lattice_vectors`][optimade.models.structures.StructureResourceAttributes.lattice_vectors] attribute.
        radians: Use radians instead of degrees (default) for angles.

    Returns:
        The unit cell parameters as a `list` of `float` values.

    """
    if globals().get("np", None) is None:
        warn(NUMPY_NOT_FOUND, AdapterPackageNotFound)
        return None  # type: ignore[return-value]

    cell = np.asarray(cell)

    lengths = [np.linalg.norm(vector) for vector in cell]
    angles = []
    for i in range(3):
        j = i - 1
        k = i - 2
        outer_product = lengths[j] * lengths[k]
        if outer_product > 1e-16:
            x_vector = np.dot(cell[j], cell[k]) / outer_product
            angle = 180.0 / np.pi * np.arccos(x_vector)
        else:
            angle = 90.0
        angles.append(angle)
    if radians:
        angles = [angle * np.pi / 180 for angle in angles]
    return np.array(lengths + angles)

cellpar_to_cell(cellpar, ab_normal=(0, 0, 1), a_direction=None)

Return a 3x3 cell matrix from cellpar=[a,b,c,alpha,beta,gamma].

Angles must be in degrees.

The returned cell is orientated such that a and b are normal to ab_normal and a is parallel to the projection of a_direction in the a-b plane.

Default a_direction is (1,0,0), unless this is parallel to ab_normal, in which case default a_direction is (0,0,1).

The returned cell has the vectors va, vb and vc along the rows. The cell will be oriented such that va and vb are normal to ab_normal and va will be along the projection of a_direction onto the a-b plane.

Example

cell = cellpar_to_cell([1, 2, 4, 10, 20, 30], (0, 1, 1), (1, 2, 3)) np.round(cell, 3) array([[ 0.816, -0.408, 0.408], [ 1.992, -0.13 , 0.13 ], [ 3.859, -0.745, 0.745]])

Note

Direct copy of ASE code.

Parameters:

Name Type Description Default
cellpar list[float]

The unit cell parameters as a list of float values.

Note: The angles must be given in degrees.

required
ab_normal tuple[int, int, int]

Unit vector normal to the ab-plane.

(0, 0, 1)
a_direction Optional[tuple[int, int, int]]

Unit vector defining the a-direction (default: (1, 0, 0)).

None

Returns:

Type Description
list[Vector3D]

A Cartesian 3x3 cell.

list[Vector3D]

This should equate to the

list[Vector3D]

lattice_vectors attribute.

Source code in optimade/adapters/structures/utils.py
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
270
271
272
273
274
275
276
def cellpar_to_cell(
    cellpar: list[float],
    ab_normal: tuple[int, int, int] = (0, 0, 1),
    a_direction: Optional[tuple[int, int, int]] = None,
) -> list[Vector3D]:
    """Return a 3x3 cell matrix from `cellpar=[a,b,c,alpha,beta,gamma]`.

    Angles must be in degrees.

    The returned cell is orientated such that a and b
    are normal to `ab_normal` and a is parallel to the projection of
    `a_direction` in the a-b plane.

    Default `a_direction` is (1,0,0), unless this is parallel to
    `ab_normal`, in which case default `a_direction` is (0,0,1).

    The returned cell has the vectors va, vb and vc along the rows. The
    cell will be oriented such that va and vb are normal to `ab_normal`
    and va will be along the projection of `a_direction` onto the a-b
    plane.

    Example:
        >>> cell = cellpar_to_cell([1, 2, 4, 10, 20, 30], (0, 1, 1), (1, 2, 3))
        >>> np.round(cell, 3)
        array([[ 0.816, -0.408,  0.408],
            [ 1.992, -0.13 ,  0.13 ],
            [ 3.859, -0.745,  0.745]])

    Note:
        Direct copy of [ASE code](https://wiki.fysik.dtu.dk/ase/_modules/ase/geometry/cell.html#cellpar_to_cell).

    Parameters:
        cellpar: The unit cell parameters as a `list` of `float` values.

            **Note**: The angles must be given in degrees.
        ab_normal: Unit vector normal to the ab-plane.
        a_direction: Unit vector defining the a-direction (default: `(1, 0, 0)`).

    Returns:
        A Cartesian 3x3 cell.

        This should equate to the
        [`lattice_vectors`][optimade.models.structures.StructureResourceAttributes.lattice_vectors] attribute.

    """
    if globals().get("np", None) is None:
        warn(NUMPY_NOT_FOUND, AdapterPackageNotFound)
        return None  # type: ignore[return-value]

    if a_direction is None:
        if np.linalg.norm(np.cross(ab_normal, (1, 0, 0))) < 1e-5:
            a_direction = (0, 0, 1)
        else:
            a_direction = (1, 0, 0)

    # Define rotated X,Y,Z-system, with Z along ab_normal and X along
    # the projection of a_direction onto the normal plane of Z.
    a_direction_array = np.array(a_direction)
    Z = unit_vector(ab_normal)  # type: ignore
    X = unit_vector(a_direction_array - np.dot(a_direction_array, Z) * Z)
    Y = np.cross(Z, X)

    # Express va, vb and vc in the X,Y,Z-system
    alpha, beta, gamma = 90.0, 90.0, 90.0
    if isinstance(cellpar, (int, float)):
        a = b = c = cellpar
    elif len(cellpar) == 1:
        a = b = c = cellpar[0]
    elif len(cellpar) == 3:
        a, b, c = cellpar
    else:
        a, b, c, alpha, beta, gamma = cellpar

    # Handle orthorhombic cells separately to avoid rounding errors
    eps = 2 * np.spacing(90.0, dtype=np.float64)  # around 1.4e-14
    # alpha
    if abs(abs(alpha) - 90) < eps:
        cos_alpha = 0.0
    else:
        cos_alpha = np.cos(alpha * np.pi / 180.0)
    # beta
    if abs(abs(beta) - 90) < eps:
        cos_beta = 0.0
    else:
        cos_beta = np.cos(beta * np.pi / 180.0)
    # gamma
    if abs(gamma - 90) < eps:
        cos_gamma = 0.0
        sin_gamma = 1.0
    elif abs(gamma + 90) < eps:
        cos_gamma = 0.0
        sin_gamma = -1.0
    else:
        cos_gamma = np.cos(gamma * np.pi / 180.0)
        sin_gamma = np.sin(gamma * np.pi / 180.0)

    # Build the cell vectors
    va = a * np.array([1, 0, 0])
    vb = b * np.array([cos_gamma, sin_gamma, 0])
    cx = cos_beta
    cy = (cos_alpha - cos_beta * cos_gamma) / sin_gamma
    cz_sqr = 1.0 - cx * cx - cy * cy
    assert cz_sqr >= 0
    cz = np.sqrt(cz_sqr)
    vc = c * np.array([cx, cy, cz])

    # Convert to the Cartesian x,y,z-system
    abc = np.vstack((va, vb, vc))
    T = np.vstack((X, Y, Z))
    cell = np.dot(abc, T)

    return cell

elements_ratios_from_species_at_sites(species_at_sites)

Compute the OPTIMADE elements_ratios field from species_at_sites in the case where species_at_sites refers to sites wholly occupied by the given elements, e.g., not arbitrary species labels or with partial/mixed occupancy.

Source code in optimade/adapters/structures/utils.py
362
363
364
365
366
367
368
369
370
def elements_ratios_from_species_at_sites(species_at_sites: list[str]) -> list[float]:
    """Compute the OPTIMADE `elements_ratios` field from `species_at_sites` in the case where `species_at_sites` refers
    to sites wholly occupied by the given elements, e.g., not arbitrary species labels or with partial/mixed occupancy.

    """
    elements = set(species_at_sites)
    counts = {e: species_at_sites.count(e) for e in elements}
    num_sites = len(species_at_sites)
    return [counts[e] / num_sites for e in sorted(elements)]

fractional_coordinates(cell, cartesian_positions)

Returns fractional coordinates and wraps coordinates to [0,1[.

Note

Based on ASE code.

Parameters:

Name Type Description Default
cell tuple[Vector3D, Vector3D, Vector3D]

A Cartesian 3x3 cell. This equates to the lattice_vectors attribute.

required
cartesian_positions list[Vector3D]

A list of cartesian atomic positions. This equates to the cartesian_site_positions attribute.

required

Returns:

Type Description
list[Vector3D]

A list of fractional coordinates for the atomic positions.

Source code in optimade/adapters/structures/utils.py
 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
def fractional_coordinates(
    cell: tuple[Vector3D, Vector3D, Vector3D], cartesian_positions: list[Vector3D]
) -> list[Vector3D]:
    """Returns fractional coordinates and wraps coordinates to `[0,1[`.

    Note:
        Based on [ASE code](https://wiki.fysik.dtu.dk/ase/_modules/ase/atoms.html#Atoms.get_scaled_positions).

    Parameters:
        cell: A Cartesian 3x3 cell. This equates to the
            [`lattice_vectors`][optimade.models.structures.StructureResourceAttributes.lattice_vectors] attribute.
        cartesian_positions: A list of cartesian atomic positions. This equates to the
            [`cartesian_site_positions`][optimade.models.structures.StructureResourceAttributes.cartesian_site_positions]
            attribute.

    Returns:
        A list of fractional coordinates for the atomic positions.

    """
    if globals().get("np", None) is None:
        warn(NUMPY_NOT_FOUND, AdapterPackageNotFound)
        return None  # type: ignore[return-value]

    cell_array = np.asarray(cell)
    cartesian_positions_array = np.asarray(cartesian_positions)

    fractional = np.linalg.solve(cell_array.T, cartesian_positions_array.T).T

    # Expecting a bulk 3D structure here, note, this may change in the future.
    # See `ase.atoms:Atoms.get_scaled_positions()` for ideas on how to handle lower dimensional structures.
    # Furthermore, according to ASE we need to modulo 1.0 twice.
    # This seems to be due to small floats % 1.0 becomes 1.0, hence twice makes it 0.0.
    for i in range(3):
        fractional[:, i] %= 1.0
        fractional[:, i] %= 1.0

    return [tuple(position) for position in fractional]  # type: ignore

pad_cell(lattice_vectors, padding=None)

Turn any null/None values into a float in given tuple of lattice_vectors.

Parameters:

Name Type Description Default
lattice_vectors tuple[Vector3D, Vector3D, Vector3D]

A 3x3 cartesian cell. This is the lattice_vectors attribute.

required
padding Optional[float]

A value with which null or None values should be replaced.

None

Returns:

Type Description
tuple

The possibly redacted/padded lattice_vectors and a bool declaring whether or not

tuple

the value has been redacted/padded or not, i.e., whether it contained null or None

tuple

values.

Source code in optimade/adapters/structures/utils.py
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
def pad_cell(
    lattice_vectors: tuple[Vector3D, Vector3D, Vector3D],
    padding: Optional[float] = None,
) -> tuple:  # Setting this properly makes MkDocs fail.
    """Turn any `null`/`None` values into a `float` in given `tuple` of
    [`lattice_vectors`][optimade.models.structures.StructureResourceAttributes.lattice_vectors].

    Parameters:
        lattice_vectors: A 3x3 cartesian cell. This is the
            [`lattice_vectors`][optimade.models.structures.StructureResourceAttributes.lattice_vectors]
            attribute.
        padding: A value with which `null` or `None` values should be replaced.

    Returns:
        The possibly redacted/padded `lattice_vectors` and a `bool` declaring whether or not
        the value has been redacted/padded or not, i.e., whether it contained `null` or `None`
        values.

    """
    return _pad_iter_of_iters(
        iterable=lattice_vectors,
        padding=padding,
        outer=tuple,
        inner=tuple,
    )

scaled_cell(cell)

Return a scaled 3x3 cell from cartesian 3x3 cell (lattice_vectors). This 3x3 matrix can be used to calculate the fractional coordinates from the cartesian_site_positions.

This is based on PDB's method of calculating SCALE from CRYST data. For more info, see this site.

Parameters:

Name Type Description Default
cell tuple[Vector3D, Vector3D, Vector3D]

A Cartesian 3x3 cell. This equates to the lattice_vectors attribute.

required

Returns:

Type Description
tuple[Vector3D, Vector3D, Vector3D]

A scaled 3x3 cell.

Source code in optimade/adapters/structures/utils.py
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
def scaled_cell(
    cell: tuple[Vector3D, Vector3D, Vector3D],
) -> tuple[Vector3D, Vector3D, Vector3D]:
    """Return a scaled 3x3 cell from cartesian 3x3 cell (`lattice_vectors`).
    This 3x3 matrix can be used to calculate the fractional coordinates from the cartesian_site_positions.

    This is based on PDB's method of calculating SCALE from CRYST data.
    For more info, see [this site](https://www.wwpdb.org/documentation/file-format-content/format33/sect8.html#SCALEn).

    Parameters:
        cell: A Cartesian 3x3 cell. This equates to the
            [`lattice_vectors`][optimade.models.structures.StructureResourceAttributes.lattice_vectors] attribute.

    Returns:
        A scaled 3x3 cell.

    """
    if globals().get("np", None) is None:
        warn(NUMPY_NOT_FOUND, AdapterPackageNotFound)
        return None  # type: ignore[return-value]

    cell = np.asarray(cell)

    volume = np.dot(cell[0], np.cross(cell[1], cell[2]))
    scale = []
    for i in range(3):
        vector = np.cross(cell[(i + 1) % 3], cell[(i + 2) % 3]) / volume
        scale.append(tuple(vector))
    return tuple(scale)  # type: ignore[return-value]

species_from_species_at_sites(species_at_sites)

When a list of species dictionaries is not provided, this function can be used to infer the species from the provided species_at_sites.

In this use case, species_at_sites is assumed to provide a list of element symbols, and refers to situations with no mixed occupancy, i.e., the constructed species list will contain all unique species with concentration equal to 1 and the species_at_site tag will be used as the chemical symbol.

Parameters:

Name Type Description Default
species_at_sites list[str]

The list found under the species_at_sites field.

required

Returns:

Type Description
list[Species]

An OPTIMADE species list.

Source code in optimade/adapters/structures/utils.py
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
def species_from_species_at_sites(
    species_at_sites: list[str],
) -> list[OptimadeStructureSpecies]:
    """When a list of species dictionaries is not provided, this function
    can be used to infer the species from the provided species_at_sites.

    In this use case, species_at_sites is assumed to provide a list of
    element symbols, and refers to situations with no mixed occupancy, i.e.,
    the constructed species list will contain all unique species with
    concentration equal to 1 and the species_at_site tag will be used as
    the chemical symbol.

    Parameters:
        species_at_sites: The list found under the species_at_sites field.

    Returns:
        An OPTIMADE species list.

    """
    return [
        OptimadeStructureSpecies(name=_, concentration=[1.0], chemical_symbols=[_])
        for _ in set(species_at_sites)
    ]

unit_vector(x)

Return a unit vector in the same direction as x.

Parameters:

Name Type Description Default
x Vector3D

A three-dimensional vector.

required

Returns:

Type Description
Vector3D

A unit vector in the same direction as x.

Source code in optimade/adapters/structures/utils.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def unit_vector(x: Vector3D) -> Vector3D:
    """Return a unit vector in the same direction as `x`.

    Parameters:
        x: A three-dimensional vector.

    Returns:
        A unit vector in the same direction as `x`.

    """
    if globals().get("np", None) is None:
        warn(NUMPY_NOT_FOUND, AdapterPackageNotFound)
        return None  # type: ignore[return-value]

    y = np.array(x, dtype="float")
    return y / np.linalg.norm(y)  # type: ignore