db = Database::getInstance(); $this->auth = new Auth(); $this->loadData(); $this->loadApprovedDirectives(); } /** * Load approved directives (impossible combinations) from OptimAIze */ protected function loadApprovedDirectives() { $query = $this->db->query(" SELECT attribute1_id, attribute2_id, choice1, choice2, llm_reasoning FROM panel_directives WHERE status = 'approved' AND is_impossible = 1 "); $this->approvedDirectives = []; if ($query) { while ($directive = $query->fetch_assoc()) { $this->approvedDirectives[] = [ 'attr1_id' => $directive['attribute1_id'], 'attr2_id' => $directive['attribute2_id'], 'choice1' => $directive['choice1'], 'choice2' => $directive['choice2'], 'reasoning' => $directive['llm_reasoning'] ]; } } error_log("Loaded " . count($this->approvedDirectives) . " approved impossible combinations for panel generation"); } protected function loadData() { // Get existing panel count $result = $this->db->query("SELECT COUNT(*) as count FROM panel_data"); $this->existingCount = $result->fetch_assoc()['count']; // Load statistics $this->statistics = []; $stats_query = $this->db->query(" SELECT s.id, s.name, s.type, GROUP_CONCAT(DISTINCT sa.attribute_id) as attribute_ids, GROUP_CONCAT(DISTINCT a.name) as attribute_names FROM statistics s LEFT JOIN statistic_attributes sa ON s.id = sa.statistic_id LEFT JOIN attributes a ON sa.attribute_id = a.id GROUP BY s.id "); while ($stat = $stats_query->fetch_assoc()) { $attribute_ids = $stat['attribute_ids'] ? explode(',', $stat['attribute_ids']) : []; // Get combinations for this statistic $combinations_query = $this->db->query(" SELECT combination_values, percentage FROM statistic_combinations WHERE statistic_id = " . $stat['id'] ); $combinations = []; while ($combo = $combinations_query->fetch_assoc()) { $combinations[] = [ 'values' => json_decode($combo['combination_values'], true), 'percentage' => floatval($combo['percentage']) ]; } $this->statistics[$stat['id']] = [ 'name' => $stat['name'], 'type' => $stat['type'], 'attribute_ids' => $attribute_ids, 'attribute_names' => $stat['attribute_names'], 'combinations' => $combinations ]; } // Load attributes $this->attributes = []; $this->attributesById = []; $attr_query = $this->db->query("SELECT * FROM attributes ORDER BY created_at ASC"); while ($attr = $attr_query->fetch_assoc()) { $attr['choices'] = json_decode($attr['choices'], true); $this->attributes[] = $attr; $this->attributesById[$attr['id']] = $attr; } } public function calculateOptimalCount() { try { if (empty($this->statistics)) { return ['success' => false, 'message' => 'No statistics available']; } $minCount = 1000; foreach ($this->statistics as $stat) { foreach ($stat['combinations'] as $combo) { if ($combo['percentage'] > 0) { $requiredCount = ceil(100 / $combo['percentage']) * 10; $minCount = max($minCount, $requiredCount); } } } return [ 'success' => true, 'optimal_count' => $minCount, 'existing_count' => $this->existingCount ]; } catch (Exception $e) { error_log("Calculate optimal count error: " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } /** * Validate attribute combination against approved directives */ protected function isValidCombination($attributeValues) { foreach ($this->approvedDirectives as $directive) { $attr1_id = $directive['attr1_id']; $attr2_id = $directive['attr2_id']; $choice1 = $directive['choice1']; $choice2 = $directive['choice2']; // Check if this combination exists in the current panelist's attributes if (isset($attributeValues[$attr1_id]) && isset($attributeValues[$attr2_id])) { $value1 = $attributeValues[$attr1_id]; $value2 = $attributeValues[$attr2_id]; // Handle both single and multiple choice attributes $hasChoice1 = false; $hasChoice2 = false; if (is_array($value1)) { $hasChoice1 = in_array($choice1, $value1); } else { $hasChoice1 = ($value1 === $choice1); } if (is_array($value2)) { $hasChoice2 = in_array($choice2, $value2); } else { $hasChoice2 = ($value2 === $choice2); } // Also check reverse combination (attr2, attr1) $hasChoice1Rev = false; $hasChoice2Rev = false; if (is_array($value1)) { $hasChoice2Rev = in_array($choice2, $value1); } else { $hasChoice2Rev = ($value1 === $choice2); } if (is_array($value2)) { $hasChoice1Rev = in_array($choice1, $value2); } else { $hasChoice1Rev = ($value2 === $choice1); } // If this impossible combination exists, return false if (($hasChoice1 && $hasChoice2) || ($hasChoice1Rev && $hasChoice2Rev)) { error_log("Invalid combination detected: {$choice1} + {$choice2} (Reason: {$directive['reasoning']})"); return false; } } } return true; } /** * Attempt to fix invalid attribute combinations */ protected function fixInvalidCombination($attributeValues) { foreach ($this->approvedDirectives as $directive) { $attr1_id = $directive['attr1_id']; $attr2_id = $directive['attr2_id']; $choice1 = $directive['choice1']; $choice2 = $directive['choice2']; if (!isset($attributeValues[$attr1_id]) || !isset($attributeValues[$attr2_id])) { continue; } $value1 = $attributeValues[$attr1_id]; $value2 = $attributeValues[$attr2_id]; // Check if we have the problematic combination $hasProblematicCombo = false; if (is_array($value1) && is_array($value2)) { $hasProblematicCombo = (in_array($choice1, $value1) && in_array($choice2, $value2)) || (in_array($choice2, $value1) && in_array($choice1, $value2)); } elseif (is_array($value1)) { $hasProblematicCombo = (in_array($choice1, $value1) && $value2 === $choice2) || (in_array($choice2, $value1) && $value2 === $choice1); } elseif (is_array($value2)) { $hasProblematicCombo = ($value1 === $choice1 && in_array($choice2, $value2)) || ($value1 === $choice2 && in_array($choice1, $value2)); } else { $hasProblematicCombo = ($value1 === $choice1 && $value2 === $choice2) || ($value1 === $choice2 && $value2 === $choice1); } if ($hasProblematicCombo) { // Try to fix by changing one of the attributes to a different valid choice $attribute1 = $this->attributesById[$attr1_id] ?? null; $attribute2 = $this->attributesById[$attr2_id] ?? null; if ($attribute1 && count($attribute1['choices']) > 1) { // Try to change attribute 1 to a different choice $availableChoices = array_diff($attribute1['choices'], [$choice1, $choice2]); if (!empty($availableChoices)) { if ($attribute1['choice_type'] === 'multiple') { // For multiple choice, replace problematic choices $newValue = is_array($value1) ? $value1 : [$value1]; $newValue = array_diff($newValue, [$choice1, $choice2]); if (empty($newValue)) { $newValue[] = $availableChoices[array_rand($availableChoices)]; } $attributeValues[$attr1_id] = $newValue; } else { // For single choice, pick a different option $attributeValues[$attr1_id] = $availableChoices[array_rand($availableChoices)]; } error_log("Fixed combination by changing attribute {$attr1_id} value"); } } } } return $attributeValues; } /** * Generate fallback panelist data when optimization fails */ protected function generateFallbackPanelistData() { // Use a more conservative approach - generate attributes one by one // and validate each addition $attributeValues = []; foreach ($this->attributesById as $attrId => $attribute) { $choices = $attribute['choices']; $validChoices = []; foreach ($choices as $choice) { $testValues = $attributeValues; if ($attribute['choice_type'] === 'multiple') { $testValues[$attrId] = [$choice]; } else { $testValues[$attrId] = $choice; } if ($this->isValidCombination($testValues)) { $validChoices[] = $choice; } } if (empty($validChoices)) { // If no valid single choices, use the first available choice as fallback $validChoices = [$choices[0]]; error_log("Warning: No valid choices found for attribute {$attrId}, using fallback"); } if ($attribute['choice_type'] === 'multiple') { // For multiple choice, randomly select 1-3 valid choices $numChoices = min(rand(1, 3), count($validChoices)); $selectedChoices = array_rand(array_flip($validChoices), $numChoices); $attributeValues[$attrId] = is_array($selectedChoices) ? $selectedChoices : [$selectedChoices]; } else { // For single choice, randomly select one valid choice $attributeValues[$attrId] = $validChoices[array_rand($validChoices)]; } } return $attributeValues; } /** * Generate optimized panelist data avoiding impossible combinations */ protected function generateOptimizedPanelistData() { $maxAttempts = 50; // Maximum attempts to generate valid combination $attempts = 0; while ($attempts < $maxAttempts) { $attempts++; // Generate panelist data using existing logic $attributeValues = $this->generateAttributeValues(); // Validate against approved directives if ($this->isValidCombination($attributeValues)) { return $attributeValues; } // If invalid, try to fix the combination $attributeValues = $this->fixInvalidCombination($attributeValues); if ($this->isValidCombination($attributeValues)) { return $attributeValues; } } error_log("Warning: Could not generate valid combination after {$maxAttempts} attempts. Using fallback method."); return $this->generateFallbackPanelistData(); } protected function generateAttributeValues() { $values = []; foreach ($this->attributes as $attr) { $choices = $attr['choices']; if ($attr['choice_type'] === 'multiple') { // For multiple choice, select 1-3 random choices $numChoices = rand(1, min(3, count($choices))); $selectedChoices = array_rand(array_flip($choices), $numChoices); $values[$attr['id']] = is_array($selectedChoices) ? $selectedChoices : [$selectedChoices]; } else { // For single choice, select one random choice $values[$attr['id']] = $choices[array_rand($choices)]; } } return $values; } public function generatePanelData($targetCount) { try { error_log("Starting panel generation with target count: {$targetCount}"); // Check if we have OptimAIze directives $hasOptimization = count($this->approvedDirectives) > 0; if ($hasOptimization) { error_log("Using OptimAIze: " . count($this->approvedDirectives) . " impossible combinations to avoid"); } else { error_log("No OptimAIze directives available, using standard generation"); } $_SESSION['panel_generation_progress'] = 0; $_SESSION['panel_generation_status'] = 'Starting generation...'; // Clear existing panel data $this->db->query("DELETE FROM panel_data"); $this->db->query("DELETE FROM panel_processing_status"); $this->db->query("UPDATE statistic_combinations SET actual_percentage = NULL"); $batchSize = 100; $totalBatches = ceil($targetCount / $batchSize); $generated = 0; for ($batch = 0; $batch < $totalBatches; $batch++) { $currentBatchSize = min($batchSize, $targetCount - $generated); if ($this->generateBatch($currentBatchSize, $generated + 1, $hasOptimization)) { $generated += $currentBatchSize; $progress = (($batch + 1) / $totalBatches) * 100; $_SESSION['panel_generation_progress'] = $progress; $_SESSION['panel_generation_status'] = "Generated {$generated} panelists..." . ($hasOptimization ? " (OptimAIze active)" : ""); error_log("Generated batch " . ($batch + 1) . "/{$totalBatches} - Total: {$generated} panelists"); } else { throw new Exception("Failed to generate batch " . ($batch + 1)); } } $_SESSION['panel_generation_progress'] = 100; $_SESSION['panel_generation_status'] = 'Complete!' . ($hasOptimization ? " (OptimAIze applied)" : ""); // Update statistics checks to compute Panel % $this->updateStatisticChecks(); $alignmentScore = $this->calculateAlignmentScore(); return [ 'success' => true, 'generated_count' => $generated, 'alignment_score' => $alignmentScore['score'] ?? null, 'optimization_used' => $hasOptimization, 'impossible_combinations_avoided' => count($this->approvedDirectives) ]; } catch (Exception $e) { error_log("Panel generation error: " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } protected function generateBatch($batchSize, $startId, $hasOptimization = false) { try { $this->db->query("START TRANSACTION"); for ($i = 0; $i < $batchSize; $i++) { $panelistId = sprintf("SYN%06d", $startId + $i); // Generate attribute values with or without optimization if ($hasOptimization) { $attributeValues = $this->generateOptimizedPanelistData(); } else { $attributeValues = $this->generateAttributeValues(); } $sql = "INSERT INTO panel_data (panelist_id, attribute_values, created_by) VALUES ( '" . $this->db->escape($panelistId) . "', '" . $this->db->escape(json_encode($attributeValues)) . "', " . $_SESSION['user_id'] . " )"; if (!$this->db->query($sql)) { throw new Exception("Failed to insert panelist data: " . $this->db->getLastError()); } } $this->db->query("COMMIT"); return true; } catch (Exception $e) { $this->db->query("ROLLBACK"); error_log("Generate batch error: " . $e->getMessage()); throw $e; } } public function updateStatisticChecks() { try { // Get total panel count $totalResult = $this->db->query("SELECT COUNT(*) as total FROM panel_data"); $totalPanelists = $totalResult ? $totalResult->fetch_assoc()['total'] : 0; if ($totalPanelists == 0) { return ['success' => true, 'message' => 'No panel data to analyze']; } // Update actual percentages for each statistic combination $statisticsQuery = $this->db->query(" SELECT s.id as statistic_id, sc.id as combination_id, sc.combination_values, sc.percentage as target_percentage, GROUP_CONCAT(sa.attribute_id ORDER BY sa.id) as attribute_ids FROM statistics s JOIN statistic_combinations sc ON s.id = sc.statistic_id JOIN statistic_attributes sa ON s.id = sa.statistic_id GROUP BY s.id, sc.id "); while ($stat = $statisticsQuery->fetch_assoc()) { $attributeIds = explode(',', $stat['attribute_ids']); $combinationValues = json_decode($stat['combination_values'], true); if (count($attributeIds) !== count($combinationValues)) { continue; // Skip if mismatch } // Build query to count matching panelists $whereConditions = []; for ($i = 0; $i < count($attributeIds); $i++) { $attrId = $attributeIds[$i]; $value = $combinationValues[$i]; $attribute = $this->attributesById[$attrId] ?? null; if (!$attribute) continue; if ($attribute['choice_type'] === 'multiple') { $whereConditions[] = "JSON_CONTAINS(attribute_values, '\"$value\"', '$.$attrId')"; } else { $whereConditions[] = "JSON_UNQUOTE(JSON_EXTRACT(attribute_values, '$.$attrId')) = '$value'"; } } if (!empty($whereConditions)) { $whereClause = implode(' AND ', $whereConditions); $countQuery = $this->db->query("SELECT COUNT(*) as count FROM panel_data WHERE $whereClause"); $count = $countQuery ? $countQuery->fetch_assoc()['count'] : 0; $actualPercentage = ($count / $totalPanelists) * 100; // Update the combination with actual percentage $updateStmt = $this->db->prepare(" UPDATE statistic_combinations SET actual_percentage = ? WHERE id = ? "); $updateStmt->bind_param('di', $actualPercentage, $stat['combination_id']); $updateStmt->execute(); } } return ['success' => true, 'message' => 'Statistics updated successfully']; } catch (Exception $e) { error_log("Update statistic checks error: " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } public function calculateAlignmentScore() { try { // If panel is empty, return 100% deviation (0% alignment) $countCheck = $this->db->query("SELECT COUNT(*) as count FROM panel_data"); if ($countCheck->fetch_assoc()['count'] == 0) { return [ 'success' => true, 'score' => 100.00, // Maximum deviation 'message' => 'Panel is empty' ]; } // Make sure statistics are updated $this->updateStatisticChecks(); // Calculate average deviation from all combinations $query = $this->db->query(" SELECT sc.percentage as target_percentage, sc.actual_percentage FROM statistic_combinations sc WHERE sc.actual_percentage IS NOT NULL "); $totalDeviation = 0; $combinationCount = 0; while ($row = $query->fetch_assoc()) { $targetPct = floatval($row['target_percentage']); $actualPct = floatval($row['actual_percentage']); // Calculate deviation as percentage difference $deviation = abs($actualPct - $targetPct); $totalDeviation += $deviation; $combinationCount++; } // Calculate average deviation $averageDeviation = $combinationCount > 0 ? ($totalDeviation / $combinationCount) : 100; // Ensure deviation is capped at 100% $averageDeviation = min(100, $averageDeviation); return [ 'success' => true, 'score' => round($averageDeviation, 2), 'message' => 'Score calculated successfully' ]; } catch (Exception $e) { error_log("Alignment score calculation error: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } public function calculateRmseScore() { try { $countCheck = $this->db->query("SELECT COUNT(*) as count FROM panel_data"); $panelCount = $countCheck->fetch_assoc()['count']; if ($panelCount == 0) { return [ 'success' => true, 'rmse' => 100, 'message' => 'Panel is empty' ]; } $columnCheck = $this->db->query("SHOW COLUMNS FROM statistic_combinations LIKE 'actual_percentage'"); if ($columnCheck->num_rows === 0) { return [ 'success' => false, 'message' => 'Actual percentage column not available' ]; } $query = $this->db->query(" SELECT SQRT(AVG(POW(percentage - actual_percentage, 2))) as rmse FROM statistic_combinations WHERE actual_percentage IS NOT NULL "); if ($query && $row = $query->fetch_assoc()) { $rmse = $row['rmse'] !== null ? floatval($row['rmse']) : 100; $rmse = $rmse > 100 ? 100 : floatval($row['rmse']); return [ 'success' => true, 'rmse' => $rmse, 'message' => 'RMSE calculated successfully' ]; } else { return [ 'success' => false, 'message' => 'No valid combinations found for RMSE calculation' ]; } } catch (Exception $e) { error_log("RMSE calculation error: " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } public function getProgress() { $alignmentScore = null; $progress = $_SESSION['panel_generation_progress'] ?? 0; if ($progress >= 100) { $alignmentResult = $this->calculateAlignmentScore(); if ($alignmentResult['success']) { $alignmentScore = $alignmentResult['score']; } } return [ 'success' => true, 'progress' => $progress, 'status' => $_SESSION['panel_generation_status'] ?? 'Initializing...', 'alignment_score' => $alignmentScore ]; } public function deletePanelist($panelistId) { try { $sql = "DELETE FROM panel_data WHERE panelist_id = '" . $this->db->escape($panelistId) . "'"; if ($this->db->query($sql)) { // Update statistics after deleting a panelist $this->updateStatisticChecks(); $alignmentScore = $this->calculateAlignmentScore(); return [ 'success' => true, 'alignment_score' => $alignmentScore['score'] ?? null ]; } throw new Exception("Failed to delete panelist"); } catch (Exception $e) { error_log("Delete panelist error: " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } public function deletePanelData() { try { if (!$this->auth->isAdmin()) { throw new Exception('Admin access required'); } $this->db->query("DELETE FROM panel_data"); $this->db->query("DELETE FROM panel_processing_status"); $this->db->query("UPDATE statistic_combinations SET actual_percentage = NULL"); return [ 'success' => true, 'message' => 'Panel data deleted successfully' ]; } catch (Exception $e) { error_log("Delete panel data error: " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } } // Handle requests header('Content-Type: application/json'); $auth = new Auth(); if (!$auth->isLoggedIn()) { echo json_encode(['success' => false, 'message' => 'Unauthorized']); exit; } $handler = new PanelAlignmentHandler(); $action = $_POST['action'] ?? ''; $response = ['success' => false, 'message' => 'Invalid action']; switch ($action) { case 'calculate_optimal': $response = $handler->calculateOptimalCount(); break; case 'generate_panel': $count = intval($_POST['count'] ?? 0); if ($count > 0) { $response = $handler->generatePanelData($count); } else { $response = ['success' => false, 'message' => 'Invalid count']; } break; case 'get_progress': $response = $handler->getProgress(); break; case 'delete_panelist': $panelistId = $_POST['panelist_id'] ?? ''; if ($panelistId) { $response = $handler->deletePanelist($panelistId); } else { $response = ['success' => false, 'message' => 'Invalid panelist ID']; } break; case 'get_alignment_score': $response = $handler->calculateAlignmentScore(); break; case 'get_rmse_score': $response = $handler->calculateRmseScore(); break; case 'delete_panel': $response = $handler->deletePanelData(); break; case 'update_statistic_checks': $response = $handler->updateStatisticChecks(); break; } echo json_encode($response); ?>