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 all 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
77 changes: 77 additions & 0 deletions content/matrices/components/gauss-solver.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@

@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;
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
250 changes: 250 additions & 0 deletions content/matrices/components/gauss-solver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@


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';

// TODO: function (px) to calculate "20+40*inRow1", and add output to the model
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!: number[][][];
$matrices!: ElementView[];

ready() {
// TODO: instead of decimals, display fractions (now? or later?)
// TODO: use "parseInput" instead of Expression.parse
// TODO: use "collision detection" to prevent same row from being highlighted
const input = Expression.parse(this.attr('matrix'))
.evaluate({'[': (...args: number[]) => [...args] as any}) as unknown as number[][];

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

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

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

this.inputStack = [this.copyMatrix(input)]; // pushes copy of input onto stack

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

/**
* When a circle is moved, update the values of "inRow1" and "inRow2"
*/
for (const [i, key] of inRows.entries()) { // inRow1
slide($circles[i], {
move: (p: Point) => {
const row = clamp(Math.round((p.y - 20) / 40), 0, this.size - 1);
// 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)
/* if (this.model.op === 'multiply' || this.model[inRows[i === 0 ? 1 : 0]] !== row)*/
// TODO: here is where row collision detection would be
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

console.log(`Slide: ${i}, key=${key}, row=${row}`);
}
});
}

/**
* Watch for change and update display of output cells
*/
this.model.watch((state) => {
console.log('update state');
// FIXME: @philipp -- without this below log statement, the model won't update when inRow2 is changed during add/swap (blue arrow)
for (const [i, row] of this.$outputCells.entries()) {
for (const [j, _val] of row.entries()) {
this.handleOperation(state, i, j);
// FIXME: @philipp is there an alternative to this? With binding?
_val.text = '' + this.model.output[i][j];
}
}
});

/**
* Watch for FACTOR change
* (can coexist with other watch function)
*/
this.model.watch((state) => {
let _parseable = true;
let expr;
let value;
try {
// TODO: this is where the parseInput should go
expr = Expression.parse(state.factorString);
value = expr.evaluate() as number;
this.model.factor = value;
} catch (e) {
_parseable = false;
this.model.factor = 1;
}
});

// APPLY + UNDO buttons
const $applyBtn = this.$('.apply');
$applyBtn?.on('click', this.actionApply.bind(this));
const $undoBtn = this.$('.undo');
$undoBtn?.on('click', this.actionUndo.bind(this));
}

private copyMatrix(matrix: number[][]) {
return matrix.map(row => [...row]);
}

private handleOperation(state: Model, i: number, j: number) {
// state.inRow2 is accessed here because "multiply" is switched on the first call,
// and the model only registers what is needed on the first call
const _inRow2 = state.inRow2;
switch (state.op) {
case 'multiply':
this.handleMultiply(state, i, j);
break;
case 'add':
this.handleAdd(state, i, j);
break;
case 'swap':
this.model.outRow1 = _inRow2;
this.model.outRow2 = state.inRow1;
this.handleSwap(state, i, j);
break;
}
}

private handleMultiply(state: Model, i: number, j: number) {
if (state.factor && i === state.inRow1) {
const m = this.model.input[state.inRow1][j];
this.model.output[i][j] = state.factor * m;
} else {
this.model.output[i][j] = this.model.input[i][j];
}
this.model.outRow1 = this.model.inRow1;
}

private handleAdd(state: Model, i: number, j: number) {
this.model.output[i][j] = this.model.input[i][j];
if (state.factor && i === state.outRow1) {
const c1 = this.model.input[state.inRow1][j];
const c2 = this.model.input[state.inRow2][j];
const write = c1 + state.factor * c2;
this.model.output[i][j] = write;

} else {
this.model.output[i][j] = this.model.input[i][j];
}
this.model.outRow1 = this.model.inRow1;
}

private handleSwap(state: Model, i: number, j: number) {
// FIXME: swap 2 (on update model)
this.model.output[i][j] = this.model.input[i][j];
if (i === state.inRow1) {
this.model.output[i][j] = this.model.input[state.inRow2][j];
} else if (i === state.inRow2) {
this.model.output[i][j] = this.model.input[state.inRow1][j];
}
}

/**
* Apply the Action button
*/
private actionApply() {
// swapping input and output
for (const [i, row] of this.$inputCells.entries()) {
for (const [j, val] of row.entries()) {
// move output to input, and update
this.model.input[i][j] = this.model.output[i][j];
val.text = '' + this.model.input[i][j];

// apply operation to get output values, and update
this.handleOperation(this.model, i, j);
this.$outputCells[i][j].text = '' + this.model.output[i][j];
}
}

// TODO: here is where the animation goes
// TODO: fade out all elements except for the output matrix
// TODO: also do this with other boxes
// TODO: slide the output matrix to the left, to replace input matrix
// TODO: Input box reverts to default (Multiply by 1)
this.$matrices[0].exit('fade');
this.inputStack.push(this.copyMatrix(this.model.input));

if (this.checkForSolvedIdentity()) {
this.$step.addHint('correct');
}
}

// TODO: replace this with a "Reset" Button
/**
* Apply the Undo button
*/
private actionUndo() {
if (this.inputStack.length === 1) return;
this.inputStack.pop();

this.model.input = this.copyMatrix(this.inputStack[this.inputStack.length - 1]);
for (const [i, row] of this.$inputCells.entries()) {
for (const [j, val] of row.entries()) {
val.text = '' + this.model.input[i][j];

// apply operation to get output values, and update
this.handleOperation(this.model, i, j);
this.$outputCells[i][j].text = '' + this.model.output[i][j];
}
}
}

/**
* Check if the cells in the input matrix equal the identity.
*/
checkForSolvedIdentity(): boolean {
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++) {
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