Published June 5, 2026 | Version v1

Modelling of nanoparticles, quantum dots and amorphous material: atomic structure and vibrational spectra

  • 1. ROR icon University College Dublin

Description

Modelling and simulation tool:

  • Nanoparticles (spherical and Wulff) and surface slabs 
  • Adsorption and solvation
  • Calculation of energies and vibrational spectra 
  • Quantum dots and amorphous material
  • GUI

import numpy as np
from ase import Atoms
from ase.build import bulk
from ase.cluster import wulff_construction, Decahedron
from ase.io import write
from ase.neighborlist import NeighborList
from ase.geometry import get_distances

class StructureBuilder:
    """
    Build QD and amorphous structures with atoms + bonds
    No energy/force calculations - just geometry
    """

    def __init__(self):
        pass

    # ---------------- Quantum Dot Builder ----------------
    def make_core_shell_qd(self, core='CdTe', shell='ZnS',
                          core_radius_A=15, shell_thickness_A=5):
        """
        Build spherical core/shell QD, atomistic
        core, shell: 'CdTe', 'ZnS', 'CdS', 'GaAs', etc
        Returns ASE Atoms object with atoms + bonds defined by distances
        """
        # Materials database: formula, structure, lattice constant A
        mat_lib = {
            'CdTe': {'structure': 'zincblende', 'a': 6.48, 'elements': ['Cd', 'Te']},
            'CdS': {'structure': 'zincblende', 'a': 5.82, 'elements': ['Cd', 'S']},
            'ZnS': {'structure': 'zincblende', 'a': 5.41, 'elements': ['Zn', 'S']},
            'GaAs': {'structure': 'zincblende', 'a': 5.65, 'elements': ['Ga', 'As']},
            'Si': {'structure': 'diamond', 'a': 5.43, 'elements': ['Si']},
        }

        if core not in mat_lib or shell not in mat_lib:
            raise ValueError(f"Material not in lib. Options: {list(mat_lib.keys())}")

        # 1. Build core as Wulff construction for ~spherical
        core_bulk = bulk(core, mat_lib[core]['structure'], a=mat_lib[core]['a'])
        surfaces = [(1, 0, 0), (1, 1, 1), (1, 1, 0)]
        esurf = [1.0, 1.0, 1.0]
        qd_core = wulff_construction(core_bulk, surfaces, esurf,
                                     size=core_radius_A, rounding='above')

        # 2. Build shell: make big shell crystal, carve spherical shell
        shell_bulk = bulk(shell, mat_lib[shell]['structure'], a=mat_lib[shell]['a'])
        shell_supercell = shell_bulk * (8, 8, 8)

        core_center = qd_core.get_center_of_mass()
        core_max_r = np.max(np.linalg.norm(qd_core.positions - core_center, axis=1))

        shell_pos = shell_supercell.positions
        shell_center = shell_supercell.get_center_of_mass()
        dist = np.linalg.norm(shell_pos - shell_center, axis=1)

        r_inner = core_max_r + 1.0 # 1A gap to avoid overlap
        r_outer = core_max_r + shell_thickness_A
        shell_mask = (dist >= r_inner) & (dist <= r_outer)
        qd_shell = shell_supercell[shell_mask]
        qd_shell.positions += core_center - shell_center

        # 3. Combine
        qd = qd_core + qd_shell
        qd.center(vacuum=10.0)

        print(f"QD built: {core} core {len(qd_core)} atoms, {shell} shell {len(qd_shell)} atoms")
        print(f"Total: {len(qd)} atoms, diameter ~{(r_outer*2)/10:.1f} nm")
        return qd

    def make_core_shell_shell_qd(self, core='CdTe', shell1='CdS', shell2='ZnS',
                                r_core_A=12, t_shell1_A=4, t_shell2_A=4):
        """CdTe/CdS/ZnS style"""
        # Build core + shell1 first
        qd = self.make_core_shell_qd(core, shell1, r_core_A, t_shell1_A)
        # Then add shell2 around it
        core_radius = r_core_A + t_shell1_A
        qd2 = self.make_core_shell_qd(shell1, shell2, core_radius, t_shell2_A)
        # Extract only the outer shell atoms from qd2
        shell2_atoms = qd2[len(qd):]
        qd += shell2_atoms
        print(f"Added 2nd shell: {shell2}, total now {len(qd)} atoms")
        return qd

    # ---------------- Amorphous Builder ----------------
    def make_amorphous(self, material='Si', structure='diamond', a=5.43,
                      size=(3,3,3), density_scale=0.95, disorder=0.3):
        """
        Create amorphous structure via random displacement + scaling
        This is FAST but not MD. For real melt-quench use LAMMPS/MACE.

        disorder: 0=crytal, 0.3=moderate amorphization, 0.6=heavy
        density_scale: 0.95 = amorphous is 5% less dense than crystal
        """
        crystal = bulk(material, structure, a=a, cubic=True) * size

        # 1. Scale to amorphous density
        crystal.set_cell(crystal.cell * (1/density_scale)**(1/3), scale_atoms=True)

        # 2. Random displacements to break symmetry
        pos = crystal.get_positions()
        bond_len = a * 0.25 * np.sqrt(3) # typical bond for diamond/zincblende
        pos += np.random.normal(0, bond_len * disorder, pos.shape)
        crystal.set_positions(pos)

        # 3. Remove atoms that are too close after displacement
        i, j, d = get_distances(crystal.positions, pbc=True)
        min_dist = bond_len * 0.6
        too_close = (d < min_dist) & (i < j)
        if np.any(too_close):
            mask = np.ones(len(crystal), dtype=bool)
            for ii, jj in zip(i[too_close], j[too_close]):
                mask[jj] = False # delete j atom
            crystal = crystal[mask]

        print(f"Amorphous {material}: {len(crystal)} atoms, density scale {density_scale}")
        return crystal

    def add_bonds_to_atoms(self, atoms, scale=1.2):
        """
        Add bond info using covalent radii. Saves to.xyz with bond info.
        ASE doesn't store bonds by default, but Ovito/VESTA will read them from distances.
        """
        cutoffs = NeighborList([scale * r for r in atoms.get_atomic_numbers()],
                               self_interaction=False, bothways=True)
        cutoffs.update(atoms)
        return atoms # bonds are implicit via distances

    def save(self, atoms, filename):
        """Save with atoms + bonds..xyz,.cif,.pdb all work"""
        atoms = self.add_bonds_to_atoms(atoms)
        write(filename, atoms)
        print(f"Saved {filename} - open in Ovito/VESTA to see atoms + bonds")

# ---------------- Examples ----------------
if __name__ == "__main__":
    builder = StructureBuilder()

    # 1. CdTe/CdS/ZnS QD: 3nm core, 0.4nm CdS, 0.4nm ZnS
    qd = builder.make_core_shell_shell_qd(
        core='CdTe', shell1='CdS', shell2='ZnS',
        r_core_A=15, t_shell1_A=4, t_shell2_A=4
    )
    builder.save(qd, 'CdTe_CdS_ZnS_QD.xyz')

    # 2. Amorphous ZnS shell material by itself
    a_zns = builder.make_amorphous('ZnS', 'zincblende', a=5.41,
                                   size=(4,4,4), density_scale=0.97, disorder=0.35)
    builder.save(a_zns, 'a-ZnS.xyz')

    # 3. Amorphous SiO2 - need to define it first
    # For SiO2 you’d build beta-cristobalite, but here’s quick way:
    sio2 = bulk('SiO2', 'beta_cristobalite', a=7.16) * (2,2,2)
    sio2.positions += np.random.normal(0, 0.4, sio2.positions.shape)
    builder.save(sio2, 'a-SiO2.xyz')

    # 4. Simple CdTe QD, no shell
    cdte_qd = builder.make_core_shell_qd('CdTe', 'CdTe', core_radius_A=20, shell_thickness_A=0)
    builder.save(cdte_qd, 'CdTe_QD.xyz')

Here's a GUI for the structure builder.

Uses `tkinter` so it runs anywhere with no extra installs. You get buttons for QDs + amorphous materials, pick sizes/materials, and save `.xyz` to view in Ovito.
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import numpy as np
from ase import Atoms
from ase.build import bulk
from ase.cluster import wulff_construction
from ase.io import write
from ase.geometry import get_distances
import threading

class StructureBuilderGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("QD + Amorphous Structure Builder")
        self.root.geometry("600x500")

        # Materials database
        self.mat_lib = {
            'CdTe': {'structure': 'zincblende', 'a': 6.48},
            'CdS': {'structure': 'zincblende', 'a': 5.82},
            'ZnS': {'structure': 'zincblende', 'a': 5.41},
            'GaAs': {'structure': 'zincblende', 'a': 5.65},
            'Si': {'structure': 'diamond', 'a': 5.43},
            'Ge': {'structure': 'diamond', 'a': 5.66},
            'SiO2': {'structure': 'beta_cristobalite', 'a': 7.16},
        }

        self.atoms = None
        self.setup_ui()

    def setup_ui(self):
        notebook = ttk.Notebook(self.root)
        notebook.pack(fill='both', expand=True, padx=10, pady=10)

        # Tab 1: Quantum Dot Builder
        qd_frame = ttk.Frame(notebook)
        notebook.add(qd_frame, text='Quantum Dot')
        self.setup_qd_tab(qd_frame)

        # Tab 2: Amorphous Builder
        amor_frame = ttk.Frame(notebook)
        notebook.add(amor_frame, text='Amorphous Material')
        self.setup_amorphous_tab(amor_frame)

        # Bottom: Status + Save
        bottom_frame = ttk.Frame(self.root)
        bottom_frame.pack(fill='x', padx=10, pady=5)

        self.status_var = tk.StringVar(value="Ready")
        ttk.Label(bottom_frame

1. Dependencies get heavy fast
   CLI = just `pip install ase`.
   GUI = `pip install ase tkinter` for basic, or `pyqt5` / `pyside6` for nice. Tkinter comes with Python but looks like 1998. Qt looks modern but is 100+ MB.

2. 3D visualization is the pain point
   Building atoms is easy. Showing atoms + bonds rotating in real time needs `py3Dmol`, `nglview`, or `vtk`. That’s where GUIs get complex.

3. web vs desktop
   Desktop: tkinter, PyQt, DearPyGui
   Web: Streamlit, Gradio, Dash
   Each has different pros/cons.

Here’s a GUI using Streamlit:

Streamlit = 1-file, runs in browser, 3D viewer included. Best balance for a science tool.

python
import streamlit as st
import numpy as np
from ase import Atoms
from ase.build import bulk
from ase.cluster import wulff_construction
from ase.io import write
from stmol import showmol
import py3Dmol
import tempfile

st.set_page_config(layout="wide")
st.title("QD + Amorphous Structure Builder")

class StructureBuilder:
    def make_core_shell_qd(self, core='CdTe', shell='ZnS',
                          core_radius_A=15, shell_thickness_A=5):
        mat_lib = {
            'CdTe': {'structure': 'zincblende', 'a': 6.48},
            'CdS': {'structure': 'zincblende', 'a': 5.82},
            'ZnS': {'structure': 'zincblende', 'a': 5.41},
            'Si': {'structure': 'diamond', 'a': 5.43},
        }
        core_bulk = bulk(core, mat_lib[core]['structure'], a=mat_lib[core]['a'])
        surfaces = [(1, 0, 0), (1, 1, 1)]
        esurf = [1.0, 1.0]
        qd_core = wulff_construction(core_bulk, surfaces, esurf,
                                     size=core_radius_A, rounding='above')

        shell_bulk = bulk(shell, mat_lib[shell]['structure'], a=mat_lib[shell]['a'])
        shell_supercell = shell_bulk * (8, 8, 8)
        core_center = qd_core.get_center_of_mass()
        core_max_r = np.max(np.linalg.norm(qd_core.positions - core_center, axis=1))

        shell_pos = shell_supercell.positions
        shell_center = shell_supercell.get_center_of_mass()
        dist = np.linalg.norm(shell_pos - shell_center, axis=1)
        r_inner = core_max_r + 1.0
        r_outer = core_max_r + shell_thickness_A
        shell_mask = (dist >= r_inner) & (dist <= r_outer)
        qd_shell = shell_supercell[shell_mask]
        qd_shell.positions += core_center - shell_center

        qd = qd_core + qd_shell
        qd.center(vacuum=5.0)
        return qd

    def make_amorphous(self, material='Si', structure='diamond', a=5.43,
                      size=3, density_scale=0.95, disorder=0.3):
        crystal = bulk(material, structure, a=a, cubic=True) * (size,size,size)
        crystal.set_cell(crystal.cell * (1/density_scale)**(1/3), scale_atoms=True)
        pos = crystal.get_positions()
        bond_len = a * 0.25 * np.sqrt(3)
        pos += np.random.normal(0, bond_len * disorder, pos.shape)
        crystal.set_positions(pos)
        return crystal

builder = StructureBuilder()

Sidebar controls
mode = st.sidebar.selectbox("Mode", ["Quantum Dot", "Amorphous Material"])

if mode == "Quantum Dot":
    st.sidebar.subheader("QD Parameters")
    core = st.sidebar.selectbox("Core", ['CdTe', 'CdS', 'ZnS', 'Si'])
    shell = st.sidebar.selectbox("Shell", ['ZnS', 'CdTe', 'Si'])
    r_core = st.sidebar.slider("Core radius [A]", 5, 30, 15)
    t_shell = st.sidebar.slider("Shell thickness [A]", 0, 15, 5)

    if st.sidebar.button("Build QD"):
        with st.spinner("Building..."):
            qd = builder.make_core_shell_qd(core, shell, r_core, t_shell)
            st.session_state['atoms'] = qd
            st.success(f"Built {core}/{shell} QD: {len(qd)} atoms")

else:
    st.sidebar.subheader("Amorphous Parameters")
    mat = st.sidebar.selectbox("Material", ['Si', 'ZnS', 'CdTe'])
    mat_dict = {'Si': ('diamond', 5.43), 'ZnS': ('zincblende', 5.41), 'CdTe': ('zincblende', 6.48)}
    struct, a = mat_dict[mat]
    size = st.sidebar.slider("Supercell size", 2, 5, 3)
    density = st.sidebar.slider("Density scale", 0.90, 1.0, 0.95)
    disorder = st.sidebar.slider("Disorder", 0.0, 0.6, 0.3)

    if st.sidebar.button("Build Amorphous"):
        with st.spinner("Building..."):
            amorph = builder.make_amorphous(mat, struct, a, size, density, disorder)
            st.session_state['atoms'] = amorph
            st.success(f"Built a-{mat}: {len(amorph)} atoms")

3D viewer + download
if 'atoms' in st.session_state:
    atoms = st.session_state['atoms']
    col1, col2 = st.columns([2,1])

    with col1:
        st.subheader("3D View - atoms + bonds")
        with tempfile.NamedTemporaryFile(suffix='.xyz', delete=False) as tmp:
            write(tmp.name, atoms)
            xyz = open(tmp.name).read()

        view = py3Dmol.view(width=600, height=400)
        view.addModel(xyz, 'xyz')
        view.setStyle({'sphere': {'scale': 0.3}, 'stick': {'radius': 0.15}})
        view.zoomTo()
        showmol(view, height=400, width=600)

    with col2:
        st.subheader("Info")
        st.write(f"**Atoms:** {len(atoms)}")
        st.write(f"**Formula:** {atoms.get_chemical_formula()}")
        st.write(f"**Cell:** {atoms.cell[0,0]:.1f} A")

        # Download buttons
        with tempfile.NamedTemporaryFile(suffix='.xyz', delete=False) as tmp:
            write(tmp.name, atoms)
            st.download_button("Download XYZ", open(tmp.name).read(),
                               file_name="structure.xyz")
        with tempfile.NamedTemporaryFile(suffix='.cif', delete=False) as tmp:
            write(tmp.name, atoms)
            st.download_button("Download CIF", open(tmp.name).read(),
                               file_name="structure.cif")
Install + run:
pip install streamlit ase stmol py3Dmol
streamlit run app.py
Opens in browser. Sliders for core/shell size, dropdowns for materials. 3D view rotates, shows bonds. http://Download.xyz/.cif buttons.

Here's a desktop GUI using PyQt5 + pyvista for real 3D atoms+bonds rendering. Looks like a mini VESTA.
 
Desktop App: QD + Amorphous Builder
import sys
import numpy as np
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
                             QHBoxLayout, QTabWidget, QLabel, QComboBox,
                             QSlider, QPushButton, QFileDialog, QMessageBox, QSpinBox,
                             QDoubleSpinBox, QGroupBox)
from PyQt5.QtCore import Qt
import pyvista as pv
from pyvistaqt import QtInteractor
from ase import Atoms
from ase.build import bulk
from ase.cluster import wulff_construction
from ase.io import write
 
class StructureBuilder:
    def __init__(self):
        self.mat_lib = {
            'CdTe': {'structure': 'zincblende', 'a': 6.48, 'Z': [48, 52]},
            'CdS': {'structure': 'zincblende', 'a': 5.82, 'Z': [48, 16]},
            'ZnS': {'structure': 'zincblende', 'a': 5.41, 'Z': [30, 16]},
            'GaAs': {'structure': 'zincblende', 'a': 5.65, 'Z': [31, 33]},
            'Si': {'structure': 'diamond', 'a': 5.43, 'Z': [14]},
            'Ge': {'structure': 'diamond', 'a': 5.66, 'Z': [32]},
        }
        self.colors = {48: 'gold', 52: 'cyan', 16: 'yellow', 30: 'slategray',
                       31: 'lightblue', 33: 'orange', 14: 'blue', 32: 'gray'}
 
    def make_core_shell_qd(self, core='CdTe', shell='ZnS',
                          core_radius_A=15, shell_thickness_A=5):
        core_bulk = bulk(core, self.mat_lib[core]['structure'],
 
Here’s a desktop GUI using `PyQt5` + `pyvistaqt`. Native window, real-time 3D viewer with atoms + bonds, sliders for QD/amorphous params, save button.
 
This is the “pro” version. Looks/works like VESTA but specialized for QDs + amorphous.
 
Desktop GUI: PyQt5 + PyVista
 
python
import sys
import numpy as np
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
                             QHBoxLayout, QTabWidget, QLabel, QComboBox,
                             QSlider, QPushButton, QFileDialog, QSpinBox,
                             QDoubleSpinBox, QGroupBox, QTextEdit, QSplitter)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from pyvistaqt import QtInteractor
import pyvista as pv
from ase import Atoms
from ase.build import bulk
from ase.cluster import wulff_construction
from ase.io import write
import tempfile
 
class StructureWorker(QThread):
    finished = pyqtSignal(object, str)
    error = pyqtSignal(str)
 
    def __init__(self, mode, params):
        super().__init__()
        self.mode = mode
        self.params = params
 
    def run(self):
        try:
            if self.mode == "qd":
                atoms = self.make_core_shell_qd(**self.params)
                msg = f"Built {self.params['core']}/{self.params['shell']} QD: {len(atoms)} atoms"
            else:
                atoms = self.make_amorphous(**self.params)
                msg = f"Built a-{self.params['material']}: {len(atoms)} atoms"
            self.finished.emit(atoms, msg)
        except Exception as e:
            self.error.emit(str(e))
 
    def make_core_shell_qd(self, core, shell, core_radius_A, shell_thickness_A):
        mat_lib = {
            'CdTe': {'structure': 'zincblende', 'a': 6.48},
            'CdS': {'structure': 'zincblende', 'a': 5.82},
            'ZnS': {'structure': 'zincblende', 'a': 5.41},
            'GaAs': {'structure': 'zincblende', 'a': 5.65},
            'Si': {'structure': 'diamond', 'a': 5.43},
        }
        core_bulk = bulk(core, mat_lib['structure'], a=mat_lib['a'])
        surfaces = [(1, 0, 0), (1, 1, 1)]
        esurf = [1.0, 1.0]
        qd_core = wulff_construction(core_bulk, surfaces, esurf,
                                     size=core_radius_A, rounding='above')
 
        if shell_thickness_A == 0:
            return qd_core
 
        shell_bulk = bulk(shell, mat_lib[shell]['structure'], a=mat_lib[shell]['a'])
        shell_supercell = shell_bulk * (8, 8, 8)
        core_center = qd_core.get_center_of_mass()
        core_max_r = np.max(np.linalg.norm(qd_core.positions - core_center, axis=1))
 
        shell_pos = shell_supercell.positions
        shell_center = shell_supercell.get_center_of_mass()
        dist = np.linalg.norm(shell_pos - shell_center, axis=1)
        r_inner = core_max_r + 1.0
        r_outer = core_max_r + shell_thickness_A
        shell_mask = (dist >= r_inner) & (dist <= r_outer)
        qd_shell = shell_supercell[shell_mask]
        qd_shell.positions += core_center - shell_center
 
        qd = qd_core + qd_shell
        qd.center(vacuum=5.0)
        return qd
 
    def make_amorphous(self, material, size, density_scale, disorder):
        mat_dict = {
            'Si': ('diamond', 5.43),
            'ZnS': ('zincblende', 5.41),
            'CdTe': ('zincblende', 6.48),
            'SiO2': ('beta_cristobalite', 7.16)
        }
        structure, a = mat_dict[material]
        crystal = bulk(material, structure, a=a, cubic=True) * (size,size,size)
        crystal.set_cell(crystal.cell * (1/density_scale)**(1/3), scale_atoms=True)
        pos = crystal.get_positions()
        bond_len = a * 0.25 * np.sqrt(3)
        pos += np.random.normal(0, bond_len * disorder, pos.shape)
        crystal.set_positions(pos)
        return crystal
 
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.atoms = None
        self.setWindowTitle("QD + Amorphous Structure Builder")
        self.setGeometry(100, 100, 1200, 700)
        self.init_ui()
 
    def init_ui(self):
        central = QWidget()
        self.setCentralWidget(central)
        layout = QHBoxLayout(central)
 
        # Left: Controls
        control_widget = QWidget()
        control_layout = QVBoxLayout(control_widget)
        control_widget.setMaximumWidth(350)
 
        tabs = QTabWidget()
        control_layout.addWidget(tabs)
 
        # QD Tab
        qd_tab = QWidget()
        qd_layout = QVBoxLayout(qd_tab)
        qd_group = QGroupBox("Quantum Dot Parameters")
        qd_form = QVBoxLayout(qd_group)
 
        qd_form.addWidget(QLabel("Core Material:"))
        self.qd_core = QComboBox()
        self.qd_core.addItems(['CdTe', 'CdS', 'ZnS', 'GaAs', 'Si'])
        qd_form.addWidget(self.qd_core)
 
        qd_form.addWidget(QLabel("Shell Material:"))
        self.qd_shell = QComboBox()
        self.qd_shell.addItems(['ZnS', 'CdTe', 'Si'])
        qd_form.addWidget(self.qd_shell)
 
        qd_form.addWidget(QLabel("Core Radius [Å]:"))
        self.qd_r_core = QSlider(Qt.Horizontal)
        self.qd_r_core.setRange(5, 30)
        self.qd_r_core.setValue(15)
        self.qd_r_label = QLabel("15 Å")
        self.qd_r_core.valueChanged.connect(lambda v: self.qd_r_label.setText(f"{v} Å"))
        qd_form.addWidget(self.qd_r_core)
        qd_form.addWidget(self.qd_r_label)
 
        qd_form.addWidget(QLabel("Shell Thickness [Å]:"))
        self.qd_t_shell = QSlider(Qt.Horizontal)
        self.qd_t_shell.setRange(0, 15)
        self.qd_t_shell.setValue(5)
        self.qd_t_label = QLabel("5 Å")
        self.qd_t_shell.valueChanged.connect(lambda v: self.qd_t_label.setText(f"{v} Å"))
        qd_form.addWidget(self.qd_t_shell)
        qd_form.addWidget(self.qd_t_label)
 
        self.qd_build_btn = QPushButton("Build QD")
        self.qd_build_btn.clicked.connect(self.build_qd)
        qd_form.addWidget(self.qd_build_btn)
        qd_layout.addWidget(qd_group)
        qd_layout.addStretch()
        tabs.addTab(qd_tab, "Quantum Dot")
 
        # Amorphous Tab
        amor_tab = QWidget()
        amor_layout = QVBoxLayout(amor_tab)
        amor_group = QGroupBox("Amorphous Parameters")
        amor_form = QVBoxLayout(amor_group)
 
        amor_form.addWidget(QLabel("Material:"))
        self.amor_mat = QComboBox()
        self.amor_mat.addItems(['Si', 'ZnS', 'CdTe', 'SiO2'])
        amor_form.addWidget(self.amor_mat)
 
        amor_form.addWidget(QLabel("Supercell Size:"))
        self.amor_size = QSpinBox()
        self.amor_size.setRange(2, 6)
        self.amor_size.setValue(3)
        amor_form.addWidget(self.amor_size)
 
        amor_form.addWidget(QLabel("Density Scale:"))
        self.amor_density = QDoubleSpinBox()
        self.amor_density.setRange(0.85, 1.0)
        self.amor_density.setSingleStep(0.01)
        self.amor_density.setValue(0.95)
        amor_form.addWidget(self.amor_density)
 
        amor_form.addWidget(QLabel("Disorder:"))
        self.amor_disorder = QDoubleSpinBox()
        self.amor_disorder.setRange(0.0, 0.6)
        self.amor_disorder.setSingleStep(0.05)
        self.amor_disorder.setValue(0.3)
        amor_form.addWidget(self.amor_disorder)
 
        self.amor_build_btn = QPushButton("Build Amorphous")
        self.amor_build_btn.clicked.connect(self.build_amorphous)
        amor_form.addWidget(self.amor_build_btn)
        amor_layout.addWidget(amor_group)
        amor_layout.addStretch()
        tabs.addTab(amor_tab, "Amorphous")
 
        # Info + Save
        self.info_text = QTextEdit()
        self.info_text.setReadOnly(True)
        self.info_text.setMaximumHeight(150)
        control_layout.addWidget(QLabel("Log:"))
        control_layout.addWidget(self.info_text)
 
        save_btn = QPushButton("Save Structure As...")
        save_btn.clicked.connect(self.save_structure)
        control_layout.addWidget(save_btn)
 
        layout.addWidget(control_widget)
 
        # Right: 3D Viewer
        self.plotter = QtInteractor(self)
        layout.addWidget(self.plotter.interactor, stretch=1)
        self.plotter.set_background('white')
 
    def log(self, text):
        self.info_text.append(text)
 
    def build_qd(self):
        params = {
            'core': self.qd_core.currentText(),
            'shell': self.qd_shell.currentText(),
            'core_radius_A': self.qd_r_core.value(),
            'shell_thickness_A': self.qd_t_shell.value()
        }
        self.run_worker("qd", params)
 
    def build_amorphous(self):
        params = {
            'material': self.amor_mat.currentText(),
            'size': self.amor_size.value(),
            'density_scale': self.amor_density.value(),
            'disorder': self.amor_disorder.value()
        }
        self.run_worker("amorphous", params)
 
    def run_worker(self, mode, params):
        self.qd_build_btn.setEnabled(False)
        self.amor_build_btn.setEnabled(False)
        self.log("Building...")
        self.worker = StructureWorker(mode, params)
        self.worker.finished.connect(self.on_build_finished)
        self.worker.error.connect(self.on_build_error)
        self.worker.start()
 
    def on_build_finished(self, atoms, msg):
        self.atoms = atoms
        self.log(msg)
        self.update_3d_view()
        self.qd_build_btn.setEnabled(True)
        self.amor_build_btn.setEnabled(True)
 
    def on_build_error(self, err):
        self.log(f"Error: {err}")
        self.qd_build_btn.setEnabled(True)
        self.amor_build_btn.setEnabled(True)
 
    def update_3d_view(self):
        self.plotter.clear()
        if self.atoms is None:
            return
 
        # Atoms as spheres
        pos = self.atoms.get_positions()
        symbols = self.atoms.get_chemical_symbols()
 
        # Color map for elements
        colors = {'Cd': 'cyan', 'Te': 'gold', 'Zn': 'slateblue',
                  'S': 'yellow', 'Si': 'dodgerblue', 'Ga': 'lightgreen',
                  'As': 'violet', 'O': 'red', 'Ge': 'gray'}
 
        for sym in set(symbols):
            mask = np.array(symbols) == sym
            points = pv.PolyData(pos[mask])
            self.plotter.add_mesh(points, color=colors.get(sym, 'white'),
                                 point_size=15, render_points_as_spheres=True,
                                 label=sym)
 
        # Bonds - simple distance cutoff
        from ase.neighborlist import NeighborList
        cutoffs = [1.2 * pv.atomic_number_to_radius.get(Atoms(sym).get_atomic_numbers()[0], 1.0)
                   for sym in symbols]
        nl = NeighborList(cutoffs, self_interaction=False, bothways=True)
        nl.update(self.atoms)
 
        bonds = []
        for i in range(len(self.atoms)):
            indices, offsets = nl.get_neighbors(i)
            for j, offset in zip(indices, offsets):
                if i < j: # avoid double counting
                    p1 = pos[i]
                    p2 = pos[j] + np.dot(offset, self.atoms.get_cell())
                    bonds.append([p1, p2])
 
        if bonds:
            for p1, p2 in bonds:
                line = pv.Line(p1, p2)
                self.plotter.add_mesh(line, color='black', line_width=2)
 
        self.plotter.add_legend()
        self.plotter.reset_camera()
        self.plotter.show()
 
    def save_structure(self):
        if self.atoms is None:
            self.log("No structure to save")
            return
        path, _ = QFileDialog.getSaveFileName(self, "Save Structure", "",
                                             "XYZ Files (*.xyz);;CIF Files (*.cif);;PDB Files (*.pdb)")
        if path:
            write(path, self.atoms)
            self.log(f"Saved to {path}")
 
if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())
 
Install + Run:
pip install PyQt5 pyvistaqt pyvista ase
python qd_builder_gui.py
 
1. Native desktop window - No browser needed
2. Real 3D viewer - Rotate, zoom, pan. Atoms are spheres, bonds are sticks. Colors per element.
3. QD tab: Pick core/shell, drag sliders for radius/thickness, hit Build
4. Amorphous tab: Pick material, supercell size, density, disorder
5. Threading - GUI doesn’t freeze while building 500-atom QDs
6. Save button - Export `.xyz`, `.cif`, `.pdb` for Ovito/VESTA

Files

nanosuite_builder-simulator.pdf

Files (146.9 kB)

Name Size Download all
md5:6d086bf8136119e29955f86cc2e6ed21
146.9 kB Preview Download

Additional details

Software

Programming language
Python
Development Status
Active