Skip to content

Structure generation

Functions for generating random glass compositions and structures, useful for building training sets.

balance_charge(amounts, atoms, charges_dict, max_iter=1)

Adjust one atom's amount so the total ionic charge of a composition is closer to zero.

Parameters:

Name Type Description Default
amounts List[int]

The (scaled integer) amounts of each atom type.

required
atoms List[str]

The chemical symbols corresponding to each entry in amounts.

required
charges_dict dict

A mapping from chemical symbol to oxidation state.

required
max_iter int

Unused; retained for API compatibility. Defaults to 1.

1

Returns:

Type Description

Tuple[List[int], float]: The (possibly adjusted) amounts, and the resulting total charge.

Source code in src/vitrum/structure_gen.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def balance_charge(amounts, atoms, charges_dict, max_iter=1):
    """
    Adjust one atom's amount so the total ionic charge of a composition is closer to zero.

    Args:
        amounts (List[int]): The (scaled integer) amounts of each atom type.
        atoms (List[str]): The chemical symbols corresponding to each entry in amounts.
        charges_dict (dict): A mapping from chemical symbol to oxidation state.
        max_iter (int, optional): Unused; retained for API compatibility. Defaults to 1.

    Returns:
        Tuple[List[int], float]: The (possibly adjusted) amounts, and the resulting total charge.
    """
    total_charge = sum(a * charges_dict[atom] for a, atom in zip(amounts, atoms))
    candidates = [(i, charges_dict[atom]) for i, atom in enumerate(atoms)]
    valid_candidates = [c for c in candidates if is_multiple(c[1], total_charge)]
    if valid_candidates:
        idx, cand_charge = min(valid_candidates, key=lambda c: abs(abs(total_charge) - abs(c[1])))
        adjustment = int(round(total_charge / cand_charge))
        amounts[idx] += -adjustment
        new_charge = sum(a * charges_dict[atom] for a, atom in zip(amounts, atoms))
    else:
        new_charge = total_charge
    return amounts, new_charge

choose_count(weights)

Randomly choose an index (interpreted as a count) according to given weights.

Parameters:

Name Type Description Default
weights List[float]

Relative weights for each possible count, starting at 0.

required

Returns:

Name Type Description
int

The chosen count.

Source code in src/vitrum/structure_gen.py
64
65
66
67
68
69
70
71
72
73
74
75
def choose_count(weights):
    """
    Randomly choose an index (interpreted as a count) according to given weights.

    Args:
        weights (List[float]): Relative weights for each possible count, starting at 0.

    Returns:
        int: The chosen count.
    """
    outcomes = list(range(len(weights)))
    return random.choices(outcomes, weights=weights, k=1)[0]

gen_random_glasses(modifiers, formers, anions, weights={}, num_structures=30, target_atoms=100, **kwargs)

Generate random glass structures from the atoms in given modifiers, formers and anions.

Parameters:

Name Type Description Default
modifiers list

A list of the chemical symbols of the modifiers.

required
formers list

A list of the chemical symbols of the network formers.

required
anions list

A list of the chemical symbols of the anions.

required
weights dict

A dictionary of weights for the number of modifiers, formers and anions.

{}
num_structures int

The number of structures to generate. Defaults to 30.

30
target_atoms int

The target number of atoms in the structure. Defaults to 100.

100
**kwargs

Additional keyword arguments to be passed to the get_random_packed function.

{}

Returns:

Name Type Description
compositions list

A list of random packed structures.

Source code in src/vitrum/structure_gen.py
 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
def gen_random_glasses(modifiers, formers, anions, weights={}, num_structures=30, target_atoms=100, **kwargs):
    """
    Generate random glass structures from the atoms in given modifiers, formers and anions.

    Parameters:
        modifiers (list): A list of the chemical symbols of the modifiers.
        formers (list): A list of the chemical symbols of the network formers.
        anions (list): A list of the chemical symbols of the anions.
        weights (dict): A dictionary of weights for the number of modifiers, formers and anions.
        num_structures (int, optional): The number of structures to generate. Defaults to 30.
        target_atoms (int, optional): The target number of atoms in the structure. Defaults to 100.
        **kwargs: Additional keyword arguments to be passed to the get_random_packed function.

    Returns:
        compositions (list): A list of random packed structures.
    """
    charges_modifiers = {e: Element(e).oxidation_states[-1] for e in modifiers}
    charges_formers = {e: Element(e).oxidation_states[-1] for e in formers}
    charges_anions = {e: Element(e).oxidation_states[0] for e in anions}

    charges = charges_modifiers | charges_formers | charges_anions
    compositions = []
    composition_sets = set()

    num_mod_weights = weights.get("num_mod_weights", [0.5, 0.5, 0, 0])
    num_former_weights = weights.get("num_former_weights", [0.05, 0.65, 0.3, 0])
    num_anion_weights = weights.get("num_anion_weights", [0, 0.6, 0.4, 0])

    bias_modifiers = weights.get("bias_modifiers", [1 for _ in modifiers])
    bias_modifiers = bias_modifiers / np.sum(bias_modifiers)
    bias_formers = weights.get("bias_formers", [1 for _ in formers])
    bias_formers = bias_formers / np.sum(bias_formers)
    bias_anions = weights.get("bias_anions", [1 for _ in anions])
    bias_anions = bias_anions / np.sum(bias_anions)

    pbar = tqdm.tqdm(total=num_structures)
    print("Generated structures")
    while len(compositions) < num_structures:
        pbar.n = len(compositions)
        pbar.refresh()

        num_mod = choose_count(num_mod_weights)
        num_former = choose_count(num_former_weights)
        num_anion = choose_count(num_anion_weights)

        if (num_mod + num_former) == 0 or num_anion == 0:
            continue

        chosen_mods = np.random.choice(modifiers, num_mod, replace=False, p=bias_modifiers) if num_mod else []
        chosen_formers = np.random.choice(formers, num_former, replace=False, p=bias_formers) if num_former else []
        chosen_anions = np.random.choice(anions, num_anion, replace=False, p=bias_anions) if num_anion else []

        amounts = []

        mod_form_ratio = random_partition(2) if num_mod and num_former else [int(bool(num_mod)), int(bool(num_former))]
        mod_ratio = random_partition(num_mod)
        form_ratio = random_partition(num_former)
        anion_ratio = random_partition(num_anion)

        avg_mod_charge = np.sum([charges[chosen_mod] * mod_ratio[ind] for ind, chosen_mod in enumerate(chosen_mods)])
        avg_form_charge = np.sum(
            [charges[chosen_form] * form_ratio[ind] for ind, chosen_form in enumerate(chosen_formers)]
        )
        avg_anion_charge = np.sum(
            [charges[chosen_anion] * anion_ratio[ind] for ind, chosen_anion in enumerate(chosen_anions)]
        )

        avg_cation_charge = mod_form_ratio[0] * avg_mod_charge + mod_form_ratio[1] * avg_form_charge
        cation_anion_ratio = abs(avg_cation_charge) / abs(avg_anion_charge)

        if num_mod > 0:
            amounts.extend([my_round(modifier_ratio * mod_form_ratio[0]) for modifier_ratio in mod_ratio])
        if num_former > 0:
            amounts.extend([my_round(former_ratio * mod_form_ratio[1]) for former_ratio in form_ratio])
        if num_anion > 0:
            amounts.extend([my_round(ani_ratio * cation_anion_ratio) for ani_ratio in anion_ratio])
        atoms = list(chosen_mods) + list(chosen_formers) + list(chosen_anions)
        int_amounts = (np.array(amounts) * 100).astype(int)
        total_charge = sum(amt * charges[atom] for amt, atom in zip(int_amounts, atoms))

        if total_charge != 0:
            int_amounts_list = int_amounts.tolist()
            int_amounts_list, final_charge = balance_charge(int_amounts_list, atoms, charges, max_iter=1)
            if final_charge != 0:
                continue
            int_amounts = np.array(int_amounts_list)

        formula = "".join(f"{atom}{amt}" for atom, amt in zip(atoms, int_amounts))
        if formula in composition_sets:
            continue
        composition_sets.add(formula)
        rand_atoms = get_random_packed(formula, target_atoms=target_atoms, **kwargs)

        if len(rand_atoms) > 200:
            continue

        compositions.append(rand_atoms)
    return compositions

is_multiple(c, total, tol=1e-09)

Check whether total is (approximately) an integer multiple of c.

Parameters:

Name Type Description Default
c float

The candidate divisor (e.g. an oxidation state).

required
total float

The value to test.

required
tol float

Absolute tolerance for the closeness check. Defaults to 1e-9.

1e-09

Returns:

Name Type Description
bool

True if total/c is close to an integer.

Source code in src/vitrum/structure_gen.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def is_multiple(c, total, tol=1e-9):
    """
    Check whether `total` is (approximately) an integer multiple of `c`.

    Args:
        c (float): The candidate divisor (e.g. an oxidation state).
        total (float): The value to test.
        tol (float, optional): Absolute tolerance for the closeness check. Defaults to 1e-9.

    Returns:
        bool: True if total/c is close to an integer.
    """
    ratio = total / c
    return math.isclose(ratio, round(ratio), abs_tol=tol)

my_round(x)

Round a value to the nearest 0.01, returned with 3 decimal places of precision.

Parameters:

Name Type Description Default
x float

The value to round.

required

Returns:

Name Type Description
float

The rounded value.

Source code in src/vitrum/structure_gen.py
 9
10
11
12
13
14
15
16
17
18
19
def my_round(x):
    """
    Round a value to the nearest 0.01, returned with 3 decimal places of precision.

    Args:
        x (float): The value to round.

    Returns:
        float: The rounded value.
    """
    return round(0.01 * round(x / 0.01), 3)

random_partition(x, total=10)

Randomly partition total into x positive parts, returned as fractions summing to 1.

Parameters:

Name Type Description Default
x int

The number of parts to partition into.

required
total int

The integer total to partition before normalizing. Defaults to 10.

10

Returns:

Type Description

np.ndarray: An array of x fractions summing to 1.

Source code in src/vitrum/structure_gen.py
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def random_partition(x, total=10):
    """
    Randomly partition `total` into `x` positive parts, returned as fractions summing to 1.

    Args:
        x (int): The number of parts to partition into.
        total (int, optional): The integer total to partition before normalizing. Defaults to 10.

    Returns:
        np.ndarray: An array of `x` fractions summing to 1.
    """
    if x <= 1:
        return [1]
    cuts = np.sort(np.random.choice(range(1, total), x - 1, replace=False))
    parts = np.diff([0] + list(cuts) + [total])
    return parts / total