Files
novelty-seeking/frontend/src/utils/crossoverToDAG.ts
2026-01-05 22:32:08 +08:00

198 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { CrossoverPair, AttributeDAG, CategoryDefinition, DAGNode } from '../types';
/**
* Result of crossover transformation - two separate DAGs
* Each path receives attributes from the other path
*/
export interface CrossoverDAGResult {
// Path A receives attributes from Path B
pathA: AttributeDAG;
// Path B receives attributes from Path A
pathB: AttributeDAG;
}
/**
* Convert selected crossover pairs into two separate AttributeDAGs.
*
* The crossover logic:
* - When pair (A's "防水布" × B's "鋁合金") is selected:
* - Path A gets "鋁合金" (from B) to transform with A's context
* - Path B gets "防水布" (from A) to transform with B's context
*
* This allows experts to think about:
* - "What if umbrella had aluminum alloy?" (A gets B's attribute)
* - "What if bicycle had waterproof fabric?" (B gets A's attribute)
*/
export function crossoverPairsToDAGs(
pairs: CrossoverPair[],
dagA: AttributeDAG,
dagB: AttributeDAG
): CrossoverDAGResult {
// Collect attributes that each path receives from the other
const attributesForA: Map<string, Set<string>> = new Map(); // category -> attributes from B
const attributesForB: Map<string, Set<string>> = new Map(); // category -> attributes from A
for (const pair of pairs) {
// Path A receives the target node (from B)
// Path B receives the source node (from A)
if (pair.sourcePathId === 'A' && pair.targetPathId === 'B') {
// A's attribute crossed with B's attribute
// A gets B's attribute, B gets A's attribute
const categoryForA = pair.targetNode.category;
const categoryForB = pair.sourceNode.category;
if (!attributesForA.has(categoryForA)) attributesForA.set(categoryForA, new Set());
if (!attributesForB.has(categoryForB)) attributesForB.set(categoryForB, new Set());
attributesForA.get(categoryForA)!.add(pair.targetNode.name);
attributesForB.get(categoryForB)!.add(pair.sourceNode.name);
} else if (pair.sourcePathId === 'B' && pair.targetPathId === 'A') {
// B's attribute crossed with A's attribute
// A gets B's attribute, B gets A's attribute (reverse direction)
const categoryForA = pair.sourceNode.category;
const categoryForB = pair.targetNode.category;
if (!attributesForA.has(categoryForA)) attributesForA.set(categoryForA, new Set());
if (!attributesForB.has(categoryForB)) attributesForB.set(categoryForB, new Set());
attributesForA.get(categoryForA)!.add(pair.sourceNode.name);
attributesForB.get(categoryForB)!.add(pair.targetNode.name);
}
}
// Build DAG for Path A (receives attributes from B)
const pathAResult = buildCrossoverDAG(
dagA.query,
attributesForA,
`Crossover: ${dagB.query}${dagA.query}`
);
// Build DAG for Path B (receives attributes from A)
const pathBResult = buildCrossoverDAG(
dagB.query,
attributesForB,
`Crossover: ${dagA.query}${dagB.query}`
);
return {
pathA: pathAResult,
pathB: pathBResult,
};
}
/**
* Build an AttributeDAG from crossover attributes
*/
function buildCrossoverDAG(
originalQuery: string,
attributesByCategory: Map<string, Set<string>>,
_crossoverDescription: string
): AttributeDAG {
const categories: CategoryDefinition[] = [];
const nodes: DAGNode[] = [];
let nodeIndex = 0;
let categoryIndex = 0;
for (const [categoryName, attributes] of attributesByCategory.entries()) {
categories.push({
name: categoryName,
description: `Crossover attributes for ${categoryName}`,
is_fixed: false,
order: categoryIndex,
});
categoryIndex++;
for (const attrName of attributes) {
nodes.push({
id: `crossover-${nodeIndex}`,
name: attrName,
category: categoryName,
order: nodeIndex,
});
nodeIndex++;
}
}
return {
query: originalQuery,
categories,
nodes,
edges: [],
};
}
/**
* Legacy function - converts to single DAG (deprecated)
* Kept for backwards compatibility
*/
export function crossoverPairsToDAG(
pairs: CrossoverPair[],
queryA: string,
queryB: string
): AttributeDAG {
// Group pairs by crossType
const pairsByType = pairs.reduce((acc, pair) => {
if (!acc[pair.crossType]) acc[pair.crossType] = [];
acc[pair.crossType].push(pair);
return acc;
}, {} as Record<string, CrossoverPair[]>);
// Create categories from crossTypes
const categories: CategoryDefinition[] = Object.keys(pairsByType).map((crossType, index) => ({
name: formatCrossTypeName(crossType),
description: getCrossTypeDescription(crossType),
is_fixed: false,
order: index,
}));
// Create nodes from pairs
const nodes: DAGNode[] = [];
let nodeIndex = 0;
for (const [crossType, typePairs] of Object.entries(pairsByType)) {
const categoryName = formatCrossTypeName(crossType);
for (const pair of typePairs) {
nodes.push({
id: `crossover-${nodeIndex}`,
name: `${pair.sourceNode.name} × ${pair.targetNode.name}`,
category: categoryName,
order: nodeIndex,
});
nodeIndex++;
}
}
return {
query: `${queryA} × ${queryB}`,
categories,
nodes,
edges: [],
};
}
function formatCrossTypeName(crossType: string): string {
if (crossType.startsWith('same-')) {
const category = crossType.replace('same-', '');
return `Same: ${category}`;
}
if (crossType.startsWith('cross-')) {
const parts = crossType.replace('cross-', '').split('-');
if (parts.length >= 2) {
return `Cross: ${parts[0]} × ${parts.slice(1).join('-')}`;
}
}
return crossType;
}
function getCrossTypeDescription(crossType: string): string {
if (crossType.startsWith('same-')) {
const category = crossType.replace('same-', '');
return `Crossover pairs from the same category: ${category}`;
}
if (crossType.startsWith('cross-')) {
return `Crossover pairs from different categories`;
}
return `Crossover pairs`;
}