@decidables/discountable-elements 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,71 @@
1
+ /* eslint no-restricted-globals: ["off", "self"] */
2
+
3
+ // Needed for d3 in WebWorker!
4
+ import 'regenerator-runtime/runtime';
5
+
6
+ import * as BayesDistributions from 'bayes.js/distributions';
7
+ import * as BayesMcmc from 'bayes.js/mcmc';
8
+ import * as d3 from 'd3';
9
+ import HTDMath from '@decidables/discountable-math';
10
+
11
+ self.onmessage = (event) => {
12
+ const params = {
13
+ k: {type: 'real', lower: 0, upper: 100},
14
+ luce: {type: 'real', lower: 0, upper: 100},
15
+ };
16
+
17
+ const logPost = (state, data) => {
18
+ let lp = 0;
19
+
20
+ // Priors
21
+ const kMean = 2;
22
+ const kShape = 3;
23
+ lp += BayesDistributions.gamma(
24
+ state.k,
25
+ kShape,
26
+ kShape / kMean,
27
+ );
28
+ // lp += BayesDistributions.unif(state.k, 0, 100);
29
+
30
+ const luceMean = 2;
31
+ const luceShape = 3;
32
+ lp += BayesDistributions.gamma(
33
+ state.luce,
34
+ luceShape,
35
+ luceShape / luceMean,
36
+ );
37
+ // lp += BayesDistributions.unif(state.luce, 0, 100);
38
+
39
+ // Likelihood
40
+ data.forEach((choice) => {
41
+ // Values
42
+ const vs = HTDMath.adk2v(choice.as, choice.ds, state.k);
43
+ const vl = HTDMath.adk2v(choice.al, choice.dl, state.k);
44
+
45
+ // Choice of sooner or later is sampled from a Bernoulli distribution
46
+ // Luce choice rule is used to compute probability of waiting! (0 = sooner, 1 = later)
47
+ const binval = 1 / (1 + Math.exp(state.luce * (vs - vl)));
48
+
49
+ // Actual response
50
+ const response = (choice.response === 'first') ? 0 : 1;
51
+
52
+ lp += BayesDistributions.bern(response, binval);
53
+ });
54
+
55
+ return lp;
56
+ };
57
+
58
+ // Initializing the sampler
59
+ const sampler = new BayesMcmc.AmwgSampler(params, logPost, event.data);
60
+ // Burning some samples to the MCMC gods and sampling 5000 draws
61
+ sampler.burn(1000);
62
+ const samples = sampler.sample(5000);
63
+
64
+ // Extract summary stats
65
+ const results = {
66
+ k: d3.median(samples.k),
67
+ luce: d3.median(samples.luce),
68
+ };
69
+
70
+ self.postMessage({results: results, samples: samples});
71
+ };
@@ -0,0 +1,197 @@
1
+
2
+ import {html, css} from 'lit';
3
+ import * as Plot from '@observablehq/plot';
4
+
5
+ // Special Web Worker import for rollup-plugin-web-worker-loader
6
+ import HTDFitWorker from 'web-worker:./htd-fit-worker'; /* eslint-disable-line import/no-unresolved */
7
+
8
+ import DiscountableElement from '../discountable-element';
9
+
10
+ /*
11
+ HTDFit element
12
+ <htd-fit>
13
+
14
+ Attributes:
15
+ interactive: true/false
16
+
17
+ */
18
+ export default class HTDFit extends DiscountableElement {
19
+ static get properties() {
20
+ return {
21
+ };
22
+ }
23
+
24
+ constructor() {
25
+ super();
26
+
27
+ this.k = 0.05;
28
+
29
+ this.choices = [];
30
+ this.samples = null;
31
+
32
+ this.working = false;
33
+ this.queued = false;
34
+ this.worker = new HTDFitWorker();
35
+
36
+ this.worker.onmessage = (event) => {
37
+ this.working = false;
38
+ this.samples = event.data.samples;
39
+ this.k = event.data.results.k;
40
+ this.requestUpdate();
41
+
42
+ this.dispatchEvent(new CustomEvent('htd-fit-update', {
43
+ detail: {
44
+ k: this.k,
45
+ },
46
+ bubbles: true,
47
+ }));
48
+
49
+ if (this.queued) {
50
+ this.fit();
51
+ }
52
+ };
53
+
54
+ this.fit();
55
+ }
56
+
57
+ fit() {
58
+ if (!this.working) {
59
+ this.worker.postMessage(this.choices);
60
+ this.working = true;
61
+ this.queued = false;
62
+ } else {
63
+ this.queued = true;
64
+ }
65
+ }
66
+
67
+ clear() {
68
+ this.choices = [];
69
+
70
+ this.fit();
71
+ }
72
+
73
+ get(name = 'default') {
74
+ const choice = this.choices.find((item) => {
75
+ return (item.name === name);
76
+ });
77
+
78
+ return (choice === undefined) ? null : choice;
79
+ }
80
+
81
+ set(as, ds, al, dl, response, name = '', label = '') {
82
+ const choice = this.choices.find((item) => {
83
+ return (item.name === name);
84
+ });
85
+ if (choice === undefined) {
86
+ this.choices.push({
87
+ as: as,
88
+ ds: ds,
89
+ al: al,
90
+ dl: dl,
91
+ response: response,
92
+ name: name,
93
+ label: label,
94
+ });
95
+ } else {
96
+ choice.as = as;
97
+ choice.ds = ds;
98
+ choice.al = al;
99
+ choice.dl = dl;
100
+ choice.response = response;
101
+ choice.label = label;
102
+ }
103
+
104
+ this.fit();
105
+ }
106
+
107
+ static get styles() {
108
+ return [
109
+ super.styles,
110
+ css`
111
+ /* :host {
112
+ display: inline-block;
113
+ } */
114
+
115
+ figure {
116
+ margin: 0.625rem;
117
+ }
118
+
119
+ figure h2 {
120
+ margin: 0.25rem 0;
121
+
122
+ font-size: 1.125rem;
123
+ font-weight: 600;
124
+ }
125
+
126
+ .trace,
127
+ .hist {
128
+ display: inline-block;
129
+ }
130
+ `,
131
+ ];
132
+ }
133
+
134
+ render() {
135
+ return html`
136
+ <div>
137
+ <div>After ${this.choices.length} trials:</div>
138
+ <div>Current:
139
+ <var class="math-var k">k</var> = ${this.k.toFixed(2)}
140
+ </div>
141
+ <div class="param">
142
+ <div class="trace k"></div>
143
+ <div class="hist k"></div>
144
+ </div>
145
+ <div class="param">
146
+ <div class="trace luce"></div>
147
+ <div class="hist luce"></div>
148
+ </div>
149
+ </div>
150
+ `;
151
+ }
152
+
153
+ plotParam(param) {
154
+ this.shadowRoot.querySelector(`.hist.${param}`).replaceChildren(
155
+ Plot.plot({
156
+ title: `Posterior of ${param}`,
157
+ x: {label: `${param}`},
158
+ width: 320,
159
+ height: 240,
160
+ style: 'font-size: 0.75rem; font-family: var(---font-family-base);',
161
+ marks: [
162
+ Plot.rectY(
163
+ this.samples[param],
164
+ Plot.binX({y: 'count'}, {x: Plot.identity}),
165
+ ),
166
+ ],
167
+ }),
168
+ );
169
+
170
+ this.shadowRoot.querySelector(`.trace.${param}`).replaceChildren(
171
+ Plot.plot({
172
+ title: `Traceplot of ${param}`,
173
+ x: {label: 'Samples'},
174
+ y: {label: `${param}`},
175
+ width: 320,
176
+ height: 240,
177
+ style: 'font-size: 0.75rem; font-family: var(---font-family-base);',
178
+ marks: [
179
+ Plot.lineY(
180
+ this.samples[param],
181
+ ),
182
+ ],
183
+ }),
184
+ );
185
+ }
186
+
187
+ update(changedProperties) {
188
+ super.update(changedProperties);
189
+
190
+ if (this.samples !== null) {
191
+ this.plotParam('k');
192
+ this.plotParam('luce');
193
+ }
194
+ }
195
+ }
196
+
197
+ customElements.define('htd-fit', HTDFit);
@@ -0,0 +1,9 @@
1
+
2
+ export {default as DiscountableControl} from './discountable-control';
3
+ export {default as DiscountableResponse} from './discountable-response';
4
+ export {default as HTDCalculation} from './htd-calculation';
5
+ export {default as HTDCurves} from './htd-curves';
6
+ export {default as HTDFit} from './htd-fit';
7
+ export {default as ITCChoice} from './itc-choice';
8
+ export {default as ITCOption} from './itc-option';
9
+ export {default as ITCTask} from './itc-task';
@@ -0,0 +1,134 @@
1
+
2
+ import {html, css} from 'lit';
3
+
4
+ import DiscountableElement from '../discountable-element';
5
+ import './itc-option';
6
+
7
+ /*
8
+ ITCChoice element
9
+ <itc-choice>
10
+
11
+ Attributes:
12
+ */
13
+ export default class ITCChoice extends DiscountableElement {
14
+ static get properties() {
15
+ return {
16
+ state: {
17
+ attribute: 'state',
18
+ type: String,
19
+ reflect: true,
20
+ },
21
+
22
+ as: {
23
+ attribute: 'amount-ss',
24
+ type: Number,
25
+ reflect: true,
26
+ },
27
+ ds: {
28
+ attribute: 'delay-ss',
29
+ type: Number,
30
+ reflect: true,
31
+ },
32
+ al: {
33
+ attribute: 'amount-ll',
34
+ type: Number,
35
+ reflect: true,
36
+ },
37
+ dl: {
38
+ attribute: 'delay-ll',
39
+ type: Number,
40
+ reflect: true,
41
+ },
42
+ };
43
+ }
44
+
45
+ constructor() {
46
+ super();
47
+
48
+ this.states = ['choice', 'fixation', 'blank']; // Possible states
49
+ this.state = 'choice'; // Current state
50
+
51
+ this.as = 10;
52
+ this.ds = 5;
53
+ this.al = 40;
54
+ this.dl = 30;
55
+ }
56
+
57
+ static get styles() {
58
+ return [
59
+ super.styles,
60
+ css`
61
+ :host {
62
+ display: inline-block;
63
+ }
64
+
65
+ .holder {
66
+ user-select: none;
67
+ }
68
+
69
+ .holder > * {
70
+ vertical-align: middle;
71
+ }
72
+
73
+ .query {
74
+ margin: 0 0.5rem;
75
+
76
+ font-family: var(--font-family-code);
77
+ font-size: 1.75rem;
78
+ }
79
+
80
+ itc-option {
81
+ width: 10rem;
82
+ height: 10rem;
83
+ }
84
+ `,
85
+ ];
86
+ }
87
+
88
+ sendEvent() {
89
+ this.dispatchEvent(new CustomEvent('itc-choice-change', {
90
+ detail: {
91
+ as: this.as,
92
+ ds: this.ds,
93
+ al: this.al,
94
+ dl: this.dl,
95
+ },
96
+ bubbles: true,
97
+ }));
98
+ }
99
+
100
+ ssChange(event) {
101
+ this.as = parseFloat(event.detail.a);
102
+ this.ds = parseFloat(event.detail.d);
103
+ this.sendEvent();
104
+ }
105
+
106
+ llChange(event) {
107
+ this.al = parseFloat(event.detail.a);
108
+ this.dl = parseFloat(event.detail.d);
109
+ this.sendEvent();
110
+ }
111
+
112
+ render() {
113
+ return html`
114
+ <div class="holder">
115
+ <itc-option
116
+ state=${this.state}
117
+ ?interactive=${this.interactive}
118
+ amount="${this.as}"
119
+ delay="${this.ds}"
120
+ @itc-option-change=${this.ssChange.bind(this)}>
121
+ </itc-option><span class="query"
122
+ >${(this.state === 'choice') ? '?' : (this.state === 'fixation') ? '+' : html`∙`}</span
123
+ ><itc-option
124
+ state=${this.state}
125
+ ?interactive=${this.interactive}
126
+ amount="${this.al}"
127
+ delay="${this.dl}"
128
+ @itc-option-change=${this.llChange.bind(this)}>
129
+ </itc-option>
130
+ </div>`;
131
+ }
132
+ }
133
+
134
+ customElements.define('itc-choice', ITCChoice);
@@ -0,0 +1,173 @@
1
+
2
+ import {html, css} from 'lit';
3
+
4
+ import DiscountableElement from '../discountable-element';
5
+
6
+ /*
7
+ ITCOption element
8
+ <itc-option>
9
+
10
+ Attributes:
11
+ State
12
+ Amount, Delay
13
+ */
14
+ export default class ITCOption extends DiscountableElement {
15
+ static get properties() {
16
+ return {
17
+ state: {
18
+ attribute: 'state',
19
+ type: String,
20
+ reflect: true,
21
+ },
22
+
23
+ a: {
24
+ attribute: 'amount',
25
+ type: Number,
26
+ reflect: true,
27
+ },
28
+ d: {
29
+ attribute: 'delay',
30
+ type: Number,
31
+ reflect: true,
32
+ },
33
+ };
34
+ }
35
+
36
+ constructor() {
37
+ super();
38
+
39
+ this.states = ['choice', 'fixation', 'blank']; // Possible states
40
+ this.state = 'choice'; // Current state
41
+
42
+ this.a = 0;
43
+ this.d = 0;
44
+ }
45
+
46
+ static get styles() {
47
+ return [
48
+ super.styles,
49
+ css`
50
+ :host {
51
+ display: inline-block;
52
+
53
+ width: 10rem;
54
+ height: 10rem;
55
+ }
56
+
57
+ .holder {
58
+ display: flex;
59
+
60
+ flex-flow: column nowrap;
61
+
62
+ align-items: center;
63
+ justify-content: center;
64
+
65
+ width: 100%;
66
+ height: 100%;
67
+ overflow: visible;
68
+
69
+
70
+ background: var(---color-element-background);
71
+ border: 2px solid var(---color-element-emphasis);
72
+ border-radius: 50%;
73
+ }
74
+
75
+ .interactive,
76
+ .static {
77
+ font-size: 1.75rem;
78
+ }
79
+
80
+ .interactive {
81
+ --decidables-spinner-font-size: 1.75rem;
82
+ }
83
+
84
+ .static {
85
+ padding: 0 0.25rem;
86
+
87
+ border-radius: var(---border-radius);
88
+ }
89
+
90
+ .amount {
91
+ --decidables-spinner-prefix: "$";
92
+ background-color: var(---color-a-light);
93
+ }
94
+
95
+ .amount.interactive {
96
+ --decidables-spinner-input-width: 4rem;
97
+ }
98
+
99
+ .delay {
100
+ background-color: var(---color-d-light);
101
+ }
102
+
103
+ .delay.interactive {
104
+ --decidables-spinner-input-width: 6.75rem;
105
+ --decidables-spinner-postfix: "days";
106
+ --decidables-spinner-postfix-padding: 3.75rem;
107
+ }
108
+ `,
109
+ ];
110
+ }
111
+
112
+ sendEvent() {
113
+ this.dispatchEvent(new CustomEvent('itc-option-change', {
114
+ detail: {
115
+ a: this.a,
116
+ d: this.d,
117
+ },
118
+ bubbles: true,
119
+ }));
120
+ }
121
+
122
+ aInput(event) {
123
+ this.a = parseFloat(event.target.value);
124
+ this.sendEvent();
125
+ }
126
+
127
+ dInput(event) {
128
+ this.d = parseFloat(event.target.value);
129
+ this.sendEvent();
130
+ }
131
+
132
+ render() { /* eslint-disable-line class-methods-use-this */
133
+ return html`
134
+ <div class="holder">
135
+ ${(this.state === 'choice')
136
+ ? this.interactive
137
+ ? html`<decidables-spinner
138
+ class="amount interactive"
139
+ ?disabled=${!this.interactive}
140
+ step="1"
141
+ .value="${this.a}"
142
+ @input=${this.aInput.bind(this)}
143
+ ></decidables-spinner>`
144
+ : html`<div
145
+ class="amount static"
146
+ >$${this.a}</div>`
147
+ : ''
148
+ }
149
+ ${(this.state === 'choice')
150
+ ? html`<div class="in">in</div>`
151
+ : ''
152
+ }
153
+ ${(this.state === 'choice')
154
+ ? this.interactive
155
+ ? html`<decidables-spinner
156
+ class="delay interactive"
157
+ ?disabled=${!this.interactive}
158
+ min="0"
159
+ step="1"
160
+ .value="${this.d}"
161
+ @input=${this.dInput.bind(this)}
162
+ ></decidables-spinner>`
163
+ : html`<div
164
+ class="delay static"
165
+ >${this.d} days</div>`
166
+ : ''
167
+ }
168
+ </div>
169
+ `;
170
+ }
171
+ }
172
+
173
+ customElements.define('itc-option', ITCOption);