""" test_synthetic.py Synthetic tests with known analytical solutions Tests basic functionality without real FEA data: - Model can be created - Forward pass works - Loss functions compute correctly - Predictions have correct shape """ import torch import numpy as np import sys from pathlib import Path # Add parent directory to path sys.path.insert(0, str(Path(__file__).parent.parent)) from neural_models.field_predictor import create_model from neural_models.physics_losses import create_loss_function from torch_geometric.data import Data def test_model_creation(): """ Test 1: Can we create the model? Expected: Model instantiates with correct number of parameters """ print(" Creating GNN model...") config = { 'node_feature_dim': 12, 'edge_feature_dim': 5, 'hidden_dim': 64, 'num_layers': 4, 'dropout': 0.1 } model = create_model(config) # Count parameters num_params = sum(p.numel() for p in model.parameters()) print(f" Model created: {num_params:,} parameters") return { 'status': 'PASS', 'message': f'Model created successfully ({num_params:,} params)', 'metrics': {'parameters': num_params} } def test_forward_pass(): """ Test 2: Can model process data? Expected: Forward pass completes without errors """ print(" Testing forward pass...") # Create model config = { 'node_feature_dim': 12, 'edge_feature_dim': 5, 'hidden_dim': 64, 'num_layers': 4, 'dropout': 0.1 } model = create_model(config) model.eval() # Create dummy data num_nodes = 100 num_edges = 300 x = torch.randn(num_nodes, 12) # Node features edge_index = torch.randint(0, num_nodes, (2, num_edges)) # Connectivity edge_attr = torch.randn(num_edges, 5) # Edge features batch = torch.zeros(num_nodes, dtype=torch.long) # Batch assignment data = Data(x=x, edge_index=edge_index, edge_attr=edge_attr, batch=batch) # Forward pass with torch.no_grad(): results = model(data, return_stress=True) # Check outputs assert 'displacement' in results, "Missing displacement output" assert 'stress' in results, "Missing stress output" assert 'von_mises' in results, "Missing von Mises output" # Check shapes assert results['displacement'].shape == (num_nodes, 6), f"Wrong displacement shape: {results['displacement'].shape}" assert results['stress'].shape == (num_nodes, 6), f"Wrong stress shape: {results['stress'].shape}" assert results['von_mises'].shape == (num_nodes,), f"Wrong von Mises shape: {results['von_mises'].shape}" print(f" Displacement shape: {results['displacement'].shape} [OK]") print(f" Stress shape: {results['stress'].shape} [OK]") print(f" Von Mises shape: {results['von_mises'].shape} [OK]") return { 'status': 'PASS', 'message': 'Forward pass successful', 'metrics': { 'num_nodes': num_nodes, 'displacement_shape': list(results['displacement'].shape), 'stress_shape': list(results['stress'].shape) } } def test_loss_computation(): """ Test 3: Do loss functions work? Expected: All loss types compute without errors """ print(" Testing loss functions...") # Create dummy predictions and targets num_nodes = 100 predictions = { 'displacement': torch.randn(num_nodes, 6), 'stress': torch.randn(num_nodes, 6), 'von_mises': torch.abs(torch.randn(num_nodes)) } targets = { 'displacement': torch.randn(num_nodes, 6), 'stress': torch.randn(num_nodes, 6) } loss_types = ['mse', 'relative', 'physics', 'max'] loss_values = {} for loss_type in loss_types: loss_fn = create_loss_function(loss_type) losses = loss_fn(predictions, targets) assert 'total_loss' in losses, f"Missing total_loss for {loss_type}" assert not torch.isnan(losses['total_loss']), f"NaN loss for {loss_type}" assert not torch.isinf(losses['total_loss']), f"Inf loss for {loss_type}" loss_values[loss_type] = losses['total_loss'].item() print(f" {loss_type.upper()} loss: {loss_values[loss_type]:.6f} [OK]") return { 'status': 'PASS', 'message': 'All loss functions working', 'metrics': loss_values } def test_batch_processing(): """ Test 4: Can model handle batches? Expected: Batch processing works correctly """ print(" Testing batch processing...") config = { 'node_feature_dim': 12, 'edge_feature_dim': 5, 'hidden_dim': 64, 'num_layers': 4, 'dropout': 0.1 } model = create_model(config) model.eval() # Create batch of 3 graphs graphs = [] for i in range(3): num_nodes = 50 + i * 10 # Different sizes num_edges = 150 + i * 30 x = torch.randn(num_nodes, 12) edge_index = torch.randint(0, num_nodes, (2, num_edges)) edge_attr = torch.randn(num_edges, 5) batch = torch.full((num_nodes,), i, dtype=torch.long) graphs.append(Data(x=x, edge_index=edge_index, edge_attr=edge_attr, batch=batch)) # Process batch total_nodes = sum(g.x.shape[0] for g in graphs) with torch.no_grad(): for i, graph in enumerate(graphs): results = model(graph, return_stress=True) print(f" Graph {i+1}: {graph.x.shape[0]} nodes -> predictions [OK]") return { 'status': 'PASS', 'message': 'Batch processing successful', 'metrics': {'num_graphs': len(graphs), 'total_nodes': total_nodes} } def test_gradient_flow(): """ Test 5: Do gradients flow correctly? Expected: Gradients computed without errors """ print(" Testing gradient flow...") config = { 'node_feature_dim': 12, 'edge_feature_dim': 5, 'hidden_dim': 64, 'num_layers': 4, 'dropout': 0.1 } model = create_model(config) model.train() # Create dummy data num_nodes = 50 num_edges = 150 x = torch.randn(num_nodes, 12) edge_index = torch.randint(0, num_nodes, (2, num_edges)) edge_attr = torch.randn(num_edges, 5) batch = torch.zeros(num_nodes, dtype=torch.long) data = Data(x=x, edge_index=edge_index, edge_attr=edge_attr, batch=batch) # Forward pass results = model(data, return_stress=True) # Compute loss targets = { 'displacement': torch.randn(num_nodes, 6), 'stress': torch.randn(num_nodes, 6) } loss_fn = create_loss_function('mse') losses = loss_fn(results, targets) # Backward pass losses['total_loss'].backward() # Check gradients has_grad = sum(1 for p in model.parameters() if p.grad is not None) total_params = sum(1 for _ in model.parameters()) print(f" Parameters with gradients: {has_grad}/{total_params} [OK]") assert has_grad == total_params, f"Not all parameters have gradients" return { 'status': 'PASS', 'message': 'Gradients computed successfully', 'metrics': { 'parameters_with_grad': has_grad, 'total_parameters': total_params } } if __name__ == "__main__": print("\nRunning synthetic tests...\n") tests = [ ("Model Creation", test_model_creation), ("Forward Pass", test_forward_pass), ("Loss Computation", test_loss_computation), ("Batch Processing", test_batch_processing), ("Gradient Flow", test_gradient_flow) ] passed = 0 failed = 0 for name, test_func in tests: print(f"[TEST] {name}") try: result = test_func() if result['status'] == 'PASS': print(f" [PASS]\n") passed += 1 else: print(f" [FAIL]: {result['message']}\n") failed += 1 except Exception as e: print(f" [FAIL]: {str(e)}\n") failed += 1 print(f"\nResults: {passed} passed, {failed} failed")