Utils Module Architecture¶
Deep dive into the utils module’s architecture, design patterns, and implementation details.
Design Philosophy¶
The utils module follows these key principles:
Optional Enhancement: All utils are optional - core LumiX works without them
Structural Typing: Use Protocol (PEP 544) instead of inheritance
Type Safety: Full type hints for IDE support and mypy checking
Single Responsibility: Each component has one clear purpose
Minimal Dependencies: Avoid external dependencies where possible
Module Overview¶
The utils module consists of three independent components:
graph TD
A[utils Module] --> B[logger.py]
A --> C[orm.py]
A --> D[rational.py]
B --> E[LXModelLogger]
C --> F[LXORMModel]
C --> G[LXORMContext]
C --> H[LXTypedQuery]
C --> I[LXNumeric]
D --> J[LXRationalConverter]
style A fill:#e1f5ff
style B fill:#fff4e1
style C fill:#ffe1e1
style D fill:#e1ffe1
Component Architecture¶
Logger (logger.py)¶
Design Pattern: Wrapper around Python’s logging module
Key Design Decisions:
Composition over Inheritance: Wraps
logging.Loggerinstead of inheritingDomain-Specific Methods: Specialized methods for optimization events
Automatic Timing: Built-in timestamp tracking for solve operations
Implementation:
class LXModelLogger:
"""Wraps Python logger with optimization-specific methods."""
def __init__(self, name: str = "lumix", level: int = logging.INFO):
self.logger = logging.getLogger(name) # Composition
self.start_time: Optional[datetime] = None
def log_solve_start(self, solver_name: str) -> None:
self.start_time = datetime.now() # Auto timing
self.logger.info(f"Starting solve with {solver_name}...")
Benefits:
Delegates to standard logging for core functionality
Easy to test - just check log output
No breaking changes when Python logging evolves
ORM Integration (orm.py)¶
Design Pattern: Structural typing with Protocol
Key Design Decisions:
Protocol over ABC: Use runtime-checkable Protocol for structural typing
Generic Types: Full generic support for type safety
ORM Agnostic: Works with any ORM via duck typing
Implementation:
@runtime_checkable
class LXORMModel(Protocol):
"""Structural protocol - no inheritance needed."""
id: Any
def __getattribute__(self, name: str) -> Any:
...
class LXORMContext(Generic[TModel]):
"""Generic context for type-safe queries."""
def __init__(self, session: Any):
self.session = session # Works with any ORM session
def query(self, model: Type[TModel]) -> "LXTypedQuery[TModel]":
return LXTypedQuery(self.session, model)
Type Flow:
sequenceDiagram
participant User
participant Context
participant TypedQuery
participant ORM
User->>Context: query(Product)
Context->>TypedQuery: LXTypedQuery[Product]
User->>TypedQuery: filter(lambda p: ...)
Note over User: IDE knows p is Product
TypedQuery->>ORM: session.query(Product).all()
ORM-->>TypedQuery: List[Product]
TypedQuery-->>User: List[Product]
Benefits:
No inheritance - works with existing ORM models
Full IDE autocomplete in lambdas
Runtime type checking via isinstance()
Rational Conversion (rational.py)¶
Design Pattern: Strategy pattern for algorithms
Key Design Decisions:
Multiple Algorithms: Support three different approximation methods
Configurable Precision: User controls max denominator
Immutable Configuration: Converter settings don’t change after init
Self-Contained: No external dependencies (uses only stdlib)
Implementation:
class LXRationalConverter:
"""Immutable converter with configurable algorithm."""
def __init__(
self,
max_denominator: int = 10000,
method: Literal["farey", "continued_fraction", "stern_brocot"] = "farey",
float_tolerance: float = 1e-9,
):
self.max_denominator = max_denominator
self.method = method
self.float_tolerance = float_tolerance
def to_rational(self, value: float, ...) -> Fraction:
# Strategy pattern - dispatch to appropriate method
if self.method == "farey":
num, den, error = self._farey_approximation(value)
elif self.method == "continued_fraction":
num, den, error = self._continued_fraction_approximation(value)
elif self.method == "stern_brocot":
num, den, error = self._stern_brocot_approximation(value)
Algorithm Architecture:
graph LR
A[to_rational] --> B{method?}
B -->|farey| C[_farey_approximation]
B -->|continued_fraction| D[_continued_fraction_approximation]
B -->|stern_brocot| E[_stern_brocot_approximation]
C --> F[Return Fraction]
D --> F
E --> F
Benefits:
Easy to add new algorithms
No runtime dependencies
Testable algorithms in isolation
Type System Design¶
Protocol Usage¶
Protocols enable structural typing without inheritance:
from typing import Protocol, runtime_checkable
@runtime_checkable
class LXORMModel(Protocol):
id: Any
# Any class with 'id' automatically satisfies protocol
class Product:
id: int = 1
assert isinstance(Product(), LXORMModel) # True!
Advantages:
Works with existing code (no refactoring)
Runtime type checking
Gradual typing - can adopt incrementally
Generic Types¶
Full generic support for type safety:
from typing import Generic, TypeVar
TModel = TypeVar("TModel")
class LXORMContext(Generic[TModel]):
def query(self, model: Type[TModel]) -> "LXTypedQuery[TModel]":
...
Type Propagation:
ctx = LXORMContext(session)
query: LXTypedQuery[Product] = ctx.query(Product)
products: List[Product] = query.all() # Type propagates!
Benefits:
IDE autocomplete
mypy type checking
Self-documenting code
Performance Considerations¶
Logger Performance¶
Design Choice: Use conditional logging for expensive operations
# Good: Check level before expensive computation
if logger.logger.isEnabledFor(logging.DEBUG):
expensive_message = compute_detailed_stats()
logger.debug(expensive_message)
Overhead:
INFO level: ~1-2μs per call
DEBUG level: ~2-5μs per call (if enabled)
Negligible for model building/solving
ORM Query Performance¶
Design Choice: Filter in Python (not at database level)
def filter(self, predicate: Callable[[TModel], bool]) -> Self:
self._filters.append(predicate) # Store for later
return self
def all(self) -> List[TModel]:
results = self.session.query(self.model).all() # Fetch all
for predicate in self._filters:
results = [r for r in results if predicate(r)] # Filter in Python
return results
Trade-off:
Pro: Type-safe lambdas with full IDE support
Con: Fetches all data then filters
Mitigation: Use ORM filters first for large datasets
Rational Conversion Performance¶
Algorithm Complexity:
Farey: O(log(max_denominator))
Continued Fraction: O(log(value))
Stern-Brocot: O(log(max_denominator))
Benchmarks (typical):
Small denominators (<1000): ~5-10μs
Medium denominators (~10000): ~15-25μs
Large denominators (>100000): ~50-100μs
Testing Strategy¶
Unit Tests¶
Each component has comprehensive unit tests:
# Test logger
def test_log_model_creation(caplog):
logger = LXModelLogger()
logger.log_model_creation("TestModel", num_vars=10, num_constraints=5)
assert "TestModel" in caplog.text
assert "10 variables" in caplog.text
# Test ORM protocol
def test_orm_protocol():
@dataclass
class Product:
id: int
assert isinstance(Product(1), LXORMModel)
# Test rational conversion
def test_farey_approximation():
converter = LXRationalConverter(method="farey")
frac = converter.to_rational(3.14159)
assert abs(float(frac) - 3.14159) < 1e-4
Type Tests¶
Use mypy for static type checking:
mypy src/lumix/utils
Integration Tests¶
Test interaction with core LumiX:
def test_orm_with_variable():
products = [Product(id=1, profit=10.0)]
production = LXVariable[Product, float]("x").from_data(products)
assert len(production.get_instances()) == 1
Extension Points¶
Custom Logger Methods¶
Subclass for domain-specific logging:
class LXTransportLogger(LXModelLogger):
"""Specialized for transportation models."""
def log_route_creation(self, origin: str, dest: str, count: int):
self.logger.debug(f"Created {count} route(s): {origin} → {dest}")
Custom ORM Protocols¶
Define additional protocols for specific needs:
class LXTimestampedModel(Protocol):
"""Protocol for models with timestamps."""
id: Any
created_at: datetime
updated_at: datetime
New Conversion Algorithms¶
Add new rational approximation methods:
class LXRationalConverter:
def _custom_approximation(self, x: float) -> Tuple[int, int, float]:
"""Implement custom algorithm here."""
# ...
return numerator, denominator, error
See Also¶
Extending Utils Components - How to extend utils components
Core Architecture - Core module architecture
Utils Module API - Utils API reference