Skip to content

Commit 5083a95

Browse files
committed
feat: implement digit change visualization and add robust p-adic parsing support
1 parent 70e79b3 commit 5083a95

9 files changed

Lines changed: 435 additions & 122 deletions

File tree

‎animated-transformer/src/app/berkovich-hub/berkovich-digit-display/berkovich-digit-display.component.html‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,26 @@
104104
class="row-outer-box"
105105
/>
106106

107-
<!-- Vertical dotted/dashed line for updated location (gradient update) -->
107+
<!-- Circular bubbles for changed digits when rho is unchanged -->
108+
@for (bubble of digitBubbles(); track bubble.power) {
109+
<g class="digit-bubble-group">
110+
<circle
111+
[attr.cx]="bubble.cx"
112+
[attr.cy]="bubbleCy()"
113+
r="9"
114+
class="bubble-circle"
115+
/>
116+
<text
117+
[attr.x]="bubble.cx"
118+
[attr.y]="bubbleCy()"
119+
class="bubble-text"
120+
>
121+
{{ bubble.newDigit }}
122+
</text>
123+
</g>
124+
}
125+
126+
<!-- Vertical dotted/dashed line for updated location (gradient update when rho changed or line specified) -->
108127
@if (updatedRhoBoundaryX() !== null) {
109128
<line
110129
[attr.x1]="updatedRhoBoundaryX()"

‎animated-transformer/src/app/berkovich-hub/berkovich-digit-display/berkovich-digit-display.component.scss‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,30 @@ clipPath rect {
130130
dominant-baseline: central;
131131
transition: x 0.3s ease, y 0.3s ease, fill 0.3s ease;
132132
}
133+
134+
/* Digit change circular bubble styling */
135+
.digit-bubble-group {
136+
transition: transform 0.3s ease;
137+
138+
.bubble-circle {
139+
fill: #eff6ff;
140+
stroke: #3b82f6;
141+
stroke-width: 1.5px;
142+
transition: r 0.3s ease, fill 0.3s ease, stroke 0.3s ease;
143+
}
144+
145+
.bubble-text {
146+
font-family: 'Roboto Mono', monospace;
147+
font-size: 10px;
148+
font-weight: 800;
149+
fill: #1d4ed8;
150+
text-anchor: middle;
151+
dominant-baseline: central;
152+
}
153+
154+
.bubble-guide-line {
155+
stroke: #93c5fd;
156+
stroke-width: 1px;
157+
stroke-dasharray: 2 2;
158+
}
159+
}

‎animated-transformer/src/app/berkovich-hub/berkovich-digit-display/berkovich-digit-display.component.ts‎

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ const BASE_CELL_HEIGHT = 24;
2929
const BASE_CELL_GAP = 4;
3030

3131
/** Base top margin for label guide lines. */
32-
const BASE_MARGIN_TOP = 24;
32+
const BASE_MARGIN_TOP = 32;
33+
3334
/** Base bottom margin spacing inside SVG box. */
3435
const BASE_MARGIN_BOTTOM = 10;
3536
/** Base left margin spacing inside SVG box. */
@@ -69,7 +70,8 @@ export interface DigitDisplayCell {
6970
imports: [CommonModule],
7071
host: {
7172
'[class.clickable]': 'isClickable()',
72-
'(click)': 'toggleRho($event)'
73+
'(click)': 'toggleRho($event)',
74+
'[style.height.px]': 'svgHeight()'
7375
}
7476
})
7577
export class BerkovichDigitDisplayComponent {
@@ -225,6 +227,65 @@ export class BerkovichDigitDisplayComponent {
225227
return this.derivedDimensions().guideLineOffset;
226228
});
227229

230+
readonly updatedCells = computed(() => {
231+
const uc = this.updatedCenter();
232+
if (!uc) return [];
233+
const p = BigInt(this.prime());
234+
const left = this.digitsLeft();
235+
const right = this.digitsRight();
236+
237+
const minPower = -right;
238+
const maxPower = left - 1;
239+
240+
const aligned = getAlignedDigits(uc, p, minPower, maxPower);
241+
return [...aligned].reverse();
242+
});
243+
244+
getUpdatedDigitAtPower(power: number): number | null {
245+
const uc = this.updatedCenter();
246+
if (!uc) return null;
247+
const cell = this.updatedCells().find(c => c.power === power);
248+
return cell ? cell.digit : 0;
249+
}
250+
251+
readonly isRhoUnchanged = computed(() => {
252+
const ur = this.updatedRho();
253+
if (ur === undefined) return true;
254+
return Math.abs(ur - this.rho()) < 1e-6;
255+
});
256+
257+
readonly digitBubbles = computed(() => {
258+
if (!this.updatedCenter() && !this.showUpdatedLocation()) return [];
259+
if (!this.isRhoUnchanged()) return [];
260+
261+
const lay = this.layout();
262+
const result: { power: number; cx: number; newDigit: number; oldDigit: number }[] = [];
263+
264+
for (const col of lay.cellPositions) {
265+
const oldD = this.getDigitAtPower(col.power);
266+
const newD = this.getUpdatedDigitAtPower(col.power);
267+
if (newD !== null && newD !== oldD) {
268+
result.push({
269+
power: col.power,
270+
cx: col.center,
271+
newDigit: newD,
272+
oldDigit: oldD
273+
});
274+
}
275+
}
276+
return result;
277+
});
278+
279+
readonly bubbleCy = computed(() => {
280+
const dims = this.derivedDimensions();
281+
const side = this.effectiveExtensionSide();
282+
if (side === 'below') {
283+
return this.rowY() + dims.cellHeight + dims.boxPadding;
284+
} else {
285+
return this.rowY() - dims.boxPadding;
286+
}
287+
});
288+
228289
readonly rowY = computed(() => {
229290
const pos = this.displayPosition();
230291
const dims = this.derivedDimensions();
@@ -233,6 +294,9 @@ export class BerkovichDigitDisplayComponent {
233294
const ext = this.effectiveExtension();
234295
top = Math.max(top, dims.boxPadding + ext + 4);
235296
}
297+
if (this.digitBubbles().length > 0 && this.effectiveExtensionSide() === 'above') {
298+
top = Math.max(top, dims.boxPadding + 10);
299+
}
236300
return top;
237301
});
238302

@@ -248,6 +312,9 @@ export class BerkovichDigitDisplayComponent {
248312
const ext = this.effectiveExtension();
249313
baseHeight = Math.max(baseHeight, this.rowY() + dims.cellHeight + dims.boxPadding + ext + dims.marginBottom + 4);
250314
}
315+
if (this.digitBubbles().length > 0 && this.effectiveExtensionSide() === 'below') {
316+
baseHeight = Math.max(baseHeight, this.rowY() + dims.cellHeight + dims.boxPadding + 10 + dims.marginBottom);
317+
}
251318
return baseHeight;
252319
});
253320

‎animated-transformer/src/app/berkovich-hub/berkovich-dual-digit-display/berkovich-dual-digit-display.component.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@ const BASE_LABEL_TO_LINE_SPACING = 8;
6262
templateUrl: './berkovich-dual-digit-display.component.html',
6363
styleUrls: ['./berkovich-dual-digit-display.component.scss'],
6464
imports: [CommonModule],
65-
changeDetection: ChangeDetectionStrategy.OnPush
65+
changeDetection: ChangeDetectionStrategy.OnPush,
66+
host: {
67+
'style': 'display: inline-block; vertical-align: middle;',
68+
'[style.height.px]': 'svgHeight()'
69+
}
6670
})
6771
export class BerkovichDualDigitDisplayComponent {
6872
private static idCounter = 0;

‎animated-transformer/src/app/berkovich-hub/berkovich-vis-tools/digit-display-tool.component.ts‎

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,24 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
2828

2929
import { BerkovichDigitDisplayComponent } from '../berkovich-digit-display/berkovich-digit-display.component';
3030
import { BerkovichHeaderComponent } from '../berkovich-header/berkovich-header.component';
31-
import { Rational, parseToRational, formatRational, formatDigitSequence, parseDigitSequence } from '../../../lib/berkovich/berkovich';
31+
import {
32+
Rational,
33+
formatRational,
34+
formatDigitSequence,
35+
parsePadicOrRationalInput
36+
} from '../../../lib/berkovich/berkovich';
3237
import { stringifyState, parseState } from './url-serializer';
3338

39+
export interface DigitDisplayPreset {
40+
name: string;
41+
prime: number;
42+
center: string;
43+
rho: number;
44+
showUpdated: boolean;
45+
updatedRho: number;
46+
updatedCenter: string;
47+
}
48+
3449
@Component({
3550
selector: 'app-digit-display-tool',
3651
templateUrl: './digit-display-tool.component.html',
@@ -172,12 +187,14 @@ export class DigitDisplayToolComponent {
172187
readonly customCellGap = signal<boolean>(false);
173188
readonly cellGap = signal<number>(6);
174189

190+
readonly currentPrecision = computed(() => ({
191+
minPower: -this.digitsRight(),
192+
maxPower: this.digitsLeft() - 1
193+
}));
194+
175195
readonly parsedCenter = computed<Rational>(() => {
176196
try {
177-
return parseDigitSequence(this.centerDigits(), BigInt(this.prime()), {
178-
minPower: -this.digitsRight(),
179-
maxPower: this.digitsLeft() - 1
180-
});
197+
return parsePadicOrRationalInput(this.centerDigits(), BigInt(this.prime()), this.currentPrecision());
181198
} catch {
182199
return { num: 0n, den: 1n };
183200
}
@@ -186,51 +203,69 @@ export class DigitDisplayToolComponent {
186203
readonly parsedUpdatedCenter = computed<Rational | undefined>(() => {
187204
if (!this.updatedCenterDigits()) return undefined;
188205
try {
189-
return parseDigitSequence(this.updatedCenterDigits(), BigInt(this.prime()), {
190-
minPower: -this.digitsRight(),
191-
maxPower: this.digitsLeft() - 1
192-
});
206+
return parsePadicOrRationalInput(this.updatedCenterDigits(), BigInt(this.prime()), this.currentPrecision());
193207
} catch {
194208
return undefined;
195209
}
196210
});
197211

198212
readonly parsedCenterError = computed<string | null>(() => {
199213
try {
200-
parseDigitSequence(this.centerDigits(), BigInt(this.prime()), {
201-
minPower: -this.digitsRight(),
202-
maxPower: this.digitsLeft() - 1
203-
});
214+
parsePadicOrRationalInput(this.centerDigits(), BigInt(this.prime()), this.currentPrecision());
204215
return null;
205216
} catch (e: any) {
206217
return `Invalid digit sequence: ${e.message ?? 'Unknown error'}`;
207218
}
208219
});
209220

210-
readonly presets = [
221+
readonly presets: DigitDisplayPreset[] = [
211222
{ name: '3/5 (p=5)', prime: 5, center: '3/5', rho: 0.5, showUpdated: false, updatedRho: 1.2, updatedCenter: '3/5' },
212223
{ name: 'SGD Step Update', prime: 5, center: '00.30', rho: 0.3, showUpdated: true, updatedRho: 1.1, updatedCenter: '00.31' },
224+
{ name: 'Digit Change (Same Rho)', prime: 3, center: '101.00', rho: 0.0, showUpdated: true, updatedRho: 0.0, updatedCenter: '102.00' },
213225
{ name: '12 (p=3)', prime: 3, center: '12', rho: -1.0, showUpdated: false, updatedRho: 0.0, updatedCenter: '12' },
214226
{ name: '-1.25 (p=2)', prime: 2, center: '-1.25', rho: 0.0, showUpdated: false, updatedRho: 0.8, updatedCenter: '-1.25' },
215227
{ name: '5/7 (p=7)', prime: 7, center: '5/7', rho: 1.2, showUpdated: true, updatedRho: 0.4, updatedCenter: '5/7' },
216228
];
217229

218-
applyPreset(preset: typeof this.presets[0]) {
230+
applyPreset(preset: DigitDisplayPreset) {
219231
this.prime.set(preset.prime);
232+
const p = BigInt(preset.prime);
233+
234+
// Auto-adjust digitsLeft / digitsRight if the preset string has more digit columns
235+
const centerParts = preset.center.split('.');
236+
if (centerParts[0] && !preset.center.includes('/')) {
237+
const neededLeft = centerParts[0].length;
238+
if (neededLeft > this.digitsLeft()) {
239+
this.digitsLeft.set(neededLeft);
240+
}
241+
}
242+
if (centerParts[1] && !preset.center.includes('/')) {
243+
const neededRight = centerParts[1].length;
244+
if (neededRight > this.digitsRight()) {
245+
this.digitsRight.set(neededRight);
246+
}
247+
}
248+
249+
const precision = this.currentPrecision();
250+
220251
try {
221-
const rat = parseToRational(preset.center);
222-
const p = BigInt(preset.prime);
223-
const seq = formatDigitSequence(rat, p, {
224-
minPower: -this.digitsRight(),
225-
maxPower: this.digitsLeft() - 1
226-
});
252+
const rat = parsePadicOrRationalInput(preset.center, p, precision);
253+
const seq = formatDigitSequence(rat, p, precision);
227254
this.centerDigits.set(seq);
228255
} catch {
229-
this.centerDigits.set('');
256+
this.centerDigits.set(preset.center);
230257
}
258+
231259
this.rho.set(preset.rho);
232260
this.showUpdatedLocation.set(preset.showUpdated);
233261
this.updatedRho.set(preset.updatedRho);
234-
this.updatedCenterDigits.set(preset.updatedCenter);
262+
263+
try {
264+
const updatedRat = parsePadicOrRationalInput(preset.updatedCenter, p, precision);
265+
const updatedSeq = formatDigitSequence(updatedRat, p, precision);
266+
this.updatedCenterDigits.set(updatedSeq);
267+
} catch {
268+
this.updatedCenterDigits.set(preset.updatedCenter);
269+
}
235270
}
236271
}

‎animated-transformer/src/app/berkovich-hub/berkovich-vis-tools/operator-tree-vis-tool.component.html‎

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<!-- Header Banner -->
1818
<app-berkovich-header
1919
title="Operator Tree Vis Sandbox"
20-
subtitle="Configure binary operator visualizers to see parallel trees for $x_1$, $x_2$ operations, and target $y$."
20+
subtitle="Configure binary operator visualizers to see parallel trees for x1, x2 operations, and target y."
2121
[isVisTool]="true"
2222
></app-berkovich-header>
2323

@@ -40,24 +40,20 @@ <h2 style="font-size: 18px; font-weight: 700; margin: 0; color: #0f172a; display
4040
[prime]="prime()"
4141
[operator]="operator()"
4242
[trackedNodes]="trackedNodes()"
43+
[editableInputs]="editableInputs()"
4344
[stepDetails]="stepDetails()"
4445
[vertexMethod]="vertexResolution()"
45-
/>
46+
[learningRateInput]="lr().toString()"
47+
(inputChange)="onInputChange($event)"
48+
(step)="onStep()"
49+
(randomize)="onRandomize()"
50+
></app-berkovich-multi-tree-vis>
4651
}
4752
</div>
4853

4954
<div style="background: #f1f5f9; border-radius: 8px; padding: 12px 16px; font-family: monospace; font-size: 13px; color: #334155;">
5055
<div><strong>Component Tag:</strong></div>
51-
<pre style="margin: 4px 0 0 0; white-space: pre-wrap; font-size: 12px; color: #0f766e;">&lt;app-berkovich-multi-tree-vis
52-
[prime]="{{ prime() }}"
53-
[operator]="'{{ operator() }}'"
54-
[centerX1]="&#123; num: {{ parsedCenterX1().num }}n, den: {{ parsedCenterX1().den }}n &#125;"
55-
[rhoX1]="{{ rhoX1() }}"
56-
[centerX2]="&#123; num: {{ parsedCenterX2().num }}n, den: {{ parsedCenterX2().den }}n &#125;"
57-
[rhoX2]="{{ rhoX2() }}"
58-
[targetCenterY]="&#123; num: {{ parsedTargetCenterY().num }}n, den: {{ parsedTargetCenterY().den }}n &#125;"
59-
[targetRhoY]="{{ targetRhoY() }}"
60-
/&gt;</pre>
56+
<div style="margin: 4px 0 0 0; font-size: 12px; color: #0f766e;">app-berkovich-multi-tree-vis</div>
6157
</div>
6258
</section>
6359

@@ -73,18 +69,17 @@ <h2 style="font-size: 18px; font-weight: 700; margin: 0; color: #0f172a; display
7369
<mat-form-field appearance="outline" style="width: 100%;">
7470
<mat-label>Binary Operator</mat-label>
7571
<mat-select [value]="operator()" (selectionChange)="operator.set($event.value)">
76-
<mat-option value="addition">Addition (+)</mat-option>
77-
<mat-option value="multiplication">Multiplication (×)</mat-option>
72+
<mat-option value="addition">Addition</mat-option>
73+
<mat-option value="multiplication">Multiplication</mat-option>
7874
</mat-select>
7975
</mat-form-field>
8076

81-
<!-- Prime -->
8277
<mat-form-field appearance="outline" style="width: 100%;">
8378
<mat-label>Base Prime (p)</mat-label>
84-
<mat-select [value]="prime()" (selectionChange)="prime.set($event.value)">
85-
<mat-option [value]="2">2</mat-option>
86-
<mat-option [value]="3">3</mat-option>
87-
<mat-option [value]="5">5</mat-option>
79+
<mat-select [value]="prime()" (selectionChange)="prime.set(+$event.value)">
80+
<mat-option value="2">2</mat-option>
81+
<mat-option value="3">3</mat-option>
82+
<mat-option value="5">5</mat-option>
8883
</mat-select>
8984
</mat-form-field>
9085

@@ -131,15 +126,6 @@ <h3 style="font-size: 13px; font-weight: 600; margin: 0 0 10px 0; color: #059669
131126
<mat-label>Target y Digits</mat-label>
132127
<input matInput [ngModel]="targetCenterYDigits()" (ngModelChange)="targetCenterYDigits.set($event)">
133128
</mat-form-field>
134-
<div>
135-
<div style="display: flex; justify-content: space-between; font-size: 12px; font-weight: 500; margin-bottom: 2px;">
136-
<span>Target Rho y</span>
137-
<span style="font-weight: 700; color: #059669;">{{ targetRhoY().toFixed(2) }}</span>
138-
</div>
139-
<mat-slider min="-3" max="3" step="0.1" style="width: 100%;">
140-
<input matSliderThumb [ngModel]="targetRhoY()" (ngModelChange)="targetRhoY.set($event)">
141-
</mat-slider>
142-
</div>
143129
</div>
144130
</div>
145131
</aside>

0 commit comments

Comments
 (0)