Skip to content

stepped-slab generation with bulk-to-slab atom mapping, fractional filtering, and robust supercell construction#459

Draft
ahmedabdelkawy wants to merge 2 commits intomainfrom
create_slab
Draft

stepped-slab generation with bulk-to-slab atom mapping, fractional filtering, and robust supercell construction#459
ahmedabdelkawy wants to merge 2 commits intomainfrom
create_slab

Conversation

@ahmedabdelkawy
Copy link
Contributor

@ahmedabdelkawy ahmedabdelkawy commented Mar 5, 2026

These additions refactor slab construction into clear steps—build a deterministic bulk supercell, track exactly which bulk atom each supercell/slab atom came from, optionally filter atoms in a convenient fractional-coordinate frame, then rotate and add vacuum to obtain the final surface slab.

Here S, K, and T are integer direction vectors in the original lattice basis: T is the terrace/surface-normal direction (Miller index of the terrace), while S and K are in-plane directions (step and kink/terrace-width directions) chosen/adjusted to be symmetry-equivalent vectors that lie in the terrace plane.

Functions added

  • print_fractional_positions(atoms, S, K, T): computes fractional coordinates in a user-defined basis (S, K, T), sorts atoms by depth (x3) then in-plane (x1, x2), and prints them (debug/inspection tool).
  • _hnf_and_U(P): helper to compute the Hermite normal form H of an integer supercell matrix P and a unimodular transform U such that P = U·H (used to enumerate translation vectors deterministically).
  • make_supercell(primitive, P): constructs an ASE Atoms supercell from an integer transformation matrix P with a deterministic atom ordering (“atom-major”), without rotating the lattice.
  • bulk_supercell_with_mapping(...): builds a bulk supercell for stepped/kinked surface construction and returns (i) the supercell, (ii) a “mapping cell”, (iii) fractional coordinates in that mapping cell, and (iv) orig_index mapping each supercell atom back to its primitive-cell atom index.
  • apply_fractional_filter(supercell, frac_coords, orig_index, filter_func): applies a user filter in fractional coordinates and returns the filtered structure plus filtered original-index mapping.
  • make_slab(supercell, step_direction, vacuum): rotates the bulk supercell into a slab orientation (step direction aligned in-plane, surface normal as z), adds vacuum, and wraps atoms back into the cell.

Summary by CodeRabbit

  • New Features
    • Enhanced surface structure generation for creating stepped surfaces with configurable terraces, steps, and kinks.
    • New capability to build high-index crystal surfaces and slabs with customizable geometry parameters and optional filtering for precise control.

…eterministic bulk supercell, track exactly which bulk atom each supercell/slab atom came from, optionally filter atoms in a convenient fractional-coordinate frame, then rotate and add vacuum to obtain the final surface slab.

Here S, K, and T are integer direction vectors in the original lattice basis: T is the terrace/surface-normal direction (Miller index of the terrace), while S and K are in-plane directions (step and kink/terrace-width directions) chosen/adjusted to be symmetry-equivalent vectors that lie in the terrace plane.

### Functions added
- **`print_fractional_positions(atoms, S, K, T)`**: computes fractional coordinates in a user-defined basis (S, K, T), sorts atoms by depth (`x3`) then in-plane (`x1`, `x2`), and prints them (debug/inspection tool).
- **`_hnf_and_U(P)`**: helper to compute the Hermite normal form **H** of an integer supercell matrix **P** and a unimodular transform **U** such that `P = U·H` (used to enumerate translation vectors deterministically).
- **`make_supercell(primitive, P)`**: constructs an ASE `Atoms` supercell from an integer transformation matrix **P** with a deterministic atom ordering (“atom-major”), without rotating the lattice.
- **`bulk_supercell_with_mapping(...)`**: builds a bulk supercell for stepped/kinked surface construction and returns (i) the supercell, (ii) a “mapping cell”, (iii) fractional coordinates in that mapping cell, and (iv) `orig_index` mapping each supercell atom back to its primitive-cell atom index.
- **`apply_fractional_filter(supercell, frac_coords, orig_index, filter_func)`**: applies a user filter in fractional coordinates and returns the filtered structure plus filtered original-index mapping.
- **`make_slab(supercell, step_direction, vacuum)`**: rotates the bulk supercell into a slab orientation (step direction aligned in-plane, surface normal as `z`), adds vacuum, and wraps atoms back into the cell.
@coderabbitai
Copy link

coderabbitai bot commented Mar 5, 2026

📝 Walkthrough

Walkthrough

Added comprehensive utilities for stepped surface generation, including in-plane direction analysis, supercell construction with atom mapping, fractional coordinate filtering, and slab creation. Introduced nine new functions providing deterministic workflows for building high-index surface structures with explicit orientation and geometry control. No existing code modified.

Changes

Cohort / File(s) Summary
Surface Generation Utilities
src/structuretoolkit/build/surface.py
Added nine functions: find_inplane_directions() for symmetry-equivalent direction analysis; _hnf_and_U() for Hermite normal form computation; make_supercell() for bulk supercell generation; bulk_supercell_with_mapping() for supercell construction with primitive index mapping; apply_fractional_filter() for atom filtering via custom predicates; print_fractional_positions() for fractional coordinate reporting; make_slab() for slab orientation and vacuum padding; create_slab() for orchestrated slab creation workflow; make_stepped_surface() as convenience wrapper for stepped surface generation.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant create_slab as create_slab()
    participant find_inplane as find_inplane_directions()
    participant bulk_supercell as bulk_supercell_with_mapping()
    participant filter as apply_fractional_filter()
    participant make_slab_fn as make_slab()
    
    User->>create_slab: Call with geometry params
    create_slab->>find_inplane: Compute step/kink directions
    find_inplane-->>create_slab: Return final orientations
    create_slab->>bulk_supercell: Build supercell with mapping
    bulk_supercell-->>create_slab: Return supercell + index map
    alt Filter function provided
        create_slab->>filter: Apply atom selection filter
        filter-->>create_slab: Return filtered atoms
    end
    create_slab->>make_slab_fn: Rotate to slab, add vacuum
    make_slab_fn-->>create_slab: Return final slab structure
    create_slab-->>User: Return slab + index mapping
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hops through crystals, layer by layer,
Building terraces with geometrical care,
Steps and kinks in fractional grace,
Supercells mapped to their rightful place,
A rabbit's delight—surfaces so fair!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main additions to the codebase: stepped-slab generation, bulk-to-slab atom mapping, fractional filtering, and supercell construction—all of which are core features introduced in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch create_slab

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ahmedabdelkawy ahmedabdelkawy changed the title stepped-slab generation with bulk-to-slab atom mapping, fractional filtering, and robust supercell constructionthese additions refactor slab construction into clear steps—build a d… stepped-slab generation with bulk-to-slab atom mapping, fractional filtering, and robust supercell construction Mar 5, 2026
@codecov
Copy link

codecov bot commented Mar 5, 2026

Codecov Report

❌ Patch coverage is 6.93642% with 161 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.26%. Comparing base (7850bd2) to head (fed280b).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
src/structuretoolkit/build/surface.py 6.93% 161 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #459      +/-   ##
==========================================
- Coverage   82.91%   76.26%   -6.65%     
==========================================
  Files          25       25              
  Lines        1820     1993     +173     
==========================================
+ Hits         1509     1520      +11     
- Misses        311      473     +162     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ahmedabdelkawy
Copy link
Contributor Author

@jan-janssen, please let me know if this should be added somewhere else and if you have any comments.

@ahmedabdelkawy ahmedabdelkawy requested a review from freyso March 5, 2026 16:12
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
src/structuretoolkit/build/surface.py (1)

352-365: Replace unconditional print(...) calls with logger-based debug output

These debug prints run in normal library usage and will pollute stdout in downstream workflows.

Proposed refactor
+import logging
@@
+logger = logging.getLogger(__name__)
@@
-    print(f"ncells={n_cells}")
+    logger.debug("ncells=%s", n_cells)
@@
-    print(H)
-    print(U)
-    print(f"P={P}")
-    print(h11, h22, h33)
-    print(f"H U = {H @ U}")
+    logger.debug("H=%s", H)
+    logger.debug("U=%s", U)
+    logger.debug("P=%s", P)
+    logger.debug("H diagonal: %s %s %s", h11, h22, h33)
+    logger.debug("H @ U = %s", H @ U)
@@
-    print(P)
     cell_super = P @ cell_prim  # (3,3)
-
-    print(cell_super)
+    logger.debug("cell_super=%s", cell_super)
@@
-    print(f"v2={v2}")
+    logger.debug("v2=%s", v2)

Also applies to: 381-385, 470-470, 814-825

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/structuretoolkit/build/surface.py` around lines 352 - 365, Unconditional
print calls around the HNF step (printing n_cells, H, U, P, h11/h22/h33 and H @
U) should be replaced with logger-based debug output: obtain or add a module
logger (logging.getLogger(__name__)) and change prints to logger.debug calls
(prefer lazy formatting or %s-style interpolation) so debug output is
controllable; do the same replacement for the other print sites in this module
that print matrix/loop-limit/state information (the ones printing matrices, P,
and loop-limit values) to avoid polluting stdout.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/structuretoolkit/build/surface.py`:
- Around line 162-178: The function find_inplane_directions currently calls
list(...) on terrace_orientation, step_orientation, and kink_orientation which
can be None; change those conversions to only call list(...) if the value is not
None and otherwise either set a sensible default or raise a clear ValueError.
Specifically, update the initialization of terrace_orientation,
step_orientation, and kink_orientation in find_inplane_directions to do a
conditional conversion (e.g., if terrace_orientation is not None:
terrace_orientation = list(terrace_orientation)) and then validate required
inputs before proceeding so the function no longer crashes on None defaults.
- Around line 789-795: The function returns the pre-filter idxmap instead of the
filtered mapping—after calling apply_fractional_filter(...) which yields slab,
idxmapf, update the return to return the filtered mapping (idxmapf) so the
API/docstring/return annotation are correct; when adjusting the return in the
block that calls make_slab(... S @ bulk_str.cell, vacuum_size) replace the final
returned variable from idxmap to idxmapf (or, if idxmapf must be transformed to
the final slab orientation, apply the same transformation you use for slab
before returning) so the returned index map matches the filtered/rotated slab.
- Around line 296-303: In _hnf_and_U, remove the unreachable bare `raise` and
instead catch the exception (e.g. `except Exception as e:`), then check whether
the input matrix P is already upper‑triangular (use numpy to test P ==
np.triu(P)); if it is, return the safe fallback (np.array(P, dtype=int),
np.eye(3, dtype=int)); otherwise re-raise or raise a new informative error that
includes the original exception info (refer to symbols _hnf_and_U, P, U, H when
making the change).
- Around line 638-641: The vacuum insertion currently writes to the wrong cell
components: in the slab creation code where you call slab.get_cell() (variable
cell) you must zero the x/y components of the third lattice vector (a3) and
extend its z-component by vacuum; change the assignments that now touch
cell[0,2] and cell[1,2] to instead set cell[2,0] = 0.0 and cell[2,1] = 0.0, and
keep the extension as cell[2,2] += float(vacuum) so the row-based cell (rows =
lattice vectors) is updated correctly.
- Around line 769-773: The bug arises because S and K can be reassigned from
numpy arrays to Python sequences when the orthogonality branches set S =
step_orientation or K = kink_orientation, causing the later use of S @
bulk_str.cell to fail; in create_slab ensure any assignment to S or K converts
the value to a numpy array (e.g., via np.asarray or np.array) so S and K always
support ndarray operations, and/or normalize inputs step_orientation and
kink_orientation to numpy arrays at the start of create_slab to guarantee
downstream operations (S @ bulk_str.cell) work correctly.

---

Nitpick comments:
In `@src/structuretoolkit/build/surface.py`:
- Around line 352-365: Unconditional print calls around the HNF step (printing
n_cells, H, U, P, h11/h22/h33 and H @ U) should be replaced with logger-based
debug output: obtain or add a module logger (logging.getLogger(__name__)) and
change prints to logger.debug calls (prefer lazy formatting or %s-style
interpolation) so debug output is controllable; do the same replacement for the
other print sites in this module that print matrix/loop-limit/state information
(the ones printing matrices, P, and loop-limit values) to avoid polluting
stdout.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e74a13fd-9ffe-4583-b221-fe2df416c646

📥 Commits

Reviewing files that changed from the base of the PR and between a648f8e and fed280b.

📒 Files selected for processing (1)
  • src/structuretoolkit/build/surface.py

Comment on lines +162 to +178
terrace_orientation=None,
step_orientation=None,
kink_orientation=None,
):
"""
Author: based on code by Shyam Katnagallu
Find symmetry-equivalent directions of step_direction and kink_direction lying in plane
given by terrace_orientation
Args:
basis: some atomic structure
terrace_orientation (list): The miller index of the terrace eg., [1,1,1]
step_orientation (list): The miller index of the step eg., [1,1,0]
kink_orientation (list): The miller index of the kink eg., [1,1,1]
"""
terrace_orientation = list(terrace_orientation)
step_orientation = list(step_orientation)
kink_orientation = list(kink_orientation)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

find_inplane_directions defaults currently crash on None inputs

Line 176–178 call list(...) on possibly None values from the function defaults, causing a TypeError before any validation.

Proposed fix
 def find_inplane_directions(
     basis,
     terrace_orientation=None,
     step_orientation=None,
     kink_orientation=None,
 ):
@@
-    terrace_orientation = list(terrace_orientation)
-    step_orientation = list(step_orientation)
-    kink_orientation = list(kink_orientation)
+    if terrace_orientation is None:
+        terrace_orientation = [1, 1, 1]
+    if step_orientation is None:
+        step_orientation = [1, 1, 0]
+    if kink_orientation is None:
+        kink_orientation = [1, 1, 1]
+    terrace_orientation = list(terrace_orientation)
+    step_orientation = list(step_orientation)
+    kink_orientation = list(kink_orientation)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
terrace_orientation=None,
step_orientation=None,
kink_orientation=None,
):
"""
Author: based on code by Shyam Katnagallu
Find symmetry-equivalent directions of step_direction and kink_direction lying in plane
given by terrace_orientation
Args:
basis: some atomic structure
terrace_orientation (list): The miller index of the terrace eg., [1,1,1]
step_orientation (list): The miller index of the step eg., [1,1,0]
kink_orientation (list): The miller index of the kink eg., [1,1,1]
"""
terrace_orientation = list(terrace_orientation)
step_orientation = list(step_orientation)
kink_orientation = list(kink_orientation)
terrace_orientation=None,
step_orientation=None,
kink_orientation=None,
):
"""
Author: based on code by Shyam Katnagallu
Find symmetry-equivalent directions of step_direction and kink_direction lying in plane
given by terrace_orientation
Args:
basis: some atomic structure
terrace_orientation (list): The miller index of the terrace eg., [1,1,1]
step_orientation (list): The miller index of the step eg., [1,1,0]
kink_orientation (list): The miller index of the kink eg., [1,1,1]
"""
if terrace_orientation is None:
terrace_orientation = [1, 1, 1]
if step_orientation is None:
step_orientation = [1, 1, 0]
if kink_orientation is None:
kink_orientation = [1, 1, 1]
terrace_orientation = list(terrace_orientation)
step_orientation = list(step_orientation)
kink_orientation = list(kink_orientation)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/structuretoolkit/build/surface.py` around lines 162 - 178, The function
find_inplane_directions currently calls list(...) on terrace_orientation,
step_orientation, and kink_orientation which can be None; change those
conversions to only call list(...) if the value is not None and otherwise either
set a sensible default or raise a clear ValueError. Specifically, update the
initialization of terrace_orientation, step_orientation, and kink_orientation in
find_inplane_directions to do a conditional conversion (e.g., if
terrace_orientation is not None: terrace_orientation =
list(terrace_orientation)) and then validate required inputs before proceeding
so the function no longer crashes on None defaults.

Comment on lines +296 to +303
except Exception:
raise
# pragma: no cover (covers missing SymPy or old version)
# Very simple fallback: assume P is already upper‑triangular (or diagonal)
# In that case U = identity and H = P.
# This will still produce a correct super‑cell, only the enumeration
# of translation vectors may be less compact.
return np.array(P, dtype=int), np.eye(3, dtype=int)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n src/structuretoolkit/build/surface.py | sed -n '290,310p'

Repository: pyiron/structuretoolkit

Length of output: 1029


Fallback in _hnf_and_U is unreachable

Line 297 re-raises immediately, making the documented fallback at lines 299–303 completely inaccessible. The bare raise statement exits the exception handler before the fallback code can execute.

The proposed fix correctly restructures the exception handler to capture the exception, check if the matrix is already upper-triangular, and return the safe fallback only when applicable. Otherwise, it raises a more informative error message.

Proposed fix
-    except Exception:
-        raise
-        # pragma: no cover   (covers missing SymPy or old version)
-        # Very simple fallback: assume P is already upper‑triangular (or diagonal)
-        # In that case U = identity and H = P.
-        # This will still produce a correct super‑cell, only the enumeration
-        # of translation vectors may be less compact.
-        return np.array(P, dtype=int), np.eye(3, dtype=int)
+    except Exception as err:
+        # pragma: no cover (missing SymPy / incompatible SymPy)
+        P_int = np.array(P, dtype=int)
+        if np.all(np.tril(P_int, k=-1) == 0):
+            return P_int, np.eye(3, dtype=int)
+        raise RuntimeError(
+            "SymPy is required for non-upper-triangular transformation matrices."
+        ) from err
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except Exception:
raise
# pragma: no cover (covers missing SymPy or old version)
# Very simple fallback: assume P is already upper‑triangular (or diagonal)
# In that case U = identity and H = P.
# This will still produce a correct super‑cell, only the enumeration
# of translation vectors may be less compact.
return np.array(P, dtype=int), np.eye(3, dtype=int)
except Exception as err:
# pragma: no cover (missing SymPy / incompatible SymPy)
P_int = np.array(P, dtype=int)
if np.all(np.tril(P_int, k=-1) == 0):
return P_int, np.eye(3, dtype=int)
raise RuntimeError(
"SymPy is required for non-upper-triangular transformation matrices."
) from err
🧰 Tools
🪛 Ruff (0.15.2)

[warning] 296-303: Remove exception handler; error is immediately re-raised

(TRY203)


[warning] 299-299: Comment contains ambiguous (NON-BREAKING HYPHEN). Did you mean - (HYPHEN-MINUS)?

(RUF003)


[warning] 301-301: Comment contains ambiguous (NON-BREAKING HYPHEN). Did you mean - (HYPHEN-MINUS)?

(RUF003)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/structuretoolkit/build/surface.py` around lines 296 - 303, In _hnf_and_U,
remove the unreachable bare `raise` and instead catch the exception (e.g.
`except Exception as e:`), then check whether the input matrix P is already
upper‑triangular (use numpy to test P == np.triu(P)); if it is, return the safe
fallback (np.array(P, dtype=int), np.eye(3, dtype=int)); otherwise re-raise or
raise a new informative error that includes the original exception info (refer
to symbols _hnf_and_U, P, U, H when making the change).

Comment on lines +638 to +641
cell = slab.get_cell() # rows again
cell[0, 2] = 0.0 # a3_x → 0
cell[1, 2] = 0.0 # a3_y → 0
cell[2, 2] += float(vacuum) # extend along z
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file around the mentioned lines to see the context
sed -n '630,650p' src/structuretoolkit/build/surface.py

Repository: pyiron/structuretoolkit

Length of output: 1033


🏁 Script executed:

# Get a broader view of the function containing these lines
sed -n '600,650p' src/structuretoolkit/build/surface.py

Repository: pyiron/structuretoolkit

Length of output: 2507


🏁 Script executed:

# Find the function definition that contains these lines
rg -B 50 "cell\[0, 2\] = 0.0" src/structuretoolkit/build/surface.py | head -100

Repository: pyiron/structuretoolkit

Length of output: 2499


Vacuum insertion edits the wrong cell components

The code at lines 639–640 modifies cell[0,2] and cell[1,2] (the z-components of lattice vectors a1 and a2), but the intent stated in the comment is to zero the x and y components of the third lattice vector a3. Since the cell matrix uses row-based representation (where rows are lattice vectors and columns are components: [x, y, z]), this should target cell[2,0] and cell[2,1] instead.

Proposed fix
-    cell[0, 2] = 0.0  # a3_x → 0
-    cell[1, 2] = 0.0  # a3_y → 0
-    cell[2, 2] += float(vacuum)  # extend along z
+    cell[2, 0] = 0.0  # a3_x -> 0
+    cell[2, 1] = 0.0  # a3_y -> 0
+    cell[2, 2] += float(vacuum)  # extend along z
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cell = slab.get_cell() # rows again
cell[0, 2] = 0.0 # a3_x 0
cell[1, 2] = 0.0 # a3_y 0
cell[2, 2] += float(vacuum) # extend along z
cell = slab.get_cell() # rows again
cell[2, 0] = 0.0 # a3_x -> 0
cell[2, 1] = 0.0 # a3_y -> 0
cell[2, 2] += float(vacuum) # extend along z
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/structuretoolkit/build/surface.py` around lines 638 - 641, The vacuum
insertion currently writes to the wrong cell components: in the slab creation
code where you call slab.get_cell() (variable cell) you must zero the x/y
components of the third lattice vector (a3) and extend its z-component by
vacuum; change the assignments that now touch cell[0,2] and cell[1,2] to instead
set cell[2,0] = 0.0 and cell[2,1] = 0.0, and keep the extension as cell[2,2] +=
float(vacuum) so the row-based cell (rows = lattice vectors) is updated
correctly.

Comment on lines +769 to +773
if np.dot(step_orientation, terrace_orientation) == 0:
S = step_orientation
if np.dot(kink_orientation, terrace_orientation) == 0:
K = kink_orientation

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and get its size
wc -l src/structuretoolkit/build/surface.py

Repository: pyiron/structuretoolkit

Length of output: 108


🏁 Script executed:

# Now let's examine the specific lines mentioned in the review
sed -n '760,800p' src/structuretoolkit/build/surface.py | cat -n

Repository: pyiron/structuretoolkit

Length of output: 1575


🏁 Script executed:

# Let's also check the broader context - the function definition and variable initialization
sed -n '750,810p' src/structuretoolkit/build/surface.py | cat -n

Repository: pyiron/structuretoolkit

Length of output: 2141


🏁 Script executed:

# Let's see the full function signature to understand parameter types
sed -n '700,770p' src/structuretoolkit/build/surface.py | cat -n

Repository: pyiron/structuretoolkit

Length of output: 3007


🏁 Script executed:

# Let's check what find_inplane_directions returns
rg -A 20 "def find_inplane_directions" src/structuretoolkit/build/surface.py

Repository: pyiron/structuretoolkit

Length of output: 853


🏁 Script executed:

# Let's check if there are any type hints in the file
rg -B 5 -A 30 "def create_slab" src/structuretoolkit/build/surface.py | head -50

Repository: pyiron/structuretoolkit

Length of output: 1165


🏁 Script executed:

# Let's test if tuple @ numpy.array actually raises TypeError
python << 'EOF'
import numpy as np

# Test case 1: tuple @ numpy array
try:
    tuple_val = (1, 0, 0)
    array_val = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=float)
    result = tuple_val @ array_val
    print("tuple @ array succeeded:", result)
except TypeError as e:
    print("tuple @ array failed with TypeError:", e)

# Test case 2: list @ numpy array
try:
    list_val = [1, 0, 0]
    array_val = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=float)
    result = list_val @ array_val
    print("list @ array succeeded:", result)
except TypeError as e:
    print("list @ array failed with TypeError:", e)

# Test case 3: numpy array @ numpy array
try:
    arr_val = np.array([1, 0, 0], dtype=int)
    array_val = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=float)
    result = arr_val @ array_val
    print("numpy array @ array succeeded:", result)
except TypeError as e:
    print("numpy array @ array failed with TypeError:", e)
EOF

Repository: pyiron/structuretoolkit

Length of output: 184


🏁 Script executed:

# Let me check what find_inplane_directions actually returns
rg -A 50 "def find_inplane_directions" src/structuretoolkit/build/surface.py | head -80

Repository: pyiron/structuretoolkit

Length of output: 2140


🏁 Script executed:

# Check the return statement of find_inplane_directions
rg -B 5 "return.*step_direction\|return.*kink_direction" src/structuretoolkit/build/surface.py | head -30

Repository: pyiron/structuretoolkit

Length of output: 49


🏁 Script executed:

# Get the complete find_inplane_directions function
sed -n '560,650p' src/structuretoolkit/build/surface.py | cat -n

Repository: pyiron/structuretoolkit

Length of output: 4656


🏁 Script executed:

# Check if there are any imports and what bulk_str is (it's ase.Atoms)
head -30 src/structuretoolkit/build/surface.py | grep -E "^import|^from"

Repository: pyiron/structuretoolkit

Length of output: 414


🏁 Script executed:

# Verify the behavior difference - tuple @ array would fail, array @ array works
# Since we don't have numpy, let's check ASE usage
rg "bulk_str\.cell" src/structuretoolkit/build/surface.py | head -10

Repository: pyiron/structuretoolkit

Length of output: 221


create_slab fails at runtime when S/K are reassigned from sequences

Lines 770–773 can reassign S/K from numpy arrays to tuples or lists if the orthogonality condition is true. Line 792 then performs S @ bulk_str.cell, which raises TypeError because the @ operator is not defined for tuples/lists. This occurs when step_orientation or kink_orientation are passed as sequences (common case) and happen to be orthogonal to the terrace.

Proposed fix
     if np.dot(step_orientation, terrace_orientation) == 0:
-        S = step_orientation
+        S = np.asarray(step_orientation, dtype=int)
     if np.dot(kink_orientation, terrace_orientation) == 0:
-        K = kink_orientation
+        K = np.asarray(kink_orientation, dtype=int)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if np.dot(step_orientation, terrace_orientation) == 0:
S = step_orientation
if np.dot(kink_orientation, terrace_orientation) == 0:
K = kink_orientation
if np.dot(step_orientation, terrace_orientation) == 0:
S = np.asarray(step_orientation, dtype=int)
if np.dot(kink_orientation, terrace_orientation) == 0:
K = np.asarray(kink_orientation, dtype=int)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/structuretoolkit/build/surface.py` around lines 769 - 773, The bug arises
because S and K can be reassigned from numpy arrays to Python sequences when the
orthogonality branches set S = step_orientation or K = kink_orientation, causing
the later use of S @ bulk_str.cell to fail; in create_slab ensure any assignment
to S or K converts the value to a numpy array (e.g., via np.asarray or np.array)
so S and K always support ndarray operations, and/or normalize inputs
step_orientation and kink_orientation to numpy arrays at the start of
create_slab to guarantee downstream operations (S @ bulk_str.cell) work
correctly.

Comment on lines +789 to +795
slab, idxmapf = apply_fractional_filter(supercell, frac, idxmap, filter_function)

# rotate cell into final orientation
slab = make_slab(slab, S @ bulk_str.cell, vacuum_size)

return slab, idxmap

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Returned index mapping is incorrect after filtering

Line 789 computes idxmapf (filtered mapping), but Line 794 returns idxmap (pre-filter mapping). This breaks the API contract in the docstring and return annotation.

Proposed fix
-    slab, idxmapf = apply_fractional_filter(supercell, frac, idxmap, filter_function)
+    slab, idxmapf = apply_fractional_filter(supercell, frac, idxmap, filter_function)
@@
-    return slab, idxmap
+    return slab, {i: int(orig_i) for i, orig_i in enumerate(idxmapf)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
slab, idxmapf = apply_fractional_filter(supercell, frac, idxmap, filter_function)
# rotate cell into final orientation
slab = make_slab(slab, S @ bulk_str.cell, vacuum_size)
return slab, idxmap
slab, idxmapf = apply_fractional_filter(supercell, frac, idxmap, filter_function)
# rotate cell into final orientation
slab = make_slab(slab, S @ bulk_str.cell, vacuum_size)
return slab, {i: int(orig_i) for i, orig_i in enumerate(idxmapf)}
🧰 Tools
🪛 Ruff (0.15.2)

[warning] 789-789: Unpacked variable idxmapf is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/structuretoolkit/build/surface.py` around lines 789 - 795, The function
returns the pre-filter idxmap instead of the filtered mapping—after calling
apply_fractional_filter(...) which yields slab, idxmapf, update the return to
return the filtered mapping (idxmapf) so the API/docstring/return annotation are
correct; when adjusting the return in the block that calls make_slab(... S @
bulk_str.cell, vacuum_size) replace the final returned variable from idxmap to
idxmapf (or, if idxmapf must be transformed to the final slab orientation, apply
the same transformation you use for slab before returning) so the returned index
map matches the filtered/rotated slab.

@jan-janssen
Copy link
Member

@jan-janssen, please let me know if this should be added somewhere else and if you have any comments.

Hi @ahmedabdelkawy , thanks a lot for the contribution. Can you add unit tests for your function?

@ahmedabdelkawy
Copy link
Contributor Author

HI @jan-janssen I dont have any experience there tbh, and @freyso wrote the entire code. I believe it will take some, unless it is LLM generated 😅

@freyso
Copy link
Contributor

freyso commented Mar 6, 2026

HI @jan-janssen I dont have any experience there tbh, and @freyso wrote the entire code. I believe it will take some, unless it is LLM generated 😅

I will take care of that (certainly LLM-supported).

@jan-janssen jan-janssen marked this pull request as draft March 10, 2026 21:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants