git.delta.rocks / unique-network / refs/commits / 1a014777c169

difftreelog

refactor use fractionals in calibrate

Yaroslav Bolyukin2022-12-19parent: #a026438.patch.diff
in: master

1 file changed

modifiedtests/src/calibrate.tsdiffbeforeafterboth
1import {IKeyringPair} from '@polkadot/types/types';1import {IKeyringPair} from '@polkadot/types/types';
2import {usingEthPlaygrounds, EthUniqueHelper} from './eth/util';2import {usingEthPlaygrounds, EthUniqueHelper} from './eth/util';
3
4class Fract {
5 static ZERO = new Fract(0n);
6 constructor(public readonly a: bigint, public readonly b: bigint = 1n) {
7 if (b === 0n) throw new Error('division by zero');
8 if (b < 0n) throw new Error('missing normalization');
9 }
10
11 mul(other: Fract) {
12 return new Fract(this.a * other.a, this.b * other.b);
13 }
14
15 div(other: Fract) {
16 return this.mul(other.inv());
17 }
18
19 plus(other: Fract) {
20 return new Fract(this.a * other.b + other.a * this.b, this.b * other.b);
21 }
22
23 minus(other: Fract) {
24 return this.plus(other.neg());
25 }
26
27 neg() {
28 return new Fract(-this.a, this.b);
29 }
30 inv() {
31 return new Fract(this.b, this.a);
32 }
33
34 toBigInt() {
35 return this.a / this.b;
36 }
37
38 lt(other: Fract) {
39 return this.a * other.b < other.a * this.b;
40 }
41 eq(other: Fract) {
42 return this.a * other.b === other.a * this.b;
43 }
44
45 sqrt() {
46 if (this.a < 0n) {
47 throw 'square root of negative numbers is not supported';
48 }
49
50 if (this.lt(new Fract(2n))) {
51 return this;
52 }
53
54 function newtonIteration(n: Fract, x0: Fract): Fract {
55 const x1 = rpn(n, x0, '/', x0, '+', new Fract(2n), '/');
56 if (x0.eq(x1) || x0.eq(x1.minus(new Fract(1n)))) {
57 return x0;
58 }
59 return newtonIteration(n, x1);
60 }
61
62 return newtonIteration(this, new Fract(1n));
63 }
64}
65
66type Op = Fract | '+' | '-' | '*' | '/' | 'dup' | Op[];
67function rpn(...ops: (Op)[]) {
68 const stack: Fract[] = [];
69 for (const op of ops) {
70 if (op instanceof Fract) {
71 stack.push(op);
72 } else if (op === '+') {
73 if (stack.length < 2)
74 throw new Error('stack underflow');
75 const a = stack.pop()!;
76 const b = stack.pop()!;
77 stack.push(a.plus(b));
78 } else if (op === '*') {
79 if (stack.length < 2)
80 throw new Error('stack underflow');
81 const a = stack.pop()!;
82 const b = stack.pop()!;
83 stack.push(a.mul(b));
84 } else if (op === '-') {
85 if (stack.length < 2)
86 throw new Error('stack underflow');
87 const a = stack.pop()!;
88 const b = stack.pop()!;
89 stack.push(a.minus(b));
90 } else if (op === '/') {
91 if (stack.length < 2)
92 throw new Error('stack underflow');
93 const a = stack.pop()!;
94 const b = stack.pop()!;
95 stack.push(a.div(b));
96 } else if (op === 'dup') {
97 if (stack.length < 1)
98 throw new Error('stack underflow');
99 const a = stack.pop()!;
100 stack.push(a);
101 stack.push(a);
102 } else if (Array.isArray(op)) {
103 stack.push(rpn(...op));
104 } else {
105 throw new Error(`unknown operand: ${op}`);
106 }
107 }
108 if (stack.length != 1)
109 throw new Error('one element should be left on stack');
110 return stack[0]!;
111}
3112
4function linearRegression(points: { x: bigint, y: bigint }[]) {113function linearRegression(points: { x: Fract, y: Fract }[]) {
5 let sumxy = 0n;114 let sumxy = Fract.ZERO;
6 let sumx = 0n;115 let sumx = Fract.ZERO;
7 let sumy = 0n;116 let sumy = Fract.ZERO;
8 let sumx2 = 0n;117 let sumx2 = Fract.ZERO;
9 const n = points.length;118 const n = points.length;
10 for (let i = 0; i < n; i++) {119 for (let i = 0; i < n; i++) {
11 const p = points[i];120 const p = points[i];
12 sumxy += p.x * p.y;121 sumxy = rpn(p.x, p.y, '*', sumxy, '+');
13 sumx += p.x;122 sumx = sumx.plus(p.x);
14 sumy += p.y;123 sumy = sumy.plus(p.y);
15 sumx2 += p.x * p.x;124 sumx2 = rpn(p.x, p.x, '*', sumx2, '+');
16 }125 }
17126
18 const nb = BigInt(n);127 const nb = new Fract(BigInt(n));
19
20 const precision = 100000000n;
21128
22 // This is a workaround to beat the lack of precision of the `Number` type.129 // This is a workaround to beat the lack of precision of the `Number` type.
23 // We divide `BigInt`s. But since it is an integer division, we should take care of the precision on our own.130 // We divide `BigInt`s. But since it is an integer division, we should take care of the precision on our own.
24 // After the division we can convert the result back to the `Number` and then we set the correct precision.131 // After the division we can convert the result back to the `Number` and then we set the correct precision.
25 // It is crucial to have the correct slope for the regression line.132 // It is crucial to have the correct slope for the regression line.
133
26 const a = Number((precision * (nb * sumxy - sumx * sumy)) / (nb * sumx2 - sumx * sumx)) / Number(precision);134 const a = rpn(
135 [nb, sumxy, '*', sumx, sumy, '*', '-'],
136 [nb, sumx, '*', sumx, sumx, '*', '-'],
137 '/',
138 );
27 const b = (Number(sumy) - a * Number(sumx)) / Number(nb);139 const b = rpn(
140 [sumy, a, sumx, '*', '-'],
141 nb,
142 '/',
143 );
28144
29 return {a, b};145 return {a, b};
30}146}
31
32// JS has no builtin function to calculate sqrt of bigint
33// https://stackoverflow.com/a/53684036/6190169
34function sqrt(value: bigint) {
35 if (value < 0n) {
36 throw 'square root of negative numbers is not supported';
37 }
38
39 if (value < 2n) {
40 return value;
41 }
42147
43 function newtonIteration(n: bigint, x0: bigint): bigint {148const hypothesisLinear = (a: Fract, b: Fract) => (x: Fract) => rpn(x, a, '*', b, '+');
44 const x1 = ((n / x0) + x0) >> 1n;149
45 if (x0 === x1 || x0 === (x1 - 1n)) {150// JS has no builtin function to calculate sqrt of bigint
46 return x0;151// https://stackoverflow.com/a/53684036/6190169
47 }
48 return newtonIteration(n, x1);
49 }
50
51 return newtonIteration(value, 1n);
52}
53
54function _error(points: { x: bigint, y: bigint }[], hypothesis: (a: bigint) => bigint) {152function error(points: { x: Fract, y: Fract }[], hypothesis: (a: Fract) => Fract) {
55 return sqrt(points.map(p => {153 return points.map(p => {
56 const v = hypothesis(p.x);154 const v = hypothesis(p.x);
57 const vv = p.y;155 const vv = p.y;
58156
59 return (v - vv) ** 2n;157 return rpn(v, vv, '-', 'dup', '*');
60 }).reduce((a, b) => a + b, 0n) / BigInt(points.length));158 }).reduce((a, b) => a.plus(b), Fract.ZERO).sqrt().div(new Fract(BigInt(points.length)));
61}159}
62160
63async function calibrateWeightToFee(helper: EthUniqueHelper, privateKey: (account: string) => Promise<IKeyringPair>) {161async function calibrateWeightToFee(helper: EthUniqueHelper, privateKey: (account: string) => Promise<IKeyringPair>) {
80 for (let i = -5; i < 5; i++) {178 for (let i = -5; i < 5; i++) {
81 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setWeightToFeeCoefficientOverride(base + base / 1000n * BigInt(i))));179 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setWeightToFeeCoefficientOverride(base + base / 1000n * BigInt(i))));
82180
83 const coefficient = (await api.query.configuration.weightToFeeCoefficientOverride() as any).toBigInt();181 const coefficient = new Fract((await api.query.configuration.weightToFeeCoefficientOverride() as any).toBigInt());
84 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});182 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});
85 const token = await collection.mintToken(alice, {Substrate: alice.address});183 const token = await collection.mintToken(alice, {Substrate: alice.address});
86184
87 const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);185 const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);
88 await token.transfer(alice, {Substrate: bob.address});186 await token.transfer(alice, {Substrate: bob.address});
89 const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);187 const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);
90188
91 const transferPrice = aliceBalanceBefore - aliceBalanceAfter;189 const transferPrice = new Fract(aliceBalanceBefore - aliceBalanceAfter);
92190
93 dataPoints.push({x: transferPrice, y: coefficient});191 dataPoints.push({x: transferPrice, y: coefficient});
94 }192 }
95 const {a, b} = linearRegression(dataPoints);193 const {a, b} = linearRegression(dataPoints);
96194
97 // console.log(`Error: ${error(dataPoints, x => a*x+b)}`);195 const hyp = hypothesisLinear(a, b);
98196 console.log(`Error: ${error(dataPoints, hyp)}`);
197
198 // 0.1 UNQ
99 const perfectValue = BigInt(Math.ceil(a * Number(helper.balance.getOneTokenNominal()) / 10 + b));199 const perfectValue = hyp(rpn(new Fract(helper.balance.getOneTokenNominal()), new Fract(1n, 10n), '*'));
100 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setWeightToFeeCoefficientOverride(perfectValue.toString())));200 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setWeightToFeeCoefficientOverride(perfectValue.toBigInt())));
101201
102 {202 {
103 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});203 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});
136 const gasPriceStr = '0x' + gasPrice.toString(16);236 const gasPriceStr = '0x' + gasPrice.toString(16);
137 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setMinGasPriceOverride(gasPrice)));237 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setMinGasPriceOverride(gasPrice)));
138238
139 const coefficient = (await api.query.configuration.minGasPriceOverride() as any).toBigInt();239 const coefficient = new Fract((await api.query.configuration.minGasPriceOverride() as any).toBigInt());
140 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});240 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});
141 const token = await collection.mintToken(alice, {Ethereum: caller});241 const token = await collection.mintToken(alice, {Ethereum: caller});
142242
143 const address = helper.ethAddress.fromCollectionId(collection.collectionId);243 const address = helper.ethAddress.fromCollectionId(collection.collectionId);
144 const contract = helper.ethNativeContract.collection(address, 'nft', caller);244 const contract = helper.ethNativeContract.collection(address, 'nft', caller);
145245
146 const transferPrice = await helper.eth.calculateFee({Ethereum: caller}, () => contract.methods.transfer(receiver, token.tokenId).send({from: caller, gasPrice: gasPriceStr, gas: helper.eth.DEFAULT_GAS}));246 const transferPrice = new Fract(await helper.eth.calculateFee({Ethereum: caller}, () => contract.methods.transfer(receiver, token.tokenId).send({from: caller, gasPrice: gasPriceStr, gas: helper.eth.DEFAULT_GAS})));
147247
148 dataPoints.push({x: transferPrice, y: coefficient});248 dataPoints.push({x: transferPrice, y: coefficient});
149 }249 }
150250
151 const {a, b} = linearRegression(dataPoints);251 const {a, b} = linearRegression(dataPoints);
152252
153 // console.log(`Error: ${error(dataPoints, x => a*x+b)}`);253 const hyp = hypothesisLinear(a, b);
154254 console.log(`Error: ${error(dataPoints, hyp)}`);
155 // * 0.15 = * 10000 / 66666255
256 // 0.15 UNQ
156 const perfectValue = BigInt(Math.ceil(a * Number(helper.balance.getOneTokenNominal() * 1000000n / 6666666n) + b));257 const perfectValue = hyp(rpn(new Fract(helper.balance.getOneTokenNominal()), new Fract(15n, 100n), '*'));
157 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setMinGasPriceOverride(perfectValue.toString())));258 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setMinGasPriceOverride(perfectValue.toBigInt())));
158259
159 {260 {
160 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});261 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});