-
Notifications
You must be signed in to change notification settings - Fork 154
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
base: master
Are you sure you want to change the base?
Changes from 33 commits
f084f03
96596df
5da5aa0
495910c
780e334
1a5cd72
bb0c70b
d0a1043
e8d8a66
801fbb0
1bc5cb0
27666b9
11d27d8
cc84705
b82e878
79b0623
930cda1
1d2016b
697a077
803d395
2a8c0a6
d952d9d
2f936d6
aa57b90
027f6b6
48e9b46
b4096d7
d804136
9f39d43
e5e727f
de97e7d
c862ecf
8223f18
90ee6c1
132ffa2
5fe8ceb
0e8e66a
46e1f5e
611fef0
7b3cb8e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
} | ||
} | ||
|
||
} |
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 |
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; | ||
} | ||
}); | ||
} | ||
|
||
// 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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than showing confetti, maybe something more subtle like There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For now, I would just make factor a number (e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some things to try: |
||
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') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should avoid storing state in the DOM. Rather than accessing |
||
// 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; | ||
} | ||
} |
There was a problem hiding this comment.
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