mirror of
https://github.com/SheffieldML/GPy.git
synced 2026-05-15 06:52:39 +02:00
Add MATLAB comparison framework for LFM kernel validation
This commit is contained in:
parent
7227ad8a17
commit
ca1d4bffe9
2 changed files with 394 additions and 0 deletions
76
backlog/features/2025-08-15_matlab-comparison-framework.md
Normal file
76
backlog/features/2025-08-15_matlab-comparison-framework.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
id: "matlab-comparison-framework"
|
||||
title: "Create MATLAB comparison framework for LFM kernel validation"
|
||||
status: "In Progress"
|
||||
priority: "High"
|
||||
created: "2025-08-15"
|
||||
last_updated: "2025-08-15"
|
||||
owner: "Neil Lawrence"
|
||||
github_issue: ""
|
||||
dependencies: "lfm-kernel-code-review"
|
||||
tags:
|
||||
- lfm
|
||||
- kernel
|
||||
- validation
|
||||
- matlab
|
||||
- comparison
|
||||
---
|
||||
|
||||
# Create MATLAB comparison framework for LFM kernel validation
|
||||
|
||||
## Description
|
||||
Create a comprehensive comparison framework to validate our GPy LFM kernel implementation against the MATLAB reference implementation in GPmat.
|
||||
|
||||
## Background
|
||||
- We have analyzed the complete MATLAB implementation (SIM, DISIM kernels)
|
||||
- Need to validate our GPy implementation against the reference
|
||||
- Comparison framework will ensure mathematical correctness and numerical accuracy
|
||||
- Will help catch implementation errors and validate parameter handling
|
||||
|
||||
## Implementation Tasks
|
||||
- [x] Create MATLAB comparison script (`scripts/compare_with_matlab.py`)
|
||||
- [ ] Test MATLAB script with existing GPmat installation
|
||||
- [ ] Create standard test cases for SIM and DISIM kernels
|
||||
- [ ] Implement GPy computation integration in comparison script
|
||||
- [ ] Add parameter validation and constraint testing
|
||||
- [ ] Create visualization tools for comparison results
|
||||
- [ ] Add cross-kernel computation validation
|
||||
- [ ] Document comparison methodology and tolerance standards
|
||||
|
||||
## Test Cases to Implement
|
||||
- [ ] Basic SIM kernel with standard parameters
|
||||
- [ ] SIM kernel with fast decay and no delay
|
||||
- [ ] Basic DISIM kernel with hierarchical parameters
|
||||
- [ ] Edge cases (zero delay, extreme decay values)
|
||||
- [ ] Multi-output scenarios
|
||||
- [ ] Cross-kernel computations (SIM × RBF, DISIM × SIM)
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] MATLAB comparison script runs successfully
|
||||
- [ ] Standard test cases produce consistent results
|
||||
- [ ] Comparison framework can detect implementation errors
|
||||
- [ ] Tolerance standards defined and documented
|
||||
- [ ] Results visualization and reporting implemented
|
||||
- [ ] Framework integrated into development workflow
|
||||
|
||||
## Implementation Notes
|
||||
- Use scipy.io for loading MATLAB .mat files
|
||||
- Support both MATLAB and Octave as reference implementations
|
||||
- Implement robust error handling for missing dependencies
|
||||
- Create standardized test data sets for reproducible comparisons
|
||||
- Consider numerical precision differences between platforms
|
||||
|
||||
## Related
|
||||
- Backlog: lfm-kernel-code-review
|
||||
- Backlog: implement-lfm-kernel-core
|
||||
- MATLAB Implementation: ~/lawrennd/GPmat/matlab/
|
||||
|
||||
## Progress Updates
|
||||
|
||||
### 2025-08-15
|
||||
Created initial MATLAB comparison framework:
|
||||
- Implemented `MATLABComparison` class with automatic MATLAB/Octave detection
|
||||
- Created test case generation for SIM and DISIM kernels
|
||||
- Added result comparison with tolerance checking
|
||||
- Framework ready for integration with GPy implementation
|
||||
- Script can generate MATLAB code dynamically for different test cases
|
||||
318
scripts/compare_with_matlab.py
Normal file
318
scripts/compare_with_matlab.py
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comparison script for LFM kernel implementation against MATLAB reference.
|
||||
|
||||
This script generates test cases and compares results between GPy and MATLAB
|
||||
implementations to validate our LFM kernel implementation.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class MATLABComparison:
|
||||
"""Compare GPy LFM kernel results with MATLAB reference."""
|
||||
|
||||
def __init__(self, matlab_path=None):
|
||||
"""Initialize with path to MATLAB/Octave executable."""
|
||||
self.matlab_path = matlab_path or self._find_matlab()
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
|
||||
def _find_matlab(self):
|
||||
"""Try to find MATLAB or Octave executable."""
|
||||
# Try common MATLAB paths
|
||||
matlab_paths = [
|
||||
'/Applications/MATLAB_R2023b.app/bin/matlab', # macOS
|
||||
'/usr/local/bin/matlab', # Linux
|
||||
'matlab', # In PATH
|
||||
]
|
||||
|
||||
# Try Octave as fallback
|
||||
octave_paths = [
|
||||
'/usr/local/bin/octave', # macOS
|
||||
'/usr/bin/octave', # Linux
|
||||
'octave', # In PATH
|
||||
]
|
||||
|
||||
for path in matlab_paths + octave_paths:
|
||||
try:
|
||||
result = subprocess.run([path, '--version'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
print(f"Found MATLAB/Octave: {path}")
|
||||
return path
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
continue
|
||||
|
||||
raise RuntimeError("Could not find MATLAB or Octave executable")
|
||||
|
||||
def create_matlab_script(self, test_case):
|
||||
"""Create MATLAB script for kernel computation."""
|
||||
script = f"""
|
||||
% MATLAB script for LFM kernel comparison
|
||||
% Generated test case: {test_case['name']}
|
||||
|
||||
% Add GPmat to path (assuming it's in ~/lawrennd/GPmat)
|
||||
addpath('~/lawrennd/GPmat/matlab');
|
||||
|
||||
% Test parameters
|
||||
{self._matlab_params(test_case)}
|
||||
|
||||
% Create kernel
|
||||
{self._matlab_kernel_creation(test_case)}
|
||||
|
||||
% Compute kernel matrix
|
||||
{self._matlab_kernel_computation(test_case)}
|
||||
|
||||
% Save results
|
||||
results = struct();
|
||||
results.K = K;
|
||||
results.params = {self._matlab_params_struct(test_case)};
|
||||
results.test_case = '{test_case['name']}';
|
||||
|
||||
save('{os.path.join(self.temp_dir, 'matlab_results.mat')}', 'results');
|
||||
fprintf('MATLAB computation completed\\n');
|
||||
"""
|
||||
return script
|
||||
|
||||
def _matlab_params(self, test_case):
|
||||
"""Generate MATLAB parameter definitions."""
|
||||
params = test_case.get('params', {})
|
||||
lines = []
|
||||
for key, value in params.items():
|
||||
if isinstance(value, (int, float)):
|
||||
lines.append(f"{key} = {value};")
|
||||
elif isinstance(value, list):
|
||||
lines.append(f"{key} = [{', '.join(map(str, value))}];")
|
||||
return '\n'.join(lines)
|
||||
|
||||
def _matlab_kernel_creation(self, test_case):
|
||||
"""Generate MATLAB kernel creation code."""
|
||||
kernel_type = test_case.get('kernel_type', 'sim')
|
||||
if kernel_type == 'sim':
|
||||
return """
|
||||
% Create SIM kernel
|
||||
kern = kernCreate(t, 'sim');
|
||||
kern.decay = decay;
|
||||
kern.delay = delay;
|
||||
kern.variance = variance;
|
||||
kern.inverseWidth = inverseWidth;
|
||||
kern = kernParamInit(kern);
|
||||
"""
|
||||
elif kernel_type == 'disim':
|
||||
return """
|
||||
% Create DISIM kernel
|
||||
kern = kernCreate(t, 'disim');
|
||||
kern.decay = decay;
|
||||
kern.di_decay = di_decay;
|
||||
kern.variance = variance;
|
||||
kern.di_variance = di_variance;
|
||||
kern.inverseWidth = inverseWidth;
|
||||
kern.rbf_variance = rbf_variance;
|
||||
kern = kernParamInit(kern);
|
||||
"""
|
||||
else:
|
||||
raise ValueError(f"Unknown kernel type: {kernel_type}")
|
||||
|
||||
def _matlab_kernel_computation(self, test_case):
|
||||
"""Generate MATLAB kernel computation code."""
|
||||
return """
|
||||
% Compute kernel matrix
|
||||
K = kernCompute(kern, t);
|
||||
"""
|
||||
|
||||
def _matlab_params_struct(self, test_case):
|
||||
"""Generate MATLAB parameter structure."""
|
||||
params = test_case.get('params', {})
|
||||
param_str = ', '.join([f"'{k}', {v}" for k, v in params.items()])
|
||||
return f"struct({param_str})"
|
||||
|
||||
def run_matlab_computation(self, test_case):
|
||||
"""Run MATLAB computation for given test case."""
|
||||
script = self.create_matlab_script(test_case)
|
||||
|
||||
# Write script to temporary file
|
||||
script_path = os.path.join(self.temp_dir, 'compute_kernel.m')
|
||||
with open(script_path, 'w') as f:
|
||||
f.write(script)
|
||||
|
||||
# Run MATLAB
|
||||
try:
|
||||
if 'octave' in self.matlab_path.lower():
|
||||
cmd = [self.matlab_path, '--no-gui', '--silent', script_path]
|
||||
else:
|
||||
cmd = [self.matlab_path, '-batch', f"run('{script_path}')"]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"MATLAB error: {result.stderr}")
|
||||
return None
|
||||
|
||||
# Load results
|
||||
import scipy.io
|
||||
results_path = os.path.join(self.temp_dir, 'matlab_results.mat')
|
||||
if os.path.exists(results_path):
|
||||
matlab_data = scipy.io.loadmat(results_path)
|
||||
return matlab_data['results'][0, 0]
|
||||
else:
|
||||
print("MATLAB results file not found")
|
||||
return None
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
print("MATLAB computation timed out")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Error running MATLAB: {e}")
|
||||
return None
|
||||
|
||||
def create_test_cases(self):
|
||||
"""Create standard test cases for comparison."""
|
||||
test_cases = [
|
||||
{
|
||||
'name': 'sim_basic',
|
||||
'kernel_type': 'sim',
|
||||
'params': {
|
||||
'decay': 1.0,
|
||||
'delay': 0.1,
|
||||
'variance': 1.0,
|
||||
'inverseWidth': 1.0
|
||||
},
|
||||
't': np.linspace(0, 5, 20).reshape(-1, 1)
|
||||
},
|
||||
{
|
||||
'name': 'sim_fast_decay',
|
||||
'kernel_type': 'sim',
|
||||
'params': {
|
||||
'decay': 2.0,
|
||||
'delay': 0.0,
|
||||
'variance': 1.0,
|
||||
'inverseWidth': 0.5
|
||||
},
|
||||
't': np.linspace(0, 3, 15).reshape(-1, 1)
|
||||
},
|
||||
{
|
||||
'name': 'disim_basic',
|
||||
'kernel_type': 'disim',
|
||||
'params': {
|
||||
'decay': 1.0,
|
||||
'di_decay': 0.5,
|
||||
'variance': 1.0,
|
||||
'di_variance': 1.0,
|
||||
'inverseWidth': 1.0,
|
||||
'rbf_variance': 1.0
|
||||
},
|
||||
't': np.linspace(0, 5, 20).reshape(-1, 1)
|
||||
}
|
||||
]
|
||||
return test_cases
|
||||
|
||||
def compare_results(self, matlab_results, gpy_results, test_case):
|
||||
"""Compare MATLAB and GPy results."""
|
||||
if matlab_results is None or gpy_results is None:
|
||||
return {'status': 'error', 'message': 'Missing results'}
|
||||
|
||||
# Extract kernel matrices
|
||||
matlab_K = matlab_results['K']
|
||||
gpy_K = gpy_results['K']
|
||||
|
||||
# Basic shape check
|
||||
if matlab_K.shape != gpy_K.shape:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'Shape mismatch: MATLAB {matlab_K.shape} vs GPy {gpy_K.shape}'
|
||||
}
|
||||
|
||||
# Compute differences
|
||||
abs_diff = np.abs(matlab_K - gpy_K)
|
||||
rel_diff = abs_diff / (np.abs(matlab_K) + 1e-10)
|
||||
|
||||
comparison = {
|
||||
'status': 'success',
|
||||
'test_case': test_case['name'],
|
||||
'shapes_match': True,
|
||||
'max_abs_diff': float(np.max(abs_diff)),
|
||||
'mean_abs_diff': float(np.mean(abs_diff)),
|
||||
'max_rel_diff': float(np.max(rel_diff)),
|
||||
'mean_rel_diff': float(np.mean(rel_diff)),
|
||||
'matlab_shape': matlab_K.shape,
|
||||
'gpy_shape': gpy_K.shape
|
||||
}
|
||||
|
||||
# Check if results are close enough
|
||||
tolerance = 1e-6
|
||||
comparison['within_tolerance'] = comparison['max_abs_diff'] < tolerance
|
||||
|
||||
return comparison
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up temporary files."""
|
||||
import shutil
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main comparison function."""
|
||||
print("LFM Kernel MATLAB Comparison")
|
||||
print("=" * 40)
|
||||
|
||||
# Initialize comparison framework
|
||||
try:
|
||||
comparator = MATLABComparison()
|
||||
except RuntimeError as e:
|
||||
print(f"Error: {e}")
|
||||
print("Please ensure MATLAB or Octave is installed and in PATH")
|
||||
return
|
||||
|
||||
# Create test cases
|
||||
test_cases = comparator.create_test_cases()
|
||||
|
||||
results = []
|
||||
|
||||
for test_case in test_cases:
|
||||
print(f"\nTesting: {test_case['name']}")
|
||||
print("-" * 20)
|
||||
|
||||
# Run MATLAB computation
|
||||
print("Running MATLAB computation...")
|
||||
matlab_results = comparator.run_matlab_computation(test_case)
|
||||
|
||||
if matlab_results is None:
|
||||
print("MATLAB computation failed")
|
||||
continue
|
||||
|
||||
# TODO: Run GPy computation (when implemented)
|
||||
print("GPy computation not yet implemented")
|
||||
gpy_results = None
|
||||
|
||||
# Compare results
|
||||
if gpy_results is not None:
|
||||
comparison = comparator.compare_results(matlab_results, gpy_results, test_case)
|
||||
results.append(comparison)
|
||||
|
||||
if comparison['status'] == 'success':
|
||||
print(f"✓ Shapes match: {comparison['shapes_match']}")
|
||||
print(f"✓ Max abs diff: {comparison['max_abs_diff']:.2e}")
|
||||
print(f"✓ Within tolerance: {comparison['within_tolerance']}")
|
||||
else:
|
||||
print(f"✗ Error: {comparison['message']}")
|
||||
else:
|
||||
print("Skipping comparison (GPy not implemented yet)")
|
||||
|
||||
# Save results
|
||||
results_file = 'matlab_comparison_results.json'
|
||||
with open(results_file, 'w') as f:
|
||||
json.dump(results, f, indent=2)
|
||||
|
||||
print(f"\nResults saved to: {results_file}")
|
||||
|
||||
# Cleanup
|
||||
comparator.cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue