diff --git a/build_arbd_docs.py b/build_arbd_docs.py new file mode 100644 index 0000000000000000000000000000000000000000..8f1f0a34cc9bc70bde99d559965a5059ec556fc1 --- /dev/null +++ b/build_arbd_docs.py @@ -0,0 +1,644 @@ +#!/usr/bin/env python3 +""" +Documentation synchronization and build script for arbdmodel. + +This script: +1. Synchronizes content from multiple repositories: + - API code from arbdmodel-simple + - Tutorials from arbdmodel-introduction +2. Generates Jupyter Book documentation structure +3. Builds the final documentation site +""" + +import os +import sys +import shutil +import subprocess +from pathlib import Path +import argparse +import yaml +import glob +import re +import importlib + +# Path configuration - update these to match your local setup +ARBDMODEL_PATH = Path("/Users/pinyili/Documents/research/arbdmodel-simple") +TUTORIALS_PATH = Path("/Users/pinyili/Documents/research/arbdmodel-introduction") +DOCS_DIR = Path("arbdmodel-docs") +BUILD_DIR = DOCS_DIR / "_build" + +# Define the structure of documentation sections and modules +DOCUMENTATION_STRUCTURE = { + "Core": [ + "arbdmodel.core_objects", + "arbdmodel.model", + "arbdmodel.sim_config", + ], + "Polymer Modeling": [ + "arbdmodel.polymer", + "arbdmodel.fjc_polymer_model", + "arbdmodel.hps_polymer_model", + "arbdmodel.kh_polymer_model", + "arbdmodel.mpipi_polymer", + "arbdmodel.onck_polymer_model", + "arbdmodel.sali_polymer_model", + "arbdmodel.ssdna_two_bead", + ], + "RigidBody Models": [ + "arbdmodel.structure_from_pdb", + "arbdmodel.structure_rigidbody", + "arbdmodel.mesh_process_volume", + "arbdmodel.mesh_process_surface", + "arbdmodel.mesh_rigidbody", + "arbdmodel.simplearbd", + ], + "Interaction Potentials": [ + "arbdmodel.interactions", + "arbdmodel.ibi", + ], + "Simulation Engines": [ + "arbdmodel.engine", + "arbdmodel.parmed_bd", + ], + "Shape-Based Models": [ + "arbdmodel.shape_cg", + ], + "Utilities": [ + "arbdmodel.coords", + "arbdmodel.grid", + "arbdmodel.logger", + "arbdmodel.version", + "arbdmodel.binary_manager", + ] +} + +# Tutorial notebook files relative to TUTORIALS_PATH +TUTORIAL_NOTEBOOKS = [ + "1-basics.ipynb", + "2-polymer-objects.ipynb", + "3-iterative-boltzmann-inversion/3-ibi.ipynb", + "4-rigid-bodies/4-rigid-bodies.ipynb" +] + +def setup_environment(): + """Ensure the Python path includes both repositories.""" + # Make arbdmodel available for import + sys.path.insert(0, str(ARBDMODEL_PATH)) + sys.path.insert(0, str(TUTORIALS_PATH)) + + # Set Python path environment variable for subprocess calls + os.environ["PYTHONPATH"] = f"{str(ARBDMODEL_PATH)}:{str(TUTORIALS_PATH)}:{os.environ.get('PYTHONPATH', '')}" + + # Verify the import works + try: + import arbdmodel + print(f"Successfully imported arbdmodel from {ARBDMODEL_PATH}") + except ImportError as e: + print(f"Error importing arbdmodel: {e}") + print("Please check your path configuration and verify arbdmodel is properly installed.") + sys.exit(1) + +def sync_tutorial_files(): + """Copy tutorial notebooks and supporting files from the tutorials repository.""" + print("\nSynchronizing tutorial files...") + + # Create the tutorials directory + tutorials_output_dir = DOCS_DIR / "tutorials" + tutorials_output_dir.mkdir(exist_ok=True, parents=True) + + # Copy main tutorial notebooks + for notebook_path in TUTORIAL_NOTEBOOKS: + src_path = TUTORIALS_PATH / notebook_path + + # Create target directory if notebook is in a subdirectory + target_dir = tutorials_output_dir + if '/' in notebook_path: + subdir = notebook_path.split('/')[0] + target_dir = tutorials_output_dir / subdir + target_dir.mkdir(exist_ok=True) + + # Copy the notebook + target_path = target_dir / Path(notebook_path).name + + if src_path.exists(): + shutil.copy2(src_path, target_path) + print(f"Copied: {src_path} -> {target_path}") + else: + print(f"Warning: Tutorial notebook not found: {src_path}") + + # Copy supporting files for tutorials + # This recursively copies directories that contain resources needed by the notebooks + resource_dirs = [ + "4-rigid-bodies/grids", + "4-rigid-bodies/0-initial" + ] + + for resource_dir in resource_dirs: + src_dir = TUTORIALS_PATH / resource_dir + if src_dir.exists() and src_dir.is_dir(): + target_dir = tutorials_output_dir / resource_dir + if target_dir.exists(): + shutil.rmtree(target_dir) + shutil.copytree(src_dir, target_dir) + print(f"Copied directory: {src_dir} -> {target_dir}") + else: + print(f"Warning: Resource directory not found: {src_dir}") + + # Copy individual resource files needed by tutorials + resource_files = [ + "4-rigid-bodies/*.pdb", + "4-rigid-bodies/*.psf", + "4-rigid-bodies/*.tcl" + ] + + for pattern in resource_files: + for src_file in glob.glob(str(TUTORIALS_PATH / pattern)): + src_path = Path(src_file) + rel_path = src_path.relative_to(TUTORIALS_PATH) + target_path = tutorials_output_dir / rel_path + + # Make sure the target directory exists + target_path.parent.mkdir(exist_ok=True, parents=True) + + # Copy the file + shutil.copy2(src_path, target_path) + print(f"Copied resource: {src_path} -> {target_path}") + + print("Tutorial files synchronized.") + return tutorials_output_dir + +def generate_api_docs(): + """Generate API documentation markdown files.""" + print("\nGenerating API documentation...") + + # Create the API documentation directory + api_dir = DOCS_DIR / "api" + api_dir.mkdir(exist_ok=True, parents=True) + + # Process each module category + category_indices = {} + for category, modules in DOCUMENTATION_STRUCTURE.items(): + print(f"Generating documentation for category: {category}") + + # Create category directory + category_dir = api_dir / category.lower().replace(' ', '_') + category_dir.mkdir(exist_ok=True, parents=True) + + # Process modules in this category + module_files = [] + for module_name in modules: + try: + # Import the module + module = __import__(module_name, fromlist=[""]) + + # Create an API documentation file for this module + module_path = create_module_doc(module, module_name, category_dir) + if module_path: + module_files.append(module_path) + except ImportError as e: + print(f"Warning: Could not import module {module_name}: {e}") + except Exception as e: + print(f"Error processing module {module_name}: {e}") + + # Create a category index + if module_files: + index_path = create_category_index(category, module_files, category_dir) + category_indices[category] = str(index_path.relative_to(DOCS_DIR)) + + # Create main API index + api_index_path = create_api_index(api_dir, category_indices) + + print(f"API documentation generated at {api_dir}") + return category_indices, api_index_path + +def create_module_doc(module, module_name, output_dir): + """Create a markdown documentation file for a module.""" + # Extract the simple module name + simple_name = module_name.split('.')[-1] + + # Generate content + content = [ + f"# {simple_name}", + "", + ] + + # Add module docstring + if module.__doc__: + content.append(module.__doc__.strip()) + content.append("") + else: + content.append(f"Module documentation for `{module_name}`.") + content.append("") + + # Add version if available + if hasattr(module, "__version__"): + content.append(f"**Version:** {module.__version__}") + content.append("") + + # Add auto-documentation directive + content.extend([ + "```{eval-rst}", + f".. automodule:: {module_name}", + " :members:", + " :undoc-members:", + " :show-inheritance:", + "```", + "" + ]) + + # Write to file + output_path = output_dir / f"{simple_name}.md" + with open(output_path, 'w') as f: + f.write('\n'.join(content)) + + print(f" Created module doc: {output_path}") + return output_path + +def create_category_index(category, module_files, category_dir): + """Create an index page for a category of modules.""" + # Build index content + content = [ + f"# {category}", + "", + f"This section documents the {category.lower()} components of the arbdmodel package.", + "", + "## Modules", + "", + ] + + # Add links to each module + for module_path in sorted(module_files, key=lambda p: p.name): + # Extract the module name + module_name = module_path.stem + + # Create entry with relative path + rel_path = module_path.name + + # Try to add a brief description + try: + # Extract first line of docstring + with open(module_path, 'r') as f: + file_content = f.read() + first_line = re.search(r'# .*?\n\n(.*?)\n', file_content) + brief_desc = first_line.group(1) if first_line else "" + except: + brief_desc = "" + + content.append(f"- [{module_name}]({rel_path}): {brief_desc}") + + # Write index file + index_path = category_dir / "index.md" + with open(index_path, 'w') as f: + f.write('\n'.join(content)) + + print(f" Created category index: {index_path}") + return index_path + +def create_api_index(api_dir, category_indices): + """Create the main API index page.""" + content = [ + "# API Reference", + "", + "This section provides detailed documentation for the ARBD Model Python API.", + "", + "## Module Categories", + "", + ] + + # Add links to category indices + for category, path in sorted(category_indices.items()): + content.append(f"- [{category}]({path})") + + # Write index file + index_path = api_dir / "index.md" + with open(index_path, 'w') as f: + f.write('\n'.join(content)) + + print(f"Created API index: {index_path}") + return index_path + +def create_tutorials_index(tutorials_dir): + """Create an index page for tutorials.""" + content = [ + "# Tutorials", + "", + "This section contains tutorials and examples for working with the arbdmodel package.", + "", + "## Available Tutorials", + "", + ] + + # Find all notebook files in the tutorials directory + notebook_paths = [] + for root, _, files in os.walk(tutorials_dir): + root_path = Path(root) + for file in files: + if file.endswith('.ipynb'): + rel_path = root_path.relative_to(tutorials_dir) + if str(rel_path) == '.': + notebook_paths.append(file) + else: + notebook_paths.append(str(rel_path / file)) + + # Group notebooks by directory + grouped_notebooks = {} + for path in notebook_paths: + parts = str(path).split('/') + if len(parts) > 1: + # Notebook is in a subdirectory + subdir = parts[0] + if subdir not in grouped_notebooks: + grouped_notebooks[subdir] = [] + grouped_notebooks[subdir].append((parts[-1], path)) + else: + # Top-level notebook + if 'top' not in grouped_notebooks: + grouped_notebooks['top'] = [] + grouped_notebooks['top'].append((path, path)) + + # Add links to tutorials grouped by directory + for group, notebooks in sorted(grouped_notebooks.items()): + if group != 'top': + content.append(f"### {group.capitalize()}") + content.append("") + + for name, path in sorted(notebooks): + # Extract title from notebook name + title = name.replace('.ipynb', '').replace('-', ' ').title() + # If the notebook name starts with a number and dash, extract just the title part + if re.match(r'^\d+-', title): + title = ' '.join(title.split(' ')[1:]) + + content.append(f"- [{title}]({path})") + + content.append("") + + # Write index file + index_path = tutorials_dir / "index.md" + with open(index_path, 'w') as f: + f.write('\n'.join(content)) + + print(f"Created tutorials index: {index_path}") + return index_path + +def create_intro_page(): + """Create the introduction/landing page for the documentation.""" + content = [ + "# ARBD Model Documentation", + "", + "Welcome to the documentation for the ARBD Model package!", + "", + "## Overview", + "", + "ARBD Model is a comprehensive package for coarse-grained molecular modeling and simulation of biomolecular systems. It provides tools for creating and simulating a variety of molecular models, including polymer systems, protein structures, and rigid bodies.", + "", + "## Features", + "", + "- **Polymer Modeling**: Create and simulate various polymer models including flexible chain models, hydrophobicity-based models, and specialized DNA models.", + "- **Rigid Body Simulation**: Simulate molecular structures as rigid bodies with accurate hydrodynamic properties.", + "- **Shape-Based Models**: Generate coarse-grained models based on the shape of molecular structures.", + "- **Interaction Potentials**: Wide range of interaction potentials, including the ability to develop custom potentials using Iterative Boltzmann Inversion (IBI).", + "- **Simulation Engines**: Multiple simulation engine options for different modeling needs.", + "", + "## Getting Started", + "", + "To get started, check out the [Tutorials](tutorials/index) section for examples and walkthroughs.", + "", + "## Python API Reference", + "", + "For detailed documentation of the Python API, see the [API Reference](api/index) section.", + "", + ] + + intro_path = DOCS_DIR / "intro.md" + with open(intro_path, 'w') as f: + f.write('\n'.join(content)) + + print(f"Created introduction page: {intro_path}") + return intro_path + +def create_jupyter_book_files(category_indices): + """Create the _toc.yml and _config.yml files for Jupyter Book.""" + # Table of Contents + toc = { + "format": "jb-book", + "root": "intro", + "parts": [ + { + "caption": "Getting Started", + "chapters": [ + # Removed "intro" as chapter since it's already the root + {"file": "tutorials/index"} + ] + }, + { + "caption": "API Reference", + "chapters": [ + {"file": "api/index"}, + ] + } + ] + } + + # Add API categories to the TOC + api_part = toc["parts"][1] + for category, path in sorted(category_indices.items()): + api_part["chapters"].append({"file": path}) + + # Write TOC file + toc_path = DOCS_DIR / "_toc.yml" + with open(toc_path, 'w') as f: + yaml.dump(toc, f, sort_keys=False) + + # Configuration + config = { + "title": "ARBD Model Documentation", + "author": "ARBD Model Team", + "logo": "", + "execute": { + "execute_notebooks": "auto" + }, + "repository": { + "url": "https://github.com/username/arbdmodel-docs", + "path_to_book": "docs", + "branch": "main" + }, + "html": { + "use_issues_button": True, + "use_repository_button": True, + "use_edit_page_button": True + }, + # Prevent duplicate registrations of file extensions + "parse": { + "myst_enable_extensions": [ + "colon_fence", + "deflist", + "dollarmath", + "substitution" + ], + "myst_heading_anchors": 3 + }, + "sphinx": { + "extra_extensions": [ + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.napoleon", + "sphinx_inline_tabs", + "sphinx_proof", + "sphinx_examples", + # Removed myst_parser as it's already included by jupyter-book + "sphinx.ext.mathjax", + "hoverxref.extension" + ], + "config": { + "autodoc_typehints": "description", + "html_theme_options": { + "show_navbar_depth": 2 + }, + "add_module_names": False, + # Moved myst configurations to the parse section to avoid conflicts + "intersphinx_mapping": { + "python": ["https://docs.python.org/3", None], + "numpy": ["https://numpy.org/doc/stable", None], + "scipy": ["https://docs.scipy.org/doc/scipy", None] + }, + "hoverxref_intersphinx": ["python", "numpy", "scipy"] + } + } + } + + # Write configuration file + config_path = DOCS_DIR / "_config.yml" + with open(config_path, 'w') as f: + yaml.dump(config, f, sort_keys=False) + + print(f"Created Jupyter Book files: {toc_path}, {config_path}") + +def build_docs(): + """Build the documentation using Jupyter Book.""" + try: + print("\nBuilding documentation...") + result = subprocess.run(['jupyter-book', 'build', str(DOCS_DIR)], + check=True, capture_output=True, text=True) + print(result.stdout) + print(f"\nDocumentation successfully built. You can view it at: {BUILD_DIR}/html/index.html") + return True + except subprocess.CalledProcessError as e: + print(f"Error building documentation: {e}") + print(e.stdout) + print(e.stderr) + return False + except FileNotFoundError: + print("Could not find jupyter-book. Make sure it is installed and in your PATH.") + print("Install with: pip install jupyter-book") + return False + +def check_dependencies(): + """Check if required packages are installed.""" + required_packages = [ + 'jupyter-book', + 'matplotlib', + 'numpy', + 'sphinx-inline-tabs', + 'sphinx-examples', + 'sphinx-proof', + 'sphinx-hoverxref' + ] + + missing_packages = [] + for package in required_packages: + try: + # Try different import strategies + package_name = package.replace('-', '_') + + # Method 1: Try direct import + try: + module_name = package_name.split('.')[0] + __import__(module_name) + continue + except ImportError: + pass + + # Method 2: Try using importlib + try: + importlib.import_module(package_name) + continue + except ImportError: + pass + + # Method 3: Check if package is installed using subprocess + try: + result = subprocess.run([sys.executable, '-m', 'pip', 'show', package], + capture_output=True, text=True) + if result.returncode == 0: + continue + except Exception: + pass + + # If all methods fail, consider the package missing + missing_packages.append(package) + + except Exception as e: + print(f"Error checking for {package}: {e}") + missing_packages.append(package) + + if missing_packages: + print("The following required packages are missing:") + for package in missing_packages: + print(f" - {package}") + print("\nPlease install them using:") + print(f"pip install {' '.join(missing_packages)}") + + response = input("\nDo you want to continue anyway? (y/n): ") + if response.lower() not in ('y', 'yes'): + sys.exit(1) + +def main(): + """Main function to synchronize content and build documentation.""" + parser = argparse.ArgumentParser(description="Sync and build ARBD Model documentation") + parser.add_argument('--no-sync', action='store_true', help='Skip synchronization of content') + parser.add_argument('--no-build', action='store_true', help='Skip building the documentation') + parser.add_argument('--clean', action='store_true', help='Clean existing documentation directory') + args = parser.parse_args() + + # Check dependencies + check_dependencies() + + # Set up environment + setup_environment() + + # Clean documentation directory if requested + if args.clean and DOCS_DIR.exists(): + print(f"Cleaning documentation directory: {DOCS_DIR}") + shutil.rmtree(DOCS_DIR) + + # Create docs directory if it doesn't exist + DOCS_DIR.mkdir(exist_ok=True, parents=True) + + if not args.no_sync: + # Synchronize content from repositories + tutorials_dir = sync_tutorial_files() + + # Create tutorials index + create_tutorials_index(tutorials_dir) + + # Generate API documentation + category_indices, api_index = generate_api_docs() + + # Create intro page + create_intro_page() + + # Create Jupyter Book files + create_jupyter_book_files(category_indices) + + # Build documentation if requested + if not args.no_build: + build_docs() + else: + print("\nSkipping documentation build. To build later, run:") + print(f"jupyter-book build {DOCS_DIR}") + + print("\nDocumentation process completed.") + +if __name__ == "__main__": + main()