Skip to content
Snippets Groups Projects
Commit 16713fed authored by pinyili2's avatar pinyili2
Browse files

add the sphinx doc builder

parent 8167389b
No related branches found
No related tags found
No related merge requests found
#!/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()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment