A Python package for analyzing resonant angle libration patterns in astronomical data using Large Language Models (LLMs). The package supports multiple LLM providers including OpenAI, Anthropic, OpenRouter, and local models via Ollama.
Note that the author has actively used Claude Code and OpenAI Codex to build this tool.
- 🔍 Automated Analysis: AI-powered detection of libration patterns in resonant angle plots
- 🌐 Multiple LLM Providers: Support for OpenAI, Anthropic, OpenRouter, and Ollama
- 📊 Plot Classification: Categorizes resonance behavior as pure, transient, or non-resonant
- 🧪 Easy Integration: Simple Python API for astronomy research workflows
- ⚙️ Configurable: Environment-based configuration for different providers and models
- 🔒 Type Safety: Full type hints and comprehensive error handling
pip install llm-librationAll LLM providers are now included by default with the installation:
- OpenAI (via
langchain-openai) - Anthropic (via
langchain-anthropic) - OpenRouter (via
langchain-openai) - Ollama (via
ollama+langchain-community)
No additional packages are required!
# Create a plot from CSV data
llm-libration plot input/463.csv
# Analyze an image with OpenAI (default)
llm-libration run input/demo.png
# Test all providers
llm-libration run input/demo.png --provider allfrom llm_libration import LibrationAnalyzer
# Initialize analyzer (uses OpenAI by default)
analyzer = LibrationAnalyzer()
# Analyze an image - returns detailed structured result
result = analyzer.analyze_image("path/to/resonance_plot.png")
print(f"Status: {result.status}") # Output: resonant
print(f"Subtype: {result.subtype}") # Output: apocentric librationfrom llm_libration import LibrationAnalyzer
# Use Anthropic Claude
analyzer = LibrationAnalyzer(provider="anthropic")
# Use OpenRouter
analyzer = LibrationAnalyzer(provider="openrouter")
# Use local Ollama model
analyzer = LibrationAnalyzer(provider="ollama")
# Custom model for any provider
analyzer = LibrationAnalyzer(
provider="anthropic",
model_name="claude-sonnet-4"
)The package uses environment variables for configuration. Create a .env file in your project root:
LLM_PROVIDER=openai
OPENAI_API_KEY=your_openai_api_key_here
OPENAI_MODEL_NAME=openai/gpt-4.1 # Optional, this is the defaultLLM_PROVIDER=anthropic
ANTHROPIC_API_KEY=your_anthropic_api_key_here
ANTHROPIC_MODEL_NAME=claude-sonnet-4 # Optional, this is the defaultLLM_PROVIDER=openrouter
OPENROUTER_API_KEY=your_openrouter_api_key_here
OPENROUTER_MODEL_NAME=anthropic/claude-sonnet-4 # Optional, this is the default
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 # Optional, this is the defaultLLM_PROVIDER=ollama
OLLAMA_BASE_URL=http://localhost:11434 # Optional, this is the default
OLLAMA_MODEL_NAME=qwen2.5vl:7b # Optional, this is the defaultAvailable Ollama Vision Models:
qwen2.5vl:7b(recommended, best performance for libration vs circulation classification)gemma3:4b(good general vision capabilities, but may struggle with circulation patterns)llama3.2-vision(good balance of performance and accuracy)llava(lightweight, good for development)gemma2:2b-vision(compact model)
Note: The package uses the ollama Python package for optimal vision support with automatic fallback handling.
# Custom prompt template (optional)
PROMPT_TEMPLATE="Your custom analysis prompt here..."
# Binary-only template used by --simplified benchmarks
PROMPT_TEMPLATE_SIMPLIFIED="Treat any bounded oscillation (even transient segments) as resonant; only pure diagonal/chaotic plots are non-resonant. Return exactly {\"status\": \"resonant\"} or {\"status\": \"non-resonant\"} on one line."PROMPT_TEMPLATE_SIMPLIFIED ships with a default binary classifier prompt in .env.dist and is automatically selected when you pass --simplified to the benchmark command unless you override --prompt-env-var.
-
Copy the example configuration:
cp .env.dist .env
-
Edit
.envwith your credentials: Choose your preferred provider and set the appropriate API key. -
For Ollama users: Make sure Ollama is running locally:
ollama serve ollama pull qwen2.5vl:7b # or your preferred vision model
After installation, you can use llm-libration from the command line:
# Basic plot creation with default parameters (single file)
llm-libration plot input/463.csv
# Process all CSV files in a folder recursively
llm-libration plot input/test
# Custom plot with specific columns and output (single file only)
llm-libration plot data.csv --x-column time --y-column resonance_angle --output-file my_plot.png
# Custom y-axis range (works for both files and folders)
llm-libration plot data.csv --y-min -3.14 --y-max 9.42Plot Command Options:
INPUT_PATH: Path to CSV file or folder containing CSV files (required)--x-column: Column for x-axis data (default:times)--y-column: Column for y-axis data (default:angle)--output-file: Output PNG path (only for single file input, ignored for folders)--y-min: Minimum y-axis value (default:0)--y-max: Maximum y-axis value (default:2π)
Folder Processing: When a folder is provided as input, the command will:
- Recursively find all
.csvfiles in the folder and subfolders - Create plots for each CSV file found
- Save PNG files in the same directories as their corresponding CSV files
- Display progress and summary information
# Analyze with default provider (OpenAI)
llm-libration run input/demo.png
# Analyze with specific provider
llm-libration run input/demo.png --provider anthropic
# Analyze multiple images
llm-libration run input/demo.png input/463.png --provider openai
# Test all providers at once
llm-libration run input/demo.png --provider all
# Use custom model
llm-libration run input/demo.png --provider anthropic --model claude-sonnet-4
# Use an alternate prompt template
llm-libration run input/demo.png --prompt-env-var PROMPT_TEMPLATE_SIMPLIFIEDRun Command Options:
IMAGE_FILES: One or more image paths (required)--provider: LLM provider (openai,anthropic,openrouter,ollama,all) (default:openai)--model: Custom model name (optional)--prompt-env-var: Environment variable holding the prompt template (default:PROMPT_TEMPLATE)--simplified: Force binary resonant/non-resonant classification (defaults prompt toPROMPT_TEMPLATE_SIMPLIFIEDwhen not overridden)
Evaluate LLM performance on categorized datasets:
# Run benchmark with default provider (OpenAI)
llm-libration benchmark input/benchmark
# Run benchmark with specific provider
llm-libration benchmark input/benchmark --provider anthropic
# Run benchmark with custom model
llm-libration benchmark input/benchmark --provider openai --model gpt-4-vision-previewBenchmark Command Options:
BENCHMARK_DIR: Path to directory with categorized subdirectories (required)--provider: LLM provider (openai,anthropic,openrouter,ollama) (default:openai)--model: Custom model name (optional)--prompt-env-var: Environment variable that stores the prompt template (default:PROMPT_TEMPLATE)--simplified: Collapse transient+libration into a single resonant class (binary resonant vs non-resonant)
The --simplified flag is useful for coarse benchmarking where any bounded oscillation (pure or transient) should count as resonant. When it is enabled, libration and transient folders become resonant, everything else becomes non-resonant, and the CLI will automatically use PROMPT_TEMPLATE_SIMPLIFIED unless you override --prompt-env-var.
Benchmark Directory Structure: The benchmark directory should contain categorized subdirectories:
libration/- Images showing resonant (libration) behaviorcirculation/ornon-resonant/- Images showing non-resonant (circulation) behaviortransient/- Images showing transient behavior (mapped to controversial)controversial/- Images that are difficult to classify
Example simplified benchmark run:
llm-libration benchmark benchmark/test --simplified --prompt-env-var PROMPT_TEMPLATE_SIMPLIFIEDThe command will:
- Recursively find all PNG files in these directories
- Analyze each image and compare with expected results
- Calculate classification metrics (Accuracy, Precision, Recall, F1 Score)
- Save detailed results in CSV and JSON formats
- Display comprehensive performance summary
class LibrationAnalyzer:
def __init__(self, model_name: str = None, provider: str = None):
"""
Initialize the analyzer.
Args:
model_name: Override the default model name
provider: Override the default provider (openai, anthropic, openrouter, ollama)
"""
def analyze_image(self, image_path: Union[str, Path], prompt: Optional[str] = None) -> LibrationAnalysisResult:
"""
Analyze a resonance plot image and return detailed structured result.
Args:
image_path: Path to the image file
prompt: Optional custom prompt template override
Returns:
LibrationAnalysisResult with status and subtype information
"""
def get_resonance_type(self, image_path: Union[str, Path], prompt: Optional[str] = None) -> ResonanceType:
"""
Analyze a resonance plot image and return the ResonanceType enum.
Args:
image_path: Path to the image file
prompt: Optional custom prompt template override
Returns:
ResonanceType enum (RESONANT, NON_RESONANT, TRANSIENT, or CONTROVERSIAL)
"""from llm_libration.data.plot import create_plot, create_plots_from_input
# Single file plotting
def create_plot(
input_file: Union[str, Path],
x_column: str = 'times',
y_column: str = 'angle',
output_file: Optional[Union[str, Path]] = None,
y_min: float = 0,
y_max: float = 2 * np.pi
) -> str:
"""Create a plot from a single CSV file."""
# Unified plotting interface (files or folders)
def create_plots_from_input(
input_path: Union[str, Path],
x_column: str = 'times',
y_column: str = 'angle',
output_file: Optional[Union[str, Path]] = None,
y_min: float = 0,
y_max: float = 2 * np.pi
) -> Union[str, List[str]]:
"""Create plot(s) from CSV file(s). Returns single path for files, list for folders."""from llm_libration.llm.schema import (
LibrationAnalysisResult,
ResonantSubtype,
NonResonantSubtype,
TransientSubtype
)
class LibrationAnalysisResult:
status: ResonanceType # ResonanceType enum (RESONANT, NON_RESONANT, TRANSIENT, or CONTROVERSIAL)
subtype: Union[ResonantSubtype, NonResonantSubtype, TransientSubtype, str]
# Union type: enum for known subtypes, string for controversial casesThe package provides specific enums for different categories of behavior:
ResonantSubtype (for status="resonant"):
CLEAR_LIBRATION= "clear libration"APOCENTRIC_LIBRATION= "apocentric libration"HIGH_AMPLITUDE_LIBRATION= "high amplitude libration"LONG_PERIOD_LIBRATION= "long period libration"NOISY_LIBRATION= "noisy libration"DOUBLE_LIBRATION= "double libration"
NonResonantSubtype (for status="non-resonant"):
CIRCULATION= "circulation"SPARSE_CIRCULATION= "sparse circulation"CHAOTIC_CIRCULATION= "chaotic circulation"MIXED_CIRCULATION= "mixed circulation"
TransientSubtype (for status="transient"):
NOISY_LIBRATION_TO_CIRCULATION= "noisy libration to circulation"APOCENTRIC_WITH_CIRCULATION= "apocentric libration with circulation phases"ALTERNATING= "alternating libration and circulation"
# Example usage
from llm_libration.types import ResonanceType
result = analyzer.analyze_image("plot.png")
# Type-safe access with enum
if result.status == ResonanceType.RESONANT:
if result.subtype == ResonantSubtype.APOCENTRIC_LIBRATION:
print("Detected apocentric libration pattern")
elif result.status == ResonanceType.TRANSIENT:
print("Detected transient behavior")
# String comparison still works for subtypes
if result.subtype == "circulation":
print("Simple circulation detected")from enum import Enum
class ResonanceType(Enum):
RESONANT = "resonant" # Pure libration detected
NON_RESONANT = "non-resonant" # Circulation detected
TRANSIENT = "transient" # Transient behavior between resonant and non-resonant
CONTROVERSIAL = "controversial" # Unclear or ambiguous behavior| Provider | Pros | Cons | Best For |
|---|---|---|---|
| OpenAI | Excellent vision capabilities, fast | Requires API key, paid service | Production use, high accuracy |
| Anthropic | Strong reasoning, good vision | Requires API key, paid service | Research, detailed analysis |
| OpenRouter | Access to many models, competitive pricing | Requires API key, paid service | Cost-effective access to multiple models |
| Ollama | Free, local, private, proper vision support | Requires local setup, model downloads | Development, privacy-sensitive work, offline use |
The package provides specific exceptions for different error scenarios:
from llm_libration.exceptions import (
ImageAnalysisError, # Image processing issues
LLMResponseError, # LLM response parsing issues
ConfigurationError # Missing or invalid configuration
)
try:
result = analyzer.analyze_image("plot.png")
print(f"Status: {result.status}, Subtype: {result.subtype}")
except ImageAnalysisError as e:
print(f"Image processing failed: {e}")
except LLMResponseError as e:
print(f"LLM response issue: {e}")
except ConfigurationError as e:
print(f"Configuration problem: {e}")To set up for development:
git clone https://github.com/your-username/llm-libration.git
cd llm-libration
pip install -e ".[dev]"Run tests:
pytestContributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.
If you use this package in your research, please cite:
@software{llm_libration,
title={LLM Libration: AI-Powered Analysis of Resonant Angle Libration Patterns},
author={Your Name},
year={2024},
url={https://github.com/your-username/llm-libration}
}