Skip to content

Commit

Permalink
Add snow2 serializer
Browse files Browse the repository at this point in the history
  • Loading branch information
tansongchen committed Jan 18, 2025
1 parent a30cee7 commit b900475
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 49 deletions.
150 changes: 117 additions & 33 deletions scripts/equivalence.ts
Original file line number Diff line number Diff line change
@@ -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<string, number>();

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
Expand All @@ -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;
Expand All @@ -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<Pair>[],
hashmap: Map<string, number>,
) {
const { data } = d;
const result = partitions.map((x) => ({ set: x, data: [] as number[] }));
for (const { initial, final, time } of data) {
Expand All @@ -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<EquivalenceData[], undefined>("equivalence");
function preprocess(modelData: EquivalenceData[]) {
const partitions = partition(手机五行七列);
const hashmap = new Map<string, number>();
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<Pair>; 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<EquivalenceData[], undefined>("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<number, number>();
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");
3 changes: 2 additions & 1 deletion src/components/ResultSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<ProForm<{ content: string[]; corners: CornerSpecifier }>
title={component}
Expand Down Expand Up @@ -63,7 +64,7 @@ const Customize = ({
)}
</ProFormList>
<ProFormGroup>
{serializer === "c3" &&
{showCorners &&
range(4).map((i) => (
<ProFormSelect
key={i}
Expand Down
7 changes: 6 additions & 1 deletion src/lib/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,10 @@ export const getComponentScheme = function (
(x) => 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);
Expand Down Expand Up @@ -277,6 +280,8 @@ export const recursiveRenderComponent = function (
};

const overrideCorners: Map<string, CornerSpecifier> = new Map([
["七", [0, 0, 1, 1]],
["九", [0, 0, 1, 1]],
["良", [0, 0, 4, 6]],
["\uE06A", [0, 0, 4, 5]],
["世", [0, 0, 4, 4]],
Expand Down
33 changes: 32 additions & 1 deletion src/lib/compound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
CompoundCharacter,
} from "./data";
import type { CornerSpecifier } from "./topology";
import type { Analysis } from "./config";

export type CompoundResults = Map<string, CompoundAnalysis | ComponentAnalysis>;

Expand Down Expand Up @@ -446,11 +447,41 @@ const zhangmaSerializer: Serializer = (operandResults, glyph) => {
};
};

const serializerMap: Record<string, Serializer> = {
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<Analysis["serializer"], undefined>,
Serializer
> = {
sequential: sequentialSerializer,
c3: c3Serializer,
zhangma: zhangmaSerializer,
zhenma: zhenmaSerializer,
snow2: snow2Serializer,
};

/**
Expand Down
2 changes: 1 addition & 1 deletion src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export interface Analysis {
customizeCorners?: Record<string, CornerSpecifier>;
strong?: string[];
weak?: string[];
serializer?: "sequential" | "c3";
serializer?: "sequential" | "c3" | "zhangma" | "zhenma" | "snow2";
}

export interface Degenerator {
Expand Down
39 changes: 27 additions & 12 deletions src/lib/equivalence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ export function partition<T>({ group, relation }: Model<T>) {
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);
}
}
Expand All @@ -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;
Expand All @@ -57,23 +63,32 @@ 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<Pair> = {
group: new Set(
range(35)
.map((n) => range(35).map((m) => ({ initial: n, final: m })))
.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;
},
};

0 comments on commit b900475

Please sign in to comment.