BREAKING CHANGE: Module paths have been reorganized for better maintainability. Backwards compatibility aliases with deprecation warnings are provided. New Structure: - core/ - Optimization runners (runner, intelligent_optimizer, etc.) - processors/ - Data processing - surrogates/ - Neural network surrogates - nx/ - NX/Nastran integration (solver, updater, session_manager) - study/ - Study management (creator, wizard, state, reset) - reporting/ - Reports and analysis (visualizer, report_generator) - config/ - Configuration management (manager, builder) - utils/ - Utilities (logger, auto_doc, etc.) - future/ - Research/experimental code Migration: - ~200 import changes across 125 files - All __init__.py files use lazy loading to avoid circular imports - Backwards compatibility layer supports old import paths with warnings - All existing functionality preserved To migrate existing code: OLD: from optimization_engine.nx_solver import NXSolver NEW: from optimization_engine.nx.solver import NXSolver OLD: from optimization_engine.runner import OptimizationRunner NEW: from optimization_engine.core.runner import OptimizationRunner 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
299 lines
9.8 KiB
Python
299 lines
9.8 KiB
Python
"""
|
|
Atomizer Structured Logging System - Phase 1.3
|
|
|
|
Provides consistent, production-ready logging across all optimization studies.
|
|
|
|
Usage:
|
|
from optimization_engine.utils.logger import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
logger.info("Starting optimization...")
|
|
logger.error("Simulation failed", exc_info=True)
|
|
|
|
# Study-specific logger with automatic file logging
|
|
logger = get_logger("drone_gimbal_arm", study_dir="studies/drone_gimbal_arm/2_results")
|
|
logger.trial_start(trial_number=5, design_vars={"thickness": 2.5})
|
|
logger.trial_complete(trial_number=5, objectives={"mass": 120, "freq": 155})
|
|
|
|
Features:
|
|
- Automatic file logging to study_dir/optimization.log
|
|
- Console output with color-coded levels (if supported)
|
|
- Structured trial logging for dashboard integration
|
|
- Log rotation (50MB max, 3 backups)
|
|
- No external dependencies (stdlib only)
|
|
"""
|
|
|
|
import logging
|
|
import sys
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
from typing import Optional, Dict, Any
|
|
from logging.handlers import RotatingFileHandler
|
|
|
|
|
|
# ANSI color codes for console output (Windows 10+ and Unix)
|
|
class LogColors:
|
|
"""ANSI color codes for console output."""
|
|
RESET = '\033[0m'
|
|
BOLD = '\033[1m'
|
|
|
|
# Levels
|
|
DEBUG = '\033[36m' # Cyan
|
|
INFO = '\033[32m' # Green
|
|
WARNING = '\033[33m' # Yellow
|
|
ERROR = '\033[31m' # Red
|
|
CRITICAL = '\033[35m' # Magenta
|
|
|
|
# Custom
|
|
TRIAL = '\033[94m' # Bright Blue
|
|
SUCCESS = '\033[92m' # Bright Green
|
|
|
|
|
|
class ColoredFormatter(logging.Formatter):
|
|
"""Formatter that adds color to console output."""
|
|
|
|
COLORS = {
|
|
logging.DEBUG: LogColors.DEBUG,
|
|
logging.INFO: LogColors.INFO,
|
|
logging.WARNING: LogColors.WARNING,
|
|
logging.ERROR: LogColors.ERROR,
|
|
logging.CRITICAL: LogColors.CRITICAL,
|
|
}
|
|
|
|
def __init__(self, fmt: str, use_colors: bool = True):
|
|
super().__init__(fmt)
|
|
self.use_colors = use_colors and self._supports_color()
|
|
|
|
def _supports_color(self) -> bool:
|
|
"""Check if terminal supports ANSI colors."""
|
|
# Windows 10+ supports ANSI
|
|
if sys.platform == 'win32':
|
|
try:
|
|
import ctypes
|
|
kernel32 = ctypes.windll.kernel32
|
|
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
|
return True
|
|
except:
|
|
return False
|
|
# Unix-like systems
|
|
return hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
|
|
|
|
def format(self, record: logging.LogRecord) -> str:
|
|
if self.use_colors:
|
|
levelname = record.levelname
|
|
color = self.COLORS.get(record.levelno, '')
|
|
record.levelname = f"{color}{levelname}{LogColors.RESET}"
|
|
|
|
return super().format(record)
|
|
|
|
|
|
class AtomizerLogger(logging.Logger):
|
|
"""Extended logger with trial-specific methods."""
|
|
|
|
def trial_start(self, trial_number: int, design_vars: Dict[str, float]):
|
|
"""Log trial start with design variables."""
|
|
self.info(f"{'='*60}")
|
|
self.info(f"Trial #{trial_number} START")
|
|
self.info(f"{'='*60}")
|
|
self.info("Design Variables:")
|
|
for name, value in design_vars.items():
|
|
if isinstance(value, float):
|
|
self.info(f" {name}: {value:.4f}")
|
|
else:
|
|
self.info(f" {name}: {value}")
|
|
|
|
def trial_complete(self, trial_number: int, objectives: Dict[str, float],
|
|
constraints: Optional[Dict[str, float]] = None,
|
|
feasible: bool = True):
|
|
"""Log trial completion with results."""
|
|
self.info(f"\nTrial #{trial_number} COMPLETE")
|
|
self.info("Objectives:")
|
|
for name, value in objectives.items():
|
|
if isinstance(value, float):
|
|
self.info(f" {name}: {value:.4f}")
|
|
else:
|
|
self.info(f" {name}: {value}")
|
|
|
|
if constraints:
|
|
self.info("Constraints:")
|
|
for name, value in constraints.items():
|
|
if isinstance(value, float):
|
|
self.info(f" {name}: {value:.4f}")
|
|
else:
|
|
self.info(f" {name}: {value}")
|
|
|
|
status = "[OK] Feasible" if feasible else "[WARNING] Infeasible"
|
|
self.info(f"{status}")
|
|
self.info(f"{'='*60}\n")
|
|
|
|
def trial_failed(self, trial_number: int, error: str):
|
|
"""Log trial failure."""
|
|
self.error(f"\nTrial #{trial_number} FAILED")
|
|
self.error(f"Error: {error}")
|
|
self.error(f"{'='*60}\n")
|
|
|
|
def study_start(self, study_name: str, n_trials: int, sampler: str):
|
|
"""Log study initialization."""
|
|
self.info("=" * 80)
|
|
self.info(f"OPTIMIZATION STUDY: {study_name}")
|
|
self.info("=" * 80)
|
|
self.info(f"Trials: {n_trials}")
|
|
self.info(f"Sampler: {sampler}")
|
|
self.info(f"Started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
self.info("=" * 80)
|
|
self.info("")
|
|
|
|
def study_complete(self, study_name: str, n_trials: int, n_successful: int):
|
|
"""Log study completion."""
|
|
self.info("")
|
|
self.info("=" * 80)
|
|
self.info(f"STUDY COMPLETE: {study_name}")
|
|
self.info("=" * 80)
|
|
self.info(f"Total trials: {n_trials}")
|
|
self.info(f"Successful: {n_successful}")
|
|
self.info(f"Failed/Pruned: {n_trials - n_successful}")
|
|
self.info(f"Completed: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
self.info("=" * 80)
|
|
|
|
|
|
# Register custom logger class
|
|
logging.setLoggerClass(AtomizerLogger)
|
|
|
|
|
|
def get_logger(
|
|
name: str,
|
|
level: int = logging.INFO,
|
|
study_dir: Optional[Path] = None,
|
|
console: bool = True,
|
|
file_logging: bool = True
|
|
) -> AtomizerLogger:
|
|
"""
|
|
Get or create a logger instance.
|
|
|
|
Args:
|
|
name: Logger name (typically __name__ or study name)
|
|
level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
study_dir: If provided, creates log file at study_dir/optimization.log
|
|
console: Enable console output (default: True)
|
|
file_logging: Enable file logging (default: True, requires study_dir)
|
|
|
|
Returns:
|
|
AtomizerLogger instance
|
|
|
|
Example:
|
|
# Simple logger
|
|
logger = get_logger(__name__)
|
|
logger.info("Starting optimization...")
|
|
|
|
# Study logger with file output
|
|
logger = get_logger(
|
|
"drone_gimbal_arm",
|
|
study_dir=Path("studies/drone_gimbal_arm/2_results")
|
|
)
|
|
logger.study_start("drone_gimbal_arm", n_trials=30, sampler="NSGAIISampler")
|
|
"""
|
|
logger = logging.getLogger(name)
|
|
|
|
# Only configure if not already configured
|
|
if not logger.handlers:
|
|
logger.setLevel(level)
|
|
|
|
# Console handler with colors
|
|
if console:
|
|
console_handler = logging.StreamHandler(sys.stdout)
|
|
console_handler.setLevel(level)
|
|
console_formatter = ColoredFormatter(
|
|
fmt='[%(levelname)s] %(message)s',
|
|
use_colors=True
|
|
)
|
|
console_handler.setFormatter(console_formatter)
|
|
logger.addHandler(console_handler)
|
|
|
|
# File handler with rotation
|
|
if file_logging and study_dir:
|
|
study_dir = Path(study_dir)
|
|
study_dir.mkdir(parents=True, exist_ok=True)
|
|
log_file = study_dir / "optimization.log"
|
|
|
|
file_handler = RotatingFileHandler(
|
|
log_file,
|
|
maxBytes=50 * 1024 * 1024, # 50MB
|
|
backupCount=3,
|
|
encoding='utf-8'
|
|
)
|
|
file_handler.setLevel(level)
|
|
file_formatter = logging.Formatter(
|
|
fmt='%(asctime)s | %(levelname)-8s | %(name)s | %(message)s',
|
|
datefmt='%Y-%m-%d %H:%M:%S'
|
|
)
|
|
file_handler.setFormatter(file_formatter)
|
|
logger.addHandler(file_handler)
|
|
|
|
# Log that file logging is enabled
|
|
logger.debug(f"File logging enabled: {log_file}")
|
|
|
|
# Prevent propagation to root logger
|
|
logger.propagate = False
|
|
|
|
return logger
|
|
|
|
|
|
def configure_root_logger(level: int = logging.WARNING):
|
|
"""
|
|
Configure root logger to catch unconfigured loggers.
|
|
|
|
Call this once at application startup to set up default logging behavior.
|
|
"""
|
|
root_logger = logging.getLogger()
|
|
root_logger.setLevel(level)
|
|
|
|
if not root_logger.handlers:
|
|
handler = logging.StreamHandler(sys.stdout)
|
|
handler.setLevel(level)
|
|
formatter = logging.Formatter('[%(levelname)s] %(name)s: %(message)s')
|
|
handler.setFormatter(formatter)
|
|
root_logger.addHandler(handler)
|
|
|
|
|
|
# Example usage and testing
|
|
if __name__ == "__main__":
|
|
# Test basic logging
|
|
print("Testing Atomizer Logging System")
|
|
print("=" * 80)
|
|
|
|
# Simple logger
|
|
logger = get_logger("test_module")
|
|
logger.debug("This is a debug message")
|
|
logger.info("This is an info message")
|
|
logger.warning("This is a warning message")
|
|
logger.error("This is an error message")
|
|
|
|
print()
|
|
|
|
# Study logger with file output
|
|
test_dir = Path("test_logs")
|
|
test_dir.mkdir(exist_ok=True)
|
|
|
|
study_logger = get_logger("test_study", study_dir=test_dir)
|
|
study_logger.study_start("test_study", n_trials=5, sampler="TPESampler")
|
|
|
|
# Simulate trial
|
|
study_logger.trial_start(1, {"thickness": 2.5, "width": 10.0})
|
|
study_logger.info("Running simulation...")
|
|
study_logger.trial_complete(
|
|
1,
|
|
objectives={"mass": 120.5, "stiffness": 1500.2},
|
|
constraints={"max_stress": 85.3},
|
|
feasible=True
|
|
)
|
|
|
|
# Failed trial
|
|
study_logger.trial_start(2, {"thickness": 1.0, "width": 5.0})
|
|
study_logger.trial_failed(2, "Simulation convergence failure")
|
|
|
|
study_logger.study_complete("test_study", n_trials=5, n_successful=4)
|
|
|
|
print()
|
|
print(f"Log file created at: {test_dir / 'optimization.log'}")
|
|
print("Check the file to see structured logging output!")
|