Optimization Configuration Guide

This guide provides detailed information about all optimization parameters available in quActuary’s PricingModel.simulate() method.

Overview

The simulate() method accepts numerous parameters to control optimization behavior:

results = model.simulate(
    n_simulations: int = 10000,
    use_jit: bool = None,
    parallel: bool = None,
    max_workers: int = None,
    vectorized: bool = True,
    memory_limit_gb: float = None,
    use_qmc: bool = False,
    qmc_engine: str = 'sobol',
    progress_bar: bool = True,
    checkpoint_interval: int = None,
    random_state: int = None
)

Core Parameters

n_simulations

Number of Monte Carlo simulation paths to generate.

Type:

int

Default:

10000

Range:

1 to 10^9 (system memory permitting)

# Standard simulation
results = model.simulate(n_simulations=100_000)

# Large-scale simulation
results = model.simulate(
    n_simulations=10_000_000,
    memory_limit_gb=8  # Manage memory for large runs
)

Note

Larger simulation counts provide better convergence but require more time and memory. Use the rule of thumb: double simulations for half the standard error.

random_state

Seed for random number generation to ensure reproducibility.

Type:

int or None

Default:

None (random seed)

# Reproducible results
results1 = model.simulate(n_simulations=10_000, random_state=42)
results2 = model.simulate(n_simulations=10_000, random_state=42)
# results1 and results2 will be identical

# Different runs
results3 = model.simulate(n_simulations=10_000, random_state=123)
# results3 will differ from results1/results2

JIT Compilation Parameters

use_jit

Enable Just-In-Time compilation using Numba for numerical operations.

Type:

bool or None

Default:

None (auto-detect based on portfolio size)

Auto-detection:

Enabled when portfolio size > 100

# Automatic detection (recommended)
results = model.simulate(n_simulations=100_000)

# Force JIT on
results = model.simulate(n_simulations=100_000, use_jit=True)

# Force JIT off (e.g., for debugging)
results = model.simulate(n_simulations=100_000, use_jit=False)

Performance characteristics:

  • First run includes compilation time (typically 0.5-2 seconds)

  • Subsequent runs use cached compiled code

  • Speedup: 10-50x for numerical operations

  • Memory overhead: Minimal

Best practices:

# Warm-up for production use
if first_run:
    model.simulate(n_simulations=100, use_jit=True)  # Compile

# Production runs benefit from compiled code
results = model.simulate(n_simulations=1_000_000, use_jit=True)

Parallel Processing Parameters

parallel

Enable parallel processing across multiple CPU cores.

Type:

bool or None

Default:

None (auto-detect based on portfolio size)

Auto-detection:

Enabled when portfolio size > 50 and CPU count > 1

# Automatic detection
results = model.simulate(n_simulations=100_000)

# Force parallel processing
results = model.simulate(n_simulations=100_000, parallel=True)

# Force single process (debugging, memory constraints)
results = model.simulate(n_simulations=100_000, parallel=False)

max_workers

Maximum number of worker processes for parallel execution.

Type:

int or None

Default:

None (uses CPU count)

Range:

1 to os.cpu_count()

import os

# Use all available cores (default)
results = model.simulate(n_simulations=1_000_000, parallel=True)

# Use half the cores (leave resources for other tasks)
results = model.simulate(
    n_simulations=1_000_000,
    parallel=True,
    max_workers=os.cpu_count() // 2
)

# Fixed worker count
results = model.simulate(
    n_simulations=1_000_000,
    parallel=True,
    max_workers=4
)

Optimal worker calculation:

def optimal_workers(portfolio_size, available_memory_gb):
    cpu_based = os.cpu_count()
    portfolio_based = max(1, portfolio_size // 50)
    memory_based = int(available_memory_gb // 2)

    return min(cpu_based, portfolio_based, memory_based)

Memory Management Parameters

memory_limit_gb

Maximum memory usage limit in gigabytes.

Type:

float or None

Default:

None (auto-detect available memory)

Range:

0.1 to system memory

# Auto-detect available memory
results = model.simulate(n_simulations=10_000_000)

# Strict memory limit
results = model.simulate(
    n_simulations=10_000_000,
    memory_limit_gb=4.0  # Limit to 4GB
)

# Conservative limit for shared systems
import psutil
available_gb = psutil.virtual_memory().available / 1e9
results = model.simulate(
    n_simulations=10_000_000,
    memory_limit_gb=available_gb * 0.5  # Use 50% of available
)

Memory estimation:

def estimate_memory_gb(n_simulations, portfolio_size):
    # Rough estimation formula
    bytes_per_simulation = 8 * portfolio_size  # 8 bytes per float
    total_bytes = n_simulations * bytes_per_simulation * 3  # 3x for overhead
    return total_bytes / 1e9

checkpoint_interval

Save progress at regular intervals for recovery from interruptions.

Type:

int or None

Default:

None (no checkpointing)

Recommended:

n_simulations // 100 for long runs

# No checkpointing (default for small runs)
results = model.simulate(n_simulations=10_000)

# Regular checkpoints for large runs
results = model.simulate(
    n_simulations=10_000_000,
    checkpoint_interval=100_000  # Save every 100k simulations
)

# Recover from checkpoint after interruption
try:
    results = model.simulate(
        n_simulations=10_000_000,
        checkpoint_interval=100_000,
        resume_from_checkpoint=True  # Continue from last checkpoint
    )
except KeyboardInterrupt:
    print("Simulation interrupted, progress saved")

Vectorization Parameters

vectorized

Enable NumPy vectorization for array operations.

Type:

bool

Default:

True

# Vectorized operations (default, recommended)
results = model.simulate(n_simulations=100_000, vectorized=True)

# Disable for debugging or special cases
results = model.simulate(n_simulations=100_000, vectorized=False)

Warning

Disabling vectorization significantly impacts performance. Only disable for debugging or when required by custom distributions.

Quasi-Monte Carlo Parameters

use_qmc

Enable Quasi-Monte Carlo using low-discrepancy sequences.

Type:

bool

Default:

False

# Standard Monte Carlo (default)
results_mc = model.simulate(n_simulations=10_000, use_qmc=False)

# Quasi-Monte Carlo for better convergence
results_qmc = model.simulate(n_simulations=10_000, use_qmc=True)

# Compare convergence
print(f"MC std error: {results_mc.standard_error:.4f}")
print(f"QMC std error: {results_qmc.standard_error:.4f}")

qmc_engine

Type of low-discrepancy sequence to use.

Type:

str

Default:

'sobol'

Options:

'sobol', 'halton', 'latin_hypercube'

# Sobol sequence (recommended for most cases)
results = model.simulate(
    n_simulations=10_000,
    use_qmc=True,
    qmc_engine='sobol'
)

# Halton sequence (better for low dimensions)
results = model.simulate(
    n_simulations=10_000,
    use_qmc=True,
    qmc_engine='halton'
)

# Latin Hypercube (good for sensitivity analysis)
results = model.simulate(
    n_simulations=10_000,
    use_qmc=True,
    qmc_engine='latin_hypercube'
)

QMC engine selection guide:

QMC Engine Comparison

Engine

Best Dimension Range

Convergence Rate

Use Case

Sobol

1-1000

O(log(N)^d/N)

General purpose, high dimensions

Halton

1-20

O(log(N)^d/N)

Low dimensions, simple problems

Latin Hypercube

1-100

O(1/N)

Design of experiments, sensitivity

User Interface Parameters

progress_bar

Display progress bar during simulation.

Type:

bool

Default:

True

# With progress bar (default)
results = model.simulate(n_simulations=1_000_000, progress_bar=True)
# Output: [████████████████████] 100% | 1000000/1000000 | ETA: 00:00

# Disable for non-interactive environments
results = model.simulate(n_simulations=1_000_000, progress_bar=False)

# Custom progress callback
def custom_progress(current, total):
    percent = 100 * current / total
    print(f"Progress: {percent:.1f}% ({current}/{total})")

results = model.simulate(
    n_simulations=1_000_000,
    progress_callback=custom_progress
)

Configuration Presets

quActuary provides configuration presets for common scenarios:

from quactuary.optimization import OptimizationPresets

# Memory-constrained environment
config = OptimizationPresets.MEMORY_CONSTRAINED
results = model.simulate(n_simulations=10_000_000, **config)

# Maximum performance
config = OptimizationPresets.MAX_PERFORMANCE
results = model.simulate(n_simulations=1_000_000, **config)

# Balanced (default)
config = OptimizationPresets.BALANCED
results = model.simulate(n_simulations=100_000, **config)

# Development/debugging
config = OptimizationPresets.DEBUG
results = model.simulate(n_simulations=1_000, **config)

Preset definitions:

class OptimizationPresets:
    MEMORY_CONSTRAINED = {
        "use_jit": False,
        "parallel": False,
        "memory_limit_gb": 2,
        "checkpoint_interval": 10_000
    }

    MAX_PERFORMANCE = {
        "use_jit": True,
        "parallel": True,
        "max_workers": None,
        "use_qmc": True,
        "vectorized": True
    }

    BALANCED = {
        "use_jit": None,
        "parallel": None,
        "memory_limit_gb": None
    }

    DEBUG = {
        "use_jit": False,
        "parallel": False,
        "progress_bar": True,
        "random_state": 42
    }

Environment Variables

Configuration via environment variables for deployment:

# Disable JIT globally
export QUACTUARY_DISABLE_JIT=1

# Set default worker count
export QUACTUARY_MAX_WORKERS=4

# Set memory limit
export QUACTUARY_MEMORY_LIMIT_GB=8

# Disable progress bars
export QUACTUARY_NO_PROGRESS=1

Access in Python:

import os

# Override defaults with environment variables
config = {
    "use_jit": not os.environ.get("QUACTUARY_DISABLE_JIT"),
    "max_workers": int(os.environ.get("QUACTUARY_MAX_WORKERS", 0)) or None,
    "memory_limit_gb": float(os.environ.get("QUACTUARY_MEMORY_LIMIT_GB", 0)) or None,
    "progress_bar": not os.environ.get("QUACTUARY_NO_PROGRESS")
}

results = model.simulate(n_simulations=100_000, **config)

Advanced Configuration

Custom Optimization Strategy

Implement custom optimization logic:

class CustomOptimizer:
    def __init__(self, portfolio):
        self.portfolio = portfolio

    def should_use_jit(self):
        # Custom heuristic based on portfolio characteristics
        if self.portfolio.size < 50:
            return False
        if self.portfolio.has_complex_dependencies:
            return True
        return self.portfolio.size > 100

    def optimal_workers(self):
        # Dynamic worker allocation
        if self.portfolio.is_correlated:
            return max(2, os.cpu_count() // 2)
        return os.cpu_count()

    def memory_limit(self):
        # Adaptive memory limit
        base_memory = 0.1 * self.portfolio.size / 1000  # GB per 1k policies
        return min(base_memory * 2, 16)  # Cap at 16GB

# Use custom optimizer
optimizer = CustomOptimizer(portfolio)
results = model.simulate(
    n_simulations=100_000,
    use_jit=optimizer.should_use_jit(),
    max_workers=optimizer.optimal_workers(),
    memory_limit_gb=optimizer.memory_limit()
)

Performance Monitoring Integration

from quactuary.monitoring import PerformanceMonitor

# Wrap simulation with monitoring
with PerformanceMonitor() as monitor:
    results = model.simulate(
        n_simulations=1_000_000,
        use_jit=True,
        parallel=True
    )

# Access detailed metrics
monitor.plot_timeline()
monitor.show_resource_usage()
monitor.export_metrics("simulation_metrics.json")

Next Steps