diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b692cf2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Batista Group (Yale University) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index e69de29..5c0376e 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,63 @@ +# Batista Template + +A modern Python project template that provides a solid foundation for building Python packages with best practices and development tools. + +## Features + +- ๐Ÿš€ Fast dependency management with `uv` +- ๐Ÿ“š Documentation with MkDocs and Material theme +- โœจ Code quality tools (Ruff for linting and formatting) +- ๐Ÿ”„ GitHub Actions for CI/CD +- ๐Ÿ“ฆ Modern Python packaging with `pyproject.toml` +- ๐Ÿงช Testing infrastructure +- ๐Ÿ“ Pre-commit hooks for code quality + +## Quick Start + +This project uses `uv` for fast and reliable Python package management: + +```bash +# Create and activate a virtual environment +uv venv +source .venv/bin/activate + +# Install the package and all development dependencies +uv pip install -e ".[dev]" + +# Install pre-commit hooks +pre-commit install +``` + +## Documentation + +After installing dependencies, you can run the documentation locally: + +```bash +mkdocs serve +``` + +Then open your browser at `http://127.0.0.1:8000` + +## Project Structure + +```md +. +โ”œโ”€โ”€ data/ # Data files and resources +โ”œโ”€โ”€ docs/ # Documentation files (MkDocs) +โ”œโ”€โ”€ scripts/ # Utility and automation scripts +โ”œโ”€โ”€ src/ # Source code +โ”‚ โ””โ”€โ”€ batistatemplate/ +โ”œโ”€โ”€ tests/ # Test files +โ”œโ”€โ”€ .github/ # GitHub Actions workflows +โ”œโ”€โ”€ mkdocs.yml # MkDocs configuration +โ”œโ”€โ”€ pyproject.toml # Project dependencies and settings +โ””โ”€โ”€ .pre-commit-config.yaml # Pre-commit hooks configuration +``` + +## Development + +For detailed development instructions, please refer to our [documentation](docs/index.md). + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/data/README.md b/data/README.md index e69de29..6c5fa74 100644 --- a/data/README.md +++ b/data/README.md @@ -0,0 +1,34 @@ +# Data Directory + +This directory is intended for storing data files and resources that are used by the project. This can include: + +- Sample data files +- Configuration files +- Resource files +- Test data +- Data templates + +## Structure + +```md +data/ +โ”œโ”€โ”€ README.md # This file +โ”œโ”€โ”€ sample/ # Sample data files (if any) +โ””โ”€โ”€ config/ # Configuration files (if any) +``` + +## Usage + +When adding data files to this directory: + +1. Ensure the files are in an appropriate format (CSV, JSON, YAML, etc.) +2. Document any data schema or format requirements +3. If the data is large, consider adding it to `.gitignore` and providing download instructions +4. Include sample data for testing and documentation purposes + +## Data Guidelines + +- Keep sensitive data out of version control +- Document data sources and any licensing requirements +- Maintain a clear structure for different types of data +- Include README files in subdirectories as needed diff --git a/docs/batistatemplate/hello.md b/docs/batistatemplate/hello.md index c95d8ff..e6c4add 100644 --- a/docs/batistatemplate/hello.md +++ b/docs/batistatemplate/hello.md @@ -1,4 +1,4 @@ -# Molecular Structure Module +# Hello module This module provides a modern, type-safe implementation of molecular structure manipulation tools. It serves as an example of Python 3.12+ features and best practices in scientific computing. @@ -10,9 +10,9 @@ This module provides a modern, type-safe implementation of molecular structure m - Error handling with generic result types - Modern API design patterns -## Core Classes +### Core Classes -### `UnitSystem` (Enum) +#### `UnitSystem` (Enum) Think of enums as a way to create a fixed set of allowed options in your code. Let's look at different approaches: @@ -63,7 +63,7 @@ def calculate_energy(value, unit_system: UnitSystem): The `auto()` function just handles the behind-the-scenes numbering so you don't have to. It's like having Python automatically number your enum options (1, 2, 3...) instead of you doing it manually. -### `Atom` (Immutable Dataclass) +#### `Atom` (Immutable Dataclass) Dataclasses are perfect for "data containers" - objects that mainly hold data with little or no behavior. They automatically generate common methods like `__init__`, `__repr__`, and `__eq__`. Here's why we use a dataclass for `Atom`: @@ -98,7 +98,7 @@ class Atom: We use `frozen=True` to make the dataclass immutable - once an atom is created, its properties can't be changed. This prevents bugs from accidental modifications and makes the code easier to reason about. -### `Molecule` (Class) +#### `Molecule` (Class) Unlike `Atom`, `Molecule` needs to be a regular class because it has complex behavior beyond just storing data. Here's why: @@ -136,111 +136,25 @@ class Molecule: - Use regular classes when you need complex behavior or calculations (like `Molecule`) - Use frozen dataclasses when you want immutability -### `Result[T]` (Generic Dataclass) - -Generic types let you write code that works with different data types while maintaining type safety. The `T` in `Result[T]` is a placeholder that can be replaced with any type. Here's why this is useful: - -```python -# Without generics - you'd need different result classes for different types -class FloatResult: - def __init__(self, value: float, success: bool = True, error_message: str | None = None): - self.value = value - self.success = success - self.error_message = error_message - -class MoleculeResult: - def __init__(self, value: Molecule, success: bool = True, error_message: str | None = None): - self.value = value - self.success = success - self.error_message = error_message - -# You'd need to create a new class for every type! -energy_result = FloatResult(value=1.23) -molecule_result = MoleculeResult(value=water_molecule) -``` - -With generics, we can write one class that works for any type: - -```python -@dataclass -class Result(Generic[T]): - value: T - success: bool = True - error_message: str | None = None - -# Now we can use it with any type: -energy_result = Result[float](value=1.23) -molecule_result = Result[Molecule](value=water_molecule) -matrix_result = Result[np.ndarray](value=distance_matrix) - -# Type checking still works: -energy: float = energy_result.value # OK -energy = molecule_result.value # Type error! -``` - -This is particularly useful for functions that might fail: - -```python -def parse_xyz_file(path: str) -> Result[Molecule]: - try: - # Try to read and parse file - molecule = read_molecule(path) - return Result(value=molecule) - except FileNotFoundError: - return Result(value=None, success=False, error_message="File not found") - except ValueError as e: - return Result(value=None, success=False, error_message=str(e)) - -# Usage: -result = parse_xyz_file("water.xyz") -if result.success: - molecule = result.value - # Do something with molecule -else: - print(f"Error: {result.error_message}") -``` - -The benefits of generic types: - -1. Write one class/function that works with multiple types -2. Maintain type safety and IDE support -3. Avoid code duplication -4. Make error handling more consistent - -## Molecular Operations - -The module provides functions for molecular geometry manipulation with carefully designed parameter passing conventions: - ### Parameter Passing Conventions The module uses modern Python parameter passing patterns to create a clear and safe API: 1. **Positional-Only Parameters (`/`)** - - - Used for the primary object being operated on (molecule) - - Makes code more concise when the parameter name is obvious - - Example: `translate_molecule(molecule, /, *, vector)` + - Used for the primary object being operated on (molecule) + - Makes code more concise when the parameter name is obvious + - Example: `translate_molecule(molecule, /, *, vector)` 2. **Keyword-Only Parameters (`*`)** - - Used for operation parameters with meaningful names - - Makes function calls self-documenting - - Prevents parameter order confusion - - Example: `rotate_molecule(mol, *, angle=np.pi, axis=[0,0,1])` + - Used for operation parameters with meaningful names + - Makes function calls self-documenting + - Prevents parameter order confusion + - Example: `rotate_molecule(mol, *, angle=np.pi, axis=[0,0,1])` ### Available Operations #### Translation -```python -def translate_molecule( - molecule: Molecule, - /, - *, - vector: FloatVector, -) -> Molecule: - """Translate molecule by a vector.""" -``` - Usage: ```python @@ -253,17 +167,6 @@ translated = translate_molecule(mol, [1.0, 0.0, 0.0]) # TypeError #### Rotation -```python -def rotate_molecule( - molecule: Molecule, - /, - *, - angle: float, - axis: FloatVector = np.array([0, 0, 1]), -) -> Molecule: - """Rotate molecule around an axis by given angle.""" -``` - Usage: ```python @@ -277,41 +180,60 @@ rotated = rotate_molecule(mol, angle=np.pi) rotated = rotate_molecule(mol, np.pi/2, [0, 1, 0]) # TypeError ``` -## Design Philosophy +### Design Philosophy + +The module follows five core principles that guide its implementation: -1. **Type Safety** +1. **Type Safety**: Leverages Python's type system to catch errors at compile time - - Custom types for domain-specific concepts - - Generic types for flexible error handling - - Numpy-aware type hints + ```python + def rotate_molecule(molecule: Molecule, /, *, angle: float) -> Molecule: + """Type hints ensure correct usage at development time.""" + ``` -2. **Immutability** +2. **Immutability**: Prevents accidental state modifications - - Atoms are immutable (frozen dataclass) - - Molecule coordinates are immutable (tuple of atoms) - - Operations return new instances instead of modifying + ```python + @dataclass(frozen=True) # Makes instances immutable + class Atom: + symbol: AtomicSymbol + position: FloatVector -3. **Error Handling** + # Operations return new instances instead of modifying + new_mol = translate_molecule(mol, vector=[1.0, 0.0, 0.0]) + ``` - - Generic `Result` type for operations that might fail - - Descriptive error messages - - No silent failures +3. **Error Handling**: Makes failures explicit and informative -4. **API Design** + ```python + def parse_xyz_file(path: str) -> Result[Molecule]: + """Returns Result type to handle both success and failure cases.""" + try: + return Result(value=read_molecule(path)) + except FileNotFoundError: + return Result(value=None, success=False, + error_message="File not found") + ``` - - Clear parameter passing conventions - - Self-documenting function calls - - Sensible defaults - - Chainable operations +4. **Clean API**: Prioritizes clarity and prevents usage errors -5. **Performance** - - Vectorized operations using NumPy - - Efficient memory usage through immutability - - Minimal object copying + ```python + # Function definition enforces calling convention + def rotate_molecule(molecule: Molecule, /, *, angle: float, axis: FloatVector): + """/ makes 'molecule' positional-only + * makes remaining args keyword-only""" -## Examples + # In actual use, just call with keywords - clean and clear + rotated = rotate_molecule( + water_molecule, # positional argument + angle=np.pi/2, # keyword arguments + axis=[0, 1, 0] + ) + ``` -### Creating and Manipulating Molecules +### Examples + +#### Creating and Manipulating Molecules ```python # Create water molecule @@ -336,11 +258,11 @@ distances = rotated.calculate_distance_matrix() energy = rotated.nuclear_repulsion() ``` -## Casting Numpy Results +### Casting Numpy Results When working with numpy operations in a statically typed Python codebase, we often need to explicitly cast the results using `typing.cast()`. This is because numpy's type system and Python's static type hints don't always align perfectly. Numpy operations typically return `numpy.ndarray` with dynamic shape and dtype information that mypy cannot infer at compile time. For example, when we perform operations like `np.sum()` or matrix multiplication, the static type checker cannot automatically determine that the result matches our custom type aliases like `FloatVector` or `Float2D`. By using `cast()`, we provide an explicit guarantee to the type checker that the result conforms to our expected type, while maintaining runtime type safety through numpy's own type system. -### How `cast()` Works +#### How `cast()` Works It's important to understand that `typing.cast()` is purely a type checker instruction - it performs no runtime checking or conversion. Here's what that means in practice: @@ -363,3 +285,12 @@ def calculate_vector() -> FloatVector: ``` Think of `cast()` as a promise to the type checker: "I know this value will have the right type at runtime." It's the developer's responsibility to ensure this promise is kept. This is why we use it carefully and only in situations where we're certain about the types, like numpy operations where we know the shape and dtype of the result. + +## Source Code + +::: batistatemplate.hello +handler: python +options: +show_root_heading: true +show_source: true +members: - Atom - Molecule - UnitSystem - apply_to_coordinates - translate_molecule - rotate_molecule diff --git a/docs/batistatemplate/tests.md b/docs/batistatemplate/tests.md index 389e776..32dcae2 100644 --- a/docs/batistatemplate/tests.md +++ b/docs/batistatemplate/tests.md @@ -5,21 +5,19 @@ This guide covers principles and practices for testing scientific code, with a f ## Why Test Scientific Code? 1. **Correctness Verification** - - - Scientific code often implements complex mathematical formulas - - Small errors can propagate and lead to incorrect results - - Tests help verify mathematical and physical principles + - Scientific code often implements complex mathematical formulas + - Small errors can propagate and lead to incorrect results + - Tests help verify mathematical and physical principles 2. **Reproducibility** - - - Tests document expected behavior - - Help ensure consistent results across different environments - - Critical for scientific reproducibility + - Tests document expected behavior + - Help ensure consistent results across different environments + - Critical for scientific reproducibility 3. **Code Evolution** - - Safe refactoring of performance-critical code - - Confidence when updating dependencies - - Protection against regression + - Safe refactoring of performance-critical code + - Confidence when updating dependencies + - Protection against regression ## What to Test @@ -168,27 +166,24 @@ def test_rotation_special_angles(angle): ## Best Practices 1. **Test Organization** - - - Group related tests in classes - - Use fixtures for common setups - - Separate unit and integration tests + - Group related tests in classes + - Use fixtures for common setups + - Separate unit and integration tests 2. **Performance Considerations** - - - Keep unit tests fast - - Use smaller datasets for tests - - Mark slow tests appropriately + - Keep unit tests fast + - Use smaller datasets for tests + - Mark slow tests appropriately 3. **Documentation** - - - Document test assumptions - - Explain physical/mathematical meaning - - Reference equations or papers + - Document test assumptions + - Explain physical/mathematical meaning + - Reference equations or papers 4. **Continuous Integration** - - Run tests on multiple platforms - - Test with different dependency versions - - Include performance benchmarks + - Run tests on multiple platforms + - Test with different dependency versions + - Include performance benchmarks ## Common Pitfalls @@ -231,21 +226,19 @@ def test_rotation_special_angles(angle): While testing is crucial, some cases might not require tests: 1. **Visualization Code** - - - Plot formatting - - Color schemes - - Interactive features + - Plot formatting + - Color schemes + - Interactive features 2. **Configuration** - - - Static configuration files - - Environment settings - - Documentation + - Static configuration files + - Environment settings + - Documentation 3. **Prototype Code** - - Exploratory analysis - - One-off scripts - - Temporary debugging code + - Exploratory analysis + - One-off scripts + - Temporary debugging code ## Conclusion diff --git a/docs/index.md b/docs/index.md index af4e050..1971fae 100644 --- a/docs/index.md +++ b/docs/index.md @@ -98,6 +98,34 @@ uv sync This ensures your virtual environment exactly matches the dependencies specified in the lock file, removing any packages you don't need and installing any that are missing. +### Writing Documentation + +This project follows a structured approach to documentation. Each module should have its own markdown file in the `docs/batistatemplate/` directory. Documentation files might include: + +1. **Overview**: A brief description of the module's purpose and key features +2. **Concepts**: Explanation of important concepts and design decisions +3. **Examples**: Code examples showing common usage patterns +4. **Source Code**: Auto-generated documentation from source code annotations + +#### Source Code Documentation + +Each documentation file should end with a Source Code section that imports and displays the module's classes and functions. Use the following structure: + +```markdown +## Source Code + +::: batistatemplate.module_name + handler: python + options: + show_root_heading: true + show_source: true + members: + - ClassName1 + - ClassName2 + - function_name1 + - function_name2 +``` + ## Getting Started For more detailed information about specific components and usage examples, please navigate through the documentation using the navigation menu. diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..f590a6b --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,14 @@ +html { + font-size: 16px; + } + + +h1 { + font-weight: 600 !important; + color: #333 !important; +} + +h2 { + font-weight: 600 !important; + color: #333 !important; +} diff --git a/mkdocs.yml b/mkdocs.yml index 0bba32f..bb64d43 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,6 +3,9 @@ theme: name: material features: - content.code.copy + +extra_css: +- stylesheets/extra.css plugins: - search diff --git a/scripts/README.md b/scripts/README.md index e69de29..548e096 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -0,0 +1,39 @@ +# Scripts Directory + +This directory contains user-facing Python scripts that demonstrate how to use the package. While the core functionality lives in `src/`, these scripts show practical examples of how to: + +- Call and combine package features +- Configure common use cases +- Customize package behavior + +## Available Scripts + +- `example-use.py`: Shows basic package usage with common configurations + +## Running Scripts + +Make sure you have the package installed: + +```bash +source .venv/bin/activate +uv pip install -e ".[dev]" +python scripts/example-use.py +``` + +## Creating New Scripts + +When adding new scripts: + +1. Follow the existing naming convention +2. Include docstrings and comments explaining the script's purpose +3. Add error handling and logging where appropriate +4. Document any command-line arguments or configuration options +5. Consider adding the script to the documentation with usage examples + +## Best Practices + +- Keep scripts focused on a single task or example +- Include proper error handling and user feedback +- Document dependencies and requirements +- Use the project's logging configuration +- Follow the project's code style guidelines diff --git a/src/batistatemplate/hello.py b/src/batistatemplate/hello.py index e4157d8..dbc7f4f 100644 --- a/src/batistatemplate/hello.py +++ b/src/batistatemplate/hello.py @@ -18,7 +18,7 @@ class UnitSystem(Enum): - """Enumeration of supported unit systems.""" + """Supported unit systems.""" ATOMIC = auto() # Atomic units (Bohr, Hartree) STANDARD = auto() # Standard units (Angstrom, eV) @@ -27,7 +27,7 @@ class UnitSystem(Enum): @dataclass(frozen=True) class Atom: - """Immutable dataclass representing an atom.""" + """Represents an atom.""" symbol: AtomicSymbol position: FloatVector @@ -36,7 +36,18 @@ class Atom: @classmethod def from_symbol(cls, symbol: AtomicSymbol, position: FloatVector, /) -> "Atom": - """Create an Atom instance from a symbol and position.""" + """Creates an Atom instance from a chemical symbol and position. + + Args: + symbol: The chemical symbol of the atom (e.g., "H", "He"). + position: The 3D coordinates of the atom as a FloatVector. + + Returns: + An Atom instance. + + Raises: + ValueError: If the chemical symbol is unknown. + """ atomic_data = { "H": (1, 1.008), "He": (2, 4.003), @@ -62,7 +73,11 @@ def from_symbol(cls, symbol: AtomicSymbol, position: FloatVector, /) -> "Atom": @dataclass class Result(Generic[T]): - """Generic result container with optional error handling.""" + """A generic container for function results, including error handling. + + Type parameter ``T`` represents the type of the ``value`` attribute, + i.e., the type of the successful result. + """ value: T success: bool = True @@ -70,38 +85,69 @@ class Result(Generic[T]): class Molecule: - """Class representing a molecular system.""" + """Represents a molecular system.""" def __init__(self, atoms: Sequence[Atom], unit_system: UnitSystem = UnitSystem.ATOMIC) -> None: + """Initializes a Molecule instance. + + Args: + atoms: A sequence of Atom objects representing the atoms in the molecule. + unit_system: The unit system to use for the molecule (default: UnitSystem.ATOMIC). + """ self.atoms = tuple(atoms) # Make immutable self.unit_system = unit_system logger.info(f"Created molecule with {len(atoms)} atoms using {unit_system.name} unit system") @property def coordinates(self) -> AtomicCoordinates: - """Get atomic coordinates as numpy array.""" + """Atomic coordinates as a numpy array. + + Returns: + A numpy array of shape (n_atoms, 3) containing the atomic coordinates. + """ return np.array([atom.position for atom in self.atoms]) @property def atomic_numbers(self) -> AtomicNumbers: - """Get atomic numbers as numpy array.""" + """Atomic numbers as a numpy array. + + Returns: + A numpy array of shape (n_atoms,) containing the atomic numbers. + """ return np.array([atom.atomic_number for atom in self.atoms], dtype=np.int32) @property def center_of_mass(self) -> FloatVector: - """Calculate center of mass of the molecule.""" + """Calculates the center of mass of the molecule. + + Returns: + A FloatVector representing the center of mass coordinates. + """ masses = np.array([atom.mass for atom in self.atoms]) weighted_coords = self.coordinates * masses[:, np.newaxis] return cast(FloatVector, np.sum(weighted_coords, axis=0) / np.sum(masses)) def calculate_distance_matrix(self) -> Float2D: - """Calculate matrix of interatomic distances.""" + """Calculates the matrix of interatomic distances. + + Returns: + A Float2D numpy array of shape (n_atoms, n_atoms) where each element (i, j) + is the distance between atom i and atom j. + """ coords = self.coordinates diff = coords[:, np.newaxis, :] - coords[np.newaxis, :, :] return cast(Float2D, np.sqrt(np.sum(diff * diff, axis=-1))) def nuclear_repulsion(self) -> Result[Energy]: - """Calculate nuclear repulsion energy with error handling.""" + """Calculates the nuclear repulsion energy. + + This method calculates the Coulomb repulsion energy between all pairs of atomic nuclei in the molecule. + It includes error handling and returns a Result object. + + Returns: + A Result object containing the nuclear repulsion energy (in atomic units) + if successful, or an error message if the calculation fails. + """ try: distance_matrix = self.calculate_distance_matrix() atomic_numbers = self.atomic_numbers @@ -123,7 +169,18 @@ def nuclear_repulsion(self) -> Result[Energy]: def apply_to_coordinates(func: Callable[[AtomicCoordinates], AtomicCoordinates], molecule: Molecule, /) -> Molecule: - """Higher-order function to transform molecular coordinates.""" + """Applies a coordinate transformation function to a molecule. + + This is a higher-order function that takes a function which transforms atomic coordinates + and applies it to the coordinates of the given molecule. + + Args: + func: A callable that takes AtomicCoordinates and returns transformed AtomicCoordinates. + molecule: The Molecule object to be transformed. + + Returns: + A new Molecule object with transformed coordinates. + """ new_coords = func(molecule.coordinates) new_atoms = [ Atom(a.symbol, np.array(pos, dtype=np.float64), a.atomic_number, a.mass) @@ -138,14 +195,14 @@ def translate_molecule( *, vector: FloatVector, ) -> Molecule: - """Translate molecule by a vector. + """Translates a molecule by a given vector. Args: - molecule: The molecule to translate - vector: Translation vector (x, y, z) + molecule: The molecule to translate. + vector: The translation vector (x, y, z). Returns: - New molecule with translated coordinates + A new Molecule object that is translated by the given vector. """ logger.debug(f"Translating molecule by vector {vector}") return apply_to_coordinates(lambda coords: coords + vector, molecule) @@ -158,15 +215,15 @@ def rotate_molecule( angle: float, axis: FloatVector | None = None, ) -> Molecule: - """Rotate molecule around an axis by given angle. + """Rotates a molecule around an axis by a given angle. Args: - molecule: The molecule to rotate - angle: Rotation angle in radians (positive = counterclockwise) - axis: Rotation axis vector (default: [0, 0, 1], i.e., z-axis) + molecule: The molecule to rotate. + angle: The rotation angle in radians (positive for counterclockwise rotation). + axis: The rotation axis vector. Defaults to the z-axis ([0, 0, 1]). Returns: - Rotated molecule + A new Molecule object that is rotated by the given angle around the given axis. """ if axis is None: axis = np.array([0, 0, 1]) @@ -174,7 +231,15 @@ def rotate_molecule( logger.debug(f"Rotating molecule by {angle:.2f} radians around axis {axis}") def rotation_matrix(theta: float, axis_vec: FloatVector) -> Float2D: - """Generate 3D rotation matrix using Rodrigues' rotation formula.""" + """Generates a 3D rotation matrix using Rodrigues' rotation formula. + + Args: + theta: The rotation angle in radians (positive for counterclockwise). + axis_vec: The rotation axis vector. + + Returns: + A 3x3 Float2D numpy array representing the rotation matrix. + """ # Normalize the axis vector axis_vec = axis_vec / np.linalg.norm(axis_vec) diff --git a/src/batistatemplate/typing/examples.py b/src/batistatemplate/typing/examples.py index 1513b35..0a48f36 100644 --- a/src/batistatemplate/typing/examples.py +++ b/src/batistatemplate/typing/examples.py @@ -13,7 +13,6 @@ type Float2D = NDArray[np.float64] # Shape: (n, m) type Complex2D = NDArray[np.complex128] # Shape: (n, m) -# Molecular structure types (keeping these as they're fundamental) type AtomicCoordinates = NDArray[np.float64] # Shape: (n_atoms, 3) type AtomicNumbers = NDArray[np.int32] # Shape: (n_atoms,) diff --git a/src/batistatemplate/utils/io.py b/src/batistatemplate/utils/io.py index 5d348df..5e57e3d 100644 --- a/src/batistatemplate/utils/io.py +++ b/src/batistatemplate/utils/io.py @@ -2,4 +2,15 @@ def load_dataset(ds_path: Path) -> None: + """Loads a dataset from the specified path. + + This function is intended to load a dataset from the given file path. + Currently, it is not implemented and always raises a NotImplementedError. + + Args: + ds_path: The path to the dataset file. + + Raises: + NotImplementedError: Always raised as the function is not yet implemented. + """ raise NotImplementedError("Not implemented") diff --git a/src/batistatemplate/utils/logging_config.py b/src/batistatemplate/utils/logging_config.py index e6df57e..392d45b 100644 --- a/src/batistatemplate/utils/logging_config.py +++ b/src/batistatemplate/utils/logging_config.py @@ -7,26 +7,55 @@ def setup_logging() -> None: - """Setup logging configuration from pyproject.toml with environment variable override""" + """Configures logging for the application. + + Reads the logging configuration from the 'pyproject.toml' file + and applies it using `logging.config.dictConfig`. + + The log level can be overridden by setting the + 'BATISTATEMPLATE_LOG_LEVEL' environment variable. If the environment + variable is not set or set to an invalid level, it defaults to 'INFO'. + + Raises: + FileNotFoundError: If 'pyproject.toml' is not found. + tomli.TOMLDecodeError: If 'pyproject.toml' is not a valid TOML file. + KeyError: If the 'tool.logging' section is missing in 'pyproject.toml'. + """ + # Construct the path to pyproject.toml, assuming it's 3 levels up from this file. pyproject_path = Path(__file__).parents[3] / "pyproject.toml" - with open(pyproject_path, "rb") as f: - config = tomli.load(f) + try: + with open(pyproject_path, "rb") as f: + config = tomli.load(f) + except FileNotFoundError as e: + raise FileNotFoundError(f"pyproject.toml not found at {pyproject_path}: {e}") from e + except tomli.TOMLDecodeError as e: + raise tomli.TOMLDecodeError(f"Failed to decode pyproject.toml: {e}") from e - # Get log level from environment variable, default to INFO if not set - log_level = os.getenv("BATISTATEMPLATE_LOG_LEVEL", "INFO").upper() + # Determine the log level: + # 1. Check for BATISTATEMPLATE_LOG_LEVEL environment variable. + # 2. Default to 'INFO' if not set or invalid. + log_level_env = os.getenv("BATISTATEMPLATE_LOG_LEVEL", "INFO").upper() - # Validate the log level valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} - if log_level not in valid_levels: - print(f"Invalid log level {log_level}, defaulting to INFO") + if log_level_env not in valid_levels: + print(f"Invalid log level '{log_level_env}' from environment, defaulting to INFO") log_level = "INFO" + else: + log_level = log_level_env + + try: + # Extract logging configuration from pyproject.toml + logging_config = config["tool"]["logging"] + except KeyError as e: + raise KeyError(f"'tool.logging' section not found in pyproject.toml: {e}") from e - # Override the log level from config - logging_config = config["tool"]["logging"] + # Override the log level specified in pyproject.toml with the determined log level. logging_config["loggers"]["batistatemplate"]["level"] = log_level + # Configure logging using the dictionary configuration. logging.config.dictConfig(logging_config) +# Get the logger for the 'batistatemplate' application. logger = logging.getLogger("batistatemplate")