Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Molecule preview button and fast plotting #620

Open
wants to merge 22 commits into
base: protos
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
75d8bd8
Added molecule preview button
Nov 18, 2024
15c1882
Merge branch 'protos' of github.com:ISISNeutronMuon/MDANSE into Molec…
Nov 18, 2024
43b33de
Added molecule name to preview
Nov 21, 2024
abe307f
Merge branch 'protos' of github.com:ISISNeutronMuon/MDANSE into Molec…
Nov 21, 2024
4eca1f2
Black formatting
Nov 21, 2024
c9e4f27
removed commented code for x, y, z axes
Nov 21, 2024
bb56614
Merge branch 'protos' of github.com:ISISNeutronMuon/MDANSE into Molec…
Dec 8, 2024
436394f
Added fast plotting and molecule preview functionality
Dec 10, 2024
3387587
Deleted unnecessary files
Dec 10, 2024
1234683
Merge branch 'protos' of github.com:ISISNeutronMuon/MDANSE into Molec…
Dec 18, 2024
2eb14c4
Merge branch 'protos' of github.com:ISISNeutronMuon/MDANSE into Molec…
Jan 20, 2025
58eadf6
created unique_molecule_names
Jan 20, 2025
bfaf550
Merge branch 'maciej/simplify-chemical-system' into molecule-preview-…
MBartkowiakSTFC Jan 23, 2025
6250ccf
Connect MoleculeWidget to the new ChemicalSystem
MBartkowiakSTFC Jan 23, 2025
31927b2
Merge branch 'maciej/simplify-chemical-system' into molecule-preview-…
MBartkowiakSTFC Jan 24, 2025
089f977
Merge branch 'maciej/simplify-chemical-system' into molecule-preview-…
MBartkowiakSTFC Jan 31, 2025
64ecfd1
Apply black to MDANSE_GUI
MBartkowiakSTFC Jan 31, 2025
46be2c8
Handle more errors when reading H5MD
MBartkowiakSTFC Jan 31, 2025
ba1dbba
Merge branch 'protos' of https://github.com/ISISNeutronMuon/MDANSE in…
MBartkowiakSTFC Feb 3, 2025
4d75783
Improve H5MD loading based on new input files
MBartkowiakSTFC Feb 3, 2025
d21f020
Merge branch 'maciej/simplify-chemical-system' into molecule-preview-…
MBartkowiakSTFC Feb 3, 2025
71e303b
Add checks for trajectories without molecules in MoleculeWidget
MBartkowiakSTFC Feb 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def configure(self, value) -> None:

self._choices = trajectory_configurator[
"instance"
].chemical_system.unique_molecules()
].chemical_system.unique_molecule_names

if value in self._choices:
self.error_status = "OK"
Expand Down
4 changes: 2 additions & 2 deletions MDANSE/Tests/UnitTests/molecules/test_connectivity.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,5 @@ def test_identify_molecules(trajectory: Trajectory):
for ms in molstrings[1:]:
result = result and ms == molstrings[0]
assert result
print(chemical_system.unique_molecules())
assert len(chemical_system.unique_molecules()) == 1
print(chemical_system.unique_molecule_names)
assert len(chemical_system.unique_molecule_names) == 1
155 changes: 155 additions & 0 deletions MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MoleculePreviewWidget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
from qtpy.QtWidgets import QWidget, QSizePolicy, QVBoxLayout, QLabel, QDialog
from qtpy.Qt3DExtras import Qt3DWindow
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need to update the pyproject.toml and add pyqt3d as a dependency. @MBartkowiakSTFC I guess we should add PyQt6-3D since PyQt6<=6.7.0 is in the pyproject.toml.

from qtpy.QtGui import QColor
from qtpy.Qt3DRender import QDirectionalLight, QGeometryRenderer
from qtpy.QtGui import QColor, QVector3D, QQuaternion, QFont
from qtpy.Qt3DExtras import (
QPhongMaterial,
QCylinderMesh,
QCuboidMesh,
QPlaneMesh,
QSphereMesh,
Qt3DWindow,
QOrbitCameraController,
)
from qtpy.QtCore import Qt as _Qt
from qtpy.Qt3DCore import QEntity, QTransform
from MDANSE.Chemistry import ATOMS_DATABASE
import numpy as np


class MoleculePreviewWidget(QDialog):
def __init__(self, parent, molecule_information, molecule_name):
super().__init__(parent)
self.setWindowTitle("Molecule Preview")
self.resize(800, 600)
self.view = Qt3DWindow()
self.view.defaultFrameGraph().setClearColor(
QColor(0x4D4D4F)
) # molecular viewer mdansechemistry atoms.json
container = QWidget.createWindowContainer(
self.view
) # from mdanse chemistry atoms database
screenSize = self.view.screen().size()
container.setMinimumSize(200, 100)
container.setMaximumSize(screenSize)
container.setSizePolicy(
QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding
)
layout = QVBoxLayout()
layout.addWidget(container)
self.rootEntity = QEntity()
self.cuboidTransform = QTransform()
self.axes = []
mass = []
coords = []
info_text = f"Molecule name: {molecule_name}\n"
for key in molecule_information["atom_number"]:
info_text += (
f"Number of {key} atoms: {molecule_information['atom_number'][key]}\n"
)

info_text += f"Number of such molecules in trajectory: {molecule_information['no_of_molecules']}\n"

for i, (index, atom) in enumerate(
molecule_information["atom_information"].items()
):
x, y, z = atom["coords"]
x, y, z = (20 * x - 10, 20 * y - 10, 20 * z - 10)
symbol = atom["symbol"]
colour = ATOMS_DATABASE.get_atom_property(symbol, "color")
radius = ATOMS_DATABASE.get_atom_property(symbol, "covalent_radius")
mass.append(ATOMS_DATABASE.get_atom_property(symbol, "atomic_weight"))
coords.append(atom["coords"])
r, g, b = [int(x) for x in colour.split(";")]
colour = QColor(r, g, b)
m_sphereEntity = QEntity(self.rootEntity)
sphereMesh = QSphereMesh()
sphereMesh.setRings(20)
sphereMesh.setSlices(20)
sphereMesh.setRadius(radius * 10)
sphereTransform = QTransform()
sphereTransform.setScale(1.0)
sphereTransform.setTranslation(QVector3D(x, y, z))
sphereMaterial = QPhongMaterial()
sphereMaterial.setDiffuse(colour)
sphereMaterial.setAmbient(colour) # Set ambient to the same as diffuse.
# sphereMaterial.setSpecular(QColor(0, 0, 0)) # Eliminate specular reflection.
# sphereMaterial.setShininess(0.0) # Eliminate shininess.
m_sphereEntity.addComponent(sphereMesh)
m_sphereEntity.addComponent(sphereMaterial)
m_sphereEntity.addComponent(sphereTransform)

atom_information = molecule_information["atom_information"]
for bond in molecule_information["bond_info"]:
i, j = bond
coord1, coord2 = (
atom_information[i]["coords"],
atom_information[j]["coords"],
)
coord1 = (20 * coord1[0] - 10, 20 * coord1[1] - 10, 20 * coord1[2] - 10)
coord2 = (20 * coord2[0] - 10, 20 * coord2[1] - 10, 20 * coord2[2] - 10)
direction = QVector3D(
coord2[0] - coord1[0], coord2[1] - coord1[1], coord2[2] - coord1[2]
)
length = direction.length()
direction.normalize()
# Compute rotation
up_vector = QVector3D(0, 1, 0)
axis = QVector3D.crossProduct(up_vector, direction)
angle = float(
np.degrees(np.arccos(QVector3D.dotProduct(up_vector, direction)))
)

# Create cylinder mesh
cylinder_mesh = QCylinderMesh()
cylinder_mesh.setRadius(radius)
cylinder_mesh.setLength(length)

# Create material
material = QPhongMaterial()
# material.setDiffuse(QColor(color))

# Set transformation
transform = QTransform()
transform.setTranslation(QVector3D(*coord1) + direction * length / 2)
transform.setRotation(QQuaternion.fromAxisAndAngle(axis, angle))

# Create entity
entity = QEntity(self.rootEntity)
entity.addComponent(cylinder_mesh)
entity.addComponent(material)
entity.addComponent(transform)

info_label = QLabel(info_text)
font = QFont("Arial", 12)
info_label.setFont(font)
info_label.setWordWrap(True)
layout.addWidget(info_label)
self.setLayout(layout)
mass = np.array(mass)
coords = np.array(coords)
com = np.einsum("i,ik->k", mass, coords) / np.sum(mass)
x_com, y_com, z_com = 20 * com - 10
# Camera
self.camera = self.view.camera()
self.camera.lens().setPerspectiveProjection(45.0, 16.0 / 9.0, 0.1, 1000.0)
self.camera.setPosition(QVector3D(x_com + 5, y_com + 5, z_com + 10))
self.camera.setViewCenter(QVector3D(x_com, y_com, z_com))
self.camera.setUpVector(QVector3D(0.0, 0.0, 1.0))
# add light
lightEntity = QEntity(self.rootEntity)
light = QDirectionalLight(lightEntity)
light.setColor(_Qt.white)
light.setIntensity(1)
lightEntity.addComponent(light)
lightTransform = QTransform(lightEntity)
lightTransform.setTranslation(self.camera.position())
lightEntity.addComponent(lightTransform)
# For camera controls
camController = QOrbitCameraController(self.rootEntity)
camController.setLinearSpeed(-20)
camController.setLookSpeed(-90)
camController.setCamera(self.camera)
self.view.setRootEntity(self.rootEntity)
# # info_box.setStandardButtons(QMessageBox.Close)
98 changes: 88 additions & 10 deletions MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MoleculeWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,20 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
from qtpy.QtWidgets import QComboBox

from qtpy.QtCore import Slot
from qtpy.QtWidgets import QComboBox, QPushButton, QDialog
from qtpy.QtWidgets import (
QHBoxLayout,
QVBoxLayout,
QWidget,
QSizePolicy,
QFrame,
QSizePolicy,
QPushButton,
QFileDialog,
)
from MDANSE_GUI.InputWidgets.WidgetBase import WidgetBase
from MDANSE_GUI.InputWidgets.MoleculePreviewWidget import MoleculePreviewWidget


class MoleculeWidget(WidgetBase):
Expand All @@ -27,7 +38,7 @@ def __init__(self, *args, **kwargs):
if trajectory_configurator is not None:
option_list = trajectory_configurator[
"instance"
].chemical_system.unique_molecules()
].chemical_system.unique_molecule_names
if len(option_list) > 0:
default_option = option_list[0]
else:
Expand All @@ -36,24 +47,91 @@ def __init__(self, *args, **kwargs):
else:
option_list = configurator.choices
default_option = configurator.default
field = QComboBox(self._base)
field.addItems(option_list)
field.setCurrentText(default_option)
field.currentTextChanged.connect(self.updateValue)
traj_config = self._configurator._configurable[
self._configurator._dependencies["trajectory"]
]
hdf_traj = traj_config["hdf_trajectory"]
unique_molecules = hdf_traj.chemical_system.unique_molecules
traj_bond_list = hdf_traj.chemical_system._bonds
self.mol_dict = {}
coords_0 = hdf_traj.trajectory.coordinates(0)
for i, mol in enumerate(unique_molecules):
indices = []
self.atom_information = {}
no_of_molecules = hdf_traj.chemical_system.number_of_molecules(mol.name)
self.atom_number = {}
for atom in mol._atoms:
element = atom.element
try:
self.atom_number[element] += 1
except KeyError:
self.atom_number[element] = 1
index = atom.index
indices.append(index)
symbol = atom.symbol
coords = coords_0[index]
atom_dict = {"symbol": symbol, "element": element, "coords": coords}
self.atom_information[index] = atom_dict

bond_list = [
bond for bond in traj_bond_list if all(atom in indices for atom in bond)
]
self.mol_dict[mol.name] = {
"no_of_molecules": no_of_molecules,
"atom_information": self.atom_information,
"atom_number": self.atom_number,
"bond_info": bond_list,
}

self.field = QComboBox(self._base)
self.field.addItems(option_list)
self.field.setCurrentText(default_option)
self.selected_name = self.field.currentText()
self.selected_mol = self.mol_dict[self.selected_name]
self.field.currentTextChanged.connect(self.updateValue)
self.field.currentTextChanged.connect(self.molecule_changed)
button = QPushButton(self._base)
button.setText("Molecule Preview")
button.clicked.connect(self.button_clicked)
if self._tooltip:
tooltip_text = self._tooltip
else:
tooltip_text = (
"A single option can be picked out of all the options listed."
)
field.setToolTip(tooltip_text)
self._field = field
self._layout.addWidget(field)
self.field.setToolTip(tooltip_text)
self._field = self.field
self._layout.addWidget(self.field)
self._layout.addWidget(button)
self._configurator = configurator
self.default_labels()
self.update_labels()
self.updateValue()

@Slot()
def molecule_changed(self):
"""
Change molecule preview and molecule information
"""
self.selected_name = self.field.currentText()
self.selected_mol = self.mol_dict[self.selected_name]
self.window = MoleculePreviewWidget(
self._base, self.selected_mol, self.selected_name
)

@Slot()
def button_clicked(self):
"""
Opens a window that shows a preview of selected molecule
"""
self.window = MoleculePreviewWidget(
self._base, self.selected_mol, self.selected_name
)
if self.window.isVisible():
self.window.close()
else:
self.window.show()

def configure_using_default(self):
"""This is too complex to have a default value"""

Expand Down
8 changes: 7 additions & 1 deletion MDANSE_GUI/Src/MDANSE_GUI/TabbedWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from MDANSE_GUI.Tabs.PlotSelectionTab import PlotSelectionTab
from MDANSE_GUI.Tabs.PlotTab import PlotTab
from MDANSE_GUI.Tabs.InstrumentTab import InstrumentTab
from MDANSE_GUI.Tabs.Views.PlotDataView import PlotDataView
from MDANSE_GUI.Widgets.StyleDialog import StyleDialog, StyleDatabase
from MDANSE_GUI.Widgets.NotificationTabWidget import NotificationTabWidget

Expand Down Expand Up @@ -109,10 +110,10 @@ def __init__(
self._tabs["Plot Creator"]._visualiser.create_new_text.connect(
self._tabs["Plot Holder"]._visualiser.new_text
)

self._tabs["Instruments"]._visualiser.instrument_details_changed.connect(
self._tabs["Actions"].update_action_after_instrument_change
)

self.tabs.currentChanged.connect(self.tabs.reset_current_color)

def createCommonModels(self):
Expand Down Expand Up @@ -326,6 +327,11 @@ def createPlotSelection(self):
self._tabs[name] = plot_tab
self._job_holder.results_for_loading.connect(plot_tab.load_results)
self._job_holder.results_for_loading.connect(plot_tab.tab_notification)
plot_tab._view.fast_plotting_data.connect(self.accept_external_data)

def accept_external_data(self, model):
self._tabs["Plot Creator"]._visualiser.new_plot()
self._tabs["Plot Holder"].accept_external_data(model)

def createPlotHolder(self):
name = "Plot Holder"
Expand Down
1 change: 1 addition & 0 deletions MDANSE_GUI/Src/MDANSE_GUI/Tabs/JobTab.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from MDANSE_GUI.Tabs.Visualisers.TextInfo import TextInfo
from MDANSE_GUI.Tabs.Models.JobTree import JobTree
from MDANSE_GUI.Tabs.Views.ActionsTree import ActionsTree
from MDANSE_GUI.InputWidgets.MoleculeWidget import MoleculeWidget


job_tab_label = """This is the list of jobs
Expand Down
5 changes: 3 additions & 2 deletions MDANSE_GUI/Src/MDANSE_GUI/Tabs/Models/PlottingContext.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,12 @@ def get_mpl_colours():

class SingleDataset:

def __init__(self, name: str, source: "h5py.File"):
def __init__(self, name: str, source: "h5py.File", linestyle: str = "-"):
self._name = name
self._filename = source.filename
self._curves = {}
self._curve_labels = {}
self._linestyle = linestyle
self._planes = {}
self._plane_labels = {}
self._data_limits = None
Expand Down Expand Up @@ -425,7 +426,7 @@ def add_dataset(self, new_dataset: SingleDataset):
new_dataset.longest_axis()[-1],
"",
self.next_colour(),
"-",
new_dataset._linestyle,
"",
]
]
Expand Down
1 change: 1 addition & 0 deletions MDANSE_GUI/Src/MDANSE_GUI/Tabs/PlotSelectionTab.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
data sets. Load the files and assign the data sets
to a plot. The plotting interface will appear
in a new tab of the interface.
DOUBLE CLICK FILE FOR FAST PLOTTING
"""


Expand Down
Loading
Loading