messthaler-wulff 2025.12.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- messthaler_wulff-2025.12.2/.gitignore +6 -0
- messthaler_wulff-2025.12.2/LICENSE.txt +21 -0
- messthaler_wulff-2025.12.2/PKG-INFO +16 -0
- messthaler_wulff-2025.12.2/README.md +0 -0
- messthaler_wulff-2025.12.2/makefile +24 -0
- messthaler_wulff-2025.12.2/messthaler_wulff/__init__.py +86 -0
- messthaler_wulff-2025.12.2/messthaler_wulff/__main__.py +3 -0
- messthaler_wulff-2025.12.2/messthaler_wulff/additive_simulation.py +456 -0
- messthaler_wulff-2025.12.2/messthaler_wulff/common_objects.py +67 -0
- messthaler_wulff-2025.12.2/messthaler_wulff/data.py +118 -0
- messthaler_wulff-2025.12.2/messthaler_wulff/explorative_simulation.py +74 -0
- messthaler_wulff-2025.12.2/messthaler_wulff/mode_explore.py +32 -0
- messthaler_wulff-2025.12.2/messthaler_wulff/mode_interactive.py +17 -0
- messthaler_wulff-2025.12.2/messthaler_wulff/mode_simulate.py +31 -0
- messthaler_wulff-2025.12.2/messthaler_wulff/mode_view.py +35 -0
- messthaler_wulff-2025.12.2/messthaler_wulff/objects.py +246 -0
- messthaler_wulff-2025.12.2/messthaler_wulff/progress.py +142 -0
- messthaler_wulff-2025.12.2/messthaler_wulff/terminal_formatting.py +61 -0
- messthaler_wulff-2025.12.2/messthaler_wulff/utils.py +150 -0
- messthaler_wulff-2025.12.2/messthaler_wulff/version.py +1 -0
- messthaler_wulff-2025.12.2/pyproject.toml +23 -0
- messthaler_wulff-2025.12.2/vinc.json +17 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 J. Meßthaler
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: messthaler-wulff
|
|
3
|
+
Version: 2025.12.2
|
|
4
|
+
Author-email: Guenthner <guenthner.jonathan@gmail.com>
|
|
5
|
+
License-File: LICENSE.txt
|
|
6
|
+
Keywords: Wulff,Wulff Crystals
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Requires-Dist: matplotlib
|
|
12
|
+
Requires-Dist: open3d
|
|
13
|
+
Requires-Dist: prettytable
|
|
14
|
+
Requires-Dist: psutil
|
|
15
|
+
Requires-Dist: scipy
|
|
16
|
+
Requires-Dist: sortedcontainers
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
py=.venv/bin/python
|
|
2
|
+
user=guenthner
|
|
3
|
+
program-name=messthaler-wulff
|
|
4
|
+
|
|
5
|
+
set_user:
|
|
6
|
+
cp ~/.pypirc_$(user) ~/.pypirc
|
|
7
|
+
|
|
8
|
+
build: clean version
|
|
9
|
+
$(py) -m build
|
|
10
|
+
|
|
11
|
+
version:
|
|
12
|
+
vinc
|
|
13
|
+
|
|
14
|
+
clean:
|
|
15
|
+
touch dist/fuck
|
|
16
|
+
rm dist/*
|
|
17
|
+
|
|
18
|
+
upload: set_user build
|
|
19
|
+
$(py) -m twine upload --repository pypi dist/* $(flags)
|
|
20
|
+
|
|
21
|
+
reload: upload
|
|
22
|
+
pipx upgrade $(program-name)
|
|
23
|
+
pipx upgrade $(program-name)
|
|
24
|
+
$(program-name) --version h
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from .data import fcc_transform
|
|
8
|
+
from .terminal_formatting import parse_color
|
|
9
|
+
from .version import program_version
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger("messthaler_wulff")
|
|
12
|
+
console = logging.StreamHandler()
|
|
13
|
+
log.addHandler(console)
|
|
14
|
+
log.setLevel(logging.DEBUG)
|
|
15
|
+
console.setFormatter(
|
|
16
|
+
logging.Formatter(parse_color("{asctime} [ℂ3.{levelname:>5}ℂ.] ℂ4.{name}ℂ.: {message}"),
|
|
17
|
+
style="{", datefmt="%W %a %I:%M"))
|
|
18
|
+
|
|
19
|
+
PROGRAM_NAME = "messthaler-wulff"
|
|
20
|
+
DEFAULT_DATE_FORMAT = "%y/%b/%NAME"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def command_entry_point():
|
|
24
|
+
try:
|
|
25
|
+
main()
|
|
26
|
+
except KeyboardInterrupt:
|
|
27
|
+
log.warning("Program was interrupted by user")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_lattice(lattice):
|
|
31
|
+
match lattice.lower():
|
|
32
|
+
case "fcc":
|
|
33
|
+
return fcc_transform()
|
|
34
|
+
|
|
35
|
+
log.info(f"Unknown lattice name {lattice}, interpreting lattice as python code")
|
|
36
|
+
|
|
37
|
+
transform = np.array(eval(lattice, {"sqrt": math.sqrt}))
|
|
38
|
+
|
|
39
|
+
log.info(f"Using result as lattice transform:\n{transform}")
|
|
40
|
+
|
|
41
|
+
input("Press enter to continue...")
|
|
42
|
+
return transform
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main():
|
|
46
|
+
parser = argparse.ArgumentParser(prog=PROGRAM_NAME,
|
|
47
|
+
description="Wudduwudduwudduwudduwudduwudduwudduwuddu",
|
|
48
|
+
allow_abbrev=True, add_help=True, exit_on_error=True)
|
|
49
|
+
|
|
50
|
+
parser.add_argument('-v', '--verbose', action='store_true', help="Show more output")
|
|
51
|
+
parser.add_argument("--version", action="store_true", help="Show the current version of the program")
|
|
52
|
+
parser.add_argument("MODE",
|
|
53
|
+
help="What subprogram to execute; Can be 'view' or 'simulate' or 'interactive' or 'explore'")
|
|
54
|
+
parser.add_argument("--goal", help="The number of atoms to add initially", default="100")
|
|
55
|
+
parser.add_argument("--dimension", default="3")
|
|
56
|
+
parser.add_argument("--lattice", default="fcc")
|
|
57
|
+
parser.add_argument("--axis", action="store_true")
|
|
58
|
+
parser.add_argument("--orthogonal", action="store_true")
|
|
59
|
+
parser.add_argument("-w", "--windows", action="store_true")
|
|
60
|
+
|
|
61
|
+
args = parser.parse_args()
|
|
62
|
+
|
|
63
|
+
log.setLevel(logging.DEBUG if args.verbose else logging.INFO)
|
|
64
|
+
log.debug("Starting program...")
|
|
65
|
+
|
|
66
|
+
if args.version:
|
|
67
|
+
log.info(f"{PROGRAM_NAME} version {program_version}")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
match args.MODE.lower():
|
|
71
|
+
case 'view':
|
|
72
|
+
from . import mode_view
|
|
73
|
+
mode_view.run_mode(use_orthogonal_projections=args.orthogonal, show_axes=args.axis)
|
|
74
|
+
case 'simulate':
|
|
75
|
+
from . import mode_simulate
|
|
76
|
+
mode_simulate.run_mode(goal=int(args.goal), lattice=parse_lattice(args.lattice))
|
|
77
|
+
case 'interactive':
|
|
78
|
+
from . import mode_interactive
|
|
79
|
+
mode_interactive.run_mode(goal=int(args.goal), dimension=int(args.dimension),
|
|
80
|
+
lattice=parse_lattice(args.lattice), windows_mode=args.windows)
|
|
81
|
+
case 'explore':
|
|
82
|
+
from . import mode_explore
|
|
83
|
+
mode_explore.run_mode(goal=int(args.goal), lattice=parse_lattice(args.lattice),
|
|
84
|
+
dimension=int(args.dimension))
|
|
85
|
+
case _:
|
|
86
|
+
log.error(f"Unknown mode {args.MODE}. Must be one of 'view' or 'simulate'")
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import random
|
|
3
|
+
import shutil
|
|
4
|
+
import time
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from .progress import ProgressBar
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EnergyTracker:
|
|
13
|
+
class EnergyLevel:
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.values = []
|
|
16
|
+
self.indices = {}
|
|
17
|
+
|
|
18
|
+
def __len__(self):
|
|
19
|
+
return len(self.values)
|
|
20
|
+
|
|
21
|
+
def __getitem__(self, i):
|
|
22
|
+
return self.values[i]
|
|
23
|
+
|
|
24
|
+
def add(self, atom):
|
|
25
|
+
if atom in self.indices:
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
self.indices[atom] = len(self.values)
|
|
29
|
+
self.values.append(atom)
|
|
30
|
+
|
|
31
|
+
def remove(self, atom):
|
|
32
|
+
if len(self.values) == 0:
|
|
33
|
+
raise ValueError("I am empty")
|
|
34
|
+
|
|
35
|
+
index = self.indices[atom]
|
|
36
|
+
try:
|
|
37
|
+
if index == len(self.values) - 1:
|
|
38
|
+
self.values.pop()
|
|
39
|
+
else:
|
|
40
|
+
self.values[index] = self.values.pop()
|
|
41
|
+
self.indices[self.values[index]] = index
|
|
42
|
+
except IndexError:
|
|
43
|
+
raise IndexError(f"List index {index} out of range for {self.values}")
|
|
44
|
+
|
|
45
|
+
del self.indices[atom]
|
|
46
|
+
|
|
47
|
+
def __str__(self):
|
|
48
|
+
return str(len(self.values))
|
|
49
|
+
|
|
50
|
+
def __repr__(self):
|
|
51
|
+
return str(self)
|
|
52
|
+
|
|
53
|
+
def __iter__(self):
|
|
54
|
+
for i in range(len(self)):
|
|
55
|
+
yield self[i]
|
|
56
|
+
|
|
57
|
+
def json_obj(self):
|
|
58
|
+
return [str(v) for v in self.values] # f"{len(self.values)} values"
|
|
59
|
+
|
|
60
|
+
def __init__(self):
|
|
61
|
+
self.min_energy = None
|
|
62
|
+
self.energy_levels = defaultdict(EnergyTracker.EnergyLevel)
|
|
63
|
+
self.atom2energy = {}
|
|
64
|
+
|
|
65
|
+
def minimum(self, choice=lambda x: 0):
|
|
66
|
+
level = self.energy_levels[self.min_energy]
|
|
67
|
+
atom = level[choice(len(level))]
|
|
68
|
+
return atom, self.min_energy
|
|
69
|
+
|
|
70
|
+
def all_minimums(self):
|
|
71
|
+
level = self.energy_levels[self.min_energy]
|
|
72
|
+
return list(level)
|
|
73
|
+
|
|
74
|
+
def __len__(self):
|
|
75
|
+
return len(self.atom2energy)
|
|
76
|
+
|
|
77
|
+
def __contains__(self, atom):
|
|
78
|
+
return atom in self.atom2energy
|
|
79
|
+
|
|
80
|
+
def get(self, atom):
|
|
81
|
+
return self.atom2energy[atom]
|
|
82
|
+
|
|
83
|
+
def set(self, atom, energy):
|
|
84
|
+
if self.min_energy is None or energy < self.min_energy:
|
|
85
|
+
self.min_energy = energy
|
|
86
|
+
|
|
87
|
+
if atom in self.atom2energy:
|
|
88
|
+
old_energy = self.atom2energy[atom]
|
|
89
|
+
if old_energy != energy:
|
|
90
|
+
old_level = self.energy_levels[old_energy]
|
|
91
|
+
old_level.remove(atom)
|
|
92
|
+
level = self.energy_levels[energy]
|
|
93
|
+
level.add(atom)
|
|
94
|
+
self.adjust_min_energy()
|
|
95
|
+
|
|
96
|
+
self.atom2energy[atom] = energy
|
|
97
|
+
else:
|
|
98
|
+
level = self.energy_levels[energy]
|
|
99
|
+
level.add(atom)
|
|
100
|
+
self.atom2energy[atom] = energy
|
|
101
|
+
|
|
102
|
+
def unset(self, atom, energy):
|
|
103
|
+
level = self.energy_levels[energy]
|
|
104
|
+
level.remove(atom)
|
|
105
|
+
del self.atom2energy[atom]
|
|
106
|
+
|
|
107
|
+
self.adjust_min_energy()
|
|
108
|
+
|
|
109
|
+
def adjust_min_energy(self):
|
|
110
|
+
if len(self) == 0:
|
|
111
|
+
self.min_energy = None
|
|
112
|
+
else:
|
|
113
|
+
while len(self.energy_levels[self.min_energy]) <= 0:
|
|
114
|
+
self.min_energy += 1
|
|
115
|
+
|
|
116
|
+
def atoms(self):
|
|
117
|
+
return self.atom2energy.keys()
|
|
118
|
+
|
|
119
|
+
def __str__(self):
|
|
120
|
+
return json.dumps({
|
|
121
|
+
"min_energy": self.min_energy,
|
|
122
|
+
"atom2energy": {
|
|
123
|
+
str(k): v for (k, v) in self.atom2energy.items()},
|
|
124
|
+
"energy_levels": {
|
|
125
|
+
k: v.json_obj() for (k, v) in self.energy_levels.items()
|
|
126
|
+
}
|
|
127
|
+
}, indent=4)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class OmniSimulation:
|
|
131
|
+
BACKWARDS = 0
|
|
132
|
+
FORWARDS = 1
|
|
133
|
+
|
|
134
|
+
def __init__(self, neighborhood, energy_maximum=None, origin=(0, 0, 0, 0)):
|
|
135
|
+
if energy_maximum is None:
|
|
136
|
+
energy_maximum = neighborhood.energy_maximum
|
|
137
|
+
|
|
138
|
+
self.energy = 0
|
|
139
|
+
self.atoms = 0
|
|
140
|
+
self.neighborhood = neighborhood
|
|
141
|
+
self.energy_maximum = energy_maximum
|
|
142
|
+
|
|
143
|
+
self.boundaries = [EnergyTracker(), EnergyTracker()]
|
|
144
|
+
self.boundaries[self.FORWARDS].set(origin, self.calculate_energy(origin, mode=self.FORWARDS))
|
|
145
|
+
|
|
146
|
+
def calculate_energy(self, atom, mode):
|
|
147
|
+
energy = 0
|
|
148
|
+
|
|
149
|
+
for neighbor in self.neighborhood(atom):
|
|
150
|
+
energy += -1 if neighbor in self.boundaries[1 - mode] else 1
|
|
151
|
+
|
|
152
|
+
return energy
|
|
153
|
+
|
|
154
|
+
def set_atom(self, atom, energy, mode):
|
|
155
|
+
mode_boundary = self.boundaries[mode]
|
|
156
|
+
reverse_boundary = self.boundaries[1 - mode]
|
|
157
|
+
|
|
158
|
+
mode_energy = energy
|
|
159
|
+
self.energy += mode_energy
|
|
160
|
+
reverse_boundary.set(atom, -mode_energy)
|
|
161
|
+
if atom in mode_boundary:
|
|
162
|
+
mode_boundary.unset(atom, mode_energy)
|
|
163
|
+
|
|
164
|
+
for neighbor in self.neighborhood(atom):
|
|
165
|
+
if neighbor in reverse_boundary:
|
|
166
|
+
neighbor_energy = reverse_boundary.get(neighbor)
|
|
167
|
+
new_energy = neighbor_energy + 2
|
|
168
|
+
|
|
169
|
+
if self.energy_maximum() == new_energy:
|
|
170
|
+
reverse_boundary.unset(neighbor, neighbor_energy)
|
|
171
|
+
else:
|
|
172
|
+
reverse_boundary.set(neighbor, new_energy)
|
|
173
|
+
elif neighbor in mode_boundary:
|
|
174
|
+
neighbor_energy = mode_boundary.get(neighbor)
|
|
175
|
+
new_energy = neighbor_energy - 2
|
|
176
|
+
|
|
177
|
+
if self.energy_maximum() == new_energy:
|
|
178
|
+
mode_boundary.unset(neighbor, neighbor_energy)
|
|
179
|
+
else:
|
|
180
|
+
mode_boundary.set(neighbor, new_energy)
|
|
181
|
+
else:
|
|
182
|
+
mode_boundary.set(neighbor, self.calculate_energy(neighbor, mode))
|
|
183
|
+
|
|
184
|
+
def next_atom(self, choice, mode):
|
|
185
|
+
return self.boundaries[mode].minimum(choice)
|
|
186
|
+
|
|
187
|
+
def next_atoms(self, mode):
|
|
188
|
+
return self.boundaries[mode].all_minimums()
|
|
189
|
+
|
|
190
|
+
def adjust_atom_count(self, mode):
|
|
191
|
+
if mode == self.BACKWARDS and self.atoms <= 0:
|
|
192
|
+
raise ValueError("No atoms left, so can't remove one")
|
|
193
|
+
|
|
194
|
+
match mode:
|
|
195
|
+
case self.FORWARDS:
|
|
196
|
+
self.atoms += 1
|
|
197
|
+
case self.BACKWARDS:
|
|
198
|
+
self.atoms -= 1
|
|
199
|
+
|
|
200
|
+
def add_atom(self, choice=lambda l: 0):
|
|
201
|
+
self.adjust_atom_count(self.FORWARDS)
|
|
202
|
+
|
|
203
|
+
atom, energy = self.next_atom(choice, self.FORWARDS)
|
|
204
|
+
self.set_atom(atom,
|
|
205
|
+
energy,
|
|
206
|
+
self.FORWARDS)
|
|
207
|
+
|
|
208
|
+
def remove_atom(self, choice=lambda l: 0):
|
|
209
|
+
self.adjust_atom_count(self.BACKWARDS)
|
|
210
|
+
|
|
211
|
+
atom, energy = self.next_atom(choice, self.BACKWARDS)
|
|
212
|
+
self.set_atom(atom,
|
|
213
|
+
energy,
|
|
214
|
+
self.BACKWARDS)
|
|
215
|
+
|
|
216
|
+
def force_set_atom(self, atom, mode=FORWARDS):
|
|
217
|
+
self.adjust_atom_count(mode)
|
|
218
|
+
|
|
219
|
+
atom2energy = self.boundaries[mode].atom2energy
|
|
220
|
+
if atom in atom2energy:
|
|
221
|
+
energy = atom2energy[atom]
|
|
222
|
+
else:
|
|
223
|
+
energy = self.calculate_energy(atom, mode)
|
|
224
|
+
self.set_atom(atom, energy, mode)
|
|
225
|
+
|
|
226
|
+
def visualise_slice(self, atomiser=lambda x, y: (0, x, y), crosshair=False, view_energies=False, color=True):
|
|
227
|
+
width, height = shutil.get_terminal_size()
|
|
228
|
+
margin = 3
|
|
229
|
+
fg_red = "\x1b[38;2;200;0;0;1m" if color else ""
|
|
230
|
+
bg_green = "\x1b[48;2;0;70;0;1m" if color else ""
|
|
231
|
+
unset = "\x1b[m" if color else ""
|
|
232
|
+
|
|
233
|
+
def bg():
|
|
234
|
+
if crosshair and (x == 0 or y == 0):
|
|
235
|
+
print(end=bg_green + " " + unset)
|
|
236
|
+
else:
|
|
237
|
+
print(end=" ")
|
|
238
|
+
|
|
239
|
+
for y in range(-(height - margin) // 2, (height - margin) // 2):
|
|
240
|
+
for x in range(-(width - margin) // 2, (width - margin) // 2):
|
|
241
|
+
atom = atomiser(x, y)
|
|
242
|
+
mode = -1
|
|
243
|
+
energy = None
|
|
244
|
+
|
|
245
|
+
for m in range(2):
|
|
246
|
+
if atom in self.boundaries[m]:
|
|
247
|
+
mode = m
|
|
248
|
+
energy = self.boundaries[m].get(atom)
|
|
249
|
+
|
|
250
|
+
if view_energies:
|
|
251
|
+
if mode == -1:
|
|
252
|
+
bg()
|
|
253
|
+
elif energy < 0:
|
|
254
|
+
print(end=fg_red + str(-energy) + unset)
|
|
255
|
+
else:
|
|
256
|
+
print(end=str(energy))
|
|
257
|
+
else:
|
|
258
|
+
match mode:
|
|
259
|
+
case self.BACKWARDS:
|
|
260
|
+
print(end=fg_red + "X" + unset)
|
|
261
|
+
case self.FORWARDS:
|
|
262
|
+
print(end="O")
|
|
263
|
+
case _:
|
|
264
|
+
bg()
|
|
265
|
+
print()
|
|
266
|
+
|
|
267
|
+
print()
|
|
268
|
+
|
|
269
|
+
def interactive(self, dimension=2, color=True):
|
|
270
|
+
z = 0
|
|
271
|
+
view_energies = False
|
|
272
|
+
crosshair = False
|
|
273
|
+
atomiser = None
|
|
274
|
+
match dimension:
|
|
275
|
+
case 1:
|
|
276
|
+
atomiser = lambda x, y: (0, x)
|
|
277
|
+
case 2:
|
|
278
|
+
atomiser = lambda x, y: (0, x, y)
|
|
279
|
+
case 3:
|
|
280
|
+
atomiser = lambda x, y: (0, x, y, z)
|
|
281
|
+
case _:
|
|
282
|
+
raise ValueError(f"Unsupported dimension: {dimension}")
|
|
283
|
+
|
|
284
|
+
def set_cmd(method):
|
|
285
|
+
try:
|
|
286
|
+
if len(args) > 0:
|
|
287
|
+
goal = int(args[0])
|
|
288
|
+
progress = ProgressBar(goal, lambda: self.energy)
|
|
289
|
+
for i in range(goal):
|
|
290
|
+
progress(i)
|
|
291
|
+
method(lambda l: random.randrange(l))
|
|
292
|
+
else:
|
|
293
|
+
method(lambda l: random.randrange(l))
|
|
294
|
+
except (ValueError, TypeError):
|
|
295
|
+
pass
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
while True:
|
|
299
|
+
wipe_screen()
|
|
300
|
+
self.visualise_slice(atomiser, crosshair=crosshair, view_energies=view_energies, color=color)
|
|
301
|
+
print(f"Total energy: {self.energy}; Z-Layer: {z}")
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
cmd, *args = input("Input Command: (add, rm, up, down, exit, ?) ").split()
|
|
305
|
+
except ValueError:
|
|
306
|
+
continue
|
|
307
|
+
|
|
308
|
+
match (cmd.lower()):
|
|
309
|
+
case "add":
|
|
310
|
+
set_cmd(self.add_atom)
|
|
311
|
+
case "rm":
|
|
312
|
+
set_cmd(self.remove_atom)
|
|
313
|
+
case "up":
|
|
314
|
+
z += 1
|
|
315
|
+
case "down":
|
|
316
|
+
z -= 1
|
|
317
|
+
case "layer":
|
|
318
|
+
if len(args) > 0:
|
|
319
|
+
z = int(args[0])
|
|
320
|
+
case "energy":
|
|
321
|
+
view_energies = not view_energies
|
|
322
|
+
case "forwards":
|
|
323
|
+
print(self.boundaries[self.FORWARDS])
|
|
324
|
+
input("Press Enter to continue")
|
|
325
|
+
case "backwards":
|
|
326
|
+
print(self.boundaries[self.BACKWARDS])
|
|
327
|
+
input("Press Enter to continue")
|
|
328
|
+
case "crosshair":
|
|
329
|
+
crosshair = not crosshair
|
|
330
|
+
case "fill":
|
|
331
|
+
self.fill(lambda l: random.randrange(l))
|
|
332
|
+
case "exit":
|
|
333
|
+
break
|
|
334
|
+
case "?":
|
|
335
|
+
input("""
|
|
336
|
+
add - Adds the next atom in the sequence
|
|
337
|
+
rm - Removes the next atom in the reverse sequence
|
|
338
|
+
up - Goes up one z-layer (if possible)
|
|
339
|
+
down - Goes down one z-layer (if possible)
|
|
340
|
+
energy - Toggle the energy view mode
|
|
341
|
+
forwards - Display the forwards boundary energy tracker
|
|
342
|
+
backwards - Display the backwards boundary energy tracker
|
|
343
|
+
crosshair - Enable a crosshair
|
|
344
|
+
fill - Adds atoms to the crystal until the energy would increase
|
|
345
|
+
exit - Exit interactive mode
|
|
346
|
+
? - Displays this help
|
|
347
|
+
|
|
348
|
+
Press Enter to continue execution
|
|
349
|
+
""")
|
|
350
|
+
|
|
351
|
+
except (ValueError, TypeError):
|
|
352
|
+
self.interactive(dimension=dimension, color=color)
|
|
353
|
+
except KeyboardInterrupt:
|
|
354
|
+
print()
|
|
355
|
+
|
|
356
|
+
def points(self):
|
|
357
|
+
return self.boundaries[self.BACKWARDS].atoms()
|
|
358
|
+
|
|
359
|
+
def fill(self, choice=lambda l: 0):
|
|
360
|
+
while True:
|
|
361
|
+
atom, energy = self.next_atom(choice, self.FORWARDS)
|
|
362
|
+
if energy > 0:
|
|
363
|
+
break
|
|
364
|
+
|
|
365
|
+
self.adjust_atom_count(self.FORWARDS)
|
|
366
|
+
|
|
367
|
+
self.set_atom(atom,
|
|
368
|
+
energy,
|
|
369
|
+
self.FORWARDS)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def wipe_screen():
|
|
373
|
+
print(f"\x1b[3J\x1b[H\x1b[J", end="")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def move(atom, offset):
|
|
377
|
+
return tuple((atom[0], *(atom[i + 1] + offset[i] for i in range(len(atom) - 1))))
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class SimpleNeighborhood:
|
|
381
|
+
def __init__(self, transform: np.ndarray):
|
|
382
|
+
start = time.time()
|
|
383
|
+
shape = transform.shape
|
|
384
|
+
self.transform = transform
|
|
385
|
+
self.n = shape[0]
|
|
386
|
+
if shape[0] != shape[1]:
|
|
387
|
+
raise ValueError("I need a square matrix")
|
|
388
|
+
|
|
389
|
+
self.base_neighborhood = set()
|
|
390
|
+
|
|
391
|
+
self.find_neighbors()
|
|
392
|
+
|
|
393
|
+
print(f"Calculated in {time.time() - start:.2f} seconds neighbors for:\n{transform}")
|
|
394
|
+
print(f"Neighbors are: {self.base_neighborhood}")
|
|
395
|
+
print()
|
|
396
|
+
|
|
397
|
+
@staticmethod
|
|
398
|
+
def points_within_dist(n, dist):
|
|
399
|
+
if n == 0:
|
|
400
|
+
yield ()
|
|
401
|
+
return
|
|
402
|
+
if n == 1:
|
|
403
|
+
for i in list(range(-dist, dist + 1)):
|
|
404
|
+
yield (i,)
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
for i in range(-dist, dist + 1):
|
|
408
|
+
for p in SimpleNeighborhood.points_within_dist(n - 1, dist):
|
|
409
|
+
yield [*p, i]
|
|
410
|
+
|
|
411
|
+
@staticmethod
|
|
412
|
+
def points_with_dist(n, dist):
|
|
413
|
+
if dist == 0:
|
|
414
|
+
yield [0] * n
|
|
415
|
+
return
|
|
416
|
+
if dist == 1:
|
|
417
|
+
for p in SimpleNeighborhood.points_within_dist(n, dist):
|
|
418
|
+
if all(x == 0 for x in p): continue
|
|
419
|
+
yield p
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
for i in range(n):
|
|
423
|
+
for sign in [-1, 1]:
|
|
424
|
+
for p in SimpleNeighborhood.points_within_dist(n - 1, dist):
|
|
425
|
+
yield *p[:i], sign * dist, *p[i:]
|
|
426
|
+
|
|
427
|
+
def find_neighbors(self):
|
|
428
|
+
for dist in range(1, 10):
|
|
429
|
+
changed = 0
|
|
430
|
+
|
|
431
|
+
for p in SimpleNeighborhood.points_with_dist(self.n, dist):
|
|
432
|
+
point = np.dot(self.transform, np.array(p))
|
|
433
|
+
if np.linalg.norm(point) <= 1:
|
|
434
|
+
p = tuple(p)
|
|
435
|
+
if p not in self.base_neighborhood:
|
|
436
|
+
self.base_neighborhood.add(p)
|
|
437
|
+
changed += 1
|
|
438
|
+
|
|
439
|
+
if changed == 0:
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
raise RuntimeError("Could not find all neighbors")
|
|
443
|
+
|
|
444
|
+
def __call__(self, pos):
|
|
445
|
+
return [move(pos, x) for x in self.base_neighborhood]
|
|
446
|
+
|
|
447
|
+
def energy_maximum(self):
|
|
448
|
+
return len(self.base_neighborhood)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def main():
|
|
452
|
+
OmniSimulation(SimpleNeighborhood(np.identity(2)), None, (0, 0, 0)).interactive(2)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
if __name__ == "__main__":
|
|
456
|
+
main()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from data import *
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
################################################################################
|
|
5
|
+
# The definitions of the fcc, hcp, etc. methods
|
|
6
|
+
|
|
7
|
+
def fcc(grid_range=range(0, 4), lower_bound=0, upper_bound=1.9, upper_clip_plane=math.inf, add_lines=True):
|
|
8
|
+
"""
|
|
9
|
+
Generates a subset of fcc
|
|
10
|
+
:grid_range: The range for the grid which will be range(-3,4) × range(-3,4) × range(-3, 4)
|
|
11
|
+
"""
|
|
12
|
+
g = grid(grid_range, grid_range, grid_range)
|
|
13
|
+
g *= fcc_transform()
|
|
14
|
+
g = g.filter(lambda p: all(lower_bound <= t <= upper_bound for t in p))
|
|
15
|
+
g = g.filter(lambda p: sum(p[i] for i in range(3)) <= upper_clip_plane)
|
|
16
|
+
if add_lines:
|
|
17
|
+
g = auto_lines(g, 1)
|
|
18
|
+
return g
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def fcc_wulff(opacity=1, corner_color='red', color='darkred'):
|
|
22
|
+
"""
|
|
23
|
+
Generates the polygon that is the wulff crystal (with side length 1)
|
|
24
|
+
"""
|
|
25
|
+
wulff = fcc_wulff_obj()
|
|
26
|
+
# wulff += pos(0,0,0) # center the crystal (somewhat)
|
|
27
|
+
wulff.foreach(Point, setter('color', corner_color))
|
|
28
|
+
wulff = convex_hull(wulff)
|
|
29
|
+
wulff.foreach(Triangle, setter('color', color))
|
|
30
|
+
wulff.foreach(Triangle, setter('opacity', opacity))
|
|
31
|
+
return wulff
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def fcc_wulff2(opacity=1, corner_color='red', color='darkred'):
|
|
35
|
+
"""
|
|
36
|
+
Generates the polygon that is the wulff crystal (with side length 2)
|
|
37
|
+
"""
|
|
38
|
+
wulff = fcc_wulff2_obj()
|
|
39
|
+
wulff.foreach(Point, setter('color', corner_color))
|
|
40
|
+
wulff = convex_hull(wulff)
|
|
41
|
+
wulff.foreach(Triangle, setter('color', color))
|
|
42
|
+
wulff.foreach(Triangle, setter('alpha', opacity))
|
|
43
|
+
return wulff
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def hcp(grid_range=range(0, 4), lower_bound=0, upper_bound=1.9, upper_clip_plane=math.inf, custom_filter=lambda p: True,
|
|
47
|
+
add_lines=True):
|
|
48
|
+
"""
|
|
49
|
+
Generates a subset of hcp
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
g = grid(grid_range, grid_range, grid_range)
|
|
53
|
+
g *= hcp_transform()
|
|
54
|
+
g @= g + hcp_vector()
|
|
55
|
+
g = g.filter(lambda p: all(lower_bound <= t <= upper_bound for t in p))
|
|
56
|
+
g = g.filter(lambda p: sum(p[i] for i in range(3)) <= upper_clip_plane)
|
|
57
|
+
g = g.filter(custom_filter)
|
|
58
|
+
if add_lines:
|
|
59
|
+
g = auto_lines(g, 1)
|
|
60
|
+
return g
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def print_point_set(s):
|
|
64
|
+
"""
|
|
65
|
+
Given a set s consisting of Point instances, prints their positions
|
|
66
|
+
"""
|
|
67
|
+
print(f"{len(s)} elements: ", *map(lambda p: p.pos, s))
|