diff --git a/scripts/equivalence.ts b/scripts/equivalence.ts index cd3703e..035fa54 100644 --- a/scripts/equivalence.ts +++ b/scripts/equivalence.ts @@ -1,22 +1,21 @@ -import { mean, round, sortBy } from "lodash-es"; +import { existsSync, readFileSync, writeFileSync } from "fs"; +import { mean, round, sortBy, sum } from "lodash-es"; import { erf, std } from "mathjs"; +import { exit } from "process"; import { get } from "~/api"; -import type { EquivalenceData } from "~/lib/equivalence"; +import type { EquivalenceData, Pair } from "~/lib/equivalence"; import { distance, partition, 手机五行七列 } from "~/lib/equivalence"; -const partitions = partition(手机五行七列); -const hashmap = new Map(); - -for (const [index, set] of partitions.entries()) { - for (const v of set) { - hashmap.set(`${v.initial},${v.final}`, index); - } +interface Estimation { + value: number; + std: number; + length: number; } -interface Measurement { - mean: number; +interface Equivalence { + distance: number; + value: number; std: number; - length: number; } // discard outliers using Chauvenet's criterion @@ -30,13 +29,19 @@ function chauvenet(data: number[]) { }); } -function measure(data: number[]): Measurement { +function measure(data: number[]): Estimation { if (data.length === 0) { return { - mean: NaN, + value: NaN, std: NaN, length: 0, }; + } else if (data.length === 1) { + return { + value: data[0]!, + std: 0, + length: 1, + }; } // recursively discard outliers let finalData = data; @@ -46,13 +51,17 @@ function measure(data: number[]): Measurement { finalData = newData; } return { - mean: mean(finalData), - std: std(finalData, "unbiased") as number, + value: mean(finalData), + std: (std(finalData, "unbiased") as number) / Math.sqrt(finalData.length), length: finalData.length, }; } -function analyze(d: EquivalenceData) { +function analyze( + d: EquivalenceData, + partitions: Set[], + hashmap: Map, +) { const { data } = d; const result = partitions.map((x) => ({ set: x, data: [] as number[] })); for (const { initial, final, time } of data) { @@ -65,32 +74,107 @@ function analyze(d: EquivalenceData) { } const stats = result.map(({ set, data }) => { const dist = distance([...set][0]!); - console.log(dist, data); return { ...measure(data), rawLength: data.length, distance: dist, }; }); - return sortBy(stats, (x) => x.distance); + return stats; } -const data = await get("equivalence"); +function preprocess(modelData: EquivalenceData[]) { + const partitions = partition(手机五行七列); + const hashmap = new Map(); + for (const [index, set] of partitions.entries()) { + for (const v of set) { + hashmap.set(`${v.initial},${v.final}`, index); + } + } -const modelData = data.find((d) => d.model === "手机五行七列")!; + const sampleData = partitions.map((x) => ({ + set: x, + data: [] as Equivalence[], + })); -const analyzeResult = analyze(modelData); -for (const { distance, mean, std } of analyzeResult) { - console.log(`键距 ${distance}:${round(mean, 2)} ± ${round(std, 2)} ms`); + for (const sample of modelData) { + // 和 partitions 一一对应 + const sampleResult = analyze(sample, partitions, hashmap); + const baseline = sampleResult.find((x) => x.distance === 0); + if (!baseline) continue; + sampleResult.forEach(({ distance, value, std }, index) => { + if (distance === 0) { + sampleData[index]!.data.push({ distance, value: 1, std: 0 }); + } else { + const ratio = value / baseline.value; + const correction = 1 + baseline.std ** 2 / baseline.value ** 2; + if (correction > 1.0003) console.log(distance, ratio, correction); + const ratioStd = + Math.sqrt((std / value) ** 2 + (baseline.std / baseline.value) ** 2) * + ratio; + sampleData[index]!.data.push({ + distance, + value: ratio * correction, + std: ratioStd, + }); + } + }); + } + return sampleData; } -const baseline = analyzeResult.find((x) => x.distance === 0)!; -const others = analyzeResult.filter((x) => x.distance !== 0); -for (const { distance, mean, std, rawLength, length } of others) { - const quotient = mean / baseline.mean; - const qstd = std / baseline.mean; - const diff = rawLength - length; - console.log( - `键距 ${distance}:${round(quotient, 2)} ± ${round(qstd, 2)}(${rawLength} 个样本${diff ? `,${diff} 个无效样本` : ""})`, - ); +function finalize(sampleData: { set: Set; data: Equivalence[] }[]) { + return sampleData.map(({ set, data }) => { + const distance = data[0]!.distance; + const values = data.map((x) => x.value); + const stds = data.map((x) => x.std); + const variances = stds.map((x) => x ** 2); + const coefficients = data.map(() => 1 / data.length); + // 另一种权重计算方法 + // const suminvvar = sum(variances.map((x) => 1 / x)); + // const coefficients = variances.map((x) => 1 / x / suminvvar); + const value = sum(values.map((x, j) => x * coefficients[j]!)); + const variance = sum(variances.map((x, j) => x * coefficients[j]! ** 2)); + const std = Math.sqrt(variance); + const equivalence: Equivalence = { distance, value, std }; + return { set, ...equivalence }; + }); +} + +let data: EquivalenceData[]; +if (existsSync("scripts/data.json")) { + data = JSON.parse(readFileSync("scripts/data.json", "utf-8")); +} else { + const res = await get("equivalence"); + if ("err" in res) exit(1); + data = res; + writeFileSync("scripts/data.json", JSON.stringify(data, null, 2)); } +const modelData = data.filter((d) => d.model === "手机五行七列"); +const sampleData = preprocess(modelData); +const equivalence = finalize(sampleData); +const sortedEquivalence = sortBy(equivalence, "distance", "value"); + +writeFileSync("scripts/equivalence.json", JSON.stringify(sampleData, null, 2)); + +const content = [["组合", "分组", "当量", "不确定度"].join("\t")]; +const counter = new Map(); +for (const { distance, value, std, set } of sortedEquivalence) { + let groupname: string; + if (distance === -1) { + groupname = "/"; + } else { + const distanceCount = counter.get(distance) ?? 0; + groupname = `${distance}${"ABCDEFGHIJKLMNOPQRSTUVWXYZ"[distanceCount]}`; + counter.set(distance, distanceCount + 1); + } + for (const { initial, final } of sortBy([...set], "initial", "final")) { + content.push( + [`${initial}-${final}`, groupname, round(value, 3), round(std, 3)].join( + "\t", + ), + ); + } +} + +writeFileSync("scripts/手机 5 × 7 当量.txt", content.join("\n") + "\n"); diff --git a/src/components/ResultSummary.tsx b/src/components/ResultSummary.tsx index c78192e..546813e 100644 --- a/src/components/ResultSummary.tsx +++ b/src/components/ResultSummary.tsx @@ -32,6 +32,7 @@ const Customize = ({ const add = useAddAtom(customizeAtom); const addCorner = useAddAtom(customizeCornersAtom); const serializer = useAtomValue(serializerAtom); + const showCorners = serializer === "c3" || serializer === "snow2"; return ( title={component} @@ -63,7 +64,7 @@ const Customize = ({ )} - {serializer === "c3" && + {showCorners && range(4).map((i) => ( x & (1 << (component.glyph.length - corner - 1)), ), ) as CornerSpecifier; - if (config.analysis.serializer === "c3") { + if ( + config.analysis.serializer === "c3" || + config.analysis.serializer === "snow2" + ) { // 根据四角信息对 sequence 进行排序 // if (sequence.length > 3) { // console.log(component.name, sequence, corners); @@ -277,6 +280,8 @@ export const recursiveRenderComponent = function ( }; const overrideCorners: Map = new Map([ + ["七", [0, 0, 1, 1]], + ["九", [0, 0, 1, 1]], ["良", [0, 0, 4, 6]], ["\uE06A", [0, 0, 4, 5]], ["世", [0, 0, 4, 4]], diff --git a/src/lib/compound.ts b/src/lib/compound.ts index fc0c865..9ac223c 100644 --- a/src/lib/compound.ts +++ b/src/lib/compound.ts @@ -14,6 +14,7 @@ import type { CompoundCharacter, } from "./data"; import type { CornerSpecifier } from "./topology"; +import type { Analysis } from "./config"; export type CompoundResults = Map; @@ -446,11 +447,41 @@ const zhangmaSerializer: Serializer = (operandResults, glyph) => { }; }; -const serializerMap: Record = { +const snow2Serializer: Serializer = (operandResults, glyph) => { + const sequence: string[] = []; + const corners: CornerSpecifier = [0, 0, 0, 0]; + const [first, last] = [operandResults[0]!, operandResults.at(-1)!]; + sequence.push(getTL(first)); + if (/[⿴⿵⿶⿷⿹⿺⿻]/.test(glyph.operator)) { + if (first.corners[0] !== first.corners[3]) { + sequence.push(getBR(first, true)); + corners[3] = 1; + } else { + sequence.push(getBR(last)); + corners[3] = 0; + } + } else { + sequence.push(getBR(last)); + corners[3] = 1; + } + return { + sequence, + corners, + full: [], + operator: glyph.operator, + operandResults, + }; +}; + +const serializerMap: Record< + Exclude, + Serializer +> = { sequential: sequentialSerializer, c3: c3Serializer, zhangma: zhangmaSerializer, zhenma: zhenmaSerializer, + snow2: snow2Serializer, }; /** diff --git a/src/lib/config.ts b/src/lib/config.ts index a418a9f..fc0088f 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -45,7 +45,7 @@ export interface Analysis { customizeCorners?: Record; strong?: string[]; weak?: string[]; - serializer?: "sequential" | "c3"; + serializer?: "sequential" | "c3" | "zhangma" | "zhenma" | "snow2"; } export interface Degenerator { diff --git a/src/lib/equivalence.ts b/src/lib/equivalence.ts index e847fa4..4637b04 100644 --- a/src/lib/equivalence.ts +++ b/src/lib/equivalence.ts @@ -24,10 +24,10 @@ export function partition({ group, relation }: Model) { const stack = [v]; while (stack.length > 0) { const u = stack.pop()!; - if (visited.has(u)) continue; visited.add(u); subgroup.add(u); for (const w of group) { + if (visited.has(w)) continue; if (relation(u, w)) stack.push(w); } } @@ -42,9 +42,15 @@ export interface Pair { } const isDifferentHand = (p: Pair) => { + if (p.initial === p.final) return false; return (p.initial < 20 && p.final >= 15) || (p.initial >= 15 && p.final < 20); }; +const isSameHand = (p1: Pair, p2: Pair) => { + const numbers = [p1.initial, p1.final, p2.initial, p2.final]; + return numbers.every((n) => n < 15) || numbers.every((n) => n >= 20); +}; + const reflect = (n: number) => { let [col, row] = [Math.floor(n / 5), n % 5]; return (6 - col) * 5 + row; @@ -57,6 +63,12 @@ export const distance = (p: Pair) => { return (x1 - x2) ** 2 + (y1 - y2) ** 2; }; +export const displacement = (p: Pair) => { + let [x1, y1] = [Math.floor(p.initial / 5), p.initial % 5]; + let [x2, y2] = [Math.floor(p.final / 5), p.final % 5]; + return [x2 - x1, y2 - y1] as const; +}; + export const 手机五行七列: Model = { group: new Set( range(35) @@ -64,16 +76,19 @@ export const 手机五行七列: Model = { .flat(), ), relation: (p1, p2) => { - return distance(p1) === distance(p2); - // // 当量 0 - // if (isDifferentHand(p1) && isDifferentHand(p2)) return true; - // // 当量 1 - // if (p1.initial === p1.final && p2.initial === p2.final) return true; - // // 时间反演 - // if (p1.initial === p2.final && p1.final === p2.initial) return true; - // // 空间镜像 - // if (reflect(p1.initial) === p2.initial && reflect(p1.final) === p2.final) - // return true; - // return false; + // 异指连击当量相同 + if (isDifferentHand(p1) && isDifferentHand(p2)) return true; + // 同键连击当量相同 + if (p1.initial === p1.final && p2.initial === p2.final) return true; + // 时间反演 + if (p1.initial === p2.final && p1.final === p2.initial) return true; + // 空间镜像 + if (reflect(p1.initial) === p2.initial && reflect(p1.final) === p2.final) + return true; + // 空间平移(同手) + const [dx1, dy1] = displacement(p1); + const [dx2, dy2] = displacement(p2); + if (isSameHand(p1, p2) && dx1 === dx2 && dy1 === dy2) return true; + return false; }, };