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

feature: Factory methods for AHS AtomArrangments by AbdullahKazi500 #989

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

AbdullahKazi500
Copy link

Issue #, if available:
fixes #969
Description of changes:
added lattice.py polygon.py and the factory ahs all in one file
Testing done:

Merge Checklist

Put an x in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your pull request.

General

Tests

  • I have added tests that prove my fix is effective or that my feature works (if appropriate)
  • I have checked that my tests are not configured for a specific region or account (if appropriate)

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

 defined the SiteType enum and AtomArrangementItem dataclass to handle the coordinates and site types, ensuring validations.
AtomArrangement Class:
 The class initializes with an empty list of sites.
Add Method: Adds a coordinate to the arrangement with a site type.
 Retrieves a list of coordinates for a given index.
 Rounds coordinates to the nearest multiple of the given resolution.
Factory Methods: Methods for creating square and rectangular lattice arrangements.
Demonstrates creating a square lattice with specified boundary points and lattice constant.
@peterkomar-aws
Copy link
Contributor

Hi @AbdullahKazi500 , There are good ideas here. But there is also a lot of unnecessary (and confusing) redefinitions and changes that are not pertinent to the issue at hand.

Also, to avoid future confusion, please work on this PR going forward. I will close the rest.

As a first start:

  1. Please remove redefinitions
  2. Please add your test to the test folder in the appropriate module.

If you need guidance how to do this, happy to point you to materials.

@peterkomar-aws peterkomar-aws changed the title Updated the PR to include all the things Factory methods for AHS AtomArrangments AbdullahKazi500 Jun 5, 2024
@peterkomar-aws peterkomar-aws changed the title Factory methods for AHS AtomArrangments AbdullahKazi500 Factory methods for AHS AtomArrangments by AbdullahKazi500 Jun 5, 2024
@AbdullahKazi500
Copy link
Author

AbdullahKazi500 commented Jun 5, 2024

@peterkomar-aws Hi peter thanks for the guidance I am new to open source before this I use to push demo code into master branches for my previous projects I have worked as an experimentalist researcher for 4 years and had my formal education in experimental validation research so I hope its okay its my first time I am doing an opensource hackathon I will try following your guidelines but please do accept my apologies for a lot of different PRs

@peterkomar-aws
Copy link
Contributor

Hi @AbdullahKazi500 , no worries. We were all there some time in our journey. Please try to follow the contributions guidelines. You can start by creating a fork of the repo, and work there on a feature branch first. Once your tests are passing locally and you are happy with the code, you can create a PR from your fork to this repo.

I have cleaned up and properly formatted the code to enhance readability and maintainability. Here are the key changes made:
Added all necessary imports at the beginning of the code.
Defined a custom exception DiscretizationError.
Organized and added necessary data classes (LatticeGeometry, DiscretizationProperties, AtomArrangementItem) and enums (SiteType).
Implemented validation methods inside AtomArrangementItem to check coordinates and site types.
Added docstrings to methods for better understanding.
Implemented methods for adding coordinates, getting coordinate lists, iterating, getting length, and discretizing arrangements.
Added factory methods for creating different lattice structures (square, rectangular, decorated Bravais, honeycomb, and Bravais lattices).
@AbdullahKazi500
Copy link
Author

Hi @peterkomar-aws I have cleaned up and properly formatted the code to enhance readability and maintainability. Here are the key changes made:

Added all necessary imports at the beginning of the code.
Defined a custom exception DiscretizationError.
Organized and added necessary data classes (LatticeGeometry, DiscretizationProperties, AtomArrangementItem) and enums (SiteType).
Implemented validation methods inside AtomArrangementItem to check coordinates and site types.
Added docstrings to methods for better understanding.
Implemented methods for adding coordinates, getting coordinate lists, iterating, getting length, and discretizing arrangements.
Added factory methods for creating different lattice structures (square, rectangular, decorated Bravais, honeycomb, and Bravais lattices).
made the PR in the same Patch
also removed unnecessary redefinitions

@peterkomar-aws peterkomar-aws changed the title Factory methods for AHS AtomArrangments by AbdullahKazi500 feature: Factory methods for AHS AtomArrangments by AbdullahKazi500 Jun 7, 2024
@peterkomar-aws
Copy link
Contributor

Hi @AbdullahKazi500 , thanks for continuing the work.

You wrote that you removed unnecessary definitions, but the atom_arrangement.py file in your latest commit still contains many repeated redefinitions. E.g. the original defition of AtomArrangementItem is defined in L41, then redefined in L187, L351, and L393; AtomArrangement is originally defined in L63, then redefined in L208, L428.

Your proposed version of the module has 527 lines, I understand that may be difficult to handle, but to move forward with this work, I ask you to review each line of change and addition that you are proposing (here) and carefully consider its function and value. Once you feel that your work is ready for a careful review please click the ready for review button to flip back the PR from draft stage to open. Thank you.

@peterkomar-aws peterkomar-aws marked this pull request as draft June 7, 2024 19:15
@AbdullahKazi500
Copy link
Author

Hi @peterkomar-aws Thanks to the review I will check and modify this again but after testing it locally I am kind of getting the desired results here but still I will try addressing the feedbacks you have advised

@AbdullahKazi500
Copy link
Author

Imports
Added from future import annotations to ensure forward compatibility with future Python releases.
Imported necessary modules and classes (Iterator, dataclass, Decimal, Enum, Number, Union, Tuple, List, np, and shapely.geometry).

    FILLED = "filled"
    VACANT = "vacant"

This Enum defines two possible types for sites: FILLED and VACANT.

class AtomArrangementItem:
    coordinate: Tuple[Number, Number]
    site_type: SiteType

This dataclass represents an item in an atom arrangement with a coordinate and a site type.
validate_coordinate: Ensures that the coordinate is a tuple of two numbers.
_validate_site_type: Ensures that the site type is either FILLED or VACANT.

    self._validate_coordinate()
    self._validate_site_type()

Validates the coordinate and site type after initializing an AtomArrangementItem.

    self._sites = []

Initializes an empty list to hold atom arrangement items.

    self._sites.append(AtomArrangementItem(tuple(coordinate), site_type))
    return self

Adds a new site to the arrangement and returns self for method chaining.

    return [site.coordinate[coordinate_index] for site in self._sites]

Returns a list of coordinates at the specified index (0 for x-coordinates, 1 for y-coordinates).

    return iter(self._sites)

def __len__(self):
    return len(self._sites)

Allows iteration over the arrangement and retrieves the number of sites

    ...````
Creates a discretized version of the atom arrangement based on the provided resolution.

@AbdullahKazi500
Copy link
Author

Hi @peterkomar-aws this are what the functions, data classes are

Copy link
Author

@AbdullahKazi500 AbdullahKazi500 left a comment

Choose a reason for hiding this comment

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

needing a review

@AbdullahKazi500
Copy link
Author

@peterkomar-aws let me know your thoughts

@peterkomar-aws
Copy link
Contributor

Hi @AbdullahKazi500 , thanks for working on this PR.

In my previous comments, I mentioned that you had many re-definitions in your atom_arrangement.py module. This is still the case. I'd like to start a conversation with you about one particular case, SiteType, since you explicitly mentioned it in your message above.

The class SiteType is an enum class.

  • It is part of the main branch, defined in L28
class SiteType(Enum):
    VACANT = "Vacant"
    FILLED = "Filled"
  • Its first definition appears in L35 in your code, but this is already different from how it is in the main branch
class SiteType(Enum):
    FILLED = "filled"
    VACANT = "vacant"
  • Question 1: Why did you change the string values (filled vs Filled), and why did you change the order in which they are listed in the class definition?
  • Then, a redefinition appears in L182, which is identical to the definition in the main branch
class SiteType(Enum):
    VACANT = "Vacant"
    FILLED = "Filled"
  • Question 2: What is the purpose of this code? Why did you not leave the original definition from the main branch in place?
  • Then, another redefinition appears in L347, where both the attribute names and the string values are different (empty vs vacant).
class SiteType(Enum):
    FILLED = "filled"
    EMPTY = "empty"
  • Question 3: What is the purpose of this? SiteType.VACANT is a public attribute in this module; changing/eliminating it would break user code built on top of this class. There should be a very good reason to change this public API. What is your reason?
  • Then, another redefinition appears in L388
class SiteType(Enum):
    FILLED = "filled"
    EMPTY = "empty"
  • Question 4: Why do you need this? Since this is repeating the same definition again, it seems to serve no purpose.

Since SiteType is such a simple class, I wish to have a productive conversation about your plans to change the atom_arrangement module through this lens.

@AbdullahKazi500
Copy link
Author

In my previous comments, I mentioned that you had many re-definitions in your atom_arrangement.py module. This is still the case. I'd like to start a conversation with you about one particular case, SiteType, since you explicitly mentioned it in your message above.

The class SiteType is an enum class.

  • It is part of the main branch, defined in L28
class SiteType(Enum):
    VACANT = "Vacant"
    FILLED = "Filled"
  • Its first definition appears in L35 in your code, but this is already different from how it is in the main branch
class SiteType(Enum):
    FILLED = "filled"
    VACANT = "vacant"
  • Question 1: Why did you change the string values (filled vs Filled), and why did you change the order in which they are listed in the class definition?
  • Then, a redefinition appears in L182, which is identical to the definition in the main branch
class SiteType(Enum):
    VACANT = "Vacant"
    FILLED = "Filled"
  • Question 2: What is the purpose of this code? Why did you not leave the original definition from the main branch in place?
  • Then, another redefinition appears in L347, where both the attribute names and the string values are different (empty vs vacant).
class SiteType(Enum):
    FILLED = "filled"
    EMPTY = "empty"
  • Question 3: What is the purpose of this? SiteType.VACANT is a public attribute in this module; changing/eliminating it would break user code built on top of this class. There should be a very good reason to change this public API. What is your reason?
  • Then, another redefinition appears in L388
class SiteType(Enum):
    FILLED = "filled"
    EMPTY = "empty"
  • Question 4: Why do you need this? Since this is repeating the same definition again, it seems to serve no purpose.

Since SiteType is such a simple class, I wish to have a productive conversation about your plans to change the atom_arrangement module through this lens.

Hi peter thanks for the feedback I will try to address your questions
Why did you change the string values (filled vs Filled), and why did you change the order in which they are listed in the class definition?
Changing the string values and the order might be a result of different naming conventions or standards being applied at different points in the development. I agree The naming conventions for enums should be consistent to avoid confusion and ensure compatibility. but also The order of definition in an enum class typically does not affect functionality but maintaining a consistent order can improve readability.

What is the purpose of this code? Why did you not leave the original definition from the main branch in place?
This redefinition might have been unintentionally introduced or left over from a refactoring attempt. thanks for catching this maybe a mistake on my end

Redefinition at Line 347 (Changing VACANT to EMPTY)
What is the purpose of this? SiteType.VACANT is a public attribute in this module; changing/eliminating it would break user code built on top of this class. There should be a very good reason to change this public API. What is your reason?

aligning with broader naming conventions across the project

Redefinition at Line 388
Why do you need this? Since this is repeating the same definition again, it seems to serve no purpose.
This redefinition seems redundant and unnecessary. It likely serves no functional purpose and could be removed to avoid confusion. I think it can be removed

so further plans would be-------------

Removimg the redundant redefinition at line 388. Ensure only one consistent definition of SiteType exists in the module.
Overall Plan for SiteType
Retain the original definition of SiteType at the beginning of the module:

from enum import Enum

class SiteType(Enum):
    VACANT = "Vacant"
    FILLED = "Filled"

Remove all other redefinitions to avoid confusion and ensure consistency.
Ensure all references to SiteType within the module and any dependent modules use this single definition.
Communicate changes clearly if any modifications to the public API are necessary, and provide a migration path if needed.
let me know if there is something else that needs to be done

@peterkomar-aws
Copy link
Contributor

Hi @AbdullahKazi500 , this sounds like a good next step.

@AbdullahKazi500
Copy link
Author

AbdullahKazi500 commented Jun 11, 2024

Hi @AbdullahKazi500 , this sounds like a good next step.

Hi peter before I make a PR I would like you to review the code if I am going wrong here feel free to correct me

#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
#     http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
from decimal import Decimal
from enum import Enum
from numbers import Number
from typing import Union, Tuple, List
import numpy as np
from shapely.geometry import Point, Polygon

# Define SiteType enum
class SiteType(Enum):
    VACANT = "Vacant"
    FILLED = "Filled"

@dataclass
class AtomArrangementItem:
    """Represents an item (coordinate and metadata) in an atom arrangement."""
    coordinate: Tuple[Number, Number]
    site_type: SiteType

    def _validate_coordinate(self) -> None:
        if len(self.coordinate) != 2:
            raise ValueError(f"{self.coordinate} must be of length 2")
        for idx, num in enumerate(self.coordinate):
            if not isinstance(num, Number):
                raise TypeError(f"{num} at position {idx} must be a number")

    def _validate_site_type(self) -> None:
        allowed_site_types = {SiteType.FILLED, SiteType.VACANT}
        if self.site_type not in allowed_site_types:
            raise ValueError(f"{self.site_type} must be one of {allowed_site_types}")

    def __post_init__(self) -> None:
        self._validate_coordinate()
        self._validate_site_type()

class AtomArrangement:
    def __init__(self):
        """Represents a set of coordinates that can be used as a register to an AnalogHamiltonianSimulation."""
        self._sites = []

    def add(self, coordinate: Union[Tuple[Number, Number], np.ndarray], site_type: SiteType = SiteType.FILLED) -> "AtomArrangement":
        """Add a coordinate to the atom arrangement.
        Args:
            coordinate (Union[tuple[Number, Number], ndarray]): The coordinate of the atom (in meters).
            site_type (SiteType): The type of site. Optional. Default is FILLED.
        Returns:
            AtomArrangement: returns self (to allow for chaining).
        """
        self._sites.append(AtomArrangementItem(tuple(coordinate), site_type))
        return self

    def coordinate_list(self, coordinate_index: Number) -> List[Number]:
        """Returns all the coordinates at the given index.
        Args:
            coordinate_index (Number): The index to get for each coordinate.
        Returns:
            List[Number]: The list of coordinates at the given index.
        Example:
            To get a list of all x-coordinates: coordinate_list(0)
            To get a list of all y-coordinates: coordinate_list(1)
        """
        return [site.coordinate[coordinate_index] for site in self._sites]

    def __iter__(self) -> Iterator:
        return iter(self._sites)

    def __len__(self):
        return len(self._sites)

    def discretize(self, properties: 'DiscretizationProperties') -> "AtomArrangement":
        """Creates a discretized version of the atom arrangement, rounding all site coordinates to the closest multiple of the resolution. The types of the sites are unchanged.
        Args:
            properties (DiscretizationProperties): Capabilities of a device that represent the
                resolution with which the device can implement the parameters.
        Raises:
            DiscretizationError: If unable to discretize the program.
        Returns:
            AtomArrangement: A new discretized atom arrangement.
        """
        try:
            position_res = properties.lattice.geometry.positionResolution
            discretized_arrangement = AtomArrangement()
            for site in self._sites:
                new_coordinates = tuple(
                    round(Decimal(c) / position_res) * position_res for c in site.coordinate
                )
                discretized_arrangement.add(new_coordinates, site.site_type)
            return discretized_arrangement
        except Exception as e:
            raise DiscretizationError(f"Failed to discretize register {e}") from e

    # Factory methods for lattice structures
    @classmethod
    def from_square_lattice(cls, lattice_constant: float, canvas_boundary_points: List[Tuple[float, float]]) -> "AtomArrangement":
        """Create an atom arrangement with a square lattice."""
        arrangement = cls()
        x_min, y_min = canvas_boundary_points[0]
        x_max, y_max = canvas_boundary_points[2]
        x_range = np.arange(x_min, x_max, lattice_constant)
        y_range = np.arange(y_min, y_max, lattice_constant)
        for x in x_range:
            for y in y_range:
                arrangement.add((x, y))
        return arrangement

    @classmethod
    def from_rectangular_lattice(cls, dx: float, dy: float, canvas_boundary_points: List[Tuple[float, float]]) -> "AtomArrangement":
        """Create an atom arrangement with a rectangular lattice."""
        arrangement = cls()
        x_min, y_min = canvas_boundary_points[0]
        x_max, y_max = canvas_boundary_points[2]
        for x in np.arange(x_min, x_max, dx):
            for y in np.arange(y_min, y_max, dy):
                arrangement.add((x, y))
        return arrangement

    @classmethod
    def from_decorated_bravais_lattice(cls, lattice_vectors: List[Tuple[float, float]], decoration_points: List[Tuple[float, float]], canvas_boundary_points: List[Tuple[float, float]]) -> "AtomArrangement":
        arrangement = cls()
        vec_a, vec_b = np.array(lattice_vectors[0]), np.array(lattice_vectors[1])
        x_min, y_min = canvas_boundary_points[0]
        x_max, y_max = canvas_boundary_points[2]
        i = 0
        while (origin := i * vec_a)[0] < x_max:
            j = 0
            while (point := origin + j * vec_b)[1] < y_max:
                if x_min <= point[0] <= x_max and y_min <= point[1] <= y_max:
                    for dp in decoration_points:
                        decorated_point = point + np.array(dp)
                        if x_min <= decorated_point[0] <= x_max and y_min <= decorated_point[1] <= y_max:
                            arrangement.add(tuple(decorated_point))
                j += 1
            i += 1
        return arrangement

    @classmethod
    def from_honeycomb_lattice(cls, lattice_constant: float, canvas_boundary_points: List[Tuple[float, float]]) -> "AtomArrangement":
        a1 = (lattice_constant, 0)
        a2 = (lattice_constant / 2, lattice_constant * np.sqrt(3) / 2)
        decoration_points = [(0, 0), (lattice_constant / 2, lattice_constant * np.sqrt(3) / 6)]
        return cls.from_decorated_bravais_lattice([a1, a2], decoration_points, canvas_boundary_points)

    @classmethod
    def from_bravais_lattice(cls, lattice_vectors: List[Tuple[float, float]], canvas_boundary_points: List[Tuple[float, float]]) -> "AtomArrangement":
        return cls.from_decorated_bravais_lattice(lattice_vectors, [(0, 0)], canvas_boundary_points)

@dataclass
class LatticeGeometry:
    positionResolution: Decimal

@dataclass
class DiscretizationProperties:
    lattice: LatticeGeometry

class DiscretizationError(Exception):
    pass

# RectangularCanvas class
class RectangularCanvas:
    def __init__(self, bottom_left: Tuple[float, float], top_right: Tuple[float, float]):
        self.bottom_left = bottom_left
        self.top_right = top_right

    def is_within(self, point: Tuple[float, float]) -> bool:
        x_min, y_min = self.bottom_left
        x_max, y_max = self.top_right
        x, y = point
        return x_min <= x <= x_max and y_min <= y <= y_max

# Example usage
if __name__ == "__main__":
    canvas_boundary_points = [(0, 0), (7.5e-5, 0), (7.5e-5, 7.5e-5), (0, 7.5e-5)]

    # Create lattice structures
    square_lattice = AtomArrangement.from_square_lattice(4e-6, canvas_boundary_points)
    rectangular_lattice = AtomArrangement.from_rectangular_lattice(3e-6, 2e-6, canvas_boundary_points)
    decorated_bravais_lattice = AtomArrangement.from_decorated_bravais_lattice([(4e-6, 0), (0, 4e-6)], [(1e-6, 1e-6)], canvas_boundary_points)
    honeycomb_lattice = AtomArrangement.from_honeycomb_lattice(4e-6, canvas_boundary_points)
    bravais_lattice = AtomArrangement.from_bravais_lattice([(4e-6, 0), (0, 4e-6)], canvas_boundary_points)

    # Validation function
    def validate_lattice(arrangement, lattice_type):
        """Validate the lattice structure."""
        num_sites = len(arrangement)
        min_distance = None
        for i, atom1 in enumerate(arrangement):
            for j, atom2 in enumerate(arrangement):
                if i != j:
                    distance = np.linalg.norm(np.array(atom1.coordinate) - np.array(atom2.coordinate))
                    if min_distance is None or distance < min_distance:
                        min_distance = distance
        print(f"Lattice Type: {lattice_type}")
        print(f"Number of lattice points: {num_sites}")
        print(f"Minimum distance between lattice points: {min_distance:.2e} meters")
        print("-" * 40)

    # Validate lattice structures
    validate_lattice(square_lattice, "Square Lattice")
    validate_lattice(rectangular_lattice, "Rectangular Lattice")
    validate_lattice(decorated_bravais_lattice, "Decorated Bravais Lattice")
    validate_lattice(honeycomb_lattice, "Honeycomb Lattice")
    validate_lattice(bravais_lattice, "Bravais Lattice")

@AbdullahKazi500
Copy link
Author

Hi @peterkomar-aws made a PR Recently here here

@peterkomar-aws
Copy link
Contributor

Hi @AbdullahKazi500 , thanks for adding the changes to the AHS.py file. Since you contribution will be modifications to the atom_arrangement.py file, please add the changes there. If you replace the content of that file with what you have in you AHS.py, then I can do a more through review, beacause I will see the changes with respect to the original atom_arrangement.py file. Thanks.

@AbdullahKazi500
Copy link
Author

Hi @AbdullahKazi500 , thanks for adding the changes to the AHS.py file. Since you contribution will be modifications to the atom_arrangement.py file, please add the changes there. If you replace the content of that file with what you have in you AHS.py, then I can do a more through review, beacause I will see the changes with respect to the original atom_arrangement.py file. Thanks.

Okay peter I will make both separate and in file PRs

@AbdullahKazi500
Copy link
Author

@peterkomar-aws Okay done now

@AbdullahKazi500 AbdullahKazi500 marked this pull request as ready for review June 11, 2024 14:43
Copy link
Contributor

@peterkomar-aws peterkomar-aws left a comment

Choose a reason for hiding this comment

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

I see you appended the latest version of the file. Instead, please replace the content of the entire file with this, so I can do a more careful, line-by-line review. Thanks.

AHS.py Outdated
Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove this file, and make all edits to the atom_arrangement.py module and the test_atom_arrangement.py module.

Copy link
Author

Choose a reason for hiding this comment

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

Hi @peterkomar-aws replaced the content of the entire file with this now going through the comments

Copy link
Author

Choose a reason for hiding this comment

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

The individual file was deleted

@@ -23,18 +23,24 @@
import numpy as np

from braket.ahs.discretization_types import DiscretizationError, DiscretizationProperties
from typing import Union, Tuple, List, Iterator
Copy link
Contributor

Choose a reason for hiding this comment

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

Please merge this line with L21 above

Copy link
Author

Choose a reason for hiding this comment

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

@peterkomar-aws this part of the import has been removed since I have rewritten the code again replacing the old content

@@ -23,18 +23,24 @@
import numpy as np

from braket.ahs.discretization_types import DiscretizationError, DiscretizationProperties
from typing import Union, Tuple, List, Iterator
from dataclasses import dataclass
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a repeated import (same as L17), please remove.

Copy link
Author

Choose a reason for hiding this comment

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

@peterkomar-aws this import is prevented from repeating by removing the old code

@@ -23,18 +23,24 @@
import numpy as np

from braket.ahs.discretization_types import DiscretizationError, DiscretizationProperties
from typing import Union, Tuple, List, Iterator
from dataclasses import dataclass
import numpy as np
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a repeated import (same as line 23), please remove.

Copy link
Author

Choose a reason for hiding this comment

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

this import is prevented from repeating by removing the old code

from typing import Union, Tuple, List, Iterator
from dataclasses import dataclass
import numpy as np
from decimal import Decimal
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a repeated import (same as line 18), please remove.

Copy link
Author

Choose a reason for hiding this comment

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

this import is prevented from repeating by removing the old code



# Define SiteType enum
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment does not tell anything beyond what the next line is telling already. Please remove.

Copy link
Author

Choose a reason for hiding this comment

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

yes removing

src/braket/ahs/atom_arrangement.py Outdated Show resolved Hide resolved
site_type: SiteType = SiteType.FILLED,
) -> AtomArrangement:
) -> "AtomArrangement":
Copy link
Contributor

Choose a reason for hiding this comment

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

The original hint referred to the AtomArrangement class as a return type. What does turning it into a string literal achieve here?

Copy link
Author

Choose a reason for hiding this comment

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

@peterkomar-aws resolved

"""Add a coordinate to the atom arrangement.

Copy link
Contributor

Choose a reason for hiding this comment

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

We are using Google formatted docstrings in this repo (see more here: https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings), please don't remove these empty lines.

Copy link
Contributor

Choose a reason for hiding this comment

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

Same for many other removed lines in the docstrings below.

Copy link
Author

Choose a reason for hiding this comment

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

@peterkomar-aws Hi peter resolved all the issues let me know if something is left

src/braket/ahs/atom_arrangement.py Outdated Show resolved Hide resolved
Copy link
Contributor

@peterkomar-aws peterkomar-aws left a comment

Choose a reason for hiding this comment

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

Great start. You have the right structure for the factory methods.

The main thing that needs change is the implementation of the from_decorated_bravais_lattice method. Currently it cuts off too many sites, sites which would fit on the canvas. See my comment for line 141. This is not easy to solve. Looking forward to your idea how to make this method work for arbitrary vec_a and vec_b values.

The goal is the fill up the entire canvas, and only remove sites that would truly fall outside of the canvas boundary.

src/braket/ahs/atom_arrangement.py Outdated Show resolved Hide resolved
src/braket/ahs/atom_arrangement.py Outdated Show resolved Hide resolved
src/braket/ahs/atom_arrangement.py Show resolved Hide resolved
src/braket/ahs/atom_arrangement.py Show resolved Hide resolved
src/braket/ahs/atom_arrangement.py Outdated Show resolved Hide resolved

@classmethod
def from_rectangular_lattice(cls, dx: float, dy: float, canvas_boundary_points: List[Tuple[float, float]]) -> AtomArrangement:
"""Create an atom arrangement with a rectangular lattice."""
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's add detailed docstrings listing what each is.

return arrangement

@classmethod
def from_decorated_bravais_lattice(cls, lattice_vectors: List[Tuple[float, float]], decoration_points: List[Tuple[float, float]], canvas_boundary_points: List[Tuple[float, float]]) -> AtomArrangement:
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here, let's use the RectangularCanvas and add docstrings.

x_min, y_min = canvas_boundary_points[0]
x_max, y_max = canvas_boundary_points[2]
i = 0
while (origin := i * vec_a)[0] < x_max:
Copy link
Contributor

Choose a reason for hiding this comment

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

There are a few issues with the implementation here:

  1. To fill in the entire canvas, depending on the values of vec_a and vec_b, it may not be enough to start from i=0 and j=0. E.g. if vec_a = (1,1) and vec_b = (-1, 1), your current implementation leaves a large empty triangle at the lower right side on a rectangular canvas.
  2. When determining where to stop adding points, your implementation first determines which unit cells to add based on whether point is inside the canvas. This misses adding decoration points from unit cells whose point variable is outside of the canvas, but the decoration points are inside the canvas boundary.
  3. If the user provides unusual, or meaningless vec_a, vec_b, or decoration_points, we should catch it. Validation is needed to be implemented here.

Getting the implementation of this method right is the crux of this issue. We can start from an inefficient but working solution, and worry about efficiency later, but the current implementation is not filling the canvas properly.

Copy link
Author

Choose a reason for hiding this comment

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

@peterkomar-aws this is a good catch need to think about it

Copy link
Author

Choose a reason for hiding this comment

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

There are a few issues with the implementation here:

  1. To fill in the entire canvas, depending on the values of vec_a and vec_b, it may not be enough to start from i=0 and j=0. E.g. if vec_a = (1,1) and vec_b = (-1, 1), your current implementation leaves a large empty triangle at the lower right side on a rectangular canvas.
  2. When determining where to stop adding points, your implementation first determines which unit cells to add based on whether point is inside the canvas. This misses adding decoration points from unit cells whose point variable is outside of the canvas, but the decoration points are inside the canvas boundary.
  3. If the user provides unusual, or meaningless vec_a, vec_b, or decoration_points, we should catch it. Validation is needed to be implemented here.

Getting the implementation of this method right is the crux of this issue. We can start from an inefficient but working solution, and worry about efficiency later, but the current implementation is not filling the canvas properly.

to address this issue we can implement something like this

import numpy as np
from typing import List, Tuple, Union
from collections.abc import Iterator
from dataclasses import dataclass
from decimal import Decimal
from enum import Enum
from numbers import Number

class SiteType(Enum):
    VACANT = "Vacant"
    FILLED = "Filled"

@dataclass
class AtomArrangementItem:
    coordinate: Tuple[Number, Number]
    site_type: SiteType

    def _validate_coordinate(self) -> None:
        if len(self.coordinate) != 2:
            raise ValueError(f"{self.coordinate} must be of length 2")
        for idx, num in enumerate(self.coordinate):
            if not isinstance(num, Number):
                raise TypeError(f"{num} at position {idx} must be a number")

    def _validate_site_type(self) -> None:
        allowed_site_types = {SiteType.FILLED, SiteType.VACANT}
        if self.site_type not in allowed_site_types:
            raise ValueError(f"{self.site_type} must be one of {allowed_site_types}")

    def __post_init__(self) -> None:
        self._validate_coordinate()
        self._validate_site_type()

class AtomArrangement:
    def __init__(self):
        self._sites = []

    def add(self, coordinate: Union[Tuple[Number, Number], np.ndarray], site_type: SiteType = SiteType.FILLED) -> AtomArrangement:
        self._sites.append(AtomArrangementItem(tuple(coordinate), site_type))
        return self

    def coordinate_list(self, coordinate_index: Number) -> List[Number]:
        return [site.coordinate[coordinate_index] for site in self._sites]

    def __iter__(self) -> Iterator:
        return iter(self._sites)

    def __len__(self):
        return len(self._sites)

    def discretize(self, properties: DiscretizationProperties) -> AtomArrangement:
        try:
            position_res = properties.lattice.positionResolution
            discretized_arrangement = AtomArrangement()
            for site in self._sites:
                new_coordinates = tuple(
                    round(Decimal(c) / position_res) * position_res for c in site.coordinate
                )
                discretized_arrangement.add(new_coordinates, site.site_type)
            return discretized_arrangement
        except Exception as e:
            raise DiscretizationError(f"Failed to discretize register {e}") from e

    @classmethod
    def from_decorated_bravais_lattice(cls, lattice_vectors: List[Tuple[float, float]], decoration_points: List[Tuple[float, float]], canvas_boundary_points: List[Tuple[float, float]]) -> AtomArrangement:
        """Create an atom arrangement with a decorated Bravais lattice."""
        # Validate input
        if len(lattice_vectors) != 2:
            raise ValueError("Two lattice vectors are required.")
        if any(len(vec) != 2 for vec in lattice_vectors):
            raise ValueError("Each lattice vector must have two components.")
        if any(len(dp) != 2 for dp in decoration_points):
            raise ValueError("Each decoration point must have two components.")
        if len(canvas_boundary_points) != 4:
            raise ValueError("Canvas boundary points should define a rectangle with 4 points.")

        arrangement = cls()
        vec_a, vec_b = np.array(lattice_vectors[0]), np.array(lattice_vectors[1])
        x_min, y_min = canvas_boundary_points[0]
        x_max, y_max = canvas_boundary_points[2]

        def is_within_canvas(point: np.ndarray) -> bool:
            return x_min <= point[0] <= x_max and y_min <= point[1] <= y_max

        # Determine number of steps needed in each direction to fill the canvas
        n_steps_x = int(np.ceil((x_max - x_min) / np.linalg.norm(vec_a)))
        n_steps_y = int(np.ceil((y_max - y_min) / np.linalg.norm(vec_b)))

        for i in range(-n_steps_x, n_steps_x + 1):
            for j in range(-n_steps_y, n_steps_y + 1):
                origin = i * vec_a + j * vec_b
                for dp in decoration_points:
                    decorated_point = origin + np.array(dp)
                    if is_within_canvas(decorated_point):
                        arrangement.add(tuple(decorated_point))

        return arrangement

@dataclass
class LatticeGeometry:
    positionResolution: Decimal

@dataclass
class DiscretizationProperties:
    lattice: LatticeGeometry

class DiscretizationError(Exception):
    pass

# Example usage
if __name__ == "__main__":
    canvas_boundary_points = [(0, 0), (7.5e-5, 0), (7.5e-5, 7.5e-5), (0, 7.5e-5)]

    # Create lattice structures
    decorated_bravais_lattice = AtomArrangement.from_decorated_bravais_lattice(
        [(4e-6, 0), (0, 4e-6)],
        [(1e-6, 1e-6)],
        canvas_boundary_points
    )

    # Validate lattice structure
    def validate_lattice(arrangement, lattice_type):
        num_sites = len(arrangement)
        min_distance = None
        for i, atom1 in enumerate(arrangement):
            for j, atom2 in enumerate(arrangement):
                if i != j:
                    distance = np.linalg.norm(np.array(atom1.coordinate) - np.array(atom2.coordinate))
                    if min_distance is None or distance < min_distance:
                        min_distance = distance
        print(f"Lattice Type: {lattice_type}")
        print(f"Number of lattice points: {num_sites}")
        print(f"Minimum distance between lattice points: {min_distance:.2e} meters")
        print("-" * 40)

    validate_lattice(decorated_bravais_lattice, "Decorated Bravais Lattice")

return arrangement

@classmethod
def from_honeycomb_lattice(cls, lattice_constant: float, canvas_boundary_points: List[Tuple[float, float]]) -> AtomArrangement:
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's add docstrings.

return cls.from_decorated_bravais_lattice([a1, a2], decoration_points, canvas_boundary_points)

@classmethod
def from_bravais_lattice(cls, lattice_vectors: List[Tuple[float, float]], canvas_boundary_points: List[Tuple[float, float]]) -> AtomArrangement:
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's add docstrings.

@AbdullahKazi500
Copy link
Author

AbdullahKazi500 commented Jun 11, 2024

Great start. You have the right structure for the factory methods.

The main thing that needs change is the implementation of the from_decorated_bravais_lattice method. Currently it cuts off too many sites, sites which would fit on the canvas. See my comment for line 141. This is not easy to solve. Looking forward to your idea how to make this method work for arbitrary vec_a and vec_b values.

The goal is the fill up the entire canvas, and only remove sites that would truly fall outside of the canvas boundary.

Hi @peterkomar-aws
before adding this to the PR May I know is this what you want me to implement so that I know I am going the right way

@classmethod
def from_decorated_bravais_lattice(cls, lattice_vectors: List[Tuple[float, float]], decoration_points: List[Tuple[float, float]], canvas_boundary_points: List[Tuple[float, float]]) -> AtomArrangement:
    """Create an atom arrangement with a decorated Bravais lattice."""
    arrangement = cls()
    vec_a, vec_b = np.array(lattice_vectors[0]), np.array(lattice_vectors[1])
    x_min, y_min = canvas_boundary_points[0]
    x_max, y_max = canvas_boundary_points[2]

    # Calculate the bounds of the lattice within the canvas
    lattice_x_min = max(0, x_min)
    lattice_x_max = min(x_max, x_max - vec_a[0] + vec_b[0])
    lattice_y_min = max(0, y_min)
    lattice_y_max = min(y_max, y_max - vec_a[1] + vec_b[1])

    for i in np.arange(lattice_x_min, lattice_x_max, vec_a[0]):
        for j in np.arange(lattice_y_min, lattice_y_max, vec_b[1]):
            origin = i * vec_a + j * vec_b
            for dp in decoration_points:
                decorated_point = origin + np.array(dp)
                if x_min <= decorated_point[0] <= x_max and y_min <= decorated_point[1] <= y_max:
                    arrangement.add(tuple(decorated_point))

    return arrangement

@peterkomar-aws
Copy link
Contributor

Hi @AbdullahKazi500 , can you explain the logic behind lattice_x_max = min(x_max, x_max - vec_a[0] + vec_b[0])? (It is very suspicious that vec_a and vec_b are not treated symmetrically by this expression. Logically, they are interchangeable.)

@peterkomar-aws
Copy link
Contributor

Hi @AbdullahKazi500 , thanks for your contributions. Looking forward to your contributions to this work and repo. Thank you. (I mark the PR draft again; feel free to add commits to it an open it again if you think it's ready for another round of reviews. Thanks.)

@peterkomar-aws peterkomar-aws marked this pull request as draft June 13, 2024 13:49
@AbdullahKazi500
Copy link
Author

AbdullahKazi500 commented Jun 20, 2024

Hi @peterkomar-aws since the Organizers has posted this message

Hackers and Maintainers,

We have received a lot of requests over the past few days to extend the review timeline for PRs that are almost ready but not yet. As such, we are extending the review time period by 1 week to June 26th.

Maintainers, if there are PRs that are very close to finalized and significant work has been done to warrant the bounty, please assign and close the unitaryHACK issue by June 26th and you can continue working with the participant until the code is ready to merge. 

Thank you, everyone! Congratulations on all the hard work put in so far!

I would like to keep going on this issue and I would like to know your review on this also I am trying to address a few of your comments

@AbdullahKazi500
Copy link
Author

can you explain the logic behind lattice_x_max = min(x_max, x_max - vec_a[0] + vec_b[0])? (It is very suspicious that vec_a and vec_b are not treated symmetrically by this expression. Logically, they are interchangeable.)

The expression lattice_x_max = min(x_max, x_max - vec_a[0] + vec_b[0]) is used to calculate the maximum bound for the x-coordinates of the lattice within the canvas. This expression, however, does seem asymmetrical and possibly incorrect due to the fact that vec_a and vec_b should be treated symmetrically.

Lattice Vectors (vec_a and vec_b):

vec_a and vec_b are the lattice vectors defining the Bravais lattice.
In a decorated Bravais lattice, these vectors are used to generate the lattice points within the canvas boundary.
Canvas Boundaries (x_min, y_min, x_max, y_max):

These values define the rectangular area within which the lattice should be generated.
Suspicious Expression:

lattice_x_max = min(x_max, x_max - vec_a[0] + vec_b[0])
This expression seems to limit the maximum x-coordinate based on the difference and sum of the components of vec_a and vec_b.
Symmetry Issue:

Logically, vec_a and vec_b are interchangeable as they both define the periodicity of the lattice.
The expression should be symmetric in terms of the treatment of vec_a and vec_b.
Potential Problems:

The expression x_max - vec_a[0] + vec_b[0] lacks symmetry and might produce incorrect bounds.
Typically, the bounds should be calculated considering the periodicity and overlap of lattice vectors in both directions (x and y).

a more well defined logic would be

@classmethod
def from_decorated_bravais_lattice(cls, lattice_vectors: List[Tuple[float, float]], decoration_points: List[Tuple[float, float]], canvas_boundary_points: List[Tuple[float, float]]) -> AtomArrangement:
    """Create an atom arrangement with a decorated Bravais lattice."""
    arrangement = cls()
    vec_a, vec_b = np.array(lattice_vectors[0]), np.array(lattice_vectors[1])
    x_min, y_min = canvas_boundary_points[0]
    x_max, y_max = canvas_boundary_points[2]

    # Iterate over lattice points within the canvas boundaries
    i = 0
    while True:
        origin = i * vec_a
        if origin[0] > x_max:
            break
        j = 0
        while True:
            point = origin + j * vec_b
            if point[1] > y_max:
                break
            if x_min <= point[0] <= x_max and y_min <= point[1] <= y_max:
                for dp in decoration_points:
                    decorated_point = point + np.array(dp)
                    if x_min <= decorated_point[0] <= x_max and y_min <= decorated_point[1] <= y_max:
                        arrangement.add(tuple(decorated_point))
            j += 1
        i += 1

    return arrangement

Used nested while loops to iterate over lattice points generated by vec_a and vec_b.
Ensured points are within the canvas boundaries before adding them to the arrangement.
Symmetric Bound Check:

Calculate the origin point using i * vec_a.
Calculate the additional lattice point using j * vec_b.
Check both x and y coordinates to ensure they lie within the canvas boundaries before adding decoration points.

@peterkomar-aws
Copy link
Contributor

Hi @AbdullahKazi500 , the implementation you proposed in your previous comment,

@classmethod
def from_decorated_bravais_lattice(cls, lattice_vectors: List[Tuple[float, float]], decoration_points: List[Tuple[float, float]], canvas_boundary_points: List[Tuple[float, float]]) -> AtomArrangement:
    """Create an atom arrangement with a decorated Bravais lattice."""
    arrangement = cls()
    vec_a, vec_b = np.array(lattice_vectors[0]), np.array(lattice_vectors[1])
    x_min, y_min = canvas_boundary_points[0]
    x_max, y_max = canvas_boundary_points[2]

    # Iterate over lattice points within the canvas boundaries
    i = 0
    while True:
        origin = i * vec_a
        if origin[0] > x_max:
            break
        j = 0
        while True:
            point = origin + j * vec_b
            if point[1] > y_max:
                break
            if x_min <= point[0] <= x_max and y_min <= point[1] <= y_max:
                for dp in decoration_points:
                    decorated_point = point + np.array(dp)
                    if x_min <= decorated_point[0] <= x_max and y_min <= decorated_point[1] <= y_max:
                        arrangement.add(tuple(decorated_point))
            j += 1
        i += 1

    return arrangement

has the same issue which I pointed out previously, namely

To fill in the entire canvas, depending on the values of vec_a and vec_b, it may not be enough to start from i=0 and j=0. E.g. if vec_a = (1,1) and vec_b = (-1, 1), your current implementation leaves a large empty triangle at the lower right side on a rectangular canvas.

More specifically, running the following test

lattice_vectors = (
    (1, 1),
    (-1, 1),
)
decoration_points = (
    (0, 0),
)
canvas_boundary_points = (
    (0, 0),
    (10, 0),
    (10, 10),
    (0, 10),
)

register = AtomArrangement.from_decorated_bravais_lattice(lattice_vectors, decoration_points, canvas_boundary_points)

produces a register which looks like this
Screenshot 2024-06-24 at 10 46 12 AM

@AbdullahKazi500
Copy link
Author

Hi @AbdullahKazi500 , the implementation you proposed in your previous comment,

@classmethod
def from_decorated_bravais_lattice(cls, lattice_vectors: List[Tuple[float, float]], decoration_points: List[Tuple[float, float]], canvas_boundary_points: List[Tuple[float, float]]) -> AtomArrangement:
    """Create an atom arrangement with a decorated Bravais lattice."""
    arrangement = cls()
    vec_a, vec_b = np.array(lattice_vectors[0]), np.array(lattice_vectors[1])
    x_min, y_min = canvas_boundary_points[0]
    x_max, y_max = canvas_boundary_points[2]

    # Iterate over lattice points within the canvas boundaries
    i = 0
    while True:
        origin = i * vec_a
        if origin[0] > x_max:
            break
        j = 0
        while True:
            point = origin + j * vec_b
            if point[1] > y_max:
                break
            if x_min <= point[0] <= x_max and y_min <= point[1] <= y_max:
                for dp in decoration_points:
                    decorated_point = point + np.array(dp)
                    if x_min <= decorated_point[0] <= x_max and y_min <= decorated_point[1] <= y_max:
                        arrangement.add(tuple(decorated_point))
            j += 1
        i += 1

    return arrangement

has the same issue which I pointed out previously, namely

To fill in the entire canvas, depending on the values of vec_a and vec_b, it may not be enough to start from i=0 and j=0. E.g. if vec_a = (1,1) and vec_b = (-1, 1), your current implementation leaves a large empty triangle at the lower right side on a rectangular canvas.

More specifically, running the following test

lattice_vectors = (
    (1, 1),
    (-1, 1),
)
decoration_points = (
    (0, 0),
)
canvas_boundary_points = (
    (0, 0),
    (10, 0),
    (10, 10),
    (0, 10),
)

register = AtomArrangement.from_decorated_bravais_lattice(lattice_vectors, decoration_points, canvas_boundary_points)

produces a register which looks like this Screenshot 2024-06-24 at 10 46 12 AM

it seems like it is not covering over the canvas

class SiteType(Enum):
    VACANT = "Vacant"
    FILLED = "Filled"

@dataclass
class AtomArrangementItem:
    coordinate: Tuple[Number, Number]
    site_type: SiteType

    def _validate_coordinate(self) -> None:
        if len(self.coordinate) != 2:
            raise ValueError(f"{self.coordinate} must be of length 2")
        for idx, num in enumerate(self.coordinate):
            if not isinstance(num, Number):
                raise TypeError(f"{num} at position {idx} must be a number")

    def _validate_site_type(self) -> None:
        allowed_site_types = {SiteType.FILLED, SiteType.VACANT}
        if self.site_type not in allowed_site_types:
            raise ValueError(f"{self.site_type} must be one of {allowed_site_types}")

    def __post_init__(self) -> None:
        self._validate_coordinate()
        self._validate_site_type()

class AtomArrangement:
    def __init__(self):
        self._sites = []

    def add(self, coordinate: Union[Tuple[Number, Number], np.ndarray], site_type: SiteType = SiteType.FILLED) -> AtomArrangement:
        self._sites.append(AtomArrangementItem(tuple(coordinate), site_type))
        return self

    def coordinate_list(self, coordinate_index: Number) -> List[Number]:
        return [site.coordinate[coordinate_index] for site in self._sites]

    def __iter__(self) -> Iterator:
        return iter(self._sites)

    def __len__(self):
        return len(self._sites)

    def discretize(self, properties: DiscretizationProperties) -> AtomArrangement:
        try:
            position_res = properties.lattice.positionResolution
            discretized_arrangement = AtomArrangement()
            for site in self._sites:
                new_coordinates = tuple(
                    round(Decimal(c) / position_res) * position_res for c in site.coordinate
                )
                discretized_arrangement.add(new_coordinates, site.site_type)
            return discretized_arrangement
        except Exception as e:
            raise DiscretizationError(f"Failed to discretize register {e}") from e

    @classmethod
    def from_decorated_bravais_lattice(cls, lattice_vectors: List[Tuple[float, float]], decoration_points: List[Tuple[float, float]], canvas_boundary_points: List[Tuple[float, float]]) -> AtomArrangement:
        """Create an atom arrangement with a decorated Bravais lattice."""
        if len(lattice_vectors) != 2 or any(len(vec) != 2 for vec in lattice_vectors):
            raise ValueError("Invalid lattice vectors provided.")
        if any(len(dp) != 2 for dp in decoration_points):
            raise ValueError("Invalid decoration points provided.")
        if len(canvas_boundary_points) != 4:
            raise ValueError("Invalid canvas boundary points provided.")

        arrangement = cls()
        vec_a, vec_b = np.array(lattice_vectors[0]), np.array(lattice_vectors[1])
        x_min, y_min = canvas_boundary_points[0]
        x_max, y_max = canvas_boundary_points[2]

        def is_within_canvas(point: np.ndarray) -> bool:
            return x_min <= point[0] <= x_max and y_min <= point[1] <= y_max

        # Determine the step directions and limits
        n_steps_x = int(np.ceil((x_max - x_min) / np.linalg.norm(vec_a)))
        n_steps_y = int(np.ceil((y_max - y_min) / np.linalg.norm(vec_b)))

        for i in range(-n_steps_x, n_steps_x + 1):
            for j in range(-n_steps_y, n_steps_y + 1):
                origin = i * vec_a + j * vec_b
                for dp in decoration_points:
                    decorated_point = origin + np.array(dp)
                    if is_within_canvas(decorated_point):
                        arrangement.add(tuple(decorated_point))

        return arrangement

@dataclass
class LatticeGeometry:
    positionResolution: Decimal

@dataclass
class DiscretizationProperties:
    lattice: LatticeGeometry

class DiscretizationError(Exception):
    pass

# Example usage
if __name__ == "__main__":
    lattice_vectors = (
        (1, 1),
        (-1, 1),
    )
    decoration_points = (
        (0, 0),
    )
    canvas_boundary_points = (
        (0, 0),
        (10, 0),
        (10, 10),
        (0, 10),
    )

    register = AtomArrangement.from_decorated_bravais_lattice(lattice_vectors, decoration_points, canvas_boundary_points)

    # Validation function
    def validate_lattice(arrangement):
        num_sites = len(arrangement)
        min_distance = None
        for i, atom1 in enumerate(arrangement):
            for j, atom2 in enumerate(arrangement):
                if i != j:
                    distance = np.linalg.norm(np.array(atom1.coordinate) - np.array(atom2.coordinate))
                    if min_distance is None or distance < min_distance:
                        min_distance = distance
        print(f"Number of lattice points: {num_sites}")
        print(f"Minimum distance between lattice points: {min_distance:.2e} meters")
        print("-" * 40)

    validate_lattice(register)

I have tried modifying the code

@peterkomar-aws
Copy link
Contributor

Hi @AbdullahKazi500 , regarding these lines in your proposal,

n_steps_x = int(np.ceil((x_max - x_min) / np.linalg.norm(vec_a)))
n_steps_y = int(np.ceil((y_max - y_min) / np.linalg.norm(vec_b)))

can you explain why n_steps_x is computed only from x_max and x_min, while you are using it to set a hard limit on how many times you step with vec_a later? See here

for i in range(-n_steps_x, n_steps_x + 1):
            for j in range(-n_steps_y, n_steps_y + 1):
                origin = i * vec_a + j * vec_b

and why n_step_y is computed only from y_max and y_min, while you are using it to set a hard limit on how many times you step step with vec_b?

Any implementation that does not treat vec_a and vec_b symmetrically is very suspicious, because if I reorder the lattice_vectors input list (which effectively swaps the values of vec_a and vec_b), there is a good chance the returned AtomArrangement will be different.

@AbdullahKazi500
Copy link
Author

regarding these lines in your proposal,

Hi @peterkomar-aws When computing n_steps_x and n_steps_y, we want to ensure that both lattice vectors vec_a and vec_b are treated symmetrically is that what you mean but swapping the order of vec_a and vec_b in lattice_vectors will it affect the outcome of AtomArrangement.

@AbdullahKazi500
Copy link
Author

Hi @AbdullahKazi500 , regarding these lines in your proposal,

n_steps_x = int(np.ceil((x_max - x_min) / np.linalg.norm(vec_a)))
n_steps_y = int(np.ceil((y_max - y_min) / np.linalg.norm(vec_b)))

can you explain why n_steps_x is computed only from x_max and x_min, while you are using it to set a hard limit on how many times you step with vec_a later? See here

for i in range(-n_steps_x, n_steps_x + 1):
            for j in range(-n_steps_y, n_steps_y + 1):
                origin = i * vec_a + j * vec_b

and why n_step_y is computed only from y_max and y_min, while you are using it to set a hard limit on how many times you step step with vec_b?

Any implementation that does not treat vec_a and vec_b symmetrically is very suspicious, because if I reorder the lattice_vectors input list (which effectively swaps the values of vec_a and vec_b), there is a good chance the returned AtomArrangement will be different.

so if Both n_steps_x and n_steps_y are now calculated based on the maximum difference between canvas boundaries (x_max - x_min and y_max - y_min). will this ensures that the steps taken in both directions (for vec_a and vec_b) are sufficient to cover the entire canvas area.
and for loops now if we use n_steps_x and n_steps_y symmetrically for both vec_a and vec_b. does This guarantees that the lattice arrangement fills the canvas correctly regardless of the order of vec_a and vec_b in lattice_vectors
what do you think of this changes

class AtomArrangementItem:
    coordinate: Tuple[Number, Number]
    site_type: SiteType

    def _validate_coordinate(self) -> None:
        if len(self.coordinate) != 2:
            raise ValueError(f"{self.coordinate} must be of length 2")
        for idx, num in enumerate(self.coordinate):
            if not isinstance(num, Number):
                raise TypeError(f"{num} at position {idx} must be a number")

    def _validate_site_type(self) -> None:
        allowed_site_types = {SiteType.FILLED, SiteType.VACANT}
        if self.site_type not in allowed_site_types:
            raise ValueError(f"{self.site_type} must be one of {allowed_site_types}")

    def __post_init__(self) -> None:
        self._validate_coordinate()
        self._validate_site_type()

class AtomArrangement:
    def __init__(self):
        self._sites = []

    def add(self, coordinate: Union[Tuple[Number, Number], np.ndarray], site_type: SiteType = SiteType.FILLED) -> AtomArrangement:
        self._sites.append(AtomArrangementItem(tuple(coordinate), site_type))
        return self

    def coordinate_list(self, coordinate_index: Number) -> List[Number]:
        return [site.coordinate[coordinate_index] for site in self._sites]

    def __iter__(self) -> Iterator:
        return iter(self._sites)

    def __len__(self):
        return len(self._sites)

    def discretize(self, properties: DiscretizationProperties) -> AtomArrangement:
        try:
            position_res = properties.lattice.positionResolution
            discretized_arrangement = AtomArrangement()
            for site in self._sites:
                new_coordinates = tuple(
                    round(Decimal(c) / position_res) * position_res for c in site.coordinate
                )
                discretized_arrangement.add(new_coordinates, site.site_type)
            return discretized_arrangement
        except Exception as e:
            raise DiscretizationError(f"Failed to discretize register {e}") from e

    @classmethod
    def from_decorated_bravais_lattice(cls, lattice_vectors: List[Tuple[float, float]], decoration_points: List[Tuple[float, float]], canvas_boundary_points: List[Tuple[float, float]]) -> AtomArrangement:
        """Create an atom arrangement with a decorated Bravais lattice."""
        if len(lattice_vectors) != 2 or any(len(vec) != 2 for vec in lattice_vectors):
            raise ValueError("Invalid lattice vectors provided.")
        if any(len(dp) != 2 for dp in decoration_points):
            raise ValueError("Invalid decoration points provided.")
        if len(canvas_boundary_points) != 4:
            raise ValueError("Invalid canvas boundary points provided.")

        arrangement = cls()
        vec_a, vec_b = np.array(lattice_vectors[0]), np.array(lattice_vectors[1])
        x_min, y_min = canvas_boundary_points[0]
        x_max, y_max = canvas_boundary_points[2]

        def is_within_canvas(point: np.ndarray) -> bool:
            return x_min <= point[0] <= x_max and y_min <= point[1] <= y_max

        # Determine the step directions and limits symmetrically
        n_steps_x = int(np.ceil(max(x_max - x_min, y_max - y_min) / np.linalg.norm(vec_a)))
        n_steps_y = int(np.ceil(max(x_max - x_min, y_max - y_min) / np.linalg.norm(vec_b)))

        for i in range(-n_steps_x, n_steps_x + 1):
            for j in range(-n_steps_y, n_steps_y + 1):
                origin = i * vec_a + j * vec_b
                for dp in decoration_points:
                    decorated_point = origin + np.array(dp)
                    if is_within_canvas(decorated_point):
                        arrangement.add(tuple(decorated_point))

        return arrangement

@dataclass
class LatticeGeometry:
    positionResolution: Decimal

@dataclass
class DiscretizationProperties:
    lattice: LatticeGeometry

class DiscretizationError(Exception):
    pass

# Example usage
if __name__ == "__main__":
    lattice_vectors = (
        (1, 1),
        (-1, 1),
    )
    decoration_points = (
        (0, 0),
    )
    canvas_boundary_points = (
        (0, 0),
        (10, 0),
        (10, 10),
        (0, 10),
    )

    register = AtomArrangement.from_decorated_bravais_lattice(lattice_vectors, decoration_points, canvas_boundary_points)

    # Validation function
    def validate_lattice(arrangement):
        num_sites = len(arrangement)
        min_distance = None
        for i, atom1 in enumerate(arrangement):
            for j, atom2 in enumerate(arrangement):
                if i != j:
                    distance = np.linalg.norm(np.array(atom1.coordinate) - np.array(atom2.coordinate))
                    if min_distance is None or distance < min_distance:
                        min_distance = distance
        print(f"Number of lattice points: {num_sites}")
        print(f"Minimum distance between lattice points: {min_distance:.2e} meters")
        print("-" * 40)

    validate_lattice(register)

@peterkomar-aws
Copy link
Contributor

Hi @AbdullahKazi500 , I appreciate that this implementation is symmetric in its treatment of vec_a and vec_b, this is a good first step. The implementation you are proposing in your comment above, namely,

@classmethod
def from_decorated_bravais_lattice(cls, lattice_vectors: List[Tuple[float, float]], decoration_points: List[Tuple[float, float]], canvas_boundary_points: List[Tuple[float, float]]) -> AtomArrangement:
    """Create an atom arrangement with a decorated Bravais lattice."""
    if len(lattice_vectors) != 2 or any(len(vec) != 2 for vec in lattice_vectors):
        raise ValueError("Invalid lattice vectors provided.")
    if any(len(dp) != 2 for dp in decoration_points):
        raise ValueError("Invalid decoration points provided.")
    if len(canvas_boundary_points) != 4:
        raise ValueError("Invalid canvas boundary points provided.")

    arrangement = cls()
    vec_a, vec_b = np.array(lattice_vectors[0]), np.array(lattice_vectors[1])
    x_min, y_min = canvas_boundary_points[0]
    x_max, y_max = canvas_boundary_points[2]

    def is_within_canvas(point: np.ndarray) -> bool:
        return x_min <= point[0] <= x_max and y_min <= point[1] <= y_max

    # Determine the step directions and limits symmetrically
    n_steps_x = int(np.ceil(max(x_max - x_min, y_max - y_min) / np.linalg.norm(vec_a)))
    n_steps_y = int(np.ceil(max(x_max - x_min, y_max - y_min) / np.linalg.norm(vec_b)))

    for i in range(-n_steps_x, n_steps_x + 1):
        for j in range(-n_steps_y, n_steps_y + 1):
            origin = i * vec_a + j * vec_b
            for dp in decoration_points:
                decorated_point = origin + np.array(dp)
                if is_within_canvas(decorated_point):
                    arrangement.add(tuple(decorated_point))

    return arrangement

also does not properly fill out the canvas for the test I proposed above, i.e.

lattice_vectors = (
    (1, 1),
    (-1, 1),
)
decoration_points = (
    (0, 0),
)
canvas_boundary_points = (
    (0, 0),
    (10, 0),
    (10, 10),
    (0, 10),
)

register = AtomArrangement.from_decorated_bravais_lattice(lattice_vectors, decoration_points, canvas_boundary_points)

produces a register that looks like this
Screenshot 2024-06-28 at 10 44 11 AM

I suspect this is due to the premature stopping of the for i and for j loops, i.e. n_steps_x and n_steps_y are too small. My guess is this is due to the fact that you are computing them by dividing the Euclidean norm of vec_a and vec_b, which is generally larger than the projection of the said vectors in the x and y directions, respectively.

@AbdullahKazi500
Copy link
Author

Hi @AbdullahKazi500 , the implementation you proposed in your previous comment,

@classmethod
def from_decorated_bravais_lattice(cls, lattice_vectors: List[Tuple[float, float]], decoration_points: List[Tuple[float, float]], canvas_boundary_points: List[Tuple[float, float]]) -> AtomArrangement:
    """Create an atom arrangement with a decorated Bravais lattice."""
    arrangement = cls()
    vec_a, vec_b = np.array(lattice_vectors[0]), np.array(lattice_vectors[1])
    x_min, y_min = canvas_boundary_points[0]
    x_max, y_max = canvas_boundary_points[2]

    # Iterate over lattice points within the canvas boundaries
    i = 0
    while True:
        origin = i * vec_a
        if origin[0] > x_max:
            break
        j = 0
        while True:
            point = origin + j * vec_b
            if point[1] > y_max:
                break
            if x_min <= point[0] <= x_max and y_min <= point[1] <= y_max:
                for dp in decoration_points:
                    decorated_point = point + np.array(dp)
                    if x_min <= decorated_point[0] <= x_max and y_min <= decorated_point[1] <= y_max:
                        arrangement.add(tuple(decorated_point))
            j += 1
        i += 1

    return arrangement

has the same issue which I pointed out previously, namely

To fill in the entire canvas, depending on the values of vec_a and vec_b, it may not be enough to start from i=0 and j=0. E.g. if vec_a = (1,1) and vec_b = (-1, 1), your current implementation leaves a large empty triangle at the lower right side on a rectangular canvas.

More specifically, running the following test

lattice_vectors = (
    (1, 1),
    (-1, 1),
)
decoration_points = (
    (0, 0),
)
canvas_boundary_points = (
    (0, 0),
    (10, 0),
    (10, 10),
    (0, 10),
)

register = AtomArrangement.from_decorated_bravais_lattice(lattice_vectors, decoration_points, canvas_boundary_points)

produces a register which looks like this Screenshot 2024-06-24 at 10 46 12 AM

it seems like it is not covering over the canvas

class SiteType(Enum):
    VACANT = "Vacant"
    FILLED = "Filled"

@dataclass
class AtomArrangementItem:
    coordinate: Tuple[Number, Number]
    site_type: SiteType

    def _validate_coordinate(self) -> None:
        if len(self.coordinate) != 2:
            raise ValueError(f"{self.coordinate} must be of length 2")
        for idx, num in enumerate(self.coordinate):
            if not isinstance(num, Number):
                raise TypeError(f"{num} at position {idx} must be a number")

    def _validate_site_type(self) -> None:
        allowed_site_types = {SiteType.FILLED, SiteType.VACANT}
        if self.site_type not in allowed_site_types:
            raise ValueError(f"{self.site_type} must be one of {allowed_site_types}")

    def __post_init__(self) -> None:
        self._validate_coordinate()
        self._validate_site_type()

class AtomArrangement:
    def __init__(self):
        self._sites = []

    def add(self, coordinate: Union[Tuple[Number, Number], np.ndarray], site_type: SiteType = SiteType.FILLED) -> AtomArrangement:
        self._sites.append(AtomArrangementItem(tuple(coordinate), site_type))
        return self

    def coordinate_list(self, coordinate_index: Number) -> List[Number]:
        return [site.coordinate[coordinate_index] for site in self._sites]

    def __iter__(self) -> Iterator:
        return iter(self._sites)

    def __len__(self):
        return len(self._sites)

    def discretize(self, properties: DiscretizationProperties) -> AtomArrangement:
        try:
            position_res = properties.lattice.positionResolution
            discretized_arrangement = AtomArrangement()
            for site in self._sites:
                new_coordinates = tuple(
                    round(Decimal(c) / position_res) * position_res for c in site.coordinate
                )
                discretized_arrangement.add(new_coordinates, site.site_type)
            return discretized_arrangement
        except Exception as e:
            raise DiscretizationError(f"Failed to discretize register {e}") from e

    @classmethod
    def from_decorated_bravais_lattice(cls, lattice_vectors: List[Tuple[float, float]], decoration_points: List[Tuple[float, float]], canvas_boundary_points: List[Tuple[float, float]]) -> AtomArrangement:
        """Create an atom arrangement with a decorated Bravais lattice."""
        if len(lattice_vectors) != 2 or any(len(vec) != 2 for vec in lattice_vectors):
            raise ValueError("Invalid lattice vectors provided.")
        if any(len(dp) != 2 for dp in decoration_points):
            raise ValueError("Invalid decoration points provided.")
        if len(canvas_boundary_points) != 4:
            raise ValueError("Invalid canvas boundary points provided.")

        arrangement = cls()
        vec_a, vec_b = np.array(lattice_vectors[0]), np.array(lattice_vectors[1])
        x_min, y_min = canvas_boundary_points[0]
        x_max, y_max = canvas_boundary_points[2]

        def is_within_canvas(point: np.ndarray) -> bool:
            return x_min <= point[0] <= x_max and y_min <= point[1] <= y_max

        # Determine the step directions and limits
        n_steps_x = int(np.ceil((x_max - x_min) / np.linalg.norm(vec_a)))
        n_steps_y = int(np.ceil((y_max - y_min) / np.linalg.norm(vec_b)))

        for i in range(-n_steps_x, n_steps_x + 1):
            for j in range(-n_steps_y, n_steps_y + 1):
                origin = i * vec_a + j * vec_b
                for dp in decoration_points:
                    decorated_point = origin + np.array(dp)
                    if is_within_canvas(decorated_point):
                        arrangement.add(tuple(decorated_point))

        return arrangement

@dataclass
class LatticeGeometry:
    positionResolution: Decimal

@dataclass
class DiscretizationProperties:
    lattice: LatticeGeometry

class DiscretizationError(Exception):
    pass

# Example usage
if __name__ == "__main__":
    lattice_vectors = (
        (1, 1),
        (-1, 1),
    )
    decoration_points = (
        (0, 0),
    )
    canvas_boundary_points = (
        (0, 0),
        (10, 0),
        (10, 10),
        (0, 10),
    )

    register = AtomArrangement.from_decorated_bravais_lattice(lattice_vectors, decoration_points, canvas_boundary_points)

    # Validation function
    def validate_lattice(arrangement):
        num_sites = len(arrangement)
        min_distance = None
        for i, atom1 in enumerate(arrangement):
            for j, atom2 in enumerate(arrangement):
                if i != j:
                    distance = np.linalg.norm(np.array(atom1.coordinate) - np.array(atom2.coordinate))
                    if min_distance is None or distance < min_distance:
                        min_distance = distance
        print(f"Number of lattice points: {num_sites}")
        print(f"Minimum distance between lattice points: {min_distance:.2e} meters")
        print("-" * 40)

    validate_lattice(register)

I have tried modifying the code

okay peter I will have a look

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.

Factory methods for AHS AtomArrangments
2 participants