Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .configuration import NanotubeConfiguration
from .builders import NanotubeBuilder
from .build_parameters import NanotubeBuilderParameters
from .helpers import create_nanotube

__all__ = [
"NanotubeConfiguration",
"NanotubeBuilder",
"NanotubeBuilderParameters",
"create_nanotube",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from mat3ra.made.tools.build_components.entities.reusable.base_builder import BaseBuilderParameters


class NanotubeBuilderParameters(BaseBuilderParameters):
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from typing import Any, Optional, Type, Union

import numpy as np

from mat3ra.made.material import Material
from mat3ra.made.tools.build_components import MaterialWithBuildMetadata, TypeConfiguration
from mat3ra.made.tools.build_components.entities.reusable.base_builder import BaseSingleBuilder
from mat3ra.made.tools.build_components.entities.reusable.one_dimensional.crystal_lattice_lines.edge_types import (
get_edge_type_from_miller_indices,
)
from ...two_dimensional.nanoribbon.builders import NanoribbonBuilder, NanoribbonBuilderParameters
from .build_parameters import NanotubeBuilderParameters
from .configuration import NanotubeConfiguration


class NanotubeBuilder(BaseSingleBuilder):
"""
Builder for single-walled nanotubes.

Creates a nanotube by first building a nanoribbon and then applying a cylindrical
folding transformation: the y-direction (width) of the nanoribbon is rolled into a
circle to form the tube wall, while the x-direction (length) becomes the tube axis.
"""

_ConfigurationType: Type[NanotubeConfiguration] = NanotubeConfiguration
_GeneratedItemType: Type[Material] = Material
_BuildParametersType: Type[NanotubeBuilderParameters] = NanotubeBuilderParameters
_DefaultBuildParameters: NanotubeBuilderParameters = NanotubeBuilderParameters()

def _generate(self, configuration: NanotubeConfiguration) -> MaterialWithBuildMetadata:
nanoribbon_builder = NanoribbonBuilder(
build_parameters=NanoribbonBuilderParameters(use_rectangular_lattice=True)
)
return nanoribbon_builder.get_material(configuration.nanoribbon)

def _post_process(
self,
item: Union[Material, MaterialWithBuildMetadata],
post_process_parameters: Optional[Any] = None,
configuration: Optional[TypeConfiguration] = None,
) -> MaterialWithBuildMetadata:
vacuum_around_tube = configuration.vacuum_around_tube if configuration is not None else 10.0
return self._fold_to_nanotube(item, vacuum_around_tube)

def _fold_to_nanotube(
self, material: Union[Material, MaterialWithBuildMetadata], vacuum_around_tube: float = 10.0
) -> MaterialWithBuildMetadata:
"""
Fold a nanoribbon into a nanotube by applying a cylindrical coordinate transformation.

The nanoribbon width (y Cartesian direction) maps to the circumference of the nanotube.
The nanoribbon length (x Cartesian direction) becomes the tube axis (a lattice vector).
The 2D material's out-of-plane direction (z) shifts the effective radius for each atom,
allowing correct handling of finite-thickness layered structures.

Args:
material: Nanoribbon material to fold.
vacuum_around_tube: Vacuum region (in Angstroms) around the tube cross-section.

Returns:
MaterialWithBuildMetadata: The folded nanotube material.
"""
new_material = material.clone()
new_material.to_cartesian()
coords = np.array(new_material.basis.coordinates.values)

y_min = coords[:, 1].min()
y_max = coords[:, 1].max()
circumference = y_max - y_min

if circumference < 1e-6:
raise ValueError(
"Nanoribbon width is too small to roll into a nanotube. "
"Increase the nanoribbon width parameter."
)

radius = circumference / (2 * np.pi)

# Compute the center of the 2D sheet in z (handles finite-thickness structures)
z_center = (coords[:, 2].min() + coords[:, 2].max()) / 2.0

# New cross-sectional cell size: diameter + vacuum
cross_section_size = 2 * radius + vacuum_around_tube

new_coords = np.empty_like(coords)
for i, (x, y, z) in enumerate(coords):
theta = 2 * np.pi * (y - y_min) / circumference
effective_radius = radius + (z - z_center)
new_coords[i, 0] = x
new_coords[i, 1] = effective_radius * np.cos(theta) + cross_section_size / 2.0
new_coords[i, 2] = effective_radius * np.sin(theta) + cross_section_size / 2.0

old_vectors = new_material.lattice.vector_arrays
new_material.set_lattice_vectors(
[old_vectors[0][0], 0.0, 0.0],
[0.0, cross_section_size, 0.0],
[0.0, 0.0, cross_section_size],
)
new_material.set_coordinates(new_coords.tolist())
new_material.to_crystal()
return MaterialWithBuildMetadata.create(new_material)

def _update_material_name(
self, material: Union[Material, MaterialWithBuildMetadata], configuration: Any
) -> MaterialWithBuildMetadata:
if isinstance(configuration, NanotubeConfiguration):
nanotape = configuration.nanoribbon.nanotape
lattice_lines = nanotape.lattice_lines
crystal_name = lattice_lines.crystal.name
miller_indices = lattice_lines.miller_indices_2d
edge_type = get_edge_type_from_miller_indices(miller_indices)
miller_str = f"{miller_indices[0]}{miller_indices[1]}"
material.name = f"{crystal_name} - {edge_type} Nanotube ({miller_str})"
return material
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from pydantic import Field

from mat3ra.made.tools.build_components.entities.reusable.base_builder import BaseConfigurationPydantic
from ...two_dimensional.nanoribbon.configuration import NanoribbonConfiguration


class NanotubeConfiguration(BaseConfigurationPydantic):
"""
Configuration for building a single-walled nanotube from a nanoribbon.

The nanotube is created by rolling a nanoribbon into a cylinder along the y-axis (width direction).
The x-axis of the nanoribbon becomes the tube axis. The vacuum_around_tube parameter controls
the vacuum region around the nanotube in the cross-sectional plane.

Args:
nanoribbon: The nanoribbon configuration to roll into a nanotube.
vacuum_around_tube: The vacuum region around the tube cross-section in Angstroms.
"""

type: str = "NanotubeConfiguration"
nanoribbon: NanoribbonConfiguration
vacuum_around_tube: float = Field(default=10.0, description="Vacuum around the tube cross-section in Angstroms.")
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from typing import Optional, Tuple, Union

from mat3ra.esse.models.core.reusable.axis_enum import AxisEnum

from mat3ra.made.material import Material
from ...two_dimensional.nanotape import NanoTapeConfiguration
from ...two_dimensional.nanoribbon.configuration import NanoribbonConfiguration
from .....build_components import MaterialWithBuildMetadata
from .....build_components.entities.core.two_dimensional.vacuum.configuration import VacuumConfiguration
from .....build_components.entities.reusable.one_dimensional.crystal_lattice_lines.edge_types import EdgeTypesEnum
from .....build_components.entities.reusable.one_dimensional.crystal_lattice_lines.helpers import (
create_lattice_lines_config_and_material,
)
from .builders import NanotubeBuilder
from .build_parameters import NanotubeBuilderParameters
from .configuration import NanotubeConfiguration


def create_nanotube(
material: Union[Material, MaterialWithBuildMetadata],
miller_indices_2d: Optional[Tuple[int, int]] = None,
edge_type: EdgeTypesEnum = EdgeTypesEnum.zigzag,
width: int = 2,
length: int = 4,
vacuum_width: float = 0.0,
vacuum_length: float = 0.0,
vacuum_around_tube: float = 10.0,
termination_formula: Optional[str] = None,
) -> Material:
"""
Creates a single-walled nanotube from a monolayer material.

The nanotube is built by first creating a nanoribbon and then folding it into a cylinder:
the width direction (y) of the nanoribbon becomes the circumference of the tube, and
the length direction (x) becomes the periodic tube axis.

Args:
material: The monolayer material to create the nanotube from (assumes vacuum is present).
miller_indices_2d: The (u,v) Miller indices for the nanotube chiral direction.
edge_type: Edge type ("zigzag"/"armchair"). Used if miller_indices_2d is not provided.
width: The width of the nanoribbon in unit cells (determines the tube circumference).
length: The length of the nanoribbon in unit cells (determines the tube period).
vacuum_width: Additional vacuum along the nanoribbon width before rolling (Angstroms).
Defaults to 0 since the rolling step handles the radial vacuum.
vacuum_length: Vacuum along the nanoribbon length / tube axis (Angstroms).
Defaults to 0 for a fully periodic tube.
vacuum_around_tube: Vacuum around the tube cross-section in the new cell (Angstroms).
termination_formula: Termination formula for edge atoms (e.g., "H").

Returns:
Material: The generated single-walled nanotube material.
"""
lattice_lines_config = create_lattice_lines_config_and_material(
material=material,
miller_indices_2d=miller_indices_2d,
edge_type=edge_type,
width=width,
length=length,
termination_formula=termination_formula,
)
nanotape_vacuum_config = VacuumConfiguration(
size=vacuum_width,
direction=AxisEnum.y,
)
nanotape_config = NanoTapeConfiguration(
stack_components=[lattice_lines_config, nanotape_vacuum_config],
direction=AxisEnum.y,
)
nanoribbon_vacuum_config = VacuumConfiguration(
size=vacuum_length,
direction=AxisEnum.x,
)
nanoribbon_config = NanoribbonConfiguration(
stack_components=[nanotape_config, nanoribbon_vacuum_config],
direction=AxisEnum.x,
)
config = NanotubeConfiguration(
nanoribbon=nanoribbon_config,
vacuum_around_tube=vacuum_around_tube,
)
builder = NanotubeBuilder(build_parameters=NanotubeBuilderParameters())
return builder.get_material(config)
7 changes: 7 additions & 0 deletions tests/py/unit/fixtures/nanotube/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .zigzag import GRAPHENE_NANOTUBE_ZIGZAG
from .armchair import GRAPHENE_NANOTUBE_ARMCHAIR

__all__ = [
"GRAPHENE_NANOTUBE_ZIGZAG",
"GRAPHENE_NANOTUBE_ARMCHAIR",
]
Loading