crossover.js 6.06 KB
/**
 * Crossover Operator — 交叉重组两个基因组
 *
 * 按维度分类处理:
 *   可合并维度:  patternLibrary, scarTissue, toolFluency → 合并去重
 *   不可合并维度: mentalModels, judgment, taste → 选择较优父本
 *
 * 来自 09-evolution-algorithm.md 的设计
 */
const evolutionQueries = require('../queries').evolutionQueries;

// ════════════════════════════════════════════════════════
// 合并策略
// ════════════════════════════════════════════════════════

/**
 * 合并两个数组,按 key 函数去重
 */
function mergeArrays(arr1 = [], arr2 = [], keyFn = JSON.stringify) {
  const seen = new Set();
  const result = [];
  for (const item of [...arr1, ...arr2]) {
    const key = keyFn(item);
    if (!seen.has(key)) {
      seen.add(key);
      result.push(item);
    }
  }
  return result;
}

/**
 * 合并两个对象,内部数组合并去重
 */
function mergeObjects(obj1 = {}, obj2 = {}) {
  const result = { ...obj1 };
  for (const [key, value] of Object.entries(obj2)) {
    if (Array.isArray(value)) {
      result[key] = mergeArrays(result[key] || [], value,
        item => typeof item === 'string' ? item : JSON.stringify(item));
    } else if (typeof value === 'object' && value !== null) {
      result[key] = mergeObjects(result[key] || {}, value);
    } else if (!(key in result)) {
      result[key] = value;
    }
  }
  return result;
}

/**
 * 选择较优父本的维度(基于该维度的适应度分数)
 */
function selectBetter(dim1, dim2, score1, score2) {
  if (score1 === undefined && score2 === undefined) {
    // 都没有分数,选内容更丰富的
    const size1 = JSON.stringify(dim1).length;
    const size2 = JSON.stringify(dim2).length;
    return size1 >= size2 ? dim1 : dim2;
  }
  return (score1 || 0) >= (score2 || 0) ? dim1 : dim2;
}

// ════════════════════════════════════════════════════════
// 交叉算子
// ════════════════════════════════════════════════════════

function parseDim(raw) {
  if (!raw) return {};
  return typeof raw === 'string' ? JSON.parse(raw) : raw;
}

function parseScores(raw) {
  if (!raw) return {};
  return typeof raw === 'string' ? JSON.parse(raw) : raw;
}

/**
 * 交叉两个基因组,生成子代
 *
 * @param {object} parent1 - 父本 1 (skill_genomes 行)
 * @param {object} parent2 - 父本 2 (skill_genomes 行)
 * @returns {object} 子代基因组数据 (不含 id/run_id 等元数据)
 */
function crossover(parent1, parent2) {
  const scores1 = parseScores(parent1.fitness_scores);
  const scores2 = parseScores(parent2.fitness_scores);

  // 可合并维度: 合并去重
  const patternLibrary = mergeObjects(
    parseDim(parent1.pattern_library),
    parseDim(parent2.pattern_library)
  );

  const scarTissue = mergeObjects(
    parseDim(parent1.scar_tissue),
    parseDim(parent2.scar_tissue)
  );

  const toolFluency = mergeObjects(
    parseDim(parent1.tool_fluency),
    parseDim(parent2.tool_fluency)
  );

  // 不可合并维度: 选择较优
  const mentalModels = selectBetter(
    parseDim(parent1.mental_models),
    parseDim(parent2.mental_models),
    scores1.mentalModels,
    scores2.mentalModels
  );

  const judgment = selectBetter(
    parseDim(parent1.judgment),
    parseDim(parent2.judgment),
    scores1.judgment,
    scores2.judgment
  );

  const taste = selectBetter(
    parseDim(parent1.taste),
    parseDim(parent2.taste),
    scores1.taste,
    scores2.taste
  );

  return {
    parentIds: [parent1.id, parent2.id],
    mentalModels,
    patternLibrary,
    judgment,
    toolFluency,
    taste,
    scarTissue,
  };
}

// ════════════════════════════════════════════════════════
// 公开 API
// ════════════════════════════════════════════════════════

/**
 * 交叉两个基因组并将子代写入数据库
 */
async function crossoverAndInsert(parent1Id, parent2Id, runId, generation) {
  const parent1 = await evolutionQueries.getGenome(parent1Id);
  const parent2 = await evolutionQueries.getGenome(parent2Id);

  if (!parent1) throw new Error(`Parent1 ${parent1Id} not found`);
  if (!parent2) throw new Error(`Parent2 ${parent2Id} not found`);

  const childData = crossover(parent1, parent2);

  const child = await evolutionQueries.insertGenome({
    runId,
    skillName: parent1.skill_name, // 同 Skill 才能交叉
    generation,
    ...childData,
  });

  return child;
}

/**
 * 对当前代所有同名 Skill 进行随机配对交叉
 */
async function crossoverGeneration(runId, generation) {
  const genomes = await evolutionQueries.getGenomesByRun(runId, {
    generation: generation - 1,
    aliveOnly: true,
  });

  // 按 Skill 分组
  const bySkill = {};
  for (const g of genomes) {
    if (!bySkill[g.skill_name]) bySkill[g.skill_name] = [];
    bySkill[g.skill_name].push(g);
  }

  const children = [];

  for (const [skillName, pool] of Object.entries(bySkill)) {
    if (pool.length < 2) continue;

    // 按适应度排序
    pool.sort((a, b) => (b.overall_fitness || 0) - (a.overall_fitness || 0));

    // 精英 × 随机 配对
    const elite = pool[0];
    for (let i = 1; i < Math.min(pool.length, 3); i++) {
      const child = await crossoverAndInsert(elite.id, pool[i].id, runId, generation);
      children.push(child);
      console.log(`  🔀 ${skillName}: ${elite.id.slice(0, 8)} × ${pool[i].id.slice(0, 8)} → ${child.id.slice(0, 8)}`);
    }
  }

  return children;
}

module.exports = {
  crossover,
  crossoverAndInsert,
  crossoverGeneration,
  mergeArrays,
  mergeObjects,
  selectBetter,
};