- nuclear_data.py: Extract Q-value from ground state branch state[7] instead of halflife_data (was reading half-life seconds as Q-value) - nuclear_data.py: Use ground state branch for forbiddenness code, with fallback - nuclear_data.py: Add None guards for beta_data and pairs - spectrum.py: Pass Z=config.Z_daughter, A=config.A_number to RadiativeCorrection - phase_space.py: Add __init__ debug logging (W0, transition_type, m_e, m_nu) - phase_space.py: Add output range logging in __call__ - phase_space.py: Fix possibly unbound forbid_factor variable - logging_utils.py: Fix broken get_git_short_hash indentation - cli.py: Add explicit SpectrumConfig annotation to fix pyright type identity - cw_extractor.py: Fix T_to_W ndarray→float conversions (6 locations) - cw_extractor.py: Fix FermiFunction() calls — was passing Z as __call__ arg - fitter.py: Add explicit dict type annotation for least_squares kwargs
165 lines
4.5 KiB
Python
165 lines
4.5 KiB
Python
"""
|
|
Logging infrastructure for beta-spectrum package.
|
|
|
|
Provides a centralized logging setup with:
|
|
- Configurable verbosity levels (WARNING, INFO, DEBUG)
|
|
- Optional file output
|
|
- Consistent formatting across all modules
|
|
- Opt-in design: logging is disabled by default for library use,
|
|
enabled via setup_logging() for CLI/workflow use
|
|
|
|
Usage (library):
|
|
# No logging by default — silent, zero overhead
|
|
|
|
Usage (CLI):
|
|
from beta_spectrum.logging_utils import setup_logging, get_logger
|
|
setup_logging(level="INFO", log_file="calc.log")
|
|
logger = get_logger("spectrum")
|
|
logger.info("Starting calculation...")
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import sys
|
|
from dataclasses import dataclass, field
|
|
from typing import Optional
|
|
|
|
|
|
@dataclass
|
|
class LoggingConfig:
|
|
"""Configuration for logging setup."""
|
|
|
|
level: str = "WARNING"
|
|
"""Logging level: 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'."""
|
|
|
|
log_file: Optional[str] = None
|
|
"""Path to log file. If None, only stdout is used."""
|
|
|
|
log_format: str = "[%(asctime)s] [%(levelname)-8s] [%(name)s] %(message)s"
|
|
"""Log message format string."""
|
|
|
|
date_format: str = "%H:%M:%S"
|
|
"""Date/time format for timestamps."""
|
|
|
|
_handlers_configured: bool = field(default=False, repr=False)
|
|
"""Internal flag to prevent double configuration."""
|
|
|
|
|
|
def setup_logging(config: Optional[LoggingConfig] = None) -> logging.Logger:
|
|
"""
|
|
Configure the root logger for the beta_spectrum package.
|
|
|
|
This is a one-shot setup — calling it multiple times with different
|
|
configs will not reconfigure existing handlers. Call setup_logging()
|
|
once at the start of your application.
|
|
|
|
Parameters
|
|
----------
|
|
config : LoggingConfig, optional
|
|
Logging configuration. Uses defaults if None.
|
|
|
|
Returns
|
|
-------
|
|
logging.Logger
|
|
The root logger for the beta_spectrum package.
|
|
"""
|
|
if config is None:
|
|
config = LoggingConfig()
|
|
|
|
logger = logging.getLogger("beta_spectrum")
|
|
|
|
# Prevent double configuration
|
|
if config._handlers_configured:
|
|
return logger
|
|
|
|
logger.setLevel(getattr(logging, config.level.upper(), logging.WARNING))
|
|
|
|
# Remove any existing handlers to avoid duplicates on re-init
|
|
for handler in logger.handlers[:]:
|
|
logger.removeHandler(handler)
|
|
handler.close()
|
|
|
|
# Console handler
|
|
console_handler = logging.StreamHandler(sys.stdout)
|
|
console_handler.setLevel(getattr(logging, config.level.upper(), logging.WARNING))
|
|
console_handler.setFormatter(
|
|
logging.Formatter(config.log_format, config.date_format)
|
|
)
|
|
logger.addHandler(console_handler)
|
|
|
|
# File handler (optional)
|
|
if config.log_file:
|
|
file_handler = logging.FileHandler(config.log_file, mode="a")
|
|
file_handler.setLevel(logging.DEBUG) # Always log everything to file
|
|
file_handler.setFormatter(
|
|
logging.Formatter(config.log_format, config.date_format)
|
|
)
|
|
logger.addHandler(file_handler)
|
|
|
|
config._handlers_configured = True
|
|
return logger
|
|
|
|
|
|
def get_logger(name: str) -> logging.Logger:
|
|
"""
|
|
Get a named logger for the beta_spectrum package.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
Logger name (e.g., "spectrum", "fitter", "nuclear_data").
|
|
|
|
Returns
|
|
-------
|
|
logging.Logger
|
|
A logger named "beta_spectrum.{name}".
|
|
|
|
Examples
|
|
--------
|
|
>>> logger = get_logger("spectrum")
|
|
>>> logger.info("Creating spectrum components")
|
|
"""
|
|
return logging.getLogger(f"beta_spectrum.{name}")
|
|
|
|
|
|
def get_git_short_hash(length: int = 7) -> str:
|
|
"""
|
|
Get the short git commit hash.
|
|
|
|
Parameters
|
|
----------
|
|
length : int
|
|
Number of characters to return (default: 7).
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
Short git hash, or "unknown" if not in a git repo.
|
|
"""
|
|
import subprocess
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
["git", "rev-parse", f"--short={length}", "HEAD"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
cwd=_find_git_root(),
|
|
)
|
|
return result.stdout.strip()
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
return "unknown"
|
|
|
|
|
|
def _find_git_root() -> str:
|
|
"""Find the root directory of the git repository."""
|
|
from pathlib import Path
|
|
|
|
current = Path(__file__).resolve()
|
|
while current != current.parent:
|
|
if (current / ".git").exists():
|
|
return str(current)
|
|
current = current.parent
|
|
return str(current)
|