diff --git a/align_browser/experiment_parser.py b/align_browser/experiment_parser.py index 2d6ce32..eaa9acd 100644 --- a/align_browser/experiment_parser.py +++ b/align_browser/experiment_parser.py @@ -161,12 +161,13 @@ def parse_experiments_directory(experiments_root: Path) -> List[ExperimentData]: """ Parse the experiments directory structure and return a list of ExperimentData. - Recursively searches through the directory structure to find all directories + First checks if the given path itself is an experiment directory, then + recursively searches through the directory structure to find all directories that contain the required experiment files (input_output.json, timing.json, and .hydra/config.yaml). scores.json is optional. Args: - experiments_root: Path to the root experiments directory + experiments_root: Path to the root experiments directory or a direct experiment directory Returns: List of successfully parsed ExperimentData objects @@ -177,6 +178,18 @@ def parse_experiments_directory(experiments_root: Path) -> List[ExperimentData]: directories_with_files = 0 directories_processed = 0 + # First check if the root path itself is an experiment directory + if ExperimentData.has_required_files(experiments_root): + directories_found += 1 + directories_with_files += 1 + + try: + directory_experiments = _create_experiments_from_directory(experiments_root) + experiments.extend(directory_experiments) + directories_processed += 1 + except Exception as e: + print(f"Error processing {experiments_root}: {e}") + # Recursively find all directories that have required experiment files for experiment_dir in experiments_root.rglob("*"): if not experiment_dir.is_dir(): diff --git a/align_browser/static/app.js b/align_browser/static/app.js index 30cdcbb..fee5a83 100644 --- a/align_browser/static/app.js +++ b/align_browser/static/app.js @@ -10,23 +10,17 @@ import { KDMAUtils, } from './state.js'; +import { + formatValue, + compareValues, + getMaxKDMAsForRun, + getMinimumRequiredKDMAs, + getValidKDMAsForRun +} from './table-formatter.js'; + // Constants -const TEXT_PREVIEW_LENGTH = 800; -const FLOATING_POINT_TOLERANCE = 0.001; const KDMA_SLIDER_DEBOUNCE_MS = 500; -// CSS Classes -const CSS_TABLE_LLM_SELECT = 'table-llm-select'; -const CSS_TABLE_ADM_SELECT = 'table-adm-select'; -const CSS_TABLE_SCENARIO_SELECT = 'table-scenario-select'; -const CSS_TABLE_RUN_VARIANT_SELECT = 'table-run-variant-select'; - -// HTML Templates -const HTML_NA_SPAN = 'N/A'; -const HTML_NO_OPTIONS_SPAN = 'No options available'; -const HTML_NO_SCENE_SPAN = 'No scene'; -const HTML_NO_KDMAS_SPAN = 'No KDMAs'; - document.addEventListener("DOMContentLoaded", () => { // UI state persistence for expandable content @@ -307,10 +301,10 @@ document.addEventListener("DOMContentLoaded", () => { window.addKDMAToRun = async function(runId) { const run = appState.pinnedRuns.get(runId); - const availableKDMAs = getValidKDMAsForRun(runId); + const availableKDMAs = getValidKDMAsForRun(runId, appState.pinnedRuns); const currentKDMAs = run.kdmaValues || {}; - const maxKDMAs = getMaxKDMAsForRun(runId); - const minimumRequired = getMinimumRequiredKDMAs(runId); + const maxKDMAs = getMaxKDMAsForRun(runId, appState.pinnedRuns); + const minimumRequired = getMinimumRequiredKDMAs(runId, appState.pinnedRuns); if (Object.keys(currentKDMAs).length >= maxKDMAs) { console.warn(`Cannot add KDMA: max limit (${maxKDMAs}) reached for run ${runId}`); @@ -414,7 +408,7 @@ document.addEventListener("DOMContentLoaded", () => { // Handle KDMA type change for pinned run - global for onclick access window.handleRunKDMATypeChange = async function(runId, oldKdmaType, newKdmaType) { - const availableKDMAs = getValidKDMAsForRun(runId); + const availableKDMAs = getValidKDMAsForRun(runId, appState.pinnedRuns); await updateKDMAsForRun(runId, (kdmas) => { const updated = { ...kdmas }; @@ -446,7 +440,7 @@ document.addEventListener("DOMContentLoaded", () => { // Update the display value immediately for responsiveness const valueDisplay = document.getElementById(`kdma-value-${runId}-${kdmaType}`); if (valueDisplay) { - valueDisplay.textContent = formatKDMAValue(normalizedValue); + valueDisplay.textContent = KDMAUtils.formatValue(normalizedValue); } // Update the KDMA values with debouncing @@ -605,7 +599,7 @@ document.addEventListener("DOMContentLoaded", () => { if (isDifferent) { td.style.borderLeft = '3px solid #007bff'; } - td.innerHTML = formatValue(pinnedValue, paramInfo.type, paramName, runData.id); + td.innerHTML = formatValue(pinnedValue, paramInfo.type, paramName, runData.id, appState.pinnedRuns); row.appendChild(td); @@ -624,6 +618,7 @@ document.addEventListener("DOMContentLoaded", () => { parameters.set("scenario", { type: "string", required: true }); parameters.set("scenario_state", { type: "longtext", required: false }); parameters.set("available_choices", { type: "choices", required: false }); + parameters.set("choice_info", { type: "choice_info", required: false }); parameters.set("kdma_values", { type: "kdma_values", required: false }); parameters.set("adm_type", { type: "string", required: true }); parameters.set("llm_backbone", { type: "string", required: true }); @@ -668,6 +663,11 @@ document.addEventListener("DOMContentLoaded", () => { return run.inputOutput.input.choices; } + // Choice info + if (paramName === 'choice_info' && run.inputOutput?.choice_info) { + return run.inputOutput.choice_info; + } + // ADM Decision - proper extraction using Pydantic model structure if (paramName === 'adm_decision' && run.inputOutput?.output && run.inputOutput?.input?.choices) { const choiceIndex = run.inputOutput.output.choice; @@ -696,572 +696,9 @@ document.addEventListener("DOMContentLoaded", () => { return 'N/A'; } - // Generic dropdown creation function - function createDropdownForRun(runId, currentValue, options) { - const { - optionsPath, - cssClass, - onChangeHandler, - noOptionsMessage = null, - preCondition = null - } = options; - - const run = appState.pinnedRuns.get(runId); - if (!run) return escapeHtml(currentValue); - - // Check pre-condition if provided - if (preCondition && !preCondition(run)) { - return noOptionsMessage || HTML_NA_SPAN; - } - - // Get options from the specified path in run.availableOptions - const availableOptions = optionsPath.split('.').reduce((obj, key) => obj?.[key], run.availableOptions); - if (!availableOptions || availableOptions.length === 0) { - return noOptionsMessage || HTML_NO_OPTIONS_SPAN; - } - - const sortedOptions = [...availableOptions].sort(); - - // Always disable dropdowns when there are few options - const isDisabled = availableOptions.length <= 1; - const disabledAttr = isDisabled ? 'disabled' : ''; - - let html = `'; - - return html; - } - - // Dropdown configuration for different parameter types - const DROPDOWN_CONFIGS = { - llm: { - optionsPath: 'llms', - cssClass: CSS_TABLE_LLM_SELECT, - onChangeHandler: 'handleRunLLMChange' - }, - adm: { - optionsPath: 'admTypes', - cssClass: CSS_TABLE_ADM_SELECT, - onChangeHandler: 'handleRunADMChange' - }, - scene: { - optionsPath: 'scenes', - cssClass: CSS_TABLE_SCENARIO_SELECT, - onChangeHandler: 'handleRunSceneChange' - }, - scenario: { - optionsPath: 'scenarios', - cssClass: CSS_TABLE_SCENARIO_SELECT, - onChangeHandler: 'handleRunScenarioChange', - preCondition: (run) => run.scene, - noOptionsMessage: HTML_NO_SCENE_SPAN - } - }; - - // Generic dropdown creation factory - const createDropdownForParameter = (parameterType) => { - return (runId, currentValue) => { - const config = DROPDOWN_CONFIGS[parameterType]; - return createDropdownForRun(runId, currentValue, config); - }; - }; - // Create dropdown functions using the factory - const createLLMDropdownForRun = createDropdownForParameter('llm'); - const createADMDropdownForRun = createDropdownForParameter('adm'); - const createSceneDropdownForRun = createDropdownForParameter('scene'); - const createSpecificScenarioDropdownForRun = createDropdownForParameter('scenario'); - // Create dropdown HTML for run variant selection in table cells - function createRunVariantDropdownForRun(runId, currentValue) { - const run = appState.pinnedRuns.get(runId); - if (!run) return escapeHtml(currentValue); - - // Use the run's actual runVariant to ensure correct selection after parameter updates - const actualCurrentValue = run.runVariant; - - return createDropdownForRun(runId, actualCurrentValue, { - optionsPath: 'runVariants', - cssClass: CSS_TABLE_RUN_VARIANT_SELECT, - onChangeHandler: 'handleRunVariantChange' - }); - } - // Get max KDMAs allowed for a specific run based on its constraints and current selections - function getMaxKDMAsForRun(runId) { - const run = appState.pinnedRuns.get(runId); - if (!run) return 0; - - const kdmaOptions = run.availableOptions?.kdmas; - if (!kdmaOptions || !kdmaOptions.validCombinations) { - return 1; // Default to at least 1 KDMA if no options available - } - - // Find the maximum number of KDMAs in any valid combination - let maxKDMAs = 0; - kdmaOptions.validCombinations.forEach(combination => { - maxKDMAs = Math.max(maxKDMAs, Object.keys(combination).length); - }); - - return Math.max(maxKDMAs, 1); // At least 1 KDMA should be possible - } - - // Get minimum required KDMAs for a run - if all combinations have the same count, return that count - function getMinimumRequiredKDMAs(runId) { - const run = appState.pinnedRuns.get(runId); - if (!run?.availableOptions?.kdmas?.validCombinations) { - return 1; // Default to 1 if no options available - } - - const combinations = run.availableOptions.kdmas.validCombinations; - if (combinations.length === 0) { - return 1; - } - - // Filter out empty combinations (unaligned cases with 0 KDMAs) - const nonEmptyCombinations = combinations.filter(combination => Object.keys(combination).length > 0); - - if (nonEmptyCombinations.length === 0) { - return 1; // Only empty combinations available - } - - // Get the count of KDMAs in each non-empty combination - const kdmaCounts = nonEmptyCombinations.map(combination => Object.keys(combination).length); - - // Check if all non-empty combinations have the same number of KDMAs - const firstCount = kdmaCounts[0]; - const allSameCount = kdmaCounts.every(count => count === firstCount); - - if (allSameCount && firstCount > 1) { - return firstCount; // All non-empty combinations require the same number > 1 - } - return 1; // Either mixed counts or all require 1, use single-add behavior - } - - // Get valid KDMAs for a specific run - function getValidKDMAsForRun(runId) { - const run = appState.pinnedRuns.get(runId); - if (!run?.availableOptions?.kdmas?.validCombinations) { - return {}; - } - - // Extract all available types and values from valid combinations - const availableOptions = {}; - run.availableOptions.kdmas.validCombinations.forEach(combination => { - Object.entries(combination).forEach(([kdmaType, value]) => { - if (!availableOptions[kdmaType]) { - availableOptions[kdmaType] = new Set(); - } - availableOptions[kdmaType].add(value); - }); - }); - - return availableOptions; - } - - // Get valid KDMA types that can be selected for a specific run - function getValidKDMATypesForRun(runId, currentKdmaType, currentKDMAs) { - const run = appState.pinnedRuns.get(runId); - if (!run?.availableOptions?.kdmas?.validCombinations) { - return [currentKdmaType]; // Fallback to just current type - } - - const validTypes = new Set([currentKdmaType]); // Always include current type - - // For each unused KDMA type, check if replacing current type would create valid combination - const availableKDMAs = getValidKDMAsForRun(runId); - Object.keys(availableKDMAs).forEach(kdmaType => { - // Skip if this type is already used (except current one we're replacing) - if (kdmaType !== currentKdmaType && currentKDMAs[kdmaType] !== undefined) { - return; - } - - // Test if this type can be used by checking valid combinations - const testKDMAs = { ...currentKDMAs }; - delete testKDMAs[currentKdmaType]; // Remove current type - - // If we're adding a different type, add it with any valid value - if (kdmaType !== currentKdmaType) { - const validValues = Array.from(availableKDMAs[kdmaType] || []); - if (validValues.length > 0) { - testKDMAs[kdmaType] = validValues[0]; // Use first valid value for testing - } - } - - // Check if this combination exists in validCombinations - const isValidCombination = run.availableOptions.kdmas.validCombinations.some(combination => { - return KDMAUtils.deepEqual(testKDMAs, combination); - }); - - if (isValidCombination) { - validTypes.add(kdmaType); - } - }); - - return Array.from(validTypes).sort(); - } - - // Check if a specific KDMA can be removed from a run - function canRemoveSpecificKDMA(runId, kdmaType) { - const run = appState.pinnedRuns.get(runId); - if (!run) return false; - - const currentKDMAs = run.kdmaValues || {}; - const kdmaOptions = run.availableOptions?.kdmas; - if (!kdmaOptions || !kdmaOptions.validCombinations) { - return false; - } - - // Create a copy of current KDMAs without the one we want to remove - const remainingKDMAs = { ...currentKDMAs }; - delete remainingKDMAs[kdmaType]; - - // Check if the remaining KDMA combination exists in validCombinations - const hasValidRemaining = kdmaOptions.validCombinations.some(combination => { - return KDMAUtils.deepEqual(remainingKDMAs, combination); - }); - - if (hasValidRemaining) { - return true; // Normal case - remaining combination is valid - } - - // Special case: If empty combination {} is valid (unaligned case), - // allow removal of any KDMA (will result in clearing all KDMAs) - const hasEmptyOption = kdmaOptions.validCombinations.some(combination => { - return Object.keys(combination).length === 0; - }); - - if (hasEmptyOption) { - return true; - } - - return false; - } - - // Format KDMA value consistently across the application - function formatKDMAValue(value) { - return KDMAUtils.formatValue(value); - } - - - // Check if we can add another KDMA given current KDMA values - function canAddKDMAToRun(runId, currentKDMAs) { - const run = appState.pinnedRuns.get(runId); - if (!run?.availableOptions?.kdmas?.validCombinations) { - return false; - } - - const currentKDMAEntries = Object.entries(currentKDMAs || {}); - const maxKDMAs = getMaxKDMAsForRun(runId); - - // First check if we're already at max - if (currentKDMAEntries.length >= maxKDMAs) { - return false; - } - - // Check if there are any valid combinations that: - // 1. Include all current KDMAs with their exact values - // 2. Have at least one additional KDMA - return run.availableOptions.kdmas.validCombinations.some(combination => { - - const combinationKeys = Object.keys(combination); - if (combinationKeys.length <= currentKDMAEntries.length) { - return false; - } - - // Check if this combination includes all current KDMAs with matching values - return currentKDMAEntries.every(([kdmaType, value]) => { - return combination.hasOwnProperty(kdmaType) && - Math.abs(combination[kdmaType] - value) < FLOATING_POINT_TOLERANCE; - }); - }); - } - - // Create KDMA controls HTML for table cells - function createKDMAControlsForRun(runId, currentKDMAs) { - const run = appState.pinnedRuns.get(runId); - if (!run) return HTML_NA_SPAN; - - const currentKDMAEntries = Object.entries(currentKDMAs || {}); - const canAddMore = canAddKDMAToRun(runId, currentKDMAs); - - let html = `