Published
Edited
May 8, 2019
Insert cell
md`# Node math`
Insert cell
import {math} from '@ajbouh/math-js-with-unitsum'
Insert cell
import {toposort} from '@ajbouh/toposort'
Insert cell
S = require('s-js')
Insert cell
class SObject {
static fromDataEntries(dataEntries, writeable) {
let signals = {}
dataEntries.forEach(
([k, v]) => signals[k] = S.data(v)
)
return new SObject(signals, writeable)
}

static fromData(data, writeable) {
return SObject.fromDataEntries(Object.entries(data), writeable)
}
constructor(signals, writeable) {
const proxy = new Proxy(signals, {
set: (target, prop, value, receiver) => {
let signal = target[prop]
if (signal !== undefined) {
if (writeable) {
signal(value)
return true
}

console.log("tried to update", prop, "to", value, "from", typeof signal === 'function' ? signal() : signal)
return false
}
return false
},
get: (target, prop, receiver) => {
if (prop === 'constructor') { return Object; }
if (prop === Symbol.toStringTag) { return undefined; }
let signal = target[prop]
if (signal !== undefined) {
return signal()
}
},
})

this.proxy = proxy
this.signals = signals
}
merge(...others) {
return new SObject(Object.assign({}, this.signals, ...others.map(o => o.signals)))
}

map(fn) {
let dsts = {}
Object.keys(this.signals).forEach(
key => {
let src = this.signals[key]
let data = fn(src(), key)
let dst = S.data(data)
dsts[key] = dst
S.on(src, _ => dst(fn(src(), key)), undefined, true)
}
)

return new SObject(dsts, false)
}
}

Insert cell
function NewArrayProxy(arr) {
// console.log("NewArrayProxy", arr)
return new Proxy(arr, {
get: (target, prop) => {
if (prop === 'constructor') { return Object }
if (prop === 'length') { return target.length }
if (prop === Symbol.toStringTag) { return undefined }
if (prop === 'toString') { return target.toString }
console.log("array", typeof prop, prop)
if (typeof prop === "string") {
let c = prop.charCodeAt(0)
if (c >= 48 && c <= 57) {
return target[prop]
}
}
let anyUndefined = false
let r = target.map(e => {
let v = e[prop]
if (v === undefined) {
anyUndefined = true
}
return v
})
console.log(target, `.${prop}`, r)
// r[NewArrayProxySymbol] = true
return anyUndefined ? undefined : r
}
})
}
Insert cell
// NewArrayProxySymbol = Symbol()
Insert cell
class MathExpr {
static compileBlock(code) {
let node = math.parse(code)
console.log("fromCode", code, node)
let nodes;
switch (node.type) {
case "BlockNode":
nodes = node.blocks.map(n => n.node)
break;
default:
nodes = [node]
}

return nodes.filter(n => n.type === "AssignmentNode")
.map(an => new MathExpr(an.object.name, an.value))
}
static compile(name, source) {
return new MathExpr(name, math.parse(source))
}

constructor(name, exprRoot) {
let depSet = new Set(
exprRoot.filter((n, path, z) => {
// console.log(n, path, z)
if (!n.isSymbolNode) {
return false
}

if (math[n.name] !== undefined) {
return false
}

return !math.type.Unit.isValuelessUnit(n.name)
}).map(n => n.name)
)

this.name = name
this.deps = Array.from(depSet)
this.root = exprRoot
}
toString() {
return this.root.toString()
}
eval(scope) {
return this.root.eval(scope)
}
}

Insert cell
class Frame {
constructor(name, exprs, initialScopeValues) {
let exprIndex = {}
exprs.forEach(expr => exprIndex[expr.name] = expr)

this.name = name
this.exprs = exprs
this.exprOrder = toposort(exprs, expr => expr.deps.map(depName => exprIndex[depName]).filter(_ => _))
let scope = Object.assign({}, initialScopeValues)
this.exprOrder.forEach(expr => {
S(() => {
let r = undefined
let shouldEval = expr.deps.every(depName => scope[depName] !== undefined)
if (shouldEval) {
// console.log("eval expr", key, expr, scope)
console.log(`[${name}.${expr.name}] eval`, expr, "in", scope)
r = expr.eval(scope)
// console.log("eval res", r)
}
scope[expr.name] = r
})
})
this.scope = SObject.fromData(scope).proxy
}
cloneAndOverride(name, withExprs, withScope) {
let exprs = new Map(withExprs.map(expr => [expr.name, expr]))
this.exprs.forEach(expr => exprs.has(expr.name) || exprs.set(expr.name, expr))

let scope = Object.assign({}, withScope)
Object.keys(this.scope).forEach(
k => {
if (scope[k] !== undefined) { return }
// This is subtly wrong. Also need to avoid copying values are invalidated by an override...
scope[k] = this.scope[k]
}
)

return new Frame(name, Array.from(exprs.values()), scope)
}
}
Insert cell
{
let o = {a: ['b', 'c'], e: ['d'], b: ['c'], c: ['d']}
return toposort(Object.keys(o), n => o[n])
}
Insert cell

math.unit('second').units[0].unit

Insert cell
objs = {
try {
class Node {
constructor(name, def, nodeDeps, frame) {
this.name = name
this.def = def
this.nodeDeps = nodeDeps
this.frame = frame
this.attrs = frame.scope
this.attrStrings = {}
Object.entries(this.attrs).forEach(([k, v]) => {
if (v !== undefined) {
this.attrStrings[k] = v.toString ? v.toString() : v
}
})
}
}

let base = new Frame(
"base",
MathExpr.compileBlock(`
varunitcost = 0
varcostrate = (sum(deps.varcost) + varunitcost) / varunit
usage = 1 varunit
varcost = dot(usage, varcostrate)
fixedcost = 0
fixedunit = 1
fixedcostself = 0
fixedcost = (sum(deps.fixedcost) + fixedcostself) / fixedunit`
),
{
deps: NewArrayProxy([]),
})

console.log("base", base)
let exprs = {
// power: { attrs: {varunitcost: '0.1 usd / hour', varunit: 'kW'} },
// computer: { deps: {power: {usage: '1 kW'}}, attrs: {fixedcostself: '50 usd / measure'}},
// // sysadmin: { attrs: {fixedcostself: '200000 usd / year'} },
// webserver: {
// deps: {
// // computer: { usage: '1 second' },
// // sysadmin: {},
// },
// },
// visit: { deps: {webserver: {usage: '1 second'}} }
tea: { deps: {hotwater: {usage: '100 mL'}} },
hotwater: { deps: {kettle: {usage: '2 kettle minute'}, water: {}}, attrs:{varunit: 'L'}},
water: { attrs: {varunitcost: '0.1 usd', varunit: 'L'} },
kettle: { deps: {power: {usage: '1 kW'} }},
power: { attrs: {varunitcost: '0.1 usd / hour', varunit: 'kW'} },
}
// Evaluate expressions in attrs and dep overrides
let nodeDepIndex = {}
Object.entries(exprs).forEach(([nodeName, nodeDef]) =>
nodeDepIndex[nodeName] = Object.keys(nodeDef.deps || {})
)
let nodeOrder = toposort(Object.keys(nodeDepIndex), node => nodeDepIndex[node])

let nodes = {}
nodeOrder.forEach(nodeName => {
let nodeDef = exprs[nodeName]
let attrExprs = Object.entries(nodeDef.attrs || {}).map(([name, src]) => MathExpr.compile(name, src))
let nodeDeps = {}
Object.entries(nodeDef.deps || {}).forEach(([depName, depOverrideDef]) => {
depOverrideDef = depOverrideDef || {}
let depOverrideExprs = Object.entries(depOverrideDef).map(([name, src]) => MathExpr.compile(name, src))
let depNode = nodes[depName]
nodeDeps[depName] = depNode.frame.cloneAndOverride(
`${nodeName}.dep < ${depName}`, depOverrideExprs, {})
})
let depsProxy = NewArrayProxy(
Object.values(nodeDeps).map(dep => dep.scope)
)
console.log(nodeName, "depsProxy", depsProxy, nodeDeps)
let scope = {deps: depsProxy}
if (!attrExprs.some(expr => expr.name === "varunit")) {
if (!math.type.Unit.isValuelessUnit(nodeName)) {
math.createUnit(nodeName)
}
scope.varunit = math.unit(nodeName)
// scope.usage = math.multiply(1, scope.varunit)
}
let frame = base.cloneAndOverride(nodeName, attrExprs, scope)

nodes[nodeName] = new Node(nodeName, nodeDef, nodeDeps, frame)
})

console.log("nodeOrder", nodeOrder)
// S.root(d => {
// invalidation.then(d)
// })

return nodes
} catch (e) {
console.log(e)
throw e
}
}
Insert cell
objs.power.attrs.varcost.toString()
Insert cell
tex`${math.parse(objs.power.attrs.varcostrate.toString()).toTex()}`
Insert cell
objs.kettle.nodeDeps.power.scope.usage.toString()
Insert cell
tex`${math.parse(objs.kettle.attrs.varcostrate.toString()).toTex()}`
Insert cell
tex`${math.parse(math.divide(1, objs.water.attrs.varcostrate).toString()).toTex()}`
Insert cell
tex`${math.parse(objs.hotwater.attrs.varcostrate.toString()).toTex()}`
Insert cell
objs.hotwater.attrs.varcostrate.toString()
Insert cell
math.eval('dot(3 usd / L + 4 usd / kWh, 1 L + 1 kWh)').toString()
Insert cell
tex`${math.parse(objs.tea.attrs.varcost.toString()).toTex()}`
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more