Skip to content
Draft
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
23 changes: 22 additions & 1 deletion src/aiidalab_qe/app/result/components/summary/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,20 +193,41 @@ def _generate_report_parameters(self):
"cell_lengths": "{:.3f} {:.3f} {:.3f}".format(*structure.cell_lengths),
"cell_angles": "{:.0f} {:.0f} {:.0f}".format(*structure.cell_angles),
}


relax_value_mapping = {
"none": "off",
"positions": "atomic positions",
"positions_cell": "full geometry",
}


# check for fixed atoms
constraints = ''
fixed_atoms = structure.base.attributes.all.get('fixed_atoms', None)
other_constraints = structure.base.attributes.all.get('CONSTRAINTS', None)

if fixed_atoms is not None:
has_fixed = any(
any((int(v) == 0) for v in row if v is not None)
for row in fixed_atoms
)
# True if any element equals 0 (works for (N,3), (N,), or nested lists)
if has_fixed:
constraints += "There are fixed atoms."

if other_constraints is not None:
constraints += '; '.join(other_constraints['list'])
if not constraints:
constraints = "None"

report |= {
"basic_settings": {
"relaxed": relax_value_mapping.get(basic["relax_type"], "off"),
"protocol": basic["protocol"],
"spin_type": "off" if basic["spin_type"] == "none" else "on",
"electronic_type": basic["electronic_type"],
"periodicity": PERIODICITY_MAPPING.get(structure.pbc, "xyz"),
"constraints": constraints
},
"advanced_settings": {},
}
Expand Down
5 changes: 5 additions & 0 deletions src/aiidalab_qe/app/result/components/summary/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@
"type": "text",
"description": "The periodicity of the structure"
},
"constraints": {
"title": "Constraints",
"type": "text",
"description": "The constraints applied to the structure"
},
"functional": {
"title": "Functional",
"type": "link",
Expand Down
167 changes: 147 additions & 20 deletions src/aiidalab_qe/app/result/components/viewer/structure/model.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from __future__ import annotations
from pydoc import html

from aiida_quantumespresso.tools import data
from matplotlib import legend
import traitlets as tl
from ase.formula import Formula

Expand Down Expand Up @@ -74,30 +77,154 @@ def _get_structure_info(self):
</div>
"""

# def _get_atom_table_data(self):
# """Build table data; if 'fixed_atoms' is present in structure attributes,
# add a 'Free x,y,z' column showing '✓' for free (1) and 'x' for fixed (0)."""
# # Try to get fixed_atoms from AiiDA StructureData attributes
# fixed_atoms = self.structure.base.attributes.all.get('fixed_atoms', None)

# ase_atoms = self.structure.get_ase()

# # Header
# data = [["Atom index", "Chemical symbol", "Tag", "x (Å)", "y (Å)", "z (Å)"]]
# if fixed_atoms is not None:
# data[0].append("Free x,y,z")

# positions = ase_atoms.positions
# chemical_symbols = ase_atoms.get_chemical_symbols()
# tags = ase_atoms.get_tags()

# def fmt_free(mask):
# """mask: tuple/list of three 0/1; 0=free -> 'X', 1=fixed -> ' '."""
# try:
# x, y, z = mask
# except Exception:
# x = y = z = 0
# # If your UI collapses spaces, replace ' ' with '·' or '\u00A0' (NBSP).
# return f"({'x' if x == 0 else '✓'} {'x' if y == 0 else '✓'} {'x' if z == 0 else '✓'})"

# for idx, (symbol, tag, pos) in enumerate(zip(chemical_symbols, tags, positions), start=1):
# formatted_position = [f"{coord:.2f}" for coord in pos]
# row = [idx, symbol, tag, *formatted_position]

# if fixed_atoms is not None:
# mask = fixed_atoms[idx - 1] if idx - 1 < len(fixed_atoms) else (0, 0, 0)
# row.append(fmt_free(mask))

# data.append(row)

# return data
def _get_atom_table_data(self):
structure = self.structure.get_ase()
data = [
[
"Atom index",
"Chemical symbol",
"Tag",
"x (Å)",
"y (Å)",
"z (Å)",
]
]
positions = structure.positions
chemical_symbols = structure.get_chemical_symbols()
tags = structure.get_tags()

for index, (symbol, tag, position) in enumerate(
zip(chemical_symbols, tags, positions), start=1
):
formatted_position = [f"{coord:.2f}" for coord in position]
data.append([index, symbol, tag, *formatted_position])
"""Build table data.

- If 'fixed_atoms' is present in AiiDA StructureData attributes (or ASE arrays),
add a 'Free x,y,z' column showing '✓' for free (1) and 'x' for fixed (0).
- If constraints exist (CONSTRAINTS block), add a 'Constr.' column with
star labels (*1, *2, …) for atoms involved in each constraint.
"""
# --- source data ---
attrs_all = getattr(getattr(getattr(self.structure, "base", None), "attributes", None), "all", {}) or {}
fixed_atoms = attrs_all.get("fixed_atoms", None)

ase_atoms = self.structure.get_ase()

# Try also to read fixed mask from ASE arrays if attributes missing
if fixed_atoms is None and "fixed_atoms" in getattr(ase_atoms, "arrays", {}):
fixed_atoms = ase_atoms.arrays["fixed_atoms"].tolist()

# constraints: prefer attributes, else ASE info
constraints_info = attrs_all.get("CONSTRAINTS")
if constraints_info is None:
constraints_info = ase_atoms.info.get("CONSTRAINTS", None) if hasattr(ase_atoms, "info") else None
# normalize constraints_info
if not isinstance(constraints_info, dict):
constraints_info = {"number": 0, "tolerance": "1e-6", "list": []}
constraints_list = constraints_info.get("list", []) or []

# Build star marks per atom and a legend (legend not returned here)
def build_marks(n_atoms, entries):
marks = {i: [] for i in range(n_atoms)}
legend = []
for k, entry in enumerate(entries, start=1):
legend.append(f"*{k} {entry}")
parts = str(entry).strip().split()
if len(parts) >= 4 and parts[0] == "distance":
try:
i1 = int(parts[1]) - 1
i2 = int(parts[2]) - 1
except Exception:
continue
label = f"*{k}"
if 0 <= i1 < n_atoms:
marks[i1].append(label)
if 0 <= i2 < n_atoms:
marks[i2].append(label)
return marks, legend

marks_map, legend = build_marks(len(ase_atoms), constraints_list)

# --- header ---
data = [["Atom index", "Chemical symbol", "Tag", "x (Å)", "y (Å)", "z (Å)"]]
if fixed_atoms is not None:
data[0].append("Free x,y,z")
# add constraints column if any constraints exist
if constraints_list:
data[0].append("Constr.")

positions = ase_atoms.positions
chemical_symbols = ase_atoms.get_chemical_symbols()
tags = ase_atoms.get_tags()

def fmt_free(mask):

Choose a reason for hiding this comment

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

I think here, you can put a condition if mask is not an instance , mask = (0,0,0) and then you do return f"({' '.join('✓' if val else 'x' for val in mask)})"

"""mask: 3-tuple/list of 0/1; 1=free → '✓', 0=fixed → 'x'."""
try:
x, y, z = mask
except Exception:
x = y = z = 1 # default free if malformed
return f"({'✓' if int(x)==1 else 'x'} {'✓' if int(y)==1 else 'x'} {'✓' if int(z)==1 else 'x'})"

for idx, (symbol, tag, pos) in enumerate(zip(chemical_symbols, tags, positions), start=1):
formatted_position = [f"{coord:.2f}" for coord in pos]
row = [idx, symbol, tag, *formatted_position]

if fixed_atoms is not None:
# robustly fetch mask; default to all free if missing
mask = (1, 1, 1)
try:
if 0 <= (idx - 1) < len(fixed_atoms):
mask = fixed_atoms[idx - 1]
except Exception:
pass
row.append(fmt_free(mask))

if constraints_list:
stars = " ".join(marks_map.get(idx - 1, []))
row.append(stars)

data.append(row)
# temporary quick fix to show teh legend of constraints
if legend:
ncols = len(data[0])
legend_text = "; ".join(legend)

injected = (
# close the last normal row
f'</td></tr>'
# add a non-interactive full-width legend row
f'<tr class="constraints-legend-row" '
f'style="pointer-events:none; user-select:none;">'
f'<td colspan="{ncols}" style="text-align:right;">{html.escape(legend_text)}</td>'
f'</tr>'
# open a dummy row/cell so the renderer’s trailing </td></tr> stays balanced
f'<tr><td>'
)
data.append([injected])



return data


def get_model_state(self):
return {
"selected_view": self.selected_view,
Expand Down
2 changes: 2 additions & 0 deletions src/aiidalab_qe/app/structure/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from aiidalab_qe.app.structure.model import StructureStepModel
from aiidalab_qe.app.utils import get_entry_items
from aiidalab_qe.common import (
AddingConstraintsEditor,
AddingTagsEditor,
LazyLoadedOptimade,
LazyLoadedStructureBrowser,
Expand Down Expand Up @@ -105,6 +106,7 @@ def _render(self):
BasicCellEditor(title="Edit cell"),
BasicStructureEditor(title="Edit structure"),
AddingTagsEditor(title="Edit atom tags"),
AddingConstraintsEditor(title="Edit constraints"),
PeriodicityEditor(title="Edit periodicity"),
ShakeNBreakEditor(title="ShakeNBreak"),
]
Expand Down
2 changes: 2 additions & 0 deletions src/aiidalab_qe/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .node_view import CalcJobNodeViewerWidget # noqa: F401
from .process import QeAppWorkChainSelector, WorkChainSelector
from .widgets import (
AddingConstraintsEditor,
AddingTagsEditor,
LazyLoadedOptimade,
LazyLoadedStructureBrowser,
Expand All @@ -10,6 +11,7 @@
)

__all__ = [
"AddingConstraintsEditor",
"AddingTagsEditor",
"LazyLoadedOptimade",
"LazyLoadedStructureBrowser",
Expand Down
Loading
Loading