Skip to content

Commit 37832c2

Browse files
Add Optional Data Overlay for MoleculeViewer (#3059)
This pull request adds support for displaying per-orbital metadata overlays in the `MoleculeViewer` widget, allowing users to view additional information (such as energy, symmetry, and occupation) alongside the 3D molecular visualization. The changes span the Python widget, the TypeScript/React component, and the notebook sample, ensuring that metadata is passed through and rendered in the user interface. --------- Co-authored-by: Bill Ticehurst <billti@hotmail.com>
1 parent 3fe484a commit 37832c2

File tree

4 files changed

+133
-93
lines changed

4 files changed

+133
-93
lines changed

samples/notebooks/benzene_molecule/benzene.ipynb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,17 @@
132132
"\n",
133133
"# Load cached molecule data from file.\n",
134134
"molecule_data = Path(\"benzene_diradical.structure.xyz\").read_text()\n",
135+
"\n",
136+
"# Note: The \"info\" field is optional and can contain any metadata you want to display with the cube data.\n",
135137
"cube_data = {\n",
136-
" \"alpha_18\": Path(\"MO_alpha_18.cube\").read_text(),\n",
137-
" \"alpha_19\": Path(\"MO_alpha_19.cube\").read_text(),\n",
138+
" \"alpha_18\": {\n",
139+
" \"data\": Path(\"MO_alpha_18.cube\").read_text(),\n",
140+
" \"info\": {\"Energy (Ha)\": -0.392, \"Symmetry\": \"A1\", \"Occupation\": 1.0}\n",
141+
" },\n",
142+
" \"alpha_19\": {\n",
143+
" \"data\": Path(\"MO_alpha_19.cube\").read_text(),\n",
144+
" \"info\": {\"Energy (Ha)\": 0.581, \"Symmetry\": \"B2\", \"Occupation\": 0.0}\n",
145+
" }\n",
138146
"}\n",
139147
"\n",
140148
"MoleculeViewer(molecule_data=molecule_data, cube_data=cube_data, isoval=0.03)"

source/npm/qsharp/ux/chem/index.tsx

Lines changed: 92 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ import { createViewer, GLViewer } from "3dmol";
77
import "./style.css";
88
import { detectThemeChange } from "../themeObserver.js";
99

10+
type CubeData = {
11+
data: string;
12+
info?: { [key: string]: string | number };
13+
};
14+
1015
export function MoleculeViewer(props: {
1116
moleculeData: string;
12-
cubeData: { [key: string]: string };
17+
cubeData: { [key: string]: string | CubeData };
1318
isoValue?: number;
1419
}) {
1520
// Holds reference to the viewer div and 3Dmol viewer object.
@@ -21,6 +26,14 @@ export function MoleculeViewer(props: {
2126
const [isoval, setIsoval] = useState(props.isoValue || 0.02);
2227
const [cubeKey, setCubeKey] = useState(Object.keys(props.cubeData)[0] || "");
2328

29+
const cubeData: CubeData | null =
30+
!cubeKey || !props.cubeData[cubeKey]
31+
? null
32+
: // Normalize the cube data to the expected form
33+
typeof props.cubeData[cubeKey] === "string"
34+
? { data: props.cubeData[cubeKey] }
35+
: (props.cubeData[cubeKey] as CubeData);
36+
2437
// Runs after the DOM has been created. Create the 3Dmol viewer and adds the model.
2538
useEffect(() => {
2639
if (props.moleculeData && viewerRef.current) {
@@ -60,21 +73,21 @@ export function MoleculeViewer(props: {
6073
return;
6174
}
6275

63-
if (cubeKey && props.cubeData[cubeKey]) {
76+
if (cubeData) {
6477
activeCubeData.current.forEach((voldata) => {
6578
currViewer.removeShape(voldata);
6679
});
6780
activeCubeData.current = [];
68-
const cubeData = props.cubeData[cubeKey];
81+
6982
activeCubeData.current.push(
70-
currViewer.addVolumetricData(cubeData.trim(), "cube", {
83+
currViewer.addVolumetricData(cubeData.data.trim(), "cube", {
7184
isoval,
7285
opacity: 1,
7386
color: "#0072B2",
7487
}),
7588
);
7689
activeCubeData.current.push(
77-
currViewer.addVolumetricData(cubeData.trim(), "cube", {
90+
currViewer.addVolumetricData(cubeData.data.trim(), "cube", {
7891
isoval: -1 * isoval,
7992
opacity: 1,
8093
color: "#FFA500",
@@ -110,74 +123,88 @@ export function MoleculeViewer(props: {
110123
style="width: 640px; height: 480px;"
111124
></div>
112125

113-
<div id="view-dropdown-container" class="view-option">
114-
<label for="viewSelector">Visualization Style:</label>
115-
<select
116-
id="viewSelector"
117-
onChange={(e) => {
118-
const style = (e.target as HTMLSelectElement).value;
119-
setViewStyle(style);
120-
}}
121-
>
122-
<option value="Sphere">Sphere</option>
123-
<option value="Stick">Stick</option>
124-
<option value="Line">Line</option>
125-
</select>
126-
</div>
127-
{cubeKey ? (
128-
<>
129-
<div id="cube-dropdown-container" class="view-option">
130-
<label for="cubeSelector">Cube selection:</label>
126+
<div id="controls-overlay">
127+
<div class="controls-column">
128+
<div id="view-dropdown-container" class="view-option">
129+
<label for="viewSelector">Visualization Style:</label>
131130
<select
132-
id="cubeSelector"
131+
id="viewSelector"
133132
onChange={(e) => {
134-
const key = (e.target as HTMLSelectElement).value;
135-
setCubeKey(key);
133+
const style = (e.target as HTMLSelectElement).value;
134+
setViewStyle(style);
136135
}}
137136
>
138-
{Object.keys(props.cubeData).map((key) => (
139-
<option value={key} selected={key === cubeKey}>
140-
{key}
141-
</option>
142-
))}
137+
<option value="Sphere">Sphere</option>
138+
<option value="Stick">Stick</option>
139+
<option value="Line">Line</option>
143140
</select>
144141
</div>
142+
{cubeKey ? (
143+
<>
144+
<div id="cube-dropdown-container" class="view-option">
145+
<label for="cubeSelector">Cube selection:</label>
146+
<select
147+
id="cubeSelector"
148+
onChange={(e) => {
149+
const key = (e.target as HTMLSelectElement).value;
150+
setCubeKey(key);
151+
}}
152+
>
153+
{Object.keys(props.cubeData).map((key) => (
154+
<option value={key} selected={key === cubeKey}>
155+
{key}
156+
</option>
157+
))}
158+
</select>
159+
</div>
145160

146-
<div id="isoval-slider-container" class="view-option">
147-
<label for="isovalSlider">Adjust isovalue:</label>
148-
<input
149-
type="range"
150-
id="isovalSlider"
151-
min="0.005"
152-
max="0.1"
153-
step="0.005"
154-
value={isoval}
155-
onInput={(e) => {
156-
const new_isoval = parseFloat(
157-
(e.target as HTMLInputElement).value,
158-
);
159-
setIsoval(new_isoval);
160-
}}
161-
/>
162-
<input
163-
type="number"
164-
id="isovalInput"
165-
min="0.005"
166-
max="0.1"
167-
step="0.001"
168-
value={isoval}
169-
onInput={(e) => {
170-
const new_isoval = parseFloat(
171-
(e.target as HTMLInputElement).value,
172-
);
173-
if (!isNaN(new_isoval)) {
174-
setIsoval(new_isoval);
175-
}
176-
}}
177-
/>
161+
<div id="isoval-slider-container" class="view-option">
162+
<label for="isovalSlider">Adjust isovalue:</label>
163+
<input
164+
type="range"
165+
id="isovalSlider"
166+
min="0.005"
167+
max="0.1"
168+
step="0.005"
169+
value={isoval}
170+
onInput={(e) => {
171+
const new_isoval = parseFloat(
172+
(e.target as HTMLInputElement).value,
173+
);
174+
setIsoval(new_isoval);
175+
}}
176+
/>
177+
<input
178+
type="number"
179+
id="isovalInput"
180+
min="0.005"
181+
max="0.1"
182+
step="0.001"
183+
value={isoval}
184+
onInput={(e) => {
185+
const new_isoval = parseFloat(
186+
(e.target as HTMLInputElement).value,
187+
);
188+
if (!isNaN(new_isoval)) {
189+
setIsoval(new_isoval);
190+
}
191+
}}
192+
/>
193+
</div>
194+
</>
195+
) : null}
196+
</div>
197+
{cubeData?.info && (
198+
<div class="controls-column">
199+
{Object.entries(cubeData.info).map(([key, value]) => (
200+
<div class="data-overlay-row" key={key}>
201+
<label class="data-overlay-key">{key}:</label>
202+
<span class="data-overlay-value">{value}</span>
203+
</div>
204+
))}
178205
</div>
179-
</>
180-
) : null}
206+
)}
207+
</div>
181208
</div>
182209
);
183210
}

source/npm/qsharp/ux/chem/style.css

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
height: 100%;
1111
margin: 0;
1212
overflow: hidden;
13+
position: relative;
1314
/* border: 1px solid gray; */
1415
}
1516

@@ -18,17 +19,24 @@
1819
height: 100%;
1920
}
2021

21-
#view-dropdown-container {
22+
#controls-overlay {
2223
position: absolute;
23-
top: 10px; /* distance from bottom */
24-
left: 10px; /* distance from right */
25-
height: 20px;
24+
top: 10px;
25+
left: 10px;
26+
display: flex;
27+
align-items: flex-start;
28+
gap: 32px;
2629
padding: 10px;
27-
border-radius: 4px;
2830
z-index: 10;
2931
font-family: system-ui;
3032
}
3133

34+
.controls-column {
35+
display: flex;
36+
flex-direction: column;
37+
gap: 8px;
38+
}
39+
3240
#viewSelector {
3341
padding-left: 4px;
3442
}
@@ -51,31 +59,25 @@
5159
background-color: var(--qdk-host-background);
5260
}
5361

54-
#cube-dropdown-container {
55-
position: absolute;
56-
top: 40px; /* distance from bottom */
57-
left: 10px; /* distance from right */
58-
padding: 10px;
59-
height: 20px;
60-
border-radius: 4px;
61-
z-index: 10;
62-
font-family: system-ui;
62+
#isovalSlider {
63+
width: 100px;
64+
outline: none;
6365
}
6466

65-
#isoval-slider-container {
66-
position: absolute;
67-
top: 70px; /* distance from bottom */
68-
left: 10px; /* distance from right */
69-
padding: 10px;
70-
height: 40px;
71-
border-radius: 4px;
72-
z-index: 10;
73-
font-family: system-ui;
67+
.data-overlay-row {
68+
display: flex;
69+
gap: 8px;
7470
}
7571

76-
#isovalSlider {
77-
width: 100px;
78-
outline: none;
72+
.data-overlay-key {
73+
min-width: 80px;
74+
font-weight: normal;
75+
white-space: nowrap;
76+
}
77+
78+
.data-overlay-value {
79+
font-variant-numeric: tabular-nums;
80+
white-space: nowrap;
7981
}
8082

8183
#isovalInput {

source/widgets/src/qsharp_widgets/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,9 @@ def __init__(self, molecule_data, cube_data={}, isoval=0.02):
239239
240240
Parameters:
241241
- molecule_data: string containing the molecular data in XYZ format.
242+
- cube_data (optional): a dictionary where keys are cube names and values are dictionaries with the following structure:
243+
- "data": string containing the cube data in .cube file format.
244+
- "info": (optional) a dictionary containing any metadata you want to display with the cube data.
242245
"""
243246
super().__init__(
244247
molecule_data=molecule_data, cube_data=cube_data, isoval=isoval

0 commit comments

Comments
 (0)