Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
History
=======

2026.7.6 -- Energy-based contact and GUI refinements
* Added an ``energy`` contact method: with a Model Chemistry step before the
Dimer Builder step, the contact distance is found from the energy minimum
along each approach direction (falling back to the van der Waals estimate
for orientations with no binding well). It uses the new ``seamm_mdi`` MDI
driver to evaluate the energy, and reports which model chemistry was used
and how many times it was called.
* Configuration names now use a comma (e.g. ``2,1``) rather than a slash,
which is reserved for separating system and configuration names.
* The dialog no longer shows the unused Flowchart tab, and reminds you to add
a Model Chemistry step when the energy contact method is chosen and none
precedes this step.

2026.6.30 -- Initial release
* Generates sets of dimer (molecule-pair) configurations across a range of
separations and relative orientations, for building interaction-energy data
Expand Down
366 changes: 264 additions & 102 deletions dimer_builder_step/dimer_builder.py

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions dimer_builder_step/dimer_builder_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,18 +254,19 @@ class DimerBuilderParameters(seamm.Parameters):
),
},
"configuration name": {
"default": "orientation/distance",
"default": "orientation,distance",
"kind": "string",
"default_units": "",
"enumeration": ("orientation/distance", "separation", "sequential"),
"enumeration": ("orientation,distance", "separation", "sequential"),
"format_string": "",
"description": "Name the configurations:",
"help_text": (
"How to name each generated configuration. 'orientation/distance' "
"labels them by orientation and point index (1/1, 1/2, ..., 2/1, "
"How to name each generated configuration. 'orientation,distance' "
"labels them by orientation and point index (1,1 1,2 ... 2,1 "
"...) so the points of one scan group together; 'separation' "
"labels them by their separation; 'sequential' numbers them 1, 2, "
"... across all configurations."
"... across all configurations. A comma (not '/') is used since "
"'/' separates system and configuration names elsewhere."
),
},
"save scan variables as properties": {
Expand Down
180 changes: 52 additions & 128 deletions dimer_builder_step/tk_dimer_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,9 @@ class TkDimerBuilder(seamm.TkNode):
"""
The graphical part of a Dimer Builder step in a flowchart.

Attributes
----------
tk_flowchart : TkFlowchart = None
The flowchart that we belong to.
node : Node = None
The corresponding node of the non-graphical flowchart
canvas: tkCanvas = None
The Tk Canvas to draw on
dialog : Dialog
The Pmw dialog object
x : int = None
The x-coordinate of the center of the picture of the node
y : int = None
The y-coordinate of the center of the picture of the node
w : int = 200
The width in pixels of the picture of the node
h : int = 50
The height in pixels of the picture of the node
self[widget] : dict
A dictionary of tk widgets built using the information
contained in dimer_builder_parameters.py

See Also
--------
DimerBuilder, TkDimerBuilder,
DimerBuilderParameters,
DimerBuilder, DimerBuilderParameters
"""

def __init__(
Expand All @@ -55,32 +32,7 @@ def __init__(
w=200,
h=50,
):
"""
Initialize a graphical node.

Parameters
----------
tk_flowchart: Tk_Flowchart
The graphical flowchart that we are in.
node: Node
The non-graphical node for this step.
namespace: str
The stevedore namespace for finding sub-nodes.
canvas: Canvas
The Tk canvas to draw on.
x: float
The x position of the nodes center on the canvas.
y: float
The y position of the nodes cetner on the canvas.
w: float
The nodes graphical width, in pixels.
h: float
The nodes graphical height, in pixels.

Returns
-------
None
"""
"""Initialize a graphical node."""
self.namespace = namespace
self.dialog = None

Expand All @@ -96,54 +48,21 @@ def __init__(
self.create_dialog()

def create_dialog(self):
"""Create the dialog for the Dimer Builder parameters.

The base class builds a notebook with 'Parameters' and 'Results' tabs
(the step has a 'results' parameter). We deliberately do NOT add a
'Flowchart' tab: the sub-flowchart is unused now that the 'energy'
contact method drives an engine through a model chemistry over MDI. If a
sub-flowchart-based fallback is added later, the tab can be reinstated
(behind an advanced option).
"""
Create the dialog. A set of widgets will be chosen by default
based on what is specified in the dimer_builder_parameters
module.

Parameters
----------
None

Returns
-------
None

See Also
--------
TkDimerBuilder.reset_dialog
"""

super().create_dialog(title="Dimer Builder", widget="notebook")

# make it large!
screen_w = self.dialog.winfo_screenwidth()
screen_h = self.dialog.winfo_screenheight()
w = int(0.9 * screen_w)
h = int(0.8 * screen_h)
x = int(0.05 * screen_w / 2)
y = int(0.1 * screen_h / 2)

self.dialog.geometry(f"{w}x{h}+{x}+{y}")

# Add a tab for the sub-flowchart (used by the 'energy' contact method)
notebook = self["notebook"]
flowchart_frame = ttk.Frame(notebook)
self["flowchart frame"] = flowchart_frame
notebook.add(flowchart_frame, text="Flowchart", sticky=tk.NSEW)

self.tk_subflowchart = seamm.TkFlowchart(
master=flowchart_frame,
flowchart=self.node.subflowchart,
namespace=self.namespace,
)
self.tk_subflowchart.draw()

# Shortcut for parameters
P = self.node.parameters

# A frame to hold the control parameters
parameters_frame = self["parameters frame"] = ttk.LabelFrame(
frame = self["parameters frame"] = ttk.LabelFrame(
self["frame"],
borderwidth=4,
relief="sunken",
Expand All @@ -154,12 +73,25 @@ def create_dialog(self):

for key in DimerBuilderParameters.parameters:
if key not in ("results",):
self[key] = P[key].widget(parameters_frame)
self[key] = P[key].widget(frame)

# Shown only when the energy contact method is selected.
self["energy note"] = ttk.Label(
frame,
text=(
"The 'energy' contact method needs a Model Chemistry step before "
"this step, to define the engine and method (e.g. MOPAC PM6-ORG)."
),
foreground="blue",
wraplength=500,
justify=tk.LEFT,
)

# Comboboxes whose value changes the layout re-lay out the dialog
# Comboboxes whose value changes the layout re-lay out the dialog.
for key in (
"input mode",
"spacing",
"contact method",
"monomer A configurations",
"monomer B configurations",
):
Expand All @@ -170,17 +102,7 @@ def create_dialog(self):
self.reset_dialog()

def reset_dialog(self, widget=None):
"""Layout the widgets in the dialog as needed for the current state.

Parameters
----------
widget : Tk Widget = None

Returns
-------
None
"""
# Remove any widgets previously packed
"""Lay out the parameters frame in the Parameters tab."""
frame = self["frame"]
for slave in frame.grid_slaves():
slave.grid_forget()
Expand All @@ -189,13 +111,13 @@ def reset_dialog(self, widget=None):
frame.columnconfigure(0, weight=1)

self.reset_parameters_frame()

return 1

def reset_parameters_frame(self):
"""Lay out the control parameters according to the current choices."""
"""Lay out the control parameters for the current choices."""
mode = self["input mode"].get()
spacing = self["spacing"].get()
energy = self["contact method"].get() == "energy"

frame = self["parameters frame"]
for slave in frame.grid_slaves():
Expand All @@ -212,12 +134,10 @@ def add(key):

add("input mode")

# Input sources
if mode == "two monomer sets":
sources = ("monomer A", "monomer B")
else:
sources = ("monomer A",)

for prefix in sources:
add(prefix)
add(f"{prefix} configurations")
Expand All @@ -228,20 +148,23 @@ def add(key):
):
add(f"{prefix} configuration name")

# Orientation sampling only applies when assembling from monomers
if mode == "two monomer sets":
add("number of orientations")
add("random seed")

# Radial scan
for key in ("contact method", "innermost gap", "maximum separation", "spacing"):
add("contact method")
if energy and not self._upstream_has_model_chemistry():
self["energy note"].grid(
row=row, column=0, columnspan=2, sticky=tk.W, pady=(0, 6)
)
row += 1
for key in ("innermost gap", "maximum separation", "spacing"):
add(key)
if spacing == "explicit":
add("separations")
else:
add("number of separations")

# Output
for key in (
"system name",
"configuration name",
Expand All @@ -252,24 +175,25 @@ def add(key):
sw.align_labels(widgets, sticky=tk.E)
frame.columnconfigure(1, weight=1)

def right_click(self, event):
"""
Handles the right click event on the node.

Parameters
----------
event : Tk Event

Returns
-------
None
def _upstream_has_model_chemistry(self):
"""True if a Model Chemistry step precedes this one in the flowchart.

See Also
--------
TkDimerBuilder.edit
Uses the shared ``previous_nodes()`` helper and checks the Python type by
name + module (no import dependency on model_chemistry_step). On any error
(e.g. the node is not yet linked into the flowchart) returns False, so the
reminder is shown -- the safe default.
"""
try:
return any(
type(node).__name__ == "ModelChemistry"
and type(node).__module__.startswith("model_chemistry_step")
for node in self.previous_nodes()
)
except Exception:
return False

def right_click(self, event):
"""Handle a right-click: add the Edit... item."""
super().right_click(event)
self.popup_menu.add_command(label="Edit..", command=self.edit)

self.popup_menu.tk_popup(event.x_root, event.y_root, 0)
12 changes: 12 additions & 0 deletions docs/developer_guide/campaigns/2026-07-06/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
=====================================
2026-07-06: MDI energy-contact method
=====================================

The Dimer Builder's ``contact method = energy`` was added as part of the
campaign that created the reusable ``seamm_mdi`` MDI driver facility. That
campaign's full write-up -- motivation, design, the socket-direction decision,
and the cross-package changes (including this plug-in) -- lives with the
facility:

* seamm_mdi developer guide:
https://molssi-seamm.github.io/seamm_mdi/developer_guide/campaigns/2026-07-06/NOTES.html
16 changes: 0 additions & 16 deletions docs/developer_guide/campaigns/YYYY-MM-DD/index.rst

This file was deleted.

9 changes: 1 addition & 8 deletions docs/developer_guide/campaigns/index.rst
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
Campaigns
=========

Initial Suggestion
------------------
Rename the subdirectory YYYY-MM-DD to the appropriate data, edit index.rst that it
contains to have the same data. It expects files of the form *scope* for a scope
document and NOTES* for potentially several sets of notes as the campaign unfolds.

Contents:
Notes from development campaigns.

.. toctree::
:glob:
:maxdepth: 1

*/index

5 changes: 4 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
Welcome to the documentation for the Dimer Builder SEAMM plug-in
================================================================

Dimer Builder <does what? replace this text>
The Dimer Builder step generates configurations of a pair (or small cluster) of
molecules across a range of separations and relative orientations -- for
computing intermolecular interaction energies and building training sets for
machine-learned force fields.

.. grid:: 1 1 2 2

Expand Down
Loading
Loading