Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Matrices Course and Gauss Solver #307

Open
wants to merge 40 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f084f03
Migrate Inverse draft from Notion into Markdown
kevindeland Nov 23, 2020
96596df
Quick edits to intro
kevindeland Nov 23, 2020
5da5aa0
Quick edits to Example Calculation
kevindeland Nov 23, 2020
495910c
Quick Edits to Inverse:Rotation
kevindeland Nov 24, 2020
780e334
Quick Edits to Inverse:Intersection
kevindeland Nov 24, 2020
1a5cd72
Quick Edits to Inverse:Derive
kevindeland Nov 24, 2020
bb0c70b
Migrate part of Gauss draft from Notion into Markdown
kevindeland Nov 24, 2020
d0a1043
Prototype for Network Analysis
kevindeland Nov 25, 2020
e8d8a66
Draft for Inverse:LeastSquares
kevindeland Nov 25, 2020
801fbb0
Draft for Gauss:ElectricBill
kevindeland Nov 25, 2020
1bc5cb0
Beginnings of x-gaussian solver
kevindeland Dec 3, 2020
27666b9
Can copy rows, can move left and right matrices
kevindeland Dec 3, 2020
11d27d8
Fully articulated and half-implemented steps for solving 2x2
kevindeland Dec 3, 2020
cc84705
Gaussian: beginning to codify 3x3 matrix solution steps
kevindeland Dec 7, 2020
b82e878
Remove extra section for singular matrices
kevindeland Dec 7, 2020
79b0623
Formula for inverse of 3x3 matrix
kevindeland Dec 7, 2020
930cda1
guass map of netherlands
kevindeland Jan 23, 2021
1d2016b
prepping for code
kevindeland Jan 27, 2021
697a077
Merge branch 'master' into matrix-inverses
kevindeland Jan 27, 2021
803d395
First pass at copying g-doc into markdown
kevindeland Jan 27, 2021
2a8c0a6
todo
kevindeland Feb 8, 2021
d952d9d
Merge branch 'master' into matrix-inverses
kevindeland Feb 8, 2021
2f936d6
hand-copied Philipp code from 'gauss-solver' branch
kevindeland Feb 8, 2021
aa57b90
gauss-solver: right outward arrows
kevindeland Feb 8, 2021
027f6b6
Row operations working, can move output to input
kevindeland Feb 9, 2021
48e9b46
Change text/display with operation
kevindeland Feb 9, 2021
b4096d7
Special treat when matrix is solved.
kevindeland Feb 9, 2021
d804136
Undo button
kevindeland Feb 9, 2021
9f39d43
An attempt to add row highlighting (CSS needs work)
kevindeland Feb 9, 2021
e5e727f
Some CSS fixing (only looks good when fully expanded)
kevindeland Feb 9, 2021
de97e7d
Removed old Gaussian solver
kevindeland Feb 9, 2021
c862ecf
Delete left.svg
plegner Feb 9, 2021
8223f18
Delete right.svg
plegner Feb 9, 2021
90ee6c1
Update styles.less
plegner Feb 9, 2021
132ffa2
Change storage of input/output from HTML text to number[][]
kevindeland Feb 11, 2021
5fe8ceb
Fixed undo button stack
kevindeland Feb 11, 2021
0e8e66a
comment out logs
kevindeland Feb 11, 2021
46e1f5e
some refactoring and cleaning
kevindeland Feb 12, 2021
611fef0
fixed swap function (weird bug found?)
kevindeland Feb 12, 2021
7b3cb8e
gauss solver TODO comments
kevindeland Mar 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions content/matrices/components/gauss-solver.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@

@import "../../shared/variables";

x-gauss-solver {
display: flex;
justify-content: center;

.highlighter {
position: absolute;
&.input { left: 50px; top: 40px; }
&.output { left: 566px; top: 40px; }

rect.red { fill: @red; opacity: 0.5;}
rect.blue { fill: @blue; opacity: 0.5;}
}

.matrix {
display: grid;
grid-auto-rows: 36px;
grid-gap: 4px;
margin-top: 12px;
> div { text-align: center; line-height: 36px; }
}

.operation { flex-grow: 1; position: relative; margin: 0 40px; max-width: 280px; }
.operation x-select { display: flex; margin-bottom: 1px; }
.op {
background: @medium-grey;
color: white;
margin-right: 1px;
flex: 40px 1 1;
opacity: 0.6;
text-align: center;
cursor: pointer;
&.active { opacity: 1; }
&:first-child { border-top-left-radius: 6px; }
&:last-child { border-top-right-radius: 6px; margin-right: 0; }
}

.operation-body {
background: mix(@medium-grey, white);
padding: 24px 0;
border-radius: 0 0 6px 6px;
p { text-align: center; margin: 0; }
input { background: white; width: 40px; }
}

.connections-left {
position: absolute; left: -55px; top: 10px;
circle { cursor: grab; }
path { stroke-width: 3px; fill: none; stroke-linecap: round; }
}

.connections-right {
position: absolute; left: 250px; top: 10px;
polygon { cursor: grab; }
path { stroke-width: 3px; fill: none; stroke-linecap: round; }
}

.description {
text-align: center;
font-size: small;
}

.bottom {
text-align: center;

button {
margin: 5px;
padding: 5px;
background: mix(@medium-grey, white, 20);
border: 1px;
border-color: black;
cursor: pointer;
}
}

}
41 changes: 41 additions & 0 deletions content/matrices/components/gauss-solver.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
svg.highlighter.input(:width="(size+2)*40+4" :height="size*40+4+12")
rect.red(:x="40*(3-size)" :y="(20+40*inRow1)" :width="(size+1)*40" height="36" rx="18" ry="18")
rect.blue(:x="40*(3-size)" :y="(20+40*inRow2)" :width="(size+1)*40" height="36" rx="18" ry="18" :show="op !== 'multiply'")
svg.highlighter.output(:width="(size+2)*40+4" :height="size*40+4+12")
rect.red(x="0" :y="(20+40*outRow1)" :width="(size+1)*40" height="36" rx="18" ry="18")
rect.blue(x="0" :y="(20+40*outRow2)" :width="(size+1)*40" height="36" rx="18" ry="18" :show="op === 'swap'")
.matrix
.operation
svg.connections-left(width=100 height=120)
path(:d="'M20,'+(20+40*inRow1)+'C50,'+(20+40*inRow1)+',50,60,80,60'" stroke="#d90000")
circle(cx=20 :cy="20+40*inRow1" r=8 fill="#d90000")
path(:d="'M20,'+(20+40*inRow2)+'C50,'+(20+40*inRow2)+',50,60,80,60'" stroke="#0f82f2" :show="op !== 'multiply'")
circle(cx=20 :cy="20+40*inRow2" r=8 fill="#0f82f2" :show="op !== 'multiply'")
svg.connections-right(width=100 height=120)
path(:d="'M20,60C50,60,50,'+(20+40*outRow1)+',80,'+(20+40*outRow1)" stroke="#d90000")
polygon(:points="'76,'+(20+40*outRow1-8)+' 76,'+(20+40*outRow1+8)+',92,'+(20+40*outRow1)" fill="#d90000")
path(:d="'M20,60C50,60,50,'+(20+40*outRow2)+',80,'+(20+40*outRow2)" stroke="#0f82f2" :show="op === 'swap'")
polygon(:points="'76,'+(20+40*outRow2-8)+' 76,'+(20+40*outRow2+8)+' 92,'+(20+40*outRow2)" fill="#0f82f2" :show="op === 'swap'")
x-select(:bind="op")
.op(value="multiply") Multiply
.op(value="add") Add
.op(value="swap") Swap
.operation-body
p(:show="op ==='add'")
svg(width=16 height=16)
circle(cx=8 cy=8 r=8 fill="#d90000" :show="op === 'add'")
| +
input(:bind="factorString")
svg(width=16 height=16)
circle(cx=8 cy=8 r=8 fill="#0f82f2" :show="op === 'add'")
p(:show="op ==='multiply'")
| x
input(:bind="factorString")
.text
.description(:show="op ==='multiply'") Multiply a row by a non-zero factor
.description(:show="op ==='add'") Add a multiple of one row to another
.description(:show="op ==='swap'") Swap two rows
.bottom
button.apply Apply
button.undo Undo
.matrix
196 changes: 196 additions & 0 deletions content/matrices/components/gauss-solver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@


import {$N, CustomElementView, ElementView, Observable, observe, register, slide} from '@mathigon/boost';
import {tabulate2D} from '@mathigon/core';
import {Point} from '@mathigon/euclid';
import {clamp} from '@mathigon/fermat';
import {Expression} from '@mathigon/hilbert';
import {Step} from '../../shared/types';


import template from './gauss-solver.pug';

type Model = {
input: number[][], output: number[][], op: string, size: number,
inRow1: number, inRow2: number, outRow1: number, outRow2: number,
factorString: string, factor: number};

@register('x-gauss-solver', {template})
export class GaussSolver extends CustomElementView {
model!: Observable<Model>;
size!: number; // Number of Matrix rows
$inputCells!: ElementView[][]; // Array of all cells in the left matrix.
$outputCells!: ElementView[][]; // Array of all cells in the right matrix.
$step!: Step;
inputStack!: string[][][];

ready() {
const input = Expression.parse(this.attr('matrix'))
.evaluate({'[': (...args: number[]) => [...args] as any}) as unknown as number[][];

this.size = input.length;
this.bindModel(observe({input, output: input, size: this.size, inRow1: 0, inRow2: 1, outRow1: 0, outRow2: 1, factor: 1, factorString: '1'}));

// Set up the input and output matrices
const $matrices = this.$$('.matrix');
for (const $m of $matrices) $m.css('grid-template-columns', `repeat(${this.size + 1}, 36px)`);

this.$inputCells = tabulate2D((i, j) =>
$N('div', {text: input[i][j]}, $matrices[0]), this.size, this.size + 1
);
this.$outputCells = tabulate2D(() => $N('div', {text: 0}, $matrices[1]), this.size, this.size + 1);

this.inputStack = [this.$inputCells.map(row => row.map(val => val.text))];

const $circles = this.$$('.connections-left circle');
const inRows = ['inRow1', 'inRow2'] as ('inRow1'|'inRow2')[];

for (const [i, key] of inRows.entries()) {
slide($circles[i], {
move: (p: Point) => {
// Check that this row is not equal to the other input
// ^ I removed this feature because it makes it difficult w/ 2x2 matrix to choose which one you want to multiply
const row = clamp(Math.round((p.y - 20) / 40), 0, this.size - 1);
/* if (this.model.op === 'multiply' || this.model[inRows[i === 0 ? 1 : 0]] !== row)*/ this.model[key] = row;
}
});
}

const $arrows = this.$$('.connections-right polygon');
const outRows = ['outRow1', 'outRow2'] as ('outRow1'|'outRow2')[];

for (const [i, key] of outRows.entries()) {
slide($arrows[i], {
move: (p: Point) => {
// Check that this row not equal to the other output
const row = clamp(Math.round((p.y - 20) / 40), 0, this.size - 1);
this.model[key] = row;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the output rows need to be interactive right? They are the same (or swapped) as the input rows

}
});
}

// Watch for change and update display of output cells
this.model.watch((state) => {

console.log(`State: {
op: ${state.op},
factor: ${state.factor}, factorString: ${state.factorString},
inRow1: ${state.inRow1}, inRow2: ${state.inRow2},
outRow1: ${state.outRow1}, outRow2: ${state.outRow2}
}`);
for (const [i, row] of this.$outputCells.entries()) {
for (const [j, val] of row.entries()) {

switch (state.op) {
case 'multiply':
// "Multiply a row by a non-zero factor"
if (state.factor && i === state.outRow1) {
const m = Expression.parse(this.$inputCells[state.inRow1][j].text).evaluate();
val.text = '' + (state.factor * m);
} else {
val.text = this.$inputCells[i][j].text;
}
break;
case 'add':
// "Add a multiple of one row to another"
val.text = this.$inputCells[i][j].text;
if (state.factor && i === state.outRow1) {
const c1 = Expression.parse(this.$inputCells[state.inRow1][j].text).evaluate();
const c2 = Expression.parse(this.$inputCells[state.inRow2][j].text).evaluate();
val.text = '' + (c1 + state.factor * c2);
} else {
val.text = this.$inputCells[i][j].text;
}
break;
case 'swap':
// "Swap two rows"
if (i === state.outRow1) {
val.text = this.$inputCells[state.inRow1][j].text;
} else if (i === state.outRow2) {
val.text = this.$inputCells[state.inRow2][j].text;
} else {
val.text = this.$inputCells[i][j].text;
}
break;
}
}
}
});

// Button to Apply Row Operation
const $applyBtn = this.$('.apply');
$applyBtn?.on('click', () => {
// swapping input and output
for (const [i, row] of this.$inputCells.entries()) {
for (const [j, val] of row.entries()) {
val.text = this.$outputCells[i][j].text;
}
}
this.inputStack.push(this.$outputCells.map(row => row.map(val => val.text)));
console.log(this.inputStack);

if (this.checkForSolvedIdentity()) {
console.log('Success!');
this.$step.tools.confetti();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than showing confetti, maybe something more subtle like this.$step.addHint('correct')

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's definitely more appropriate, I just really wanted a chance to use the confetti 😆

}
});

const $undoBtn = this.$('.undo');
$undoBtn?.on('click', () => {
if (this.inputStack.length === 1) return;
this.inputStack.pop();

const newTop = this.inputStack[this.inputStack.length - 1];
for (const [i, row] of this.$inputCells.entries()) {
for (const [j, val] of row.entries()) {
val.text = newTop[i][j];
}
}
});

// watch for factor change
this.model.watch((state) => {
let _parseable = true;
let expr;
let value;
try {
// FIXME: @philipp this might throw an error, Not sure how else to handle it.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, I would just make factor a number (e.g. 1 or 0.5). We can allow things like 1/2 in the future, but then we should use a proper equation editor rather than an input field

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will try this for now, but I'm not sure it would work with numbers like 1/3 or 1/12.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some things to try:
(a) Rounding? With error margins?
(b) Just switch the problems until we add the expression editor.
(c) Ignore the problem (but China matrix will be close to unsolvable, as checkForSolvedIdentity checks for exact value)

expr = Expression.parse(state.factorString);
value = expr.evaluate() as number;
this.model.factor = value;
} catch (e) {
_parseable = false;
this.model.factor = 1;
}
});
}

checkForSolvedIdentity(): boolean {
console.log('Checking for solved identity');
const numRows = this.$inputCells.length;

for (let i = 0; i < numRows; i++) {
const values = this.$inputCells[i];
// i is also the expectedOneIndex

// check all values
for (let j = 0; j < numRows; j++) {
// console.log(`Checking row=${i}; col=${j}; value=${values[j]}`);
if (j === i) {
if (values[j].text != '1') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should avoid storing state in the DOM. Rather than accessing .text (which is quite an expensive operation), we should just have a numeric, nested array that contains the current value of the output matrix

// diagonal should be 1
return false;
}
} else if (values[j].text != '0') {
// non-diag should be 0
return false;
}
}
}
return true;
}

bindStep($step: Step) {
this.$step = $step;
}
}
Loading