""" Physics strategy for physical equations with units and constants. This strategy handles formatting for physics equations, including physical constants, SI units, vector notation, and standard physics symbols. """ from lxml import etree as ET from typing import Set, Dict from ..core.interfaces import Strategy # Import from existing data modules from ..data import UNICODE_TO_LATEX, MATH_FUNCTIONS, COMMON_UNITS, UNICODE_PASSTHROUGH class PhysicsStrategy(Strategy): """ Physical equation notation strategy. This strategy provides formatting for physics content: - Physical constants → proper formatting (c, h, ħ, kB) - SI units → \\mathrm{} (upright) - Mathematical functions → \\function - Variables remain italic (LaTeX default) - Vector notation support """ # Mathematical functions that get special LaTeX commands FUNCTIONS: Set[str] = MATH_FUNCTIONS # Common physical constants (typically italic in physics) PHYSICAL_CONSTANTS: Set[str] = { 'c', # speed of light 'h', # Planck's constant 'ℏ', # reduced Planck's constant (hbar) 'G', # gravitational constant 'e', # elementary charge 'k', # Boltzmann constant (also kB) 'kB', # Boltzmann constant 'NA', # Avogadro's number 'R', # gas constant 'ε0', # permittivity of free space 'μ0', # permeability of free space 'g', # gravitational acceleration } # SI units (upright formatting) UNITS: Set[str] = COMMON_UNITS def __init__(self): """Initialize the physics strategy.""" pass def format_identifier(self, text: str, node: ET.Element) -> str: """ Format an identifier ( element) for physics. Args: text: The identifier text content node: The element node Returns: Formatted LaTeX string Logic: 1. Mathematical functions → \\function (e.g., "sin" → "\\sin") 2. SI units → \\mathrm{unit} (e.g., "kg" → "\\mathrm{kg}") 3. Physical constants → unchanged (remain italic) 4. Single letters → unchanged (variables remain italic) 5. Multi-letter non-units → check context Examples: "sin" → "\\sin" "kg" → "\\mathrm{kg}" "m" → "m" (could be mass or meter, context-dependent) "c" → "c" (speed of light, italic) "force" → "force" (variable name, italic) """ # Check if it's a mathematical function if text in self.FUNCTIONS: return f"\\{text}" # Check if it's an SI unit (upright) if text in self.UNITS: return f"\\mathrm{{{text}}}" # UNICODE_PASSTHROUGH: Keep certain symbols as Unicode for MathJax compatibility if text in UNICODE_PASSTHROUGH: return text # Check for Unicode symbols if text in UNICODE_TO_LATEX: return UNICODE_TO_LATEX[text] # Special physics symbols if text == 'ℏ': # hbar return '\\hbar' elif text == 'ħ': # alternative hbar return '\\hbar' # Physical constants remain italic (default LaTeX) if text in self.PHYSICAL_CONSTANTS: return text # Handle multi-character strings if len(text) > 1: # Check if it contains Unicode (passthrough or convertible) has_unicode = any(char in UNICODE_TO_LATEX for char in text) has_passthrough = any(char in UNICODE_PASSTHROUGH for char in text) if has_unicode or has_passthrough: converted_parts = [] for char in text: if char in UNICODE_PASSTHROUGH: converted_parts.append(char) elif char in UNICODE_TO_LATEX: converted_parts.append(UNICODE_TO_LATEX[char]) elif char.isspace(): converted_parts.append(' ') else: converted_parts.append(char) return ' '.join(converted_parts) # Check if it looks like a unit (multiple known units combined) # e.g., "kg·m/s²" → should be upright # For now, let units be upright if explicitly in UNITS set # Otherwise, remain italic (variable names) # Default: return as-is (variables remain italic in LaTeX) return text def format_operator(self, text: str, node: ET.Element) -> str: """ Format an operator ( element) for physics. Args: text: The operator text content node: The element node Returns: Formatted LaTeX string Logic: 1. Unicode symbols → LaTeX equivalents (via UNICODE_TO_LATEX) 2. Standard operators → unchanged (+, -, *, /, =) 3. Vector operators → proper notation (⃗, ×, ·) Examples: "+" → "+" "×" → "\\times" "·" → "\\cdot" "∇" → "\\nabla" "∂" → "\\partial" """ # Handle < and > specifically (LaTeX convention) if text == "<": return "\\lt" if text == ">": return "\\gt" # Vector cross product if text == '×': return '\\times' elif text == '·': return '\\cdot' # UNICODE_PASSTHROUGH: Keep certain symbols as Unicode for MathJax compatibility if text in UNICODE_PASSTHROUGH: return text # Check for Unicode symbol mapping if text in UNICODE_TO_LATEX: return UNICODE_TO_LATEX[text] # Handle multi-character operators with Unicode if len(text) > 1: has_unicode = any(char in UNICODE_TO_LATEX for char in text) has_passthrough = any(char in UNICODE_PASSTHROUGH for char in text) if has_unicode or has_passthrough: converted_parts = [] for char in text: if char in UNICODE_PASSTHROUGH: converted_parts.append(char) elif char in UNICODE_TO_LATEX: converted_parts.append(UNICODE_TO_LATEX[char]) else: converted_parts.append(char) return ' '.join(converted_parts) # Default: return as-is return text def format_number(self, text: str, node: ET.Element) -> str: """ Format a number ( element) for physics. Args: text: The number text content node: The element node Returns: Formatted LaTeX string Logic: Numbers are returned as-is in physics mode. Examples: "9.8" → "9.8" (gravitational acceleration) "3.0×10⁸" → "3.0×10⁸" (speed of light) """ # Numbers are returned as-is return text def detect_context(self, equation_text: str) -> bool: """ Detect if an equation is physics content. Args: equation_text: The full equation text Returns: True if likely physics, False otherwise Detection indicators: - SI units (kg, m, s, N, J, etc.) - Physical constants (c, h, G, etc.) - Vector operators (∇, ∂, ×, ·) - Common physics notation Examples: "F = ma" → True (if 'a' is acceleration) "E = mc²" → True (energy-mass equivalence) "∇×B = μ₀J" → True (Maxwell's equation) "sin(x) + cos(x)" → False "T = C" → False (just math variables, not Tesla/Coulomb) """ # CRITICAL: Check if we're in a mathematical context first # If equation contains math functions like sin, cos, tan, log, etc., # it's likely a pure math equation, not physics # This prevents false positives like "C = 2πr" being detected as physics if any(func in equation_text for func in ['sin', 'cos', 'tan', 'cot', 'sec', 'csc', 'arcsin', 'arccos', 'arctan', 'sinh', 'cosh', 'tanh', 'log', 'ln', 'lg', 'exp', 'lim', 'max', 'min', 'sup', 'inf']): return False # CRITICAL: Filter out single-letter units that are too ambiguous # 'C', 'T', 'P', 'A', 'K', 'S', 'H', 'N', 'V', 'F', 'W', 'J' can all be variables! # Only check for multi-character units or units in clear physics context ambiguous_single_letters = {'C', 'T', 'P', 'A', 'K', 'S', 'H', 'N', 'V', 'F', 'W', 'J', 'L', 'M', 'R', 'G'} # Check for UNAMBIGUOUS multi-character units unambiguous_units = {u for u in self.UNITS if len(u) > 1 or u not in ambiguous_single_letters} unit_count = sum(1 for unit in unambiguous_units if unit in equation_text) if unit_count >= 1: return True # Check for physical constants (these are less ambiguous) const_count = sum(1 for const in self.PHYSICAL_CONSTANTS if const in equation_text) if const_count >= 1: return True # Check for vector operators if any(op in equation_text for op in ['∇', '∂', '×', '·', '⃗']): return True # Check for common physics patterns (units with superscripts: m/s², kg·m, etc.) import re if re.search(r'(kg|m|s|N|J|W|Pa|V|A|K|mol|cd)[²³⁴]', equation_text): return True if re.search(r'(m|kg|s|N|J)/s', equation_text): return True return False @property def name(self) -> str: """Get the strategy name.""" return "physics" def __repr__(self) -> str: """String representation for debugging.""" return f"PhysicsStrategy(units={len(self.UNITS)}, constants={len(self.PHYSICAL_CONSTANTS)})"