├── .env ├── .eslintrc.cjs ├── .github └── workflows │ └── test_coverage.yaml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── devel ├── test_autocorr.py └── test_ess.py ├── doc ├── deployment.md ├── developing.md └── release.md ├── examples ├── .gitignore ├── finite-mixture.stan ├── multi-normal.stan ├── test_finite_mixture.py └── test_multi_normal.py ├── index.html ├── package.json ├── public ├── mcmc-monitor-logo-2.png ├── mcmc-monitor-logo.png └── vite.svg ├── service ├── .gitignore ├── Dockerfile ├── bin │ └── mcmc-monitor ├── devel │ └── readme.txt ├── package.json ├── src │ ├── index.ts │ ├── logic │ │ ├── ChainFile.ts │ │ └── OutputManager.ts │ ├── networking │ │ ├── OutgoingProxyConnection.ts │ │ ├── RemotePeer.ts │ │ ├── Server.ts │ │ ├── SignalCommunicator.ts │ │ └── handleApiRequest.ts │ └── types │ │ ├── ConnectorHttpProxyTypes.ts │ │ ├── MCMCMonitorPeerRequestTypes.ts │ │ ├── MCMCMonitorRequestTypes.ts │ │ ├── MCMCMonitorTypes.ts │ │ ├── Typeguards.ts │ │ ├── WebsocketMessageTypes.ts │ │ ├── index.ts │ │ ├── validateObject.ts │ │ └── wrtc.d.ts ├── test │ └── logic │ │ └── ChainFile.test.ts.disable ├── tsconfig.json └── vitest.config.ts.disable ├── src ├── App.css ├── App.tsx ├── MCMCMonitorDataManager │ ├── MCMCMonitorData.ts │ ├── MCMCMonitorDataManager.ts │ ├── MCMCMonitorDataTypes.ts │ ├── MCMCMonitorTypeguards.ts │ ├── SetupMCMCMonitor.tsx │ ├── stats │ │ ├── ess.ts │ │ └── fft.ts │ ├── updateChains.ts │ ├── updateSequenceStats.ts │ ├── updateSequences.ts │ ├── updateVariableStats.ts │ └── useMCMCMonitor.ts ├── components │ ├── AutocorrelationPlot.tsx │ ├── AutocorrelationPlotWidget.tsx │ ├── ChainsSelector.tsx │ ├── CollapsibleElement.tsx │ ├── ConnectionStatusWidget.tsx │ ├── CookieLogic.tsx │ ├── GeneralOptsControl.tsx │ ├── Hyperlink.tsx │ ├── MatrixOfPlots.tsx │ ├── RunControlPanel.tsx │ ├── RunsTable.tsx │ ├── SequenceHistogram.tsx │ ├── SequenceHistogramWidget.tsx │ ├── SequencePlot.tsx │ ├── SequencePlotWidget.tsx │ ├── SequenceScatterplot.tsx │ ├── SequenceScatterplot3D.tsx │ ├── SequenceScatterplot3DWidget.tsx │ ├── SequenceScatterplotWidget.tsx │ ├── Splitter.tsx │ └── VariablesSelector.tsx ├── config.ts ├── main.tsx ├── networking │ ├── WebrtcConnectionToService.ts │ └── postApiRequest.ts ├── pages │ ├── Home.tsx │ ├── Logo.tsx │ ├── MainWindow.tsx │ ├── RunPage.tsx │ └── index.css ├── spaInterface │ ├── getSpaChainsForRun.ts │ ├── getSpaSequenceUpdates.ts │ ├── postStanPlaygroundRequest.ts │ ├── spaOutputsForRunIds.ts │ └── util.ts ├── tabs │ ├── AutoCorrelationTab.tsx │ ├── CollapsibleTabFrame.tsx │ ├── ConnectionTab.tsx │ ├── ExportTab.tsx │ ├── HistogramTab.tsx │ ├── RunInfoTab.tsx │ ├── ScatterplotsTab.tsx │ ├── TabWidget │ │ ├── TabWidget.tsx │ │ └── TabWidgetTabBar.tsx │ ├── TablesTab │ │ ├── MainTable.tsx │ │ ├── SummaryStatsTab.tsx │ │ └── VariableTable.tsx │ ├── TabsUtility │ │ ├── CollapsibleTabUtility.tsx │ │ ├── PlotSizes.tsx │ │ ├── WarmupExclusionUtility.tsx │ │ └── index.ts │ ├── TracePlotsTab.tsx │ └── index.ts ├── util │ ├── chainColorList.ts │ ├── randomAlphaString.ts │ ├── sleepMsec.ts │ ├── sortedListsAreEqual.ts │ ├── toggleListItem.ts │ ├── useRoute.ts │ └── useWindowDimensions.ts └── vite-env.d.ts ├── test ├── MCMCMonitorDataManager │ ├── MCMCMonitorData.test.ts │ └── updateSequences.test.ts ├── components │ └── ChainsSelector.test.tsx ├── networking │ ├── WebrtcConnectionToService.test.ts │ └── postApiRequest.test.ts └── util │ ├── chainColorList.test.ts │ ├── randomAlphaString.test.ts │ ├── sleepMsec.test.ts │ ├── sortedListsAreEqual.test.ts │ ├── toggleListItem.test.ts │ ├── useRoute.test.tsx │ └── useWindowDimensions.test.tsx ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts ├── vite.dev-config.ts └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | VITE_GOOGLE_ANALYTICS_ID="G-33SWX083FG" 2 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = { 3 | "env": { 4 | "browser": true, 5 | "es2021": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:react-hooks/recommended" 12 | ], 13 | "overrides": [ 14 | ], 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaVersion": "latest", 18 | "sourceType": "module" 19 | }, 20 | "plugins": [ 21 | "react", 22 | "@typescript-eslint" 23 | ], 24 | "rules": { 25 | "@typescript-eslint/no-explicit-any": "off", 26 | "no-constant-condition": ["error", { "checkLoops": false }], 27 | "react/react-in-jsx-scope": "off", 28 | "@typescript-eslint/no-empty-function": "off", 29 | "@typescript-eslint/no-extra-semi": "off" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/test_coverage.yaml: -------------------------------------------------------------------------------- 1 | name: test coverage 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | name: Test 9 | steps: 10 | - uses: actions/checkout@v3 11 | # TODO: Consider caching dependencies 12 | # - uses: actions/cache@v3 13 | # with: 14 | # path: ~/.npm 15 | # key: ${{ runnrer.os }}-node=${{ hashFiles('**/package-lock.json') }} 16 | # restore-keys: | 17 | # ${{ runner.os }}-node- 18 | - name: Install Dependencies 19 | run: | 20 | yarn install 21 | cd service 22 | yarn install 23 | - name: Run tests with coverage 24 | run: | 25 | yarn coverage 26 | cd service 27 | yarn coverage 28 | - name: Prove files exist 29 | run: | 30 | ls -la ./coverage/lcov*.info 31 | ls -la ./service/coverage/lcov*.info 32 | # Codecov's action is notoriously flaky 33 | # - name: Upload to Codecov (GUI) 34 | # uses: codecov/codecov-action@v3 35 | # with: 36 | # token: ${{ secrets.CODECOV_TOKEN }} 37 | # fail_ci_if_error: true 38 | # file: ./coverage/lcov*.info 39 | # flags: gui_tests 40 | # - name: Upload to Codecov (Service) 41 | # uses: codecov/codecov-action@v3 42 | # with: 43 | # token: ${{ secrets.CODECOV_TOKEN }} 44 | # fail_ci_if_error: true 45 | # file: ./service/coverage/lcov*.info 46 | # flags: svc_tests 47 | - name: Upload to Codecov 48 | run: | 49 | pip install codecov 50 | codecov --token ${{ secrets.CODECOV_TOKEN }} --flags gui_units --file ./coverage/lcov.info 51 | codecov --token ${{ secrets.CODECOV_TOKEN }} --flags svc_units --file ./service/coverage/lcov.info 52 | 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dev-dist 13 | dist-ssr 14 | *.local 15 | 16 | # Coverages 17 | coverage/ 18 | lcov.info 19 | lcov-report/ 20 | 21 | # Editor directories and files 22 | .vscode/* 23 | !.vscode/extensions.json 24 | .idea 25 | .DS_Store 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw? 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Placeholder--policy to be established 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 MCMC Monitor Developers 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /devel/test_autocorr.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import numpy.typing as npt 3 | 4 | FloatType = np.float64 5 | IntType = np.int64 6 | VectorType = npt.NDArray[FloatType] 7 | 8 | def autocorr_fft(chain: VectorType) -> VectorType: 9 | """ 10 | Return sample autocorrelations at all lags for the specified sequence. 11 | Algorithmically, this function calls a fast Fourier transform (FFT). 12 | Parameters: 13 | chain: sequence whose autocorrelation is returned 14 | Returns: 15 | autocorrelation estimates at all lags for the specified sequence 16 | """ 17 | size = 2 ** np.ceil(np.log2(2 * len(chain) - 1)).astype("int") 18 | print(size) 19 | var = np.var(chain) 20 | ndata = chain - np.mean(chain) 21 | fft = np.fft.fft(ndata, size) 22 | pwr = np.abs(fft) ** 2 23 | N = len(ndata) 24 | acorr = np.fft.ifft(pwr).real / var / N 25 | return acorr 26 | 27 | # x = np.random.normal(0, 1, (100,)) 28 | y = autocorr_fft([1, 0, 0, 0]) 29 | print(y) -------------------------------------------------------------------------------- /devel/test_ess.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import numpy.typing as npt 3 | 4 | FloatType = np.float64 5 | IntType = np.int64 6 | VectorType = npt.NDArray[FloatType] 7 | 8 | def autocorr_fft(chain: VectorType) -> VectorType: 9 | """ 10 | Return sample autocorrelations at all lags for the specified sequence. 11 | Algorithmically, this function calls a fast Fourier transform (FFT). 12 | Parameters: 13 | chain: sequence whose autocorrelation is returned 14 | Returns: 15 | autocorrelation estimates at all lags for the specified sequence 16 | """ 17 | size = 2 ** np.ceil(np.log2(2 * len(chain) - 1)).astype("int") 18 | var = np.var(chain) 19 | ndata = chain - np.mean(chain) 20 | fft = np.fft.fft(ndata, size) 21 | pwr = np.abs(fft) ** 2 22 | N = len(ndata) 23 | acorr = np.fft.ifft(pwr).real / var / N 24 | return acorr 25 | 26 | def autocorr_np(chain: VectorType) -> VectorType: 27 | """ 28 | Return sample autocorrelations at all lags for the specified sequence. 29 | Algorithmically, this function delegates to the Numpy `correlation()` function. 30 | Parameters: 31 | chain: sequence whose autocorrelation is returned 32 | Returns: 33 | autocorrelation estimates at all lags for the specified sequence 34 | """ 35 | chain_ctr = chain - np.mean(chain) 36 | N = len(chain_ctr) 37 | acorrN = np.correlate(chain_ctr, chain_ctr, "full")[N - 1 :] 38 | return acorrN / acorrN[0] 39 | 40 | def autocorr(chain: VectorType) -> VectorType: 41 | """ 42 | Return sample autocorrelations at all lags for the specified sequence. 43 | Algorithmically, this function delegates to `autocorr_fft`. 44 | Parameters: 45 | chain: sequence whose autocorrelation is returned 46 | Returns: 47 | autocorrelation estimates at all lags for the specified sequence 48 | """ 49 | # return autocorr_fft(chain) 50 | return autocorr_np(chain) 51 | 52 | def first_neg_pair_start(chain: VectorType) -> IntType: 53 | """ 54 | Return the index of first element of the sequence whose sum with the following 55 | element is negative, or the length of the sequence if there is no such element. 56 | 57 | Parameters: 58 | chain: input sequence 59 | Return: 60 | index of first element whose sum with following element is negative, or 61 | the number of elements if there is no such element 62 | """ 63 | N = len(chain) 64 | n = 0 65 | while n + 1 < N: 66 | if chain[n] + chain[n + 1] < 0: 67 | return n 68 | n = n + 1 69 | return N 70 | 71 | def ess_ipse(chain: VectorType) -> FloatType: 72 | """ 73 | Return an estimate of the effective sample size (ESS) of the specified Markov chain 74 | using the initial positive sequence estimator (IPSE). 75 | Parameters: 76 | chain: Markov chain whose ESS is returned 77 | Return: 78 | estimated effective sample size for the specified Markov chain 79 | Throws: 80 | ValueError: if there are fewer than 4 elements in the chain 81 | """ 82 | if len(chain) < 4: 83 | raise ValueError(f"ess requires len(chains) >=4, but {len(chain) = }") 84 | acor = autocorr(chain) 85 | n = first_neg_pair_start(acor) 86 | sigma_sq_hat = acor[0] + 2 * sum(acor[1:n]) 87 | ess = len(chain) / sigma_sq_hat 88 | return ess 89 | 90 | def ess_imse(chain: VectorType) -> FloatType: 91 | """ 92 | Return an estimate of the effective sample size (ESS) of the specified Markov chain 93 | using the initial monotone sequence estimator (IMSE). This is the most accurate 94 | of the available ESS estimators. Because of the convex minorization used, 95 | this approach is slower than using the IPSE function `ess_ipse`. 96 | This estimator was introduced in the following paper. 97 | Geyer, C.J., 1992. Practical Markov chain Monte Carlo. Statistical Science 98 | 7(4):473--483. 99 | 100 | Parameters: 101 | chain: Markov chain whose ESS is returned 102 | Return: 103 | estimated effective sample size for the specified Markov chain 104 | Throws: 105 | ValueError: if there are fewer than 4 elements in the chain 106 | """ 107 | if len(chain) < 4: 108 | raise ValueError(f"ess requires len(chains) >=4, but {len(chain) = }") 109 | acor = autocorr(chain) 110 | n = first_neg_pair_start(acor) 111 | prev_min = 1 112 | # convex minorization uses slow loop 113 | accum = 0 114 | i = 1 115 | while i + 1 < n: 116 | prev_min = min(prev_min, acor[i] + acor[i + 1]) 117 | accum = accum + prev_min 118 | i = i + 2 119 | # end diff code 120 | sigma_sq_hat = acor[0] + 2 * accum 121 | ess = len(chain) / sigma_sq_hat 122 | return ess 123 | 124 | def ess(chain: VectorType) -> FloatType: 125 | """ 126 | Return an estimate of the effective sample size of the specified Markov chain 127 | using the default ESS estimator (currently IMSE). Evaluated by delegating 128 | to `ess_imse()`. 129 | Parameters: 130 | chain: Markov chains whose ESS is returned 131 | Return: 132 | estimated effective sample size for the specified Markov chain 133 | Throws: 134 | ValueError: if there are fewer than 4 elements in the chain 135 | """ 136 | return ess_imse(chain) 137 | 138 | import matplotlib.pyplot as plt 139 | 140 | sizes = np.arange(10, 512) 141 | esses = [] 142 | for n in sizes: 143 | gg = [] 144 | for i in range(1000): 145 | x = np.random.normal(0, 1, (n, )) 146 | gg.append(ess(x)) 147 | esses.append(np.mean(gg)) 148 | # plt.plot(sizes, esses, 'b', sizes, sizes, 'r') 149 | plt.plot(sizes, esses / sizes, 'b') 150 | plt.show() -------------------------------------------------------------------------------- /doc/deployment.md: -------------------------------------------------------------------------------- 1 | # MCMC Monitor Deployment 2 | 3 | As discussed in [the MCMC Monitor readme](../README.md), MCMC Monitor 4 | consists of a **service** and a **client**. 5 | 6 | As with many modern web applications, the MCMC client runs entirely in 7 | the web browser. The browser downloads prepackaged Javascript code for the client 8 | from a public website, and then the downloaded code handles all further 9 | communication with the service (which usually runs on the same machine 10 | that's running Stan). 11 | 12 | The MCMC Monitor developers make the client code available for download 13 | from the [Github Pages](https://pages.github.com/) page associated with 14 | this repository, https://flatironinstitute.github.io/mcmc-monitor. 15 | *Deploying* a new version of the client consists of packaging up the 16 | Javascript code and making it available for download at that URL. 17 | 18 | If you've 19 | [forked](https://docs.github.com/en/get-started/quickstart/fork-a-repo) 20 | this repo, you can deploy your own version of the MCMC Monitor client 21 | using the same scripts as the MCMC Monitor developers. 22 | This might be useful as a way to test some changes you're thinking of 23 | [contributing to the project](../CONTRIBUTING.md) 24 | (which is highly encouraged!). While it is also possible to run 25 | your own version of the client, we strongly recommend and humbly request 26 | that you share with the broader community any extensions you have made. 27 | 28 | Alternatively, it is probably easier to just run 29 | the client locally. See our [developer documentation](./developing.md) 30 | for more details on recommended workflow. 31 | 32 | 33 | ## Deployment 34 | 35 | The project offers two deployment scripts, one for the production 36 | version of the client and one for a development/testing/staging branch. 37 | Configuration of the deployment verbs is handled in the `package.json` 38 | of the client `src` directory, and the build process is managed 39 | using [Vite](https://vitejs.dev/). As typical for github-pages-based 40 | deployments, the deployment commands push the fully built pacakge into 41 | the `gh-pages` branch of the repository's configured `origin` remote. 42 | 43 | 44 | ### To Dev Branch 45 | 46 | Having a publicly-available `dev` or `staging` branch gives a broad 47 | audience of end users a convenient want to test-drive changes and see how 48 | their use cases would be impacted, before those changes become part 49 | of the main production version of the code. 50 | 51 | To do a dev deployment, execute the `dev-deploy` script as: 52 | ```bash 53 | $ yarn dev-deploy 54 | ``` 55 | 56 | This will automatically build the current branch using the 57 | `vite.dev-config.ts` configuration and push it into a `/dev` directory 58 | on the repository, so it will be available at 59 | `https://flatironinstitute.github.io/mcmc-monitor/dev`. 60 | 61 | 62 | ### To Production 63 | 64 | To deploy to the production branch, execute: 65 | ```bash 66 | $ yarn deploy 67 | ``` 68 | 69 | This will automatically execute the `build` script as well, using the 70 | (default) `vite.config.ts` configuration. Additionally, the `build` script 71 | can be executed by itself using `yarn build` to confirm that everything has 72 | built correctly before attempting to deploy. 73 | -------------------------------------------------------------------------------- /doc/release.md: -------------------------------------------------------------------------------- 1 | # MCMC Monitor--Releasing a New Version 2 | 3 | This documentation is intended for (and of interest mainly to) MCMC Monitor 4 | developers with the ability to push production versions of MCMC Monitor. 5 | 6 | To push a new release, follow these steps: 7 | 8 | 1. Ensure that the `protocolVersion` string has been updated if there 9 | were any changes to the communication structure, protocol, or types. 10 | 11 | 2. Update the `service` version in `service/package.json` if any changes 12 | have been made to the service. 13 | 14 | 3. Commit and push changes 15 | 16 | 4. Execute `yarn deploy` for the client, as per the 17 | [deployment instructions](./deployment.md). 18 | 19 | 5. If any changes were made to the service, cd to the `service` directory, 20 | then update the version of the service on NPM: 21 | - `$ npm login` 22 | - `$ export TAG=A.B.C # where A.B.C should match the version in package.json` 23 | - Manually enter the command in the "release" verb from `service/package.json`, i.e.: 24 | - `$ yarn build && yarn coverage && npm publish && git tag $TAG && git push --tags` 25 | 26 | The last command builds and publishes the service and tags the deployed version 27 | of the code in one serial set of operations. 28 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | example-output 2 | *.hpp 3 | multi-normal 4 | finite-mixture -------------------------------------------------------------------------------- /examples/finite-mixture.stan: -------------------------------------------------------------------------------- 1 | // https://khakieconomics.github.io/2016/10/17/An-introduction-to-finite-mixtures.html 2 | 3 | data { 4 | int N; 5 | vector[N] y; 6 | int n_groups; 7 | } 8 | parameters { 9 | vector[n_groups] mu; 10 | vector[n_groups] sigma; 11 | simplex[n_groups] Theta; 12 | } 13 | model { 14 | vector[n_groups] contributions; 15 | // priors 16 | mu ~ normal(0, 10); 17 | sigma ~ cauchy(0, 2); 18 | Theta ~ dirichlet(rep_vector(2.0, n_groups)); 19 | 20 | 21 | // likelihood 22 | for(i in 1:N) { 23 | for(k in 1:n_groups) { 24 | contributions[k] = log(Theta[k]) + normal_lpdf(y[i] | mu[k], sigma[k]); 25 | } 26 | target += log_sum_exp(contributions); 27 | } 28 | } -------------------------------------------------------------------------------- /examples/multi-normal.stan: -------------------------------------------------------------------------------- 1 | data { 2 | real rho; 3 | int N; 4 | } 5 | transformed data { 6 | vector[N] mu = rep_vector(0, N); 7 | cov_matrix[N] Sigma; 8 | for (m in 1:N) 9 | for (n in 1:N) 10 | Sigma[m, n] = rho^fabs(m - n); 11 | } 12 | parameters { 13 | vector[N] y; 14 | } 15 | model { 16 | y ~ multi_normal(mu, Sigma); 17 | } -------------------------------------------------------------------------------- /examples/test_finite_mixture.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from cmdstanpy import CmdStanModel 3 | import os 4 | import shutil 5 | 6 | def main(): 7 | N = 400 8 | y = np.concatenate([np.random.normal(0, 2, 200), np.random.normal(5, 3, 200)]) 9 | n_groups = 2 10 | 11 | iter_warmup = 50 # Number of warmup iterations 12 | iter_sampling = 200 # Number of sampling iterations 13 | 14 | # specify .stan file for this model 15 | thisdir = os.path.dirname(os.path.realpath(__file__)) 16 | model_fname = f'{thisdir}/finite-mixture.stan' 17 | 18 | model = CmdStanModel(stan_file=model_fname) 19 | 20 | output_dir = f'{thisdir}/example-output/finite-mixture-1' 21 | if os.path.exists(output_dir): 22 | shutil.rmtree(output_dir) 23 | 24 | # Start sampling the posterior for this model/data 25 | fit = model.sample( 26 | data={'N': N, 'y': y, 'n_groups': n_groups}, 27 | output_dir=output_dir, 28 | iter_sampling=iter_sampling, 29 | iter_warmup=iter_warmup, 30 | save_warmup=True, 31 | chains=10, 32 | parallel_chains=10 33 | ) 34 | 35 | if __name__ == '__main__': 36 | main() -------------------------------------------------------------------------------- /examples/test_multi_normal.py: -------------------------------------------------------------------------------- 1 | from cmdstanpy import CmdStanModel 2 | import os 3 | 4 | def main(example_num: int): 5 | if example_num == 1: 6 | # These are adjustable parameters 7 | rho = 0.9 # rho should be <1 8 | N = 400 9 | iter_warmup = 20 # Number of warmup iterations 10 | iter_sampling = 100 # Number of sampling iterations 11 | ################################## 12 | elif example_num == 2: 13 | # These are adjustable parameters 14 | rho = 0.8 # rho should be <1 15 | N = 200 16 | iter_warmup = 20 # Number of warmup iterations 17 | iter_sampling = 200 # Number of sampling iterations 18 | ################################## 19 | 20 | # specify .stan file for this model 21 | thisdir = os.path.dirname(os.path.realpath(__file__)) 22 | model_fname = f'{thisdir}/multi-normal.stan' 23 | 24 | model = CmdStanModel(stan_file=model_fname) 25 | 26 | # Start sampling the posterior for this model/data 27 | fit = model.sample( 28 | data={'N': N, 'rho': rho}, 29 | output_dir=f'{thisdir}/example-output/multi-normal-{example_num}', 30 | iter_sampling=iter_sampling, 31 | iter_warmup=iter_warmup, 32 | save_warmup=True 33 | ) 34 | 35 | if __name__ == '__main__': 36 | main(1) 37 | main(2) -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MCMC Monitor 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcmc-monitor", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "build": "tsc && vite build", 8 | "predeploy": "npm run build && npm run coverage", 9 | "deploy": "gh-pages -d dist --no-history", 10 | "predev-deploy": "tsc && vite --config vite.dev-config.ts build && npm run coverage", 11 | "dev-deploy": "gh-pages -d dev-dist --no-history --dest dev", 12 | "dev": "vite", 13 | "preview": "vite preview", 14 | "test": "vitest", 15 | "coverage": "vitest run --coverage" 16 | }, 17 | "dependencies": { 18 | "@emotion/react": "^11.10.5", 19 | "@emotion/styled": "^11.10.5", 20 | "@mui/icons-material": "^5.11.0", 21 | "@mui/material": "^5.11.6", 22 | "js-yaml": "^4.1.0", 23 | "plotly.js": "^2.18.0", 24 | "react": "^18.2.0", 25 | "react-cookie-consent": "^8.0.1", 26 | "react-dom": "^18.2.0", 27 | "react-draggable": "^4.4.5", 28 | "react-ga4": "^2.1.0", 29 | "react-plotly.js": "^2.6.0", 30 | "react-router-dom": "^6.8.0", 31 | "simple-peer": "^9.11.1" 32 | }, 33 | "devDependencies": { 34 | "@testing-library/jest-dom": "^5.16.5", 35 | "@testing-library/react": "^14.0.0", 36 | "@testing-library/user-event": "^14.4.3", 37 | "@types/js-yaml": "^4.0.5", 38 | "@types/material-ui": "^0.21.12", 39 | "@types/react": "^18.0.26", 40 | "@types/react-dom": "^18.0.9", 41 | "@types/react-plotly.js": "^2.6.0", 42 | "@types/simple-peer": "^9.11.5", 43 | "@typescript-eslint/eslint-plugin": "^5.49.0", 44 | "@typescript-eslint/parser": "^5.49.0", 45 | "@vitejs/plugin-react": "^3.0.0", 46 | "@vitest/coverage-c8": "^0.30.1", 47 | "eslint": "^8.32.0", 48 | "eslint-plugin-react": "^7.32.1", 49 | "eslint-plugin-react-hooks": "^4.6.0", 50 | "gh-pages": "^5.0.0", 51 | "happy-dom": "^9.18.3", 52 | "jsdom": "^22.0.0", 53 | "typescript": "^4.9.3", 54 | "vite": "^4.0.0", 55 | "vitest": "^0.30.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /public/mcmc-monitor-logo-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flatironinstitute/mcmc-monitor/0a1c538a1d8e57489b64f0d05b347ee2b6d180ba/public/mcmc-monitor-logo-2.png -------------------------------------------------------------------------------- /public/mcmc-monitor-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flatironinstitute/mcmc-monitor/0a1c538a1d8e57489b64f0d05b347ee2b6d180ba/public/mcmc-monitor-logo.png -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service/.gitignore: -------------------------------------------------------------------------------- 1 | example-output 2 | 3 | node_modules 4 | dist 5 | yarn.lock 6 | -------------------------------------------------------------------------------- /service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | ADD package.json /service/ 4 | ADD tsconfig.json /service/ 5 | ADD src /service/src 6 | ADD example-output /service/example-output 7 | ADD bin /service/bin 8 | 9 | WORKDIR /service 10 | 11 | RUN npm install 12 | RUN npm run build 13 | 14 | CMD [ "./bin/mcmc-monitor", "start", "--dir", "./example-output", "--verbose" ] -------------------------------------------------------------------------------- /service/bin/mcmc-monitor: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../dist/src/index') -------------------------------------------------------------------------------- /service/devel/readme.txt: -------------------------------------------------------------------------------- 1 | # build docker 2 | docker build -t magland/mcmc-monitor . 3 | 4 | # test docker 5 | docker run -p 61542:61542 -it magland/mcmc-monitor 6 | 7 | # deploy to heroku 8 | https://dev.to/pacheco/how-to-dockerize-a-node-app-and-deploy-to-heroku-3cch 9 | 10 | heroku container:login 11 | 12 | heroku create 13 | 14 | docker tag magland/mcmc-monitor:latest registry.heroku.com/lit-bayou-76056/web 15 | 16 | docker push registry.heroku.com/lit-bayou-76056/web 17 | 18 | heroku container:release web --app lit-bayou-76056 19 | 20 | heroku open --app lit-bayou-76056 21 | 22 | # to restart 23 | heroku ps:restart web --app lit-bayou-76056 -------------------------------------------------------------------------------- /service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcmc-monitor", 3 | "version": "0.1.19", 4 | "description": "MCMC Monitor service", 5 | "main": "dist/src/index.js", 6 | "bin": { 7 | "mcmc-monitor": "./bin/mcmc-monitor" 8 | }, 9 | "scripts": { 10 | "build": "tsc && cp package.json dist/", 11 | "dev": "nodemon src/index.ts start --v --dir ../examples/example-output --enable-remote-access", 12 | "start-demo": "node src/index.ts start --v --dir ./example-output", 13 | "release": "yarn build && yarn coverage && npm publish && git tag $npm_package_version && git push --tags", 14 | "test": "vitest", 15 | "coverage": "vitest run --coverage" 16 | }, 17 | "author": "Jeremy Magland", 18 | "license": "Apache-2.0", 19 | "devDependencies": { 20 | "@types/express": "^4.17.15", 21 | "@types/node": "^18.11.10", 22 | "@types/simple-peer": "^9.11.5", 23 | "@types/ws": "^8.5.4", 24 | "@types/yargs": "^17.0.15", 25 | "node-pre-gyp": "^0.17.0", 26 | "nodemon": "^2.0.20", 27 | "ts-node": "^10.9.1", 28 | "typescript": "^4.9.3" 29 | }, 30 | "dependencies": { 31 | "@types/js-yaml": "^4.0.5", 32 | "check-node-version": "^4.2.1", 33 | "express": "^4.18.2", 34 | "js-yaml": "^4.1.0", 35 | "simple-peer": "^9.11.1", 36 | "ts-node": "^10.9.1", 37 | "ws": "^8.12.0", 38 | "yargs": "^17.6.2" 39 | }, 40 | "optionalDependencies": { 41 | "wrtc": "^0.4.7" 42 | }, 43 | "engines": { 44 | "node": ">= 16.0.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /service/src/index.ts: -------------------------------------------------------------------------------- 1 | import yargs from 'yargs' 2 | import { hideBin } from 'yargs/helpers' 3 | import Server from './networking/Server' 4 | 5 | const main = () => { 6 | yargs(hideBin(process.argv)) 7 | .command('start', 'Start monitoring', (yargs) => { 8 | return yargs 9 | }, (argv) => { 10 | const dir: string = argv.dir as string 11 | start({port: parseInt(process.env.PORT || "61542"), dir, verbose: argv.verbose ? true : false, enableRemoteAccess: argv['enable-remote-access'] ? true : false}) 12 | }) 13 | .option('verbose', { 14 | alias: 'v', 15 | type: 'boolean', 16 | description: 'Run with verbose logging' 17 | }) 18 | .option('dir', { 19 | type: 'string', 20 | description: 'Parent directory where the output subdirectory lives' 21 | }) 22 | .option('enable-remote-access', { 23 | type: 'boolean', 24 | description: 'Enable remote access' 25 | }) 26 | .strictCommands() 27 | .demandCommand(1) 28 | .parse() 29 | } 30 | 31 | let server: Server 32 | function start({port, dir, verbose, enableRemoteAccess}: {port: number, dir: string, verbose: boolean, enableRemoteAccess: boolean}) { 33 | server = new Server({port, dir, verbose, enableRemoteAccess}) 34 | server.start() 35 | } 36 | 37 | process.on('SIGINT', function() { 38 | if (server) { 39 | console.info('Stopping server.') 40 | server.stop().then(() => { 41 | console.info('Exiting.') 42 | process.exit() 43 | }) 44 | } 45 | setTimeout(() => { 46 | // exit no matter what after a few seconds 47 | process.exit() 48 | }, 3000) 49 | }) 50 | 51 | main() -------------------------------------------------------------------------------- /service/src/logic/OutputManager.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { MCMCChain, MCMCRun } from '../types' 3 | import ChainFile from './ChainFile' 4 | 5 | class OutputManager { 6 | #chainFiles: {[key: string]: ChainFile} = {} // by runId/chainId 7 | constructor(private dir: string) { 8 | 9 | } 10 | async getRuns(): Promise { 11 | const runs: MCMCRun[] = [] 12 | let x: string[] 13 | try { 14 | x = await fs.promises.readdir(this.dir) 15 | } 16 | catch(err) { 17 | console.warn(`Error reading directory: ${this.dir}`) 18 | return [] 19 | } 20 | for (const fname of x) { 21 | const p = `${this.dir}/${fname}` 22 | const s = await fs.promises.stat(p) 23 | if (s.isDirectory()) { 24 | runs.push({runId: fname}) 25 | } 26 | } 27 | return runs 28 | } 29 | async getChainsForRun(runId: string): Promise { 30 | const chains: MCMCChain[] = [] 31 | const path = `${this.dir}/${runId}` 32 | let fnames: string[] 33 | try { 34 | fnames = await fs.promises.readdir(path) 35 | } 36 | catch(err) { 37 | console.warn(`Error reading directory: ${path}`) 38 | return [] 39 | } 40 | for (const fname of fnames) { 41 | if (fname.endsWith('.csv')) { 42 | const chainId = chainIdFromCsvFileName(fname) 43 | const p = `${path}/${fname}` 44 | const kk = `${runId}/${chainId}` 45 | if (!this.#chainFiles[kk]) { 46 | this.#chainFiles[kk] = new ChainFile(p, chainId) 47 | } 48 | const cf = this.#chainFiles[kk] 49 | await cf.update() 50 | chains.push({ 51 | runId, 52 | chainId, 53 | variableNames: cf.variableNames, 54 | rawHeader: cf.rawHeader, 55 | rawFooter: cf.rawFooter, 56 | variablePrefixesExcluded: cf.variablePrefixesExcluded, 57 | excludedInitialIterationCount: cf.excludedInitialIterationCount, 58 | lastChangeTimestamp: cf.timestampLastChange 59 | }) 60 | } 61 | } 62 | return chains 63 | } 64 | async getSequenceData(runId: string, chainId: string, variableName: string, startPosition: number): Promise<{data: number[]}> { 65 | const kk = `${runId}/${chainId}` 66 | const p = await this._getPathForChainId(runId, chainId) 67 | if (!p) return {data: []} 68 | 69 | if (!this.#chainFiles[kk]) { 70 | this.#chainFiles[kk] = new ChainFile(p, chainId) 71 | } 72 | const cf = this.#chainFiles[kk] 73 | await cf.update() 74 | const data = cf.sequenceData(variableName, startPosition) 75 | return { 76 | data 77 | } 78 | } 79 | async clearOldData() { 80 | const keys = Object.keys(this.#chainFiles) 81 | for (const k of keys) { 82 | const cf = this.#chainFiles[k] 83 | if (!fs.existsSync(cf.path)) { 84 | console.warn(`File does no longer exists. Clearing data for ${cf.path}`) 85 | delete this.#chainFiles[k] 86 | } 87 | } 88 | } 89 | async _getPathForChainId(runId: string, chainId: string) { 90 | const p = `${this.dir}/${runId}/${chainId}.csv` 91 | if (fs.existsSync(p)) return p 92 | let x: string[] 93 | try { 94 | x = await fs.promises.readdir(`${this.dir}/${runId}`) 95 | } 96 | catch(err) { 97 | console.warn(`Error reading directory: ${this.dir}/${runId}`) 98 | return undefined 99 | } 100 | for (const fname of x) { 101 | if (fname.endsWith('.csv')) { 102 | const chainId2 = chainIdFromCsvFileName(fname) 103 | if (chainId2 === chainId) { 104 | return `${this.dir}/${runId}/${fname}` 105 | } 106 | } 107 | } 108 | return undefined 109 | } 110 | } 111 | 112 | function chainIdFromCsvFileName(path: string) { 113 | const a = path.slice(0, path.length - '.csv'.length) 114 | const ii = a.lastIndexOf('_') 115 | if (ii >= 0) { 116 | if (isIntString(a.slice(ii + 1))) { 117 | return `chain_${a.slice(ii + 1)}` 118 | } 119 | } 120 | return a 121 | } 122 | 123 | function isIntString(x: string) { 124 | return (parseInt(x) + '') === x 125 | } 126 | 127 | export default OutputManager -------------------------------------------------------------------------------- /service/src/networking/OutgoingProxyConnection.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from 'ws' 2 | import OutputManager from '../logic/OutputManager' 3 | import { InitializeMessageFromService, MCMCMonitorResponse, PingMessageFromService, RequestFromClient, ResponseToClient, isAcknowledgeMessageToService, isMCMCMonitorRequest, isRequestFromClient } from '../types' 4 | import SignalCommunicator from './SignalCommunicator' 5 | import { handleApiRequest } from './handleApiRequest' 6 | 7 | const proxyUrl = process.env['MCMC_MONITOR_PROXY'] || `https://mcmc-monitor-proxy-0b1b73a9f2a0.herokuapp.com` 8 | const proxySecret = process.env['MCMC_MONITOR_PROXY_SECRET'] || 'mcmc-monitor-no-secret' 9 | 10 | class OutgoingProxyConnection { 11 | #acknowledged: boolean 12 | #webSocket: WebSocket 13 | constructor(private serviceId: string, private servicePrivateId: string, private outputManager: OutputManager, private signalCommunicator: SignalCommunicator, private o: {verbose: boolean, webrtc?: boolean}) { 14 | this.initializeWebSocket() 15 | const keepAlive = () => { 16 | if (this.#webSocket) { 17 | if (this.#acknowledged) { 18 | const msg: PingMessageFromService = { 19 | type: 'ping' 20 | } 21 | try { 22 | this.#webSocket.send(JSON.stringify(msg)) 23 | } 24 | catch(err) { 25 | console.error(err) 26 | console.warn('Problem sending ping message to proxy server') 27 | } 28 | } 29 | } 30 | else { 31 | try { 32 | this.initializeWebSocket() 33 | } 34 | catch(err) { 35 | console.error(err) 36 | console.warn('Problem initializing websocket') 37 | } 38 | } 39 | setTimeout(keepAlive, 20 * 1000) 40 | } 41 | setTimeout(keepAlive, 1000) 42 | } 43 | initializeWebSocket() { 44 | this.#acknowledged = false 45 | console.info(`Connecting to ${proxyUrl}`) 46 | const wsUrl = proxyUrl.replace('http:','ws:').replace('https:','wss:') 47 | const ws = new WebSocket(wsUrl) 48 | this.#webSocket = ws 49 | ws.on('open', () => { 50 | console.info('Connected') 51 | const msg: InitializeMessageFromService = { 52 | type: 'initialize', 53 | serviceId: this.serviceId, 54 | servicePrivateId: this.servicePrivateId, 55 | proxySecret: proxySecret 56 | } 57 | ws.send(JSON.stringify(msg)) 58 | }) 59 | ws.on('close', () => { 60 | console.info('Websocket closed.') 61 | this.#webSocket = undefined 62 | this.#acknowledged = false 63 | }) 64 | ws.on('error', err => { 65 | console.error(err) 66 | console.warn('Websocket error.') 67 | this.#webSocket = undefined 68 | this.#acknowledged = false 69 | }) 70 | ws.on('message', msg => { 71 | const messageJson = msg.toString() 72 | let message: any 73 | try { 74 | message = JSON.parse(messageJson) 75 | } 76 | catch(err) { 77 | console.error(`Error parsing message. Closing.`) 78 | ws.close() 79 | return 80 | } 81 | if (isAcknowledgeMessageToService(message)) { 82 | console.info('Connection acknowledged by proxy server') 83 | this.#acknowledged = true 84 | return 85 | } 86 | if (!this.#acknowledged) { 87 | console.info('Unexpected, message before connection acknowledged. Closing.') 88 | ws.close() 89 | return 90 | } 91 | if (isRequestFromClient(message)) { 92 | this.handleRequestFromClient(message) 93 | } 94 | else { 95 | console.warn(message) 96 | console.warn('Unexpected message from proxy server') 97 | } 98 | }) 99 | } 100 | async handleRequestFromClient(request: RequestFromClient) { 101 | if (!this.#webSocket) return 102 | const rr = request.request 103 | if (!isMCMCMonitorRequest(rr)) { 104 | const resp: ResponseToClient = { 105 | type: 'responseToClient', 106 | requestId: request.requestId, 107 | response: {}, 108 | error: 'Invalid MCMC Monitor request' 109 | } 110 | this.#webSocket.send(JSON.stringify(resp)) 111 | return 112 | } 113 | let mcmcMonitorResponse: MCMCMonitorResponse 114 | try { 115 | mcmcMonitorResponse = await handleApiRequest({request: rr, outputManager: this.outputManager, signalCommunicator: this.signalCommunicator, options: {...this.o, proxy: true}}) 116 | } 117 | catch(err) { 118 | const resp: ResponseToClient = { 119 | type: 'responseToClient', 120 | requestId: request.requestId, 121 | response: {}, 122 | error: `Error handling request: ${err.message}` 123 | } 124 | this.#webSocket.send(JSON.stringify(resp)) 125 | return 126 | } 127 | if (!this.#webSocket) return 128 | const responseToClient: ResponseToClient = { 129 | type: 'responseToClient', 130 | requestId: request.requestId, 131 | response: mcmcMonitorResponse 132 | } 133 | this.#webSocket.send(JSON.stringify(responseToClient)) 134 | } 135 | public get url() { 136 | return `${proxyUrl}/s/${this.serviceId}` 137 | } 138 | close() { 139 | if (this.#webSocket) { 140 | this.#webSocket.close() 141 | this.#webSocket = undefined 142 | } 143 | } 144 | } 145 | 146 | export default OutgoingProxyConnection -------------------------------------------------------------------------------- /service/src/networking/RemotePeer.ts: -------------------------------------------------------------------------------- 1 | import SimplePeer from 'simple-peer'; 2 | import OutputManager from '../logic/OutputManager'; 3 | import { MCMCMonitorPeerResponse, isMCMCMonitorPeerRequest } from '../types'; 4 | import SignalCommunicator, { SignalCommunicatorConnection } from './SignalCommunicator'; 5 | import { handleApiRequest } from './handleApiRequest'; 6 | 7 | 8 | type callbackProps = { 9 | peer: SimplePeer.Instance, 10 | id: string, 11 | cnxn: SignalCommunicatorConnection, 12 | outputMgr: OutputManager, 13 | signalCommunicator: SignalCommunicator 14 | } 15 | 16 | 17 | const createPeer = async (connection: SignalCommunicatorConnection, outputMgr: OutputManager, signalCommunicator: SignalCommunicator): Promise => { 18 | // wrtc is a conditional dependency (doesn't install on some versions of mac os). If it's not available, we can't use webrtc. 19 | let wrtc: any 20 | try { 21 | // throw Error('Manually bypassing webrtc support') 22 | wrtc = await import('wrtc') 23 | } 24 | catch(err) { 25 | console.error(err) 26 | console.warn('Problem importing wrtc. Falling back to non-webrtc connection.') 27 | return undefined 28 | } 29 | 30 | const peer = new SimplePeer({initiator: false, wrtc}) 31 | const id = Math.random().toString(36).substring(2, 10) 32 | const props: callbackProps = { 33 | peer, 34 | id, 35 | cnxn: connection, 36 | outputMgr: outputMgr, 37 | signalCommunicator 38 | } 39 | peer.on('data', d => onData(d, props)) 40 | peer.on('signal', s => onPeerSignal(s, props)) 41 | peer.on('error', e => onError(e, props)) 42 | peer.on('connect', () => { 43 | console.info(`webrtc peer ${id} connected`) 44 | }) 45 | peer.on('close', () => onClose(props)) 46 | connection.onSignal(signal => onConnectionSignal(signal, props)) 47 | 48 | return peer 49 | } 50 | 51 | 52 | const onData = (d: string, props: callbackProps) => { 53 | const { peer, id, cnxn, outputMgr, signalCommunicator } = props 54 | const peerRequest = JSON.parse(d) 55 | if (!isMCMCMonitorPeerRequest(peerRequest)) { 56 | console.warn('Invalid webrtc peer request. Disconnecting.') 57 | try { 58 | peer.destroy() 59 | } catch(err) { 60 | console.error(err) 61 | console.warn(`\tProblem destroying webrtc peer ${id} in response to bad peer request.`) 62 | } 63 | cnxn.close() 64 | return 65 | } 66 | handleApiRequest({request: peerRequest.request, outputManager: outputMgr, signalCommunicator, options: {verbose: true, webrtc: true}}).then(response => { 67 | const resp: MCMCMonitorPeerResponse = { 68 | type: 'mcmcMonitorPeerResponse', 69 | response, 70 | requestId: peerRequest.requestId 71 | } 72 | try { 73 | if (cnxn.wasClosed()) { 74 | console.warn(`\tSignal communicator connection was closed before the response could be sent.`) 75 | } else { 76 | peer.send(JSON.stringify(resp)) 77 | } 78 | } catch(err) { 79 | console.error(err) 80 | console.warn(`\tProblem sending API response to webrtc peer ${id}.`) 81 | } 82 | }) 83 | } 84 | 85 | 86 | const onClose = (props: callbackProps) => { 87 | const { peer, id, cnxn } = props 88 | 89 | console.info(`webrtc peer ${id} disconnected`) 90 | peer.removeAllListeners('data') 91 | peer.removeAllListeners('signal') 92 | peer.removeAllListeners('connect') 93 | peer.removeAllListeners('close') 94 | cnxn.close() 95 | } 96 | 97 | 98 | const onPeerSignal = (s: SimplePeer.SignalData, props: callbackProps) => { 99 | props.cnxn.sendSignal(JSON.stringify(s)) 100 | } 101 | 102 | 103 | const onError = (e: Error, props: callbackProps) => { 104 | const { peer, cnxn, id } = props 105 | console.error('Error in webrtc peer', e.message) 106 | try { 107 | peer.destroy() 108 | } catch(err) { 109 | console.error(err) 110 | console.warn(`\tProblem destroying webrtc peer ${id} in response to peer error.`) 111 | } 112 | cnxn.close() 113 | } 114 | 115 | 116 | const onConnectionSignal = (signal: string, props: callbackProps) => { 117 | const { peer, id } = props 118 | try { 119 | peer.signal(JSON.parse(signal)) 120 | } catch(err) { 121 | console.error(err) 122 | console.warn(`\tProblem sending signal to webrtc peer ${id}.`) 123 | } 124 | } 125 | 126 | 127 | export default createPeer 128 | -------------------------------------------------------------------------------- /service/src/networking/SignalCommunicator.ts: -------------------------------------------------------------------------------- 1 | import { WebrtcSignalingRequest, WebrtcSignalingResponse } from "../types"; 2 | 3 | class SignalCommunicator { 4 | #onConnectionCallbacks: ((connection: SignalCommunicatorConnection) => Promise)[] = [] 5 | #connections: {[clientId: string]: SignalCommunicatorConnection} = {} 6 | async handleRequest(request: WebrtcSignalingRequest): Promise { 7 | if (!(request.clientId in this.#connections)) { 8 | const cc = new SignalCommunicatorConnection() 9 | this.#connections[request.clientId] = cc 10 | cc.onClose(() => { 11 | if (request.clientId in this.#connections) { 12 | delete this.#connections[request.clientId] 13 | } 14 | }) 15 | await Promise.all(this.#onConnectionCallbacks.map(cb => cb(cc))) 16 | } 17 | return await this.#connections[request.clientId].handleRequest(request) 18 | } 19 | onConnection(cb: (connection: SignalCommunicatorConnection) => Promise) { 20 | this.#onConnectionCallbacks.push(cb) 21 | } 22 | } 23 | 24 | export class SignalCommunicatorConnection { 25 | #onSignalCallbacks: ((s: string) => void)[] = [] 26 | #onCloseCallbacks: (() => void)[] = [] 27 | #pendingSignalsToSend: string[] = [] 28 | #closed = false 29 | constructor() { } 30 | async handleRequest(request: WebrtcSignalingRequest): Promise { 31 | if (request.signal) { 32 | this.#onSignalCallbacks.forEach(cb => {cb(request.signal)}) 33 | } 34 | if (this.#pendingSignalsToSend.length === 0) { 35 | const timer = Date.now() 36 | while ((Date.now() - timer) < 1000) { 37 | if (this.#pendingSignalsToSend.length > 0) { 38 | break 39 | } 40 | await sleepMsec(100) 41 | } 42 | } 43 | const signals0 = this.#pendingSignalsToSend 44 | this.#pendingSignalsToSend = [] 45 | return { 46 | type: 'webrtcSignalingResponse', 47 | signals: signals0 48 | } 49 | } 50 | sendSignal(s: string) { 51 | this.#pendingSignalsToSend.push(s) 52 | } 53 | close() { 54 | this.#onCloseCallbacks.forEach(cb => {cb()}) 55 | this.#closed = true 56 | } 57 | onClose(cb: () => void) { 58 | this.#onCloseCallbacks.push(cb) 59 | } 60 | wasClosed() { 61 | return this.#closed 62 | } 63 | onSignal(cb: (s: string) => void) { 64 | this.#onSignalCallbacks.push(cb) 65 | } 66 | } 67 | 68 | export const sleepMsec = async (msec: number): Promise => { 69 | return new Promise((resolve) => { 70 | setTimeout(() => { 71 | resolve() 72 | }, msec) 73 | }) 74 | } 75 | 76 | export default SignalCommunicator -------------------------------------------------------------------------------- /service/src/networking/handleApiRequest.ts: -------------------------------------------------------------------------------- 1 | import OutputManager from "../logic/OutputManager"; 2 | import { GetChainsForRunRequest, GetChainsForRunResponse, GetRunsResponse, GetSequencesRequest, GetSequencesResponse, MCMCMonitorRequest, MCMCMonitorResponse, ProbeResponse, WebrtcSignalingRequest, WebrtcSignalingResponse, isGetChainsForRunRequest, isGetRunsRequest, isGetSequencesRequest, isProbeRequest, isWebrtcSignalingRequest, protocolVersion } from "../types"; 3 | import SignalCommunicator from "./SignalCommunicator"; 4 | 5 | type apiRequestOptions = { 6 | verbose: boolean, 7 | webrtc?: boolean, 8 | proxy?: boolean 9 | } 10 | 11 | type apiRequest = { 12 | request: MCMCMonitorRequest, 13 | outputManager: OutputManager, 14 | signalCommunicator: SignalCommunicator, 15 | options: apiRequestOptions 16 | } 17 | 18 | 19 | export const handleApiRequest = async (props: apiRequest): Promise => { 20 | const { request, outputManager, signalCommunicator, options } = props 21 | const webrtcFlag = options.webrtc ? "Webrtc" : "" 22 | 23 | if (isProbeRequest(request)) { 24 | return handleProbeRequest(options.proxy) 25 | } 26 | 27 | if (isGetRunsRequest(request)) { 28 | options.verbose && console.info(`${webrtcFlag} getRuns`) 29 | return handleGetRunsRequest(outputManager) 30 | } 31 | 32 | if (isGetChainsForRunRequest(request)) { 33 | options.verbose && console.info(`${webrtcFlag} getChainsForRun ${request.runId}`) 34 | return handleGetChainsForRunRequest(request, outputManager) 35 | } 36 | 37 | if (isGetSequencesRequest(request)) { 38 | options.verbose && console.info(`${webrtcFlag} getSequences ${request.sequences.length}`) 39 | return handleGetSequencesRequest(request, outputManager) 40 | } 41 | 42 | if (isWebrtcSignalingRequest(request)) { 43 | options.verbose && console.info(`${webrtcFlag} webrtcSignalingRequest`) 44 | return handleWebrtcSignalingRequest(request, signalCommunicator) 45 | } 46 | 47 | throw Error('Unexpected request type') 48 | } 49 | 50 | 51 | const handleProbeRequest = async (usesProxy?: boolean): Promise => { 52 | const response: ProbeResponse = { 53 | type: 'probeResponse', 54 | protocolVersion: protocolVersion, 55 | proxy: usesProxy 56 | } 57 | if (usesProxy) { 58 | response.proxy = true 59 | } 60 | return response 61 | } 62 | 63 | 64 | const handleGetRunsRequest = async (outputManager: OutputManager): Promise => { 65 | const runs = await outputManager.getRuns() 66 | const response: GetRunsResponse = {type: 'getRunsResponse', runs} 67 | return response 68 | } 69 | 70 | 71 | const handleGetChainsForRunRequest = async (request: GetChainsForRunRequest, outputManager: OutputManager): Promise => { 72 | const {runId} = request 73 | const chains = await outputManager.getChainsForRun(runId) 74 | const response: GetChainsForRunResponse ={ 75 | type: 'getChainsForRunResponse', 76 | chains 77 | } 78 | return response 79 | } 80 | 81 | 82 | const handleGetSequencesRequest = async (request: GetSequencesRequest, outputManager: OutputManager): Promise => { 83 | const response: GetSequencesResponse = { type: 'getSequencesResponse', sequences: [] }; 84 | for (const s of request.sequences) { 85 | const { runId, chainId, variableName, position } = s; 86 | 87 | const sd = await outputManager.getSequenceData(runId, chainId, variableName, position); 88 | response.sequences.push({ 89 | runId, 90 | chainId, 91 | variableName, 92 | position, 93 | data: sd.data 94 | }); 95 | } 96 | return response; 97 | } 98 | 99 | 100 | const handleWebrtcSignalingRequest = async (request: WebrtcSignalingRequest, signalCommunicator: SignalCommunicator): Promise => { 101 | return await signalCommunicator.handleRequest(request) 102 | } 103 | -------------------------------------------------------------------------------- /service/src/types/ConnectorHttpProxyTypes.ts: -------------------------------------------------------------------------------- 1 | export type RequestFromClient = { 2 | type: 'requestFromClient' 3 | request: any 4 | requestId: string 5 | } 6 | 7 | export type ResponseToClient = { 8 | type: 'responseToClient' 9 | requestId?: string // added when sending over websocket 10 | response: any 11 | error?: string 12 | } 13 | 14 | export type InitializeMessageFromService = { 15 | type: 'initialize' 16 | serviceId: string 17 | servicePrivateId: string 18 | proxySecret: string 19 | } 20 | 21 | export type AcknowledgeMessageToService = { 22 | type: 'acknowledge' 23 | } 24 | 25 | export type PingMessageFromService = { 26 | type: 'ping' 27 | } 28 | -------------------------------------------------------------------------------- /service/src/types/MCMCMonitorPeerRequestTypes.ts: -------------------------------------------------------------------------------- 1 | import { MCMCMonitorRequest, MCMCMonitorResponse } from "./MCMCMonitorRequestTypes" 2 | 3 | export type MCMCMonitorPeerRequest = { 4 | type: 'mcmcMonitorPeerRequest' 5 | request: MCMCMonitorRequest 6 | requestId: string 7 | } 8 | 9 | export type MCMCMonitorPeerResponse = { 10 | type: 'mcmcMonitorPeerResponse' 11 | response: MCMCMonitorResponse 12 | requestId: string 13 | } 14 | -------------------------------------------------------------------------------- /service/src/types/MCMCMonitorRequestTypes.ts: -------------------------------------------------------------------------------- 1 | import { MCMCChain, MCMCRun } from "./MCMCMonitorTypes" 2 | 3 | export type ProbeRequest = { 4 | type: 'probeRequest' 5 | } 6 | 7 | export type ProbeResponse = { 8 | type: 'probeResponse' 9 | protocolVersion: string 10 | proxy?: boolean 11 | } 12 | 13 | export type GetRunsRequest = { 14 | type: 'getRunsRequest' 15 | } 16 | 17 | export type GetRunsResponse = { 18 | type: 'getRunsResponse' 19 | runs: MCMCRun[] 20 | } 21 | 22 | export type GetChainsForRunRequest = { 23 | type: 'getChainsForRunRequest' 24 | runId: string 25 | } 26 | 27 | 28 | export type GetChainsForRunResponse = { 29 | type: 'getChainsForRunResponse' 30 | chains: MCMCChain[] 31 | } 32 | 33 | 34 | export type GetSequencesRequest = { 35 | type: 'getSequencesRequest' 36 | sequences: { 37 | runId: string 38 | chainId: string 39 | variableName: string 40 | position: number 41 | }[] 42 | } 43 | 44 | 45 | export type GetSequencesResponse = { 46 | type: 'getSequencesResponse' 47 | sequences: MCMCSequenceUpdate[] 48 | } 49 | 50 | 51 | export type MCMCSequenceUpdate = { 52 | runId: string 53 | chainId: string 54 | variableName: string 55 | position: number 56 | data: number[] 57 | } 58 | 59 | 60 | export type WebrtcSignalingRequest = { 61 | type: 'webrtcSignalingRequest' 62 | clientId: string 63 | signal?: string 64 | } 65 | 66 | export type WebrtcSignalingResponse = { 67 | type: 'webrtcSignalingResponse' 68 | signals: string[] 69 | } 70 | 71 | export type MCMCMonitorRequest = 72 | ProbeRequest | 73 | GetRunsRequest | 74 | GetChainsForRunRequest | 75 | GetSequencesRequest | 76 | WebrtcSignalingRequest 77 | 78 | export type MCMCMonitorResponse = 79 | ProbeResponse | 80 | GetRunsResponse | 81 | GetChainsForRunResponse | 82 | GetSequencesResponse | 83 | WebrtcSignalingResponse 84 | -------------------------------------------------------------------------------- /service/src/types/MCMCMonitorTypes.ts: -------------------------------------------------------------------------------- 1 | export type MCMCRun = { 2 | runId: string 3 | } 4 | 5 | export type MCMCChain = { 6 | runId: string 7 | chainId: string 8 | variableNames: string[] 9 | rawHeader?: string 10 | rawFooter?: string 11 | variablePrefixesExcluded?: string[] 12 | excludedInitialIterationCount?: number 13 | lastChangeTimestamp: number 14 | } 15 | 16 | export type MCMCSequence = { 17 | runId: string 18 | chainId: string 19 | variableName: string 20 | data: number[] 21 | updateRequested?: boolean 22 | } 23 | -------------------------------------------------------------------------------- /service/src/types/WebsocketMessageTypes.ts: -------------------------------------------------------------------------------- 1 | export type WebsocketMessage = { 2 | type: 'signal' 3 | signal: string 4 | } 5 | -------------------------------------------------------------------------------- /service/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ConnectorHttpProxyTypes' 2 | export * from './MCMCMonitorPeerRequestTypes' 3 | export * from './MCMCMonitorRequestTypes' 4 | export * from './MCMCMonitorTypes' 5 | export * from './Typeguards' 6 | export * from './WebsocketMessageTypes' 7 | 8 | export const protocolVersion = '0.1.4' 9 | -------------------------------------------------------------------------------- /service/src/types/wrtc.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'wrtc'; -------------------------------------------------------------------------------- /service/test/logic/ChainFile.test.ts.disable: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | import { TEST_isComment, TEST_isEmptyComment } from '../../src/logic/ChainFile' 3 | 4 | describe("Comment detection function", () => { 5 | test("Identifies lines beginning with # as comments", () => { 6 | expect(TEST_isComment("#comment line")).toBeTruthy() 7 | }) 8 | test("Identifies lines not beginning with # as non-comments", () => { 9 | expect(TEST_isComment("/*Comment line*/")).toBeFalsy() 10 | }) 11 | }) 12 | 13 | describe("Empty-comment detection function", () => { 14 | test("Identifies lines containing only space as empty comments", () => { 15 | expect(TEST_isEmptyComment("# ")).toBeTruthy() 16 | }) 17 | test("Identifies lines with only tabs as empty comments", () => { 18 | expect(TEST_isEmptyComment("#\t")).toBeTruthy() 19 | }) 20 | test("Identifies lines with visible characters as non-empty", () => { 21 | expect(TEST_isEmptyComment("# Some content")).toBeFalsy() 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2017", 5 | "esModuleInterop": true, 6 | "moduleResolution": "node", 7 | "sourceMap": false, 8 | "outDir": "dist" 9 | } 10 | } -------------------------------------------------------------------------------- /service/vitest.config.ts.disable: -------------------------------------------------------------------------------- 1 | import { coverageConfigDefaults, defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | mockReset: true, 6 | coverage: { 7 | all: true, 8 | enabled: true, 9 | exclude: [ 10 | ...coverageConfigDefaults.exclude, 11 | "**/*Types.ts", 12 | "**/index.ts", 13 | "vite*ts" 14 | ], 15 | provider: "c8", 16 | reporter: ["text", "lcov"], 17 | } 18 | } 19 | }) -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | /* max-width: 1280px; */ 3 | /* margin: 0 auto; */ 4 | /* padding: 2rem; */ 5 | /* text-align: center; */ 6 | } -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import ReactGA from 'react-ga4' 3 | import { HashRouter } from 'react-router-dom' 4 | import './App.css' 5 | import MCMCDataManager from './MCMCMonitorDataManager/MCMCMonitorDataManager' 6 | import SetupMCMCMonitor from './MCMCMonitorDataManager/SetupMCMCMonitor' 7 | import CookieBanner from './components/CookieLogic' 8 | import MainWindow from './pages/MainWindow' 9 | 10 | 11 | function App() { 12 | const [dataManager, setDataManager] = useState() 13 | ReactGA.send('pageview') 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | 24 | export default App 25 | -------------------------------------------------------------------------------- /src/MCMCMonitorDataManager/MCMCMonitorDataManager.ts: -------------------------------------------------------------------------------- 1 | import { initialMCMCMonitorData } from "./MCMCMonitorData"; 2 | import { MCMCMonitorAction, MCMCMonitorData } from "./MCMCMonitorDataTypes"; 3 | import updateSequences from "./updateSequences"; 4 | import updateSequenceStats from "./updateSequenceStats"; 5 | import updateVariableStats from "./updateVariableStats"; 6 | 7 | class MCMCDataManager { 8 | #data: MCMCMonitorData = initialMCMCMonitorData 9 | #stopped = false 10 | constructor(private dispatch: (a: MCMCMonitorAction) => void) { 11 | } 12 | setData(data: MCMCMonitorData) { 13 | this.#data = data 14 | } 15 | async start() { 16 | this.#stopped = false 17 | while (true) { 18 | if (this.#stopped) return 19 | await this._iterate() 20 | await sleepMsec(1000) 21 | } 22 | } 23 | stop() { 24 | this.#stopped = true 25 | } 26 | async _iterate() { 27 | try { 28 | await updateSequences(this.#data, this.dispatch) 29 | await sleepMsec(10) // allow the new data to be set onto this.#data 30 | await updateSequenceStats(this.#data, this.dispatch) 31 | await sleepMsec(10) 32 | await updateVariableStats(this.#data, this.dispatch) 33 | } 34 | catch(err) { 35 | console.error('Error in data manager iteration.') 36 | console.error(err) 37 | } 38 | } 39 | } 40 | 41 | async function sleepMsec(msec: number) { 42 | return new Promise(resolve => setTimeout(resolve, msec)) 43 | } 44 | 45 | export default MCMCDataManager -------------------------------------------------------------------------------- /src/MCMCMonitorDataManager/MCMCMonitorDataTypes.ts: -------------------------------------------------------------------------------- 1 | import { MCMCChain, MCMCRun, MCMCSequence, MCMCSequenceUpdate } from '../../service/src/types' 2 | 3 | export type GeneralOpts = { 4 | dataRefreshMode: 'auto' | 'manual' 5 | dataRefreshIntervalSec: number 6 | requestedInitialDrawsToExclude: number 7 | } 8 | 9 | export type SequenceStats = { 10 | mean?: number 11 | stdev?: number 12 | count?: number 13 | ess?: number 14 | acor?: number[] 15 | isUpToDate?: boolean 16 | } 17 | 18 | export type VariableStats = { 19 | mean?: number 20 | stdev?: number 21 | count?: number 22 | ess?: number 23 | rhat?: number 24 | isUpToDate?: boolean 25 | } 26 | 27 | export type SequenceStatsDict = { [key: string]: SequenceStats } 28 | export type VariableStatsDict = { [key: string]: VariableStats } 29 | 30 | export type WebrtcConnectionStatus = 'unused' | 'pending' | 'connected' | 'error' 31 | 32 | export type MCMCMonitorData = { 33 | connectedToService: boolean | undefined 34 | serviceProtocolVersion: string | undefined 35 | usingProxy: boolean | undefined 36 | runs: MCMCRun[] 37 | chains: MCMCChain[] 38 | sequences: MCMCSequence[] 39 | sequenceStats: SequenceStatsDict 40 | variableStats: VariableStatsDict 41 | selectedRunId?: string 42 | selectedVariableNames: string[] 43 | selectedChainIds: string[] 44 | effectiveInitialDrawsToExclude: number 45 | generalOpts: GeneralOpts 46 | } 47 | 48 | export type MCMCMonitorAction = { 49 | type: 'setRuns' 50 | runs: MCMCRun[] 51 | } | { 52 | type: 'setChainsForRun' 53 | runId: string 54 | chains: MCMCChain[] 55 | } | { 56 | type: 'updateChainsForRun' 57 | runId: string 58 | chains: MCMCChain[] 59 | } | { 60 | type: 'updateSequenceData' 61 | sequences: MCMCSequenceUpdate[] 62 | } | { 63 | type: 'setSelectedVariableNames' 64 | variableNames: string[] 65 | } | { 66 | type: 'setSelectedRunId' 67 | runId: string | undefined 68 | } | { 69 | type: 'setSelectedChainIds' 70 | chainIds: string[] 71 | } | { 72 | type: 'setConnectedToService' 73 | connected: boolean | undefined 74 | } | { 75 | type: 'setServiceProtocolVersion' 76 | version: string | undefined 77 | } | { 78 | type: 'setWebrtcConnectionStatus' 79 | status: 'unused' | 'pending' | 'connected' | 'error' 80 | } | { 81 | type: 'setUsingProxy' 82 | usingProxy: boolean | undefined 83 | } | { 84 | type: 'requestSequenceUpdate' 85 | runId: string 86 | } | { 87 | type: 'setGeneralOpts' 88 | opts: GeneralOpts 89 | } | { 90 | type: 'setSequenceStats' 91 | runId: string 92 | chainId: string 93 | variableName: string 94 | stats: SequenceStats 95 | } | { 96 | type: 'setVariableStats' 97 | runId: string 98 | variableName: string 99 | stats: VariableStats 100 | } 101 | -------------------------------------------------------------------------------- /src/MCMCMonitorDataManager/MCMCMonitorTypeguards.ts: -------------------------------------------------------------------------------- 1 | import { isMCMCChain, isMCMCRun, isMCMCSequence } from '../../service/src/types' 2 | import validateObject, { isArrayOf, isBoolean, isNumber, isString, optional } from "../../service/src/types/validateObject" 3 | import { GeneralOpts, MCMCMonitorData, SequenceStats, SequenceStatsDict, VariableStats, VariableStatsDict } from "./MCMCMonitorDataTypes" 4 | 5 | export const isMCMCSequenceStats = (x: any): x is SequenceStats => { 6 | return validateObject(x, { 7 | mean: optional(isNumber), 8 | stdev: optional(isNumber), 9 | count: optional(isNumber), 10 | ess: optional(isNumber), 11 | rhat: optional(isNumber), 12 | isUpToDate: optional(isBoolean), 13 | }) 14 | } 15 | 16 | export const isMCMCVariableStats = (x: any): x is VariableStats => { 17 | return validateObject(x, { 18 | mean: optional(isNumber), 19 | stdev: optional(isNumber), 20 | count: optional(isNumber), 21 | ess: optional(isNumber), 22 | acor: optional(isArrayOf(isNumber)), 23 | isUpToDate: optional(isBoolean), 24 | }) 25 | } 26 | 27 | export const isSequenceStatsDict = (x: any): x is SequenceStatsDict => { 28 | return Object.values(x).every(v => isMCMCSequenceStats(v)) 29 | } 30 | 31 | export const isVariableStatsDict = (x: any): x is VariableStatsDict => { 32 | return Object.values(x).every(v => isMCMCVariableStats(v)) 33 | } 34 | 35 | export const isGeneralOpts = (x: any): x is GeneralOpts => { 36 | return validateObject(x, { 37 | dataRefreshMode: (x) => ['auto', 'manual'].includes(x), 38 | dataRefreshIntervalSec: isNumber, 39 | requestedInitialDrawsToExclude: isNumber 40 | }) 41 | } 42 | 43 | export const isMCMCMonitorData = (x: any): x is MCMCMonitorData => { 44 | return validateObject(x, { 45 | connectedToService: optional(isBoolean), 46 | serviceProtocolVersion: optional(isString), 47 | usingProxy: optional(isBoolean), 48 | runs: isArrayOf(isMCMCRun), 49 | chains: isArrayOf(isMCMCChain), 50 | sequences: isArrayOf(isMCMCSequence), 51 | sequenceStats: isSequenceStatsDict, 52 | variableStats: isVariableStatsDict, 53 | selectedRunId: optional(isString), 54 | selectedVariableNames: isArrayOf(isString), 55 | selectedChainIds: isArrayOf(isString), 56 | effectiveInitialDrawsToExclude: isNumber, 57 | generalOpts: isGeneralOpts 58 | }) 59 | } 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/MCMCMonitorDataManager/SetupMCMCMonitor.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, FunctionComponent, PropsWithChildren, SetStateAction, useCallback, useEffect, useMemo, useReducer, useState } from "react" 2 | import { ProbeRequest, isProbeResponse, protocolVersion } from "../../service/src/types" 3 | import postApiRequest from "../networking/postApiRequest" 4 | import { MCMCMonitorContext, initialMCMCMonitorData, mcmcMonitorReducer } from "./MCMCMonitorData" 5 | import MCMCDataManager from "./MCMCMonitorDataManager" 6 | 7 | type SetupMcmcMonitorProps = { 8 | dataManager: MCMCDataManager | undefined 9 | setDataManager: Dispatch> 10 | } 11 | 12 | const SetupMCMCMonitor: FunctionComponent> = (props: PropsWithChildren) => { 13 | const { children, dataManager, setDataManager } = props 14 | const [data, dataDispatch] = useReducer(mcmcMonitorReducer, initialMCMCMonitorData) 15 | const [usingProxy, setUsingProxy] = useState(undefined) 16 | 17 | // instantiate the data manager 18 | useEffect(() => { 19 | // should only be instantiated once 20 | const dm = new MCMCDataManager(dataDispatch) 21 | setDataManager(dm) 22 | }, [dataDispatch, setDataManager]) 23 | 24 | 25 | // every time data changes, the dataManager needs to get the updated data 26 | useEffect(() => { 27 | dataManager && dataManager.setData(data) 28 | }, [data, dataManager]) 29 | 30 | const [connectionCheckRefreshCode, setConnectionCheckRefreshCode] = useState(0) 31 | const checkConnectionStatus = useCallback(() => { 32 | setConnectionCheckRefreshCode(c => (c + 1)) 33 | }, []) 34 | 35 | const value = useMemo(() => ({ 36 | data, 37 | dispatch: dataDispatch, 38 | checkConnectionStatus 39 | }), [data, dataDispatch, checkConnectionStatus]) 40 | 41 | useEffect(() => { 42 | dataDispatch({type: 'setUsingProxy', usingProxy}) 43 | }, [usingProxy]) 44 | 45 | // check whether we are connected 46 | // TODO: consider alternate mechanism for refresh other than manual check-again button 47 | useEffect(() => { 48 | setUsingProxy(undefined) 49 | ;(async () => { 50 | try { 51 | const req: ProbeRequest = { 52 | type: 'probeRequest' 53 | } 54 | const resp = await postApiRequest(req) 55 | if (!isProbeResponse(resp)) { 56 | console.warn(resp) 57 | throw Error('Unexpected probe response') 58 | } 59 | setUsingProxy(resp.proxy ? true : false) 60 | dataDispatch({type: 'setServiceProtocolVersion', version: resp.protocolVersion}) 61 | if (resp.protocolVersion !== protocolVersion) { 62 | throw Error(`Unexpected protocol version: ${resp.protocolVersion} <> ${protocolVersion}`) 63 | } 64 | dataDispatch({type: 'setConnectedToService', connected: true}) 65 | } 66 | catch(err) { 67 | console.warn(err) 68 | dataDispatch({type: 'setConnectedToService', connected: false}) 69 | } 70 | })() 71 | }, [connectionCheckRefreshCode]) 72 | 73 | return ( 74 | 75 | {children} 76 | 77 | ) 78 | } 79 | 80 | export default SetupMCMCMonitor -------------------------------------------------------------------------------- /src/MCMCMonitorDataManager/stats/ess.ts: -------------------------------------------------------------------------------- 1 | // See: https://github.com/flatironinstitute/bayes-kit/blob/main/bayes_kit/ess.py 2 | 3 | import { computeMean } from "../updateSequenceStats" 4 | import { inverseTransform, transform as transformFft } from "./fft" 5 | 6 | // def autocorr_fft(chain: VectorType) -> VectorType: 7 | // """ 8 | // Return sample autocorrelations at all lags for the specified sequence. 9 | // Algorithmically, this function calls a fast Fourier transform (FFT). 10 | // Parameters: 11 | // chain: sequence whose autocorrelation is returned 12 | // Returns: 13 | // autocorrelation estimates at all lags for the specified sequence 14 | // """ 15 | // size = 2 ** np.ceil(np.log2(2 * len(chain) - 1)).astype("int") 16 | // var = np.var(chain) 17 | // ndata = chain - np.mean(chain) 18 | // fft = np.fft.fft(ndata, size) 19 | // pwr = np.abs(fft) ** 2 20 | // N = len(ndata) 21 | // acorr = np.fft.ifft(pwr).real / var / N 22 | // return acorr 23 | 24 | export function autocorr_fft(chain: number[], n: number): number[] { 25 | const size = Math.round(Math.pow(2, Math.ceil(Math.log2(2 * chain.length - 1)))) 26 | const variance = computeVariance(chain) 27 | if (variance === undefined) return [] 28 | const mean = computeMean(chain) 29 | const ndata = chain.map(x => (x - (mean || 0))) 30 | while (ndata.length < size) { 31 | ndata.push(0) 32 | } 33 | const ndataFftReal = [...ndata] 34 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 35 | const ndataFftImag = ndata.map(_x => (0)) 36 | transformFft(ndataFftReal, ndataFftImag) 37 | const pwr = ndataFftReal.map((r, i) => (r * r + ndataFftImag[i] * ndataFftImag[i])) 38 | const N = ndata.length 39 | const acorrReal = [...pwr] 40 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 41 | const acorrImag = pwr.map(x => (0)) 42 | inverseTransform(acorrReal, acorrImag) // doesn't include scaling 43 | return acorrReal.slice(0, n).map(x => (x / variance / N / chain.length)) 44 | } 45 | 46 | export function autocorr_slow(chain: number[], n: number): number[] { 47 | // todo: use FFT 48 | 49 | const mu = chain.length ? sum(chain) / chain.length : 0 50 | const chain_ctr = chain.map(a => (a - mu)) 51 | const N = chain_ctr.length 52 | 53 | ////////////////////////////////////////////////////////////// 54 | // acorrN = np.correlate(chain_ctr, chain_ctr, "full")[N - 1 :] 55 | let acorrN: number[] = [] 56 | for (let i = 0; i < n; i++) { 57 | let aa = 0 58 | for (let j = 0; j < N - i; j++) { 59 | aa += chain_ctr[j] * chain_ctr[j + i] 60 | } 61 | acorrN.push(aa) 62 | } 63 | ////////////////////////////////////////////////////////////// 64 | 65 | // normalize so that acorrN[0] = 1 66 | const a0 = acorrN[0] 67 | acorrN = acorrN.map(a => (a / a0)) 68 | 69 | return acorrN 70 | } 71 | 72 | export function first_neg_pair_start(chain: number[]): number { 73 | const N = chain.length 74 | let n = 0 75 | while (n + 1 < N) { 76 | if (chain[n] + chain[n + 1] < 0) { 77 | return n 78 | } 79 | n = n + 1 80 | } 81 | return N 82 | } 83 | 84 | export function ess_ipse(chain: number[]): number { 85 | if (chain.length < 4) { 86 | console.warn('ess requires chain.length >=4') 87 | return 0 88 | } 89 | 90 | // for verifying we get the same answer with both methods 91 | // console.log('test autocor_slow', autocorr_slow([1, 2, 3, 4, 0, 0, 0], 5)) 92 | // console.log('test autocor_fft', autocorr_fft([1, 2, 3, 4, 0, 0, 0], 5)) 93 | 94 | // const acor = autocorr_slow(chain, chain.length) 95 | const acor = autocorr_fft(chain, chain.length) 96 | const n = first_neg_pair_start(acor) 97 | const sigma_sq_hat = acor[0] + 2 * sum(acor.slice(1, n)) 98 | const ess = chain.length / sigma_sq_hat 99 | return ess 100 | } 101 | 102 | export function ess_imse(chain: number[]): {ess: number, acor: number[]} { 103 | if (chain.length < 4) { 104 | console.warn('ess requires chain.length >=4') 105 | return {ess: 0, acor: []} 106 | } 107 | // const acor = autocorr_slow(chain, chain.length) 108 | const acor = autocorr_fft(chain, chain.length) 109 | const n = first_neg_pair_start(acor) 110 | let prev_min = 1 111 | let accum = 0 112 | let i = 1 113 | while (i + 1 < n) { 114 | prev_min = Math.min(prev_min, acor[i] + acor[i + 1]) 115 | accum = accum + prev_min 116 | i = i + 2 117 | } 118 | 119 | const sigma_sq_hat = acor[0] + 2 * accum 120 | const ess = chain.length / sigma_sq_hat 121 | return {ess, acor} 122 | } 123 | 124 | export function ess(chain: number[]) { 125 | // use ess_imse for now 126 | return ess_imse(chain) 127 | } 128 | 129 | function sum(x: number[]) { 130 | return x.reduce((a, b) => (a + b), 0) 131 | } 132 | 133 | function computeVariance(x: number[]) { 134 | const mu = computeMean(x) 135 | if (mu === undefined) return undefined 136 | return sum(x.map(a => ( 137 | (a - mu) * (a - mu) 138 | ))) / x.length 139 | } -------------------------------------------------------------------------------- /src/MCMCMonitorDataManager/updateChains.ts: -------------------------------------------------------------------------------- 1 | import { GetChainsForRunRequest, isGetChainsForRunResponse } from "../../service/src/types" 2 | import postApiRequest from "../networking/postApiRequest" 3 | import { MCMCMonitorAction } from "./MCMCMonitorDataTypes" 4 | 5 | 6 | 7 | const updateChains = async (runId: string | undefined, dispatch: (a: MCMCMonitorAction) => void) => { 8 | if (runId === undefined) return 9 | 10 | const req: GetChainsForRunRequest = { 11 | type: 'getChainsForRunRequest', 12 | runId: runId 13 | } 14 | const resp = await postApiRequest(req) 15 | if (!isGetChainsForRunResponse(resp)) { 16 | console.warn(JSON.stringify(resp)) 17 | throw Error('Chain update request returned invalid response.') 18 | } 19 | dispatch({ 20 | type: 'updateChainsForRun', 21 | runId: runId, 22 | chains: resp.chains 23 | }) 24 | } 25 | 26 | export default updateChains -------------------------------------------------------------------------------- /src/MCMCMonitorDataManager/updateSequenceStats.ts: -------------------------------------------------------------------------------- 1 | import { MCMCMonitorAction, MCMCMonitorData, SequenceStats } from "./MCMCMonitorDataTypes"; 2 | import { ess } from "./stats/ess"; 3 | 4 | const CALCULATION_BUDGET_MS = 200 5 | 6 | export default async function updateSequenceStats(data: MCMCMonitorData, dispatch: (a: MCMCMonitorAction) => void) { 7 | const runId = data.selectedRunId 8 | if (!runId) return 9 | const timer = Date.now() 10 | for (const chainId of data.selectedChainIds) { 11 | for (const variableName of data.selectedVariableNames) { 12 | const k = `${runId}/${chainId}/${variableName}` 13 | const s = data.sequenceStats[k] || {} 14 | if (!s.isUpToDate) { 15 | const seq = data.sequences.filter(s => (s.runId === runId && s.chainId === chainId && s.variableName === variableName))[0] 16 | if (seq) { 17 | const seqData = seq.data.slice(data.effectiveInitialDrawsToExclude) 18 | const newStats = computeStatsForSequence(seqData) 19 | dispatch({ 20 | type: 'setSequenceStats', 21 | runId, 22 | chainId, 23 | variableName, 24 | stats: newStats 25 | }) 26 | } 27 | } 28 | const elapsed = Date.now() - timer 29 | // We'd rather return some result than hang forever, so if we aren't done 30 | // computing all the stats before the budget runs out, return early 31 | if (elapsed > CALCULATION_BUDGET_MS) { 32 | return 33 | } 34 | } 35 | } 36 | } 37 | 38 | function computeStatsForSequence(seqData: number[]): SequenceStats { 39 | const mean = computeMean(seqData) 40 | const stdev = computeStdev(seqData) 41 | const {ess: ess0, acor} = ess(seqData) 42 | return { 43 | mean, 44 | stdev, 45 | ess: ess0, 46 | acor, 47 | count: seqData.length, 48 | isUpToDate: seqData.length > 0 49 | } 50 | } 51 | 52 | export function computeMean(d: number[]) { 53 | if (d.length === 0) return undefined 54 | return d.reduce((a, b) => (a + b), 0) / d.length 55 | } 56 | 57 | export function computeStdev(d: number[]) { 58 | if (d.length <= 1) return undefined 59 | const sumsqr = d.reduce((a, b) => (a + b * b), 0) 60 | const m0 = computeMean(d) 61 | if (m0 === undefined) return undefined 62 | return Math.sqrt(sumsqr / d.length - m0 * m0) 63 | } -------------------------------------------------------------------------------- /src/MCMCMonitorDataManager/updateSequences.ts: -------------------------------------------------------------------------------- 1 | import { GetSequencesRequest, isGetSequencesResponse, MCMCSequenceUpdate } from "../../service/src/types"; 2 | import postApiRequest from "../networking/postApiRequest"; 3 | import getSpaSequenceUpdates from "../spaInterface/getSpaSequenceUpdates"; 4 | import { isSpaRunId } from "../spaInterface/util"; 5 | import { MCMCMonitorAction, MCMCMonitorData } from "./MCMCMonitorDataTypes"; 6 | 7 | export default async function updateSequences(data: MCMCMonitorData, dispatch: (a: MCMCMonitorAction) => void) { 8 | const X = data.sequences.filter(s => (s.updateRequested)) 9 | if (X.length > 0) { 10 | const numSpaRuns = X.filter(s => isSpaRunId(s.runId)).length 11 | if ((numSpaRuns > 0) && (numSpaRuns < X.length)) { 12 | throw Error('Cannot mix SPA and non-SPA runs in a single updateSequences call') 13 | } 14 | let sequenceUpdates: MCMCSequenceUpdate[] | undefined 15 | if (numSpaRuns === X.length) { 16 | const runId = X[0].runId 17 | // handle the special case of a stan playground run 18 | sequenceUpdates = await getSpaSequenceUpdates(runId, X) 19 | } 20 | else { 21 | // handle the usual case 22 | const req: GetSequencesRequest = { 23 | type: 'getSequencesRequest', 24 | sequences: X.map(s => ({ 25 | runId: s.runId, chainId: s.chainId, variableName: s.variableName, position: s.data.length 26 | })) 27 | } 28 | const resp = await postApiRequest(req) 29 | if (!isGetSequencesResponse(resp)) { 30 | console.warn(resp) 31 | throw Error('Unexpected getSequences response') 32 | } 33 | sequenceUpdates = resp.sequences 34 | } 35 | if (sequenceUpdates) { 36 | dispatch({ 37 | type: "updateSequenceData", 38 | sequences: sequenceUpdates 39 | }) 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/MCMCMonitorDataManager/updateVariableStats.ts: -------------------------------------------------------------------------------- 1 | import { MCMCMonitorAction, MCMCMonitorData, SequenceStats, VariableStats } from "./MCMCMonitorDataTypes"; 2 | import { computeMean, computeStdev } from "./updateSequenceStats"; 3 | 4 | export default async function updateVariableStats(data: MCMCMonitorData, dispatch: (a: MCMCMonitorAction) => void) { 5 | const runId = data.selectedRunId 6 | if (!runId) return 7 | const timer = Date.now() 8 | for (const variableName of data.selectedVariableNames) { 9 | const k = `${runId}/${variableName}` 10 | const s = data.variableStats[k] || {} 11 | if (!s.isUpToDate) { 12 | const chainStats: SequenceStats[] = data.selectedChainIds.map(chainId => { 13 | const k2 = `${runId}/${chainId}/${variableName}` 14 | return data.sequenceStats[k2] || {} 15 | }) 16 | const mean = meanFromMeans(chainStats.map(cs => (cs.mean)), chainStats.map(cs => (cs.count))) 17 | const stdev = stdevFromStdevs(chainStats.map(cs => (cs.stdev)), chainStats.map(cs => (cs.count))) 18 | const ess = essFromEsses(chainStats.map(cs => (cs.ess))) 19 | const count = countFromCounts(chainStats.map(cs => (cs.count))) 20 | const rhat = rhatFromData(chainStats.map(cs => (cs.count)), chainStats.map(cs => (cs.mean)), chainStats.map(cs => (cs.stdev))) 21 | if (mean !== undefined) { 22 | const newStats: VariableStats = { 23 | mean, 24 | stdev, 25 | ess, 26 | count, 27 | rhat, 28 | isUpToDate: true 29 | } 30 | dispatch({ 31 | type: 'setVariableStats', 32 | runId, 33 | variableName, 34 | stats: newStats 35 | }) 36 | } 37 | } 38 | const elapsed = Date.now() - timer 39 | if (elapsed > 200) { 40 | // wait for next iteration 41 | return 42 | } 43 | } 44 | } 45 | 46 | function meanFromMeans(means: (number | undefined)[], counts: (number | undefined)[]) { 47 | if (means.indexOf(undefined) >= 0) return undefined 48 | if (counts.indexOf(undefined) >= 0) return undefined 49 | const totalCount = (counts as number[]).reduce((a, b) => (a + b), 0) 50 | const totalSum = (means as number[]).map((m, i) => (m * (counts as number[])[i])).reduce((a, b) => (a + b), 0) 51 | if (totalCount === 0) return undefined 52 | return totalSum / totalCount 53 | } 54 | 55 | function countFromCounts(counts: (number | undefined)[]) { 56 | if (counts.indexOf(undefined) >= 0) return undefined 57 | const totalCount = (counts as number[]).reduce((a, b) => (a + b), 0) 58 | return totalCount 59 | } 60 | 61 | function stdevFromStdevs(stdevs: (number | undefined)[], counts: (number | undefined)[]) { 62 | if (stdevs.indexOf(undefined) >= 0) return undefined 63 | if (counts.indexOf(undefined) >= 0) return undefined 64 | const totalCount = (counts as number[]).reduce((a, b) => (a + b), 0) 65 | const totalSumsqrs = (stdevs as number[]).map((s, i) => (s * s * (counts as number[])[i])).reduce((a, b) => (a + b), 0) 66 | if (totalCount === 0) return undefined 67 | const var0 = totalSumsqrs / totalCount 68 | return Math.sqrt(var0) 69 | } 70 | 71 | function essFromEsses(esses: (number | undefined)[]) { 72 | if (esses.indexOf(undefined) >= 0) return undefined 73 | const totalEss = (esses as number[]).reduce((a, b) => (a + b), 0) 74 | return totalEss 75 | } 76 | 77 | function rhatFromData(counts: (number | undefined)[], means: (number | undefined)[], stdevs: (number | undefined)[]) { 78 | // chain_lengths = [len(chain) for chain in chains] 79 | // mean_chain_length = np.mean(chain_lengths) 80 | // means = [np.mean(chain) for chain in chains] 81 | // vars = [np.var(chain, ddof=1) for chain in chains] 82 | // r_hat: np.float64 = np.sqrt( 83 | // (mean_chain_length - 1) / mean_chain_length + np.var(means, ddof=1) / np.mean(vars) 84 | // ) 85 | if (counts.indexOf(undefined) >= 0) return undefined 86 | if (means.indexOf(undefined) >= 0) return undefined 87 | if (stdevs.indexOf(undefined) >= 0) return undefined 88 | const cc = counts as number[] 89 | const mm = means as number[] 90 | const ss = stdevs as number[] 91 | if (cc.length <= 1) return undefined 92 | for (const count of cc) { 93 | if (count <= 1) return undefined 94 | } 95 | const mean_chain_length = computeMean(cc) 96 | if (mean_chain_length === undefined) return undefined 97 | const vars = ss.map((s, i) => (s * s * cc[i] / (cc[i] - 1))) 98 | const stdevMeans = computeStdev(mm) 99 | if (stdevMeans === undefined) return undefined 100 | const varMeans = stdevMeans * stdevMeans * cc.length / (cc.length - 1) 101 | const meanVars = computeMean(vars) 102 | if (meanVars === undefined) return undefined 103 | const r_hat = Math.sqrt((mean_chain_length - 1) / mean_chain_length + varMeans / meanVars) 104 | return r_hat 105 | } -------------------------------------------------------------------------------- /src/MCMCMonitorDataManager/useMCMCMonitor.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext, useMemo } from 'react' 2 | import { GetChainsForRunRequest, GetRunsRequest, MCMCChain, MCMCRun, isGetChainsForRunResponse, isGetRunsResponse } from '../../service/src/types' 3 | import { serviceBaseUrl, spaMode } from '../config' 4 | import postApiRequest from '../networking/postApiRequest' 5 | import getSpaChainsForRun from '../spaInterface/getSpaChainsForRun' 6 | import { isSpaRunId } from '../spaInterface/util' 7 | import { MCMCMonitorContext, detectedWarmupIterationCount } from './MCMCMonitorData' 8 | import { GeneralOpts } from './MCMCMonitorDataTypes' 9 | import updateChains from './updateChains' 10 | 11 | const defaultInitialDrawExclusionOptions = [ 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000 ] 12 | 13 | export const useMCMCMonitor = () => { 14 | const { data, dispatch, checkConnectionStatus } = useContext(MCMCMonitorContext) 15 | 16 | const setRuns = useCallback((runs: MCMCRun[]) => { 17 | dispatch({ type: 'setRuns', runs }) 18 | }, [dispatch]) 19 | 20 | const setChainsForRun = useCallback((runId: string, chains: MCMCChain[]) => { 21 | dispatch({ type: 'updateChainsForRun', runId, chains }) 22 | }, [dispatch]) 23 | 24 | const setSelectedVariableNames = useCallback((variableNames: string[]) => { 25 | dispatch({ type: 'setSelectedVariableNames', variableNames }) 26 | }, [dispatch]) 27 | 28 | const setSelectedChainIds = useCallback((chainIds: string[]) => { 29 | dispatch({ type: 'setSelectedChainIds', chainIds }) 30 | }, [dispatch]) 31 | 32 | const setSelectedRunId = useCallback((runId: string) => { 33 | dispatch({ type: 'setSelectedRunId', runId }) 34 | }, [dispatch]) 35 | 36 | const updateRuns = useCallback(() => { 37 | ; (async () => { 38 | if (spaMode) return // no need to update runs in spa mode (we only have one run) 39 | if (!serviceBaseUrl) { 40 | throw Error('Unexpected: cannot update runs. ServiceBaseUrl not set') 41 | } 42 | const req: GetRunsRequest = { 43 | type: 'getRunsRequest' 44 | } 45 | const resp = await postApiRequest(req) 46 | if (!isGetRunsResponse(resp)) { 47 | console.warn(resp) 48 | throw Error('Unexpected getRuns response') 49 | } 50 | setRuns(resp.runs) 51 | })() 52 | }, [setRuns]) 53 | 54 | const updateChainsForRun = useCallback((runId: string) => { 55 | ; (async () => { 56 | let chains: MCMCChain[] 57 | if (isSpaRunId(runId)) { 58 | // handle the special case where we have a stan playground run 59 | chains = await getSpaChainsForRun(runId) 60 | } 61 | else { 62 | // handle the usual case 63 | const req: GetChainsForRunRequest = { 64 | type: 'getChainsForRunRequest', 65 | runId 66 | } 67 | const resp = await postApiRequest(req) 68 | if (!isGetChainsForRunResponse(resp)) { 69 | console.warn(JSON.stringify(resp)) 70 | throw Error('Unexpected getChainsForRun response') 71 | } 72 | chains = resp.chains 73 | } 74 | setChainsForRun(runId, chains) 75 | })() 76 | }, [setChainsForRun]) 77 | 78 | const updateKnownData = useCallback((runId: string) => { 79 | updateChains(runId, dispatch) 80 | dispatch({ 81 | type: 'requestSequenceUpdate', 82 | runId 83 | }) 84 | }, [dispatch]) 85 | 86 | const setGeneralOpts = useCallback((opts: GeneralOpts) => { 87 | dispatch({ 88 | type: 'setGeneralOpts', 89 | opts 90 | }) 91 | }, [dispatch]) 92 | 93 | 94 | const initialDrawExclusionOptions = useMemo(() => { 95 | const detectedCount = detectedWarmupIterationCount(data.chains) 96 | return { 97 | warmupOptions: defaultInitialDrawExclusionOptions, 98 | detectedInitialDrawExclusion: detectedCount } 99 | }, [data.chains]) 100 | 101 | return { 102 | runs: data.runs, 103 | chains: data.chains, 104 | sequences: data.sequences, 105 | selectedVariableNames: data.selectedVariableNames, 106 | selectedChainIds: data.selectedChainIds, 107 | selectedRunId: data.selectedRunId, 108 | connectedToService: data.connectedToService, 109 | serviceProtocolVersion: data.serviceProtocolVersion, 110 | usingProxy: data.usingProxy, 111 | generalOpts: data.generalOpts, 112 | sequenceStats: data.sequenceStats, 113 | variableStats: data.variableStats, 114 | effectiveInitialDrawsToExclude: data.effectiveInitialDrawsToExclude, 115 | initialDrawExclusionOptions, 116 | updateRuns, 117 | updateChainsForRun, 118 | updateKnownData, 119 | setSelectedVariableNames, 120 | setSelectedChainIds, 121 | setSelectedRunId, 122 | setGeneralOpts, 123 | checkConnectionStatus 124 | } 125 | } -------------------------------------------------------------------------------- /src/components/AutocorrelationPlot.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, useMemo } from "react"; 2 | import { useMCMCMonitor } from "../MCMCMonitorDataManager/useMCMCMonitor"; 3 | import AutocorrelationPlotWidget from "./AutocorrelationPlotWidget"; 4 | 5 | type Props = { 6 | runId: string 7 | chainId: string 8 | variableName: string 9 | drawRange: [number, number] | undefined 10 | width: number 11 | height: number 12 | } 13 | 14 | const AutocorrelationPlot: FunctionComponent = ({runId, chainId, variableName, drawRange, width, height}) => { 15 | const {sequenceStats} = useMCMCMonitor() 16 | 17 | const autocorrelationData = useMemo(() => { 18 | const k = `${runId}/${chainId}/${variableName}` 19 | const ss = sequenceStats[k] 20 | if (!ss) { 21 | return undefined 22 | } 23 | const {acor} = ss 24 | if (!acor) return undefined 25 | const dx: number[] = [] 26 | const y: number[] = [] 27 | for (let i = 0; i < Math.min(acor.length, 100); i++) { 28 | dx.push(i) 29 | y.push(acor[i]) 30 | } 31 | return {dx, y} 32 | }, [sequenceStats, runId, chainId, variableName]) 33 | return ( 34 | 41 | ) 42 | } 43 | 44 | export default AutocorrelationPlot 45 | -------------------------------------------------------------------------------- /src/components/AutocorrelationPlotWidget.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, Suspense, useMemo } from "react"; 2 | 3 | type Props = { 4 | autocorrelationData: {dx: number[], y: number[]} | undefined 5 | title: string 6 | variableName: string 7 | width: number 8 | height: number 9 | } 10 | 11 | const Plot = React.lazy(() => (import('react-plotly.js'))) 12 | 13 | const AutocorrelationPlotWidget: FunctionComponent = ({ autocorrelationData, title, width, height, variableName }) => { 14 | const data = useMemo(() => ( 15 | autocorrelationData ? { 16 | x: autocorrelationData.dx, 17 | y: autocorrelationData.y, 18 | type: 'bar', 19 | marker: {color: '#506050'} 20 | } as any : undefined 21 | ), [autocorrelationData]) 22 | return ( 23 |
24 | Loading plotly
}> 25 | 36 | 37 | 38 | ) 39 | } 40 | 41 | export default AutocorrelationPlotWidget 42 | -------------------------------------------------------------------------------- /src/components/ChainsSelector.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox, FormControlLabel } from "@mui/material"; 2 | import { FunctionComponent } from "react"; 3 | import { MCMCChain } from "../../service/src/types"; 4 | import { useMCMCMonitor } from "../MCMCMonitorDataManager/useMCMCMonitor"; 5 | import toggleListItem from "../util/toggleListItem"; 6 | 7 | const SOLID_SQUARE = "\u25A0" // equivalent to HTML entity ■ 8 | 9 | type Props = { 10 | chains: MCMCChain[] 11 | allChainIds: string[] 12 | chainColors: {[chainId: string]: string} 13 | } 14 | 15 | const ChainsSelector: FunctionComponent = ({chains, allChainIds, chainColors}) => { 16 | const {selectedChainIds, setSelectedChainIds} = useMCMCMonitor() 17 | return ( 18 |
19 | 20 |   21 | 22 |
23 | { 24 | chains.map(c => ( 25 | 26 | setSelectedChainIds(toggleListItem(selectedChainIds, c.chainId))} 31 | checked={selectedChainIds.includes(c.chainId)} /> 32 | } 33 | label={{SOLID_SQUARE} {c.chainId}} 34 | /> 35 |
36 |
37 | )) 38 | } 39 |
40 |
41 | ) 42 | } 43 | 44 | export default ChainsSelector 45 | -------------------------------------------------------------------------------- /src/components/CollapsibleElement.tsx: -------------------------------------------------------------------------------- 1 | import { Grid } from "@mui/material"; 2 | import { FunctionComponent, PropsWithChildren } from "react"; 3 | import { CollapseControl, CollapsedVariablesAction } from "../tabs/TabsUtility"; 4 | 5 | export type CollapsibleElementProps = { 6 | variableName: string, 7 | isCollapsed: boolean, 8 | collapsedDispatch: (value: CollapsedVariablesAction) => void 9 | } 10 | 11 | const CollapsibleElement: FunctionComponent> = (props) => { 12 | const { variableName, isCollapsed, collapsedDispatch, children } = props 13 | if (!children) return
14 | return ( 15 |
16 |
17 |

18 | collapsedDispatch({type: 'toggle', variableName})} /> 19 | {variableName} 20 |

21 | {!isCollapsed && 22 | {children} 23 | } 24 |
25 |
 
26 |
27 | ) 28 | } 29 | 30 | export default CollapsibleElement 31 | -------------------------------------------------------------------------------- /src/components/ConnectionStatusWidget.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import { useMCMCMonitor } from "../MCMCMonitorDataManager/useMCMCMonitor"; 3 | import { serviceBaseUrl, webrtcConnectionToService } from "../config"; 4 | import Hyperlink from "./Hyperlink"; 5 | 6 | type Props = any 7 | 8 | const ConnectionStatusWidget: FunctionComponent = () => { 9 | const {usingProxy, connectedToService, checkConnectionStatus} = useMCMCMonitor() 10 | 11 | const checkButton = ( 12 |
Check connection status
13 | ) 14 | 15 | if (!connectedToService) { 16 | return ( 17 |
18 |
Not connected to service: {serviceBaseUrl}
19 | {checkButton} 20 |
21 | ) 22 | } 23 | 24 | return ( 25 |
26 |
Connected to service: {serviceBaseUrl}
27 |
 
28 | { 29 | usingProxy ? ( 30 |
- Using proxy
31 | ) : ( 32 |
- Not using proxy
33 | ) 34 | } 35 | { 36 | webrtcConnectionToService === undefined ? ( 37 |
- Not using WebRTC
38 | ) : webrtcConnectionToService.status === 'pending' ? ( 39 |
- WebRTC connection pending, using HTTP in the meantime
40 | ) : webrtcConnectionToService.status === 'error' ? ( 41 |
- WebRTC connection error--using HTTP fallback
42 | ) : webrtcConnectionToService.status === 'connected' ? ( 43 |
- Connected using WebRTC
44 | ) : 45 | } 46 |
 
47 | {checkButton} 48 |
49 | ) 50 | } 51 | 52 | export default ConnectionStatusWidget 53 | -------------------------------------------------------------------------------- /src/components/CookieLogic.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, useEffect } from 'react' 2 | import CookieConsent, { Cookies, getCookieConsentValue } from "react-cookie-consent" 3 | import ReactGA from 'react-ga4' 4 | 5 | 6 | const handleAcceptCookie = () => { 7 | const analyticsTag = import.meta.env.VITE_GOOGLE_ANALYTICS_ID 8 | if (analyticsTag) { 9 | ReactGA.initialize(analyticsTag) 10 | } 11 | } 12 | 13 | const handleDeclineCookie = () => { 14 | Cookies.remove("_ga") 15 | Cookies.remove("_gat") 16 | Cookies.remove("_gid") 17 | } 18 | 19 | const CookieBanner: FunctionComponent = () => { 20 | useEffect(() => { 21 | const consenting = getCookieConsentValue() 22 | if (consenting === "true") { 23 | handleAcceptCookie() 24 | } else { 25 | console.log(`Cookies rejected, doing nothing`) 26 | } 27 | }, []) 28 | 29 | return ( 30 | 35 | This website uses an analytics cookie to count page views and estimate 36 | the size of the user base. No advertising information is collected. 37 | 38 | ) 39 | } 40 | 41 | export default CookieBanner 42 | -------------------------------------------------------------------------------- /src/components/GeneralOptsControl.tsx: -------------------------------------------------------------------------------- 1 | import { FormControl, MenuItem, Select, SelectChangeEvent } from "@mui/material"; 2 | import { FunctionComponent } from "react"; 3 | import { useMCMCMonitor } from "../MCMCMonitorDataManager/useMCMCMonitor"; 4 | 5 | type Props = { 6 | warmupOptions: number[], 7 | detectedInitialDrawExclusion?: number 8 | } 9 | 10 | const GeneralOptsControl: FunctionComponent = (props: Props) => { 11 | const { warmupOptions, detectedInitialDrawExclusion } = props 12 | const { generalOpts, setGeneralOpts, updateKnownData, selectedRunId: runId } = useMCMCMonitor() 13 | if (!runId) return
No runId
14 | return ( 15 |
16 | Exclude draws 17 | 18 | 26 | 27 |
 
28 | Data refresh 29 | 30 | 37 | 38 | { 39 | generalOpts.dataRefreshMode === 'auto' && ( 40 | 41 |
 
42 | Refresh interval 43 | 44 | 54 | 55 |
56 | ) 57 | } 58 |
 
59 | { 60 | // generalOpts.dataRefreshMode === 'manual' && ( 61 | 62 | 63 | 64 | // ) 65 | } 66 |
67 | ) 68 | } 69 | 70 | 71 | const getReadFromFileOptionText = (detectedInitialDrawExclusion: number | undefined) => { 72 | const value = detectedInitialDrawExclusion === undefined ? "" : ` (${detectedInitialDrawExclusion})` 73 | const text = `Auto detect${value}` 74 | return {text} 75 | } 76 | 77 | 78 | const getWarmupCountList = (warmupOptions: number[]) => { 79 | return warmupOptions.map(n => ( 80 | First {n} 81 | )) 82 | } 83 | 84 | export default GeneralOptsControl 85 | -------------------------------------------------------------------------------- /src/components/Hyperlink.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, PropsWithChildren } from "react"; 2 | 3 | type Props ={ 4 | onClick: () => void 5 | } 6 | 7 | const Hyperlink: FunctionComponent> = ({children, onClick}) => { 8 | return ( 9 | {children} 10 | ) 11 | } 12 | 13 | export default Hyperlink 14 | -------------------------------------------------------------------------------- /src/components/MatrixOfPlots.tsx: -------------------------------------------------------------------------------- 1 | import { Grid } from "@mui/material"; 2 | import { FunctionComponent, PropsWithChildren, ReactElement, useMemo } from "react"; 3 | 4 | type Props = { 5 | numColumns: number 6 | width: number 7 | } 8 | 9 | const MatrixOfPlots: FunctionComponent> = ({numColumns, children, width}) => { 10 | const childList = useMemo(() => (Array.isArray(children) ? children as ReactElement[] : [children] as ReactElement[]), [children]) 11 | const numRows = Math.ceil(childList.length / (numColumns || 1)) 12 | const rowNumbers = [...Array(numRows).keys()] 13 | const columnNumbers = [...Array(numColumns).keys()] 14 | const plotWidth = (width - 10) / numColumns 15 | const plotHeight = plotWidth 16 | const plotElements = useMemo(() => ( 17 | childList.map((ch, i) => ( 18 | 19 | )) 20 | ), [childList, plotWidth, plotHeight]) 21 | if (numRows === 0) { 22 | return
23 | } 24 | return ( 25 | 26 | { 27 | rowNumbers.map(r => ( 28 | 29 | { 30 | columnNumbers.map(c => ( 31 | 32 | { 33 | plotElements[c + r * numColumns] 34 | } 35 | 36 | )) 37 | } 38 | 39 | )) 40 | } 41 | 42 | ) 43 | } 44 | 45 | export default MatrixOfPlots 46 | -------------------------------------------------------------------------------- /src/components/RunControlPanel.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, useEffect, useMemo, useState } from "react"; 2 | import { useMCMCMonitor } from "../MCMCMonitorDataManager/useMCMCMonitor"; 3 | import useRoute from "../util/useRoute"; 4 | import ChainsSelector from "./ChainsSelector"; 5 | import GeneralOptsControl from "./GeneralOptsControl"; 6 | import Hyperlink from "./Hyperlink"; 7 | import VariablesSelector from "./VariablesSelector"; 8 | 9 | type Props = { 10 | numDrawsForRun: number 11 | chainColors: {[chainId: string]: string} 12 | } 13 | 14 | const RunControlPanel: FunctionComponent = ({numDrawsForRun, chainColors}) => { 15 | const {chains, setSelectedVariableNames, selectedRunId: runId, initialDrawExclusionOptions} = useMCMCMonitor() 16 | const {setRoute} = useRoute() 17 | const chainsForRun = useMemo(() => (chains.filter(c => (c.runId === runId))), [chains, runId]) 18 | const { warmupOptions, detectedInitialDrawExclusion } = initialDrawExclusionOptions 19 | const [ knownVariableNames, setKnownVariableNames ] = useState([]) 20 | 21 | const allVariableNames = useMemo(() => { 22 | const s = new Set() 23 | for (const c of chainsForRun) { 24 | for (const v of c.variableNames) { 25 | s.add(v) 26 | } 27 | } 28 | return [...s].sort().sort((v1, v2) => { 29 | if ((v1.includes('__')) && (!v2.includes('__'))) return -1 30 | if ((!v1.includes('__')) && (v2.includes('__'))) return 1 31 | return 0 32 | }) 33 | }, [chainsForRun]) 34 | 35 | const allVariablePrefixesExcluded = useMemo(() => { 36 | const s = new Set() 37 | for (const c of chainsForRun) { 38 | for (const v of (c.variablePrefixesExcluded || [])) { 39 | s.add(v) 40 | } 41 | } 42 | return [...s].sort() 43 | }, [chainsForRun]) 44 | 45 | useEffect(() => { 46 | const known = new Set(knownVariableNames) 47 | if (knownVariableNames.length !== allVariableNames.length || allVariableNames.some(n => !known.has(n))) { 48 | setKnownVariableNames(allVariableNames) 49 | } 50 | }, [knownVariableNames, allVariableNames]) 51 | 52 | useEffect(() => { 53 | // start with just lp__ selected 54 | if (knownVariableNames.includes('lp__')) { 55 | setSelectedVariableNames(['lp__']) 56 | } else { 57 | setSelectedVariableNames([]) 58 | } 59 | }, [runId, setSelectedVariableNames, knownVariableNames]) 60 | 61 | return ( 62 |
63 | setRoute({page: 'home'})}>Back to home 64 |

Run: {runId}

65 |

{numDrawsForRun} draws | {chainsForRun.length} chains

66 | 67 |

Chains

68 |
69 | (c.chainId))} chainColors={chainColors} /> 70 |
71 |

Variables

72 |
73 | 74 |
75 |

Options

76 | 77 |
78 | ) 79 | } 80 | 81 | export default RunControlPanel 82 | -------------------------------------------------------------------------------- /src/components/RunsTable.tsx: -------------------------------------------------------------------------------- 1 | import { Table, TableBody, TableCell, TableHead, TableRow } from "@mui/material"; 2 | import { FunctionComponent, useMemo } from "react"; 3 | import { useMCMCMonitor } from "../MCMCMonitorDataManager/useMCMCMonitor"; 4 | import useRoute from "../util/useRoute"; 5 | import Hyperlink from "./Hyperlink"; 6 | 7 | type Props = any 8 | 9 | const RunsTable: FunctionComponent = () => { 10 | const { runs } = useMCMCMonitor() 11 | const { setRoute } = useRoute() 12 | 13 | const columns = useMemo(() => ([ 14 | { key: 'runId', label: 'Run' } 15 | ]), []) 16 | const rows = useMemo(() => ( 17 | runs.map(run => ({ 18 | key: `${run.runId}`, 19 | runId: setRoute({page: 'run', runId: run.runId})}>{run.runId} 20 | } as { [key: string]: any })) 21 | ), [runs, setRoute]) 22 | return ( 23 | 24 | 25 | 26 | { 27 | columns.map(c => ( 28 | {c.label} 29 | )) 30 | } 31 | 32 | 33 | 34 | { 35 | rows.map((row) => ( 36 | 37 | { 38 | columns.map(c => ( 39 | {row[c.key]} 40 | )) 41 | } 42 | 43 | )) 44 | } 45 | 46 |
47 | ) 48 | } 49 | 50 | export default RunsTable 51 | -------------------------------------------------------------------------------- /src/components/SequenceHistogram.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, useMemo } from "react"; 2 | import { useMCMCMonitor } from "../MCMCMonitorDataManager/useMCMCMonitor"; 3 | import SequenceHistogramWidget from "./SequenceHistogramWidget"; 4 | 5 | type Props = { 6 | runId: string 7 | chainId: string | string[] 8 | variableName: string 9 | drawRange: [number, number] | undefined 10 | title: string 11 | width: number 12 | height: number 13 | } 14 | 15 | const SequenceHistogram: FunctionComponent = ({runId, chainId, variableName, drawRange, title, width, height}) => { 16 | const {sequences} = useMCMCMonitor() 17 | const histData = useMemo(() => { 18 | function getDataForChain(ch: string) { 19 | const s = sequences.filter(s => (s.runId === runId && s.chainId === ch && s.variableName === variableName))[0] 20 | if (s) { 21 | return applyDrawRange(s.data, drawRange) 22 | } 23 | else { 24 | return [] 25 | } 26 | } 27 | if (!Array.isArray(chainId)) { 28 | return getDataForChain(chainId) 29 | } 30 | else { 31 | return chainId.map(ch => (getDataForChain(ch))).flat() 32 | } 33 | }, [chainId, sequences, runId, variableName, drawRange]) 34 | return ( 35 | 36 | ) 37 | } 38 | 39 | export function applyDrawRange(data: number[], drawRange: [number, number] | undefined) { 40 | if (!drawRange) return data 41 | return data.slice(drawRange[0], drawRange[1]) 42 | } 43 | 44 | export default SequenceHistogram 45 | -------------------------------------------------------------------------------- /src/components/SequenceHistogramWidget.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, Suspense, useMemo } from "react"; 2 | 3 | type Props = { 4 | histData: number[] 5 | title: string 6 | variableName: string 7 | width: number 8 | height: number 9 | } 10 | 11 | const Plot = React.lazy(() => (import('react-plotly.js'))) 12 | 13 | const SequenceHistogramWidget: FunctionComponent = ({ histData, title, width, height, variableName }) => { 14 | const data = useMemo(() => ( 15 | { 16 | x: histData, 17 | type: 'histogram', 18 | nbinsx: Math.ceil(1.5 * Math.sqrt(histData.length)), 19 | marker: {color: '#505060'} 20 | } as any // had to do it this way because ts was not recognizing nbinsx 21 | ), [histData]) 22 | return ( 23 |
24 | Loading plotly
}> 25 | 36 | 37 |
38 | ) 39 | } 40 | 41 | export default SequenceHistogramWidget 42 | -------------------------------------------------------------------------------- /src/components/SequencePlot.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, useMemo } from "react"; 2 | import { useMCMCMonitor } from "../MCMCMonitorDataManager/useMCMCMonitor"; 3 | import SequencePlotWidget, { PlotSequence } from "./SequencePlotWidget"; 4 | 5 | type Props = { 6 | runId: string 7 | chainIds: string[] 8 | variableName: string 9 | highlightDrawRange?: [number, number] 10 | chainColors: {[chainId: string]: string} 11 | width: number 12 | height: number 13 | } 14 | 15 | const SequencePlot: FunctionComponent = ({runId, chainIds, variableName, highlightDrawRange, chainColors, width, height}) => { 16 | const {sequences} = useMCMCMonitor() 17 | const plotSequences = useMemo(() => { 18 | const ret: PlotSequence[] = [] 19 | for (const chainId of chainIds) { 20 | const s = sequences.filter(s => (s.runId === runId && s.chainId === chainId && s.variableName === variableName))[0] 21 | if (s) { 22 | ret.push({ 23 | label: chainId, 24 | data: s.data, 25 | color: chainColors[chainId] || 'black' 26 | }) 27 | } 28 | } 29 | return ret 30 | }, [chainIds, sequences, runId, variableName, chainColors]) 31 | return ( 32 | 39 | ) 40 | } 41 | 42 | export default SequencePlot 43 | -------------------------------------------------------------------------------- /src/components/SequencePlotWidget.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, Suspense, useMemo } from "react"; 2 | 3 | export type PlotSequence = { 4 | label: string 5 | data: number[] 6 | color: string 7 | } 8 | 9 | type Props = { 10 | plotSequences: PlotSequence[] 11 | variableName: string 12 | highlightDrawRange?: [number, number] 13 | width: number 14 | height: number 15 | } 16 | 17 | const Plot = React.lazy(() => (import('react-plotly.js'))) 18 | 19 | const SequencePlotWidget: FunctionComponent = ({ plotSequences, variableName, highlightDrawRange, width, height }) => { 20 | const shapes = useMemo(() => ( 21 | (highlightDrawRange ? ( 22 | [{type: 'rect', x0: highlightDrawRange[0], x1: highlightDrawRange[1], y0: 0, y1: 1, yref: 'paper', fillcolor: 'yellow', opacity: 0.1}] 23 | ) : []) as any 24 | ), [highlightDrawRange]) 25 | return ( 26 |
27 | Loading plotly
}> 28 | ( 31 | { 32 | x: [...new Array(ps.data.length).keys()].map(i => (i + 1)), 33 | y: ps.data, 34 | type: 'scatter', 35 | mode: 'lines+markers', 36 | marker: {color: ps.color} 37 | } 38 | )) 39 | } 40 | layout={{ 41 | width: width, 42 | height, 43 | title: '', 44 | yaxis: {title: variableName}, 45 | xaxis: {title: 'draw'}, 46 | shapes, 47 | margin: { 48 | t: 30, b: 40, r: 0 49 | }, 50 | showlegend: false 51 | }} 52 | /> 53 | 54 |
55 | ) 56 | } 57 | 58 | export default SequencePlotWidget 59 | -------------------------------------------------------------------------------- /src/components/SequenceScatterplot.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, useMemo } from "react"; 2 | import { useMCMCMonitor } from "../MCMCMonitorDataManager/useMCMCMonitor"; 3 | import { applyDrawRange } from "./SequenceHistogram"; 4 | import SequenceScatterplotWidget, { ScatterplotSequence } from "./SequenceScatterplotWidget"; 5 | 6 | type Props = { 7 | runId: string 8 | chainIds: string[] 9 | xVariableName: string 10 | yVariableName: string 11 | highlightDrawRange?: [number, number] 12 | chainColors: {[chainId: string]: string} 13 | width: number 14 | height: number 15 | } 16 | 17 | const SequenceScatterplot: FunctionComponent = ({runId, chainIds, xVariableName, yVariableName, highlightDrawRange, chainColors, width, height}) => { 18 | const {sequences} = useMCMCMonitor() 19 | const scatterplotSequences = useMemo(() => { 20 | const ret: ScatterplotSequence[] = [] 21 | for (const chainId of chainIds) { 22 | const sX = sequences.filter(s => (s.runId === runId && s.chainId === chainId && s.variableName === xVariableName))[0] 23 | const sY = sequences.filter(s => (s.runId === runId && s.chainId === chainId && s.variableName === yVariableName))[0] 24 | if ((sX) && (sY)) { 25 | ret.push({ 26 | label: chainId, 27 | xData: applyDrawRange(sX.data, highlightDrawRange), 28 | yData: applyDrawRange(sY.data, highlightDrawRange), 29 | color: chainColors[chainId] || 'black' 30 | }) 31 | } 32 | } 33 | return ret 34 | }, [chainIds, chainColors, sequences, runId, xVariableName, yVariableName, highlightDrawRange]) 35 | return ( 36 | 43 | ) 44 | } 45 | 46 | export default SequenceScatterplot 47 | -------------------------------------------------------------------------------- /src/components/SequenceScatterplot3D.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, useMemo } from "react"; 2 | import { useMCMCMonitor } from "../MCMCMonitorDataManager/useMCMCMonitor"; 3 | import { applyDrawRange } from "./SequenceHistogram"; 4 | import SequenceScatterplot3DWidget, { Scatterplot3DSequence } from "./SequenceScatterplot3DWidget"; 5 | 6 | type Props = { 7 | runId: string 8 | chainIds: string[] 9 | xVariableName: string 10 | yVariableName: string 11 | zVariableName: string 12 | highlightDrawRange?: [number, number] 13 | chainColors: {[chainId: string]: string} 14 | width: number 15 | height: number 16 | } 17 | 18 | const SequenceScatterplot3D: FunctionComponent = ({runId, chainIds, xVariableName, yVariableName, zVariableName, highlightDrawRange, chainColors, width, height}) => { 19 | const {sequences} = useMCMCMonitor() 20 | const scatterplot3DSequences = useMemo(() => { 21 | const ret: Scatterplot3DSequence[] = [] 22 | for (const chainId of chainIds) { 23 | const sX = sequences.filter(s => (s.runId === runId && s.chainId === chainId && s.variableName === xVariableName))[0] 24 | const sY = sequences.filter(s => (s.runId === runId && s.chainId === chainId && s.variableName === yVariableName))[0] 25 | const sZ = sequences.filter(s => (s.runId === runId && s.chainId === chainId && s.variableName === zVariableName))[0] 26 | if ((sX) && (sY) && (sZ)) { 27 | ret.push({ 28 | label: chainId, 29 | xData: applyDrawRange(sX.data, highlightDrawRange), 30 | yData: applyDrawRange(sY.data, highlightDrawRange), 31 | zData: applyDrawRange(sZ.data, highlightDrawRange), 32 | color: chainColors[chainId] || 'black' 33 | }) 34 | } 35 | } 36 | return ret 37 | }, [chainIds, chainColors, sequences, runId, xVariableName, yVariableName, zVariableName, highlightDrawRange]) 38 | return ( 39 | 47 | ) 48 | } 49 | 50 | export default SequenceScatterplot3D 51 | -------------------------------------------------------------------------------- /src/components/SequenceScatterplot3DWidget.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, Suspense } from "react"; 2 | 3 | export type Scatterplot3DSequence = { 4 | label: string 5 | xData: number[] 6 | yData: number[] 7 | zData: number[] 8 | color: string 9 | } 10 | 11 | type Props = { 12 | scatterplot3DSequences: Scatterplot3DSequence[] 13 | xVariableName: string 14 | yVariableName: string 15 | zVariableName: string 16 | width: number 17 | height: number 18 | } 19 | 20 | const Plot = React.lazy(() => (import('react-plotly.js'))) 21 | 22 | const SequenceScatterplot3DWidget: FunctionComponent = ({ scatterplot3DSequences, xVariableName, yVariableName, zVariableName, width, height }) => { 23 | return ( 24 |
25 | Loading plotly
}> 26 | ( 29 | { 30 | x: ss.xData, 31 | y: ss.yData, 32 | z: ss.zData, 33 | type: 'scatter3d', 34 | mode: 'markers', 35 | marker: {size: 3, color: ss.color} 36 | } 37 | )) 38 | } 39 | layout={{ 40 | width, 41 | height, 42 | title: '', 43 | scene: { 44 | xaxis: {title: xVariableName}, 45 | yaxis: {title: yVariableName}, 46 | zaxis: {title: zVariableName} 47 | }, 48 | showlegend: false 49 | }} 50 | /> 51 | 52 | 53 | ) 54 | } 55 | 56 | export default SequenceScatterplot3DWidget 57 | -------------------------------------------------------------------------------- /src/components/SequenceScatterplotWidget.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, Suspense } from "react"; 2 | 3 | export type ScatterplotSequence = { 4 | label: string 5 | xData: number[] 6 | yData: number[] 7 | color: string 8 | } 9 | 10 | type Props = { 11 | scatterplotSequences: ScatterplotSequence[] 12 | xVariableName: string 13 | yVariableName: string 14 | width: number 15 | height: number 16 | } 17 | 18 | const Plot = React.lazy(() => (import('react-plotly.js'))) 19 | 20 | const SequenceScatterplotWidget: FunctionComponent = ({ scatterplotSequences, xVariableName, yVariableName, width, height }) => { 21 | return ( 22 |
23 | Loading plotly
}> 24 | ( 27 | { 28 | x: ss.xData, 29 | y: ss.yData, 30 | type: 'scatter', 31 | mode: 'markers', 32 | marker: {size: 5, color: ss.color} 33 | } 34 | )) 35 | } 36 | layout={{ 37 | width, 38 | height, 39 | title: '', 40 | yaxis: {title: yVariableName}, 41 | xaxis: {title: xVariableName}, 42 | margin: { 43 | t: 30, b: 40, r: 0 44 | }, 45 | showlegend: false 46 | }} 47 | /> 48 | 49 | 50 | ) 51 | } 52 | 53 | export default SequenceScatterplotWidget 54 | -------------------------------------------------------------------------------- /src/components/VariablesSelector.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox, FormControlLabel } from "@mui/material"; 2 | import { FunctionComponent } from "react"; 3 | import { useMCMCMonitor } from "../MCMCMonitorDataManager/useMCMCMonitor"; 4 | import toggleListItem from "../util/toggleListItem"; 5 | 6 | type Props = { 7 | variableNames: string[] 8 | variablePrefixesExcluded: string[] 9 | } 10 | 11 | const VariablesSelector: FunctionComponent = ({variableNames, variablePrefixesExcluded}) => { 12 | const {selectedVariableNames, setSelectedVariableNames} = useMCMCMonitor() 13 | return ( 14 |
15 | { 16 | variablePrefixesExcluded.length > 0 && ( 17 |
18 | The following variables were excluded: {`${variablePrefixesExcluded.join(', ')}`} 19 |
20 | ) 21 | } 22 | 23 |
24 | { 25 | variableNames.map(v => ( 26 | 27 | setSelectedVariableNames(toggleListItem(selectedVariableNames, v))} checked={selectedVariableNames.includes(v)} />} 29 | label={v} 30 | /> 31 | 32 | )) 33 | } 34 |
35 |
36 | ) 37 | } 38 | 39 | export default VariablesSelector 40 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import SimplePeer from "simple-peer" 2 | import WebrtcConnectionToService from "./networking/WebrtcConnectionToService" 3 | 4 | const urlSearchParams = new URLSearchParams(window.location.search) 5 | const queryParams = Object.fromEntries(urlSearchParams.entries()) 6 | 7 | export const defaultServiceBaseUrl = 'http://localhost:61542' 8 | 9 | export const exampleServiceBaseUrl = 'https://lit-bayou-76056.herokuapp.com' 10 | 11 | export const spaMode = queryParams.s === 'spa' 12 | 13 | export const serviceBaseUrl = queryParams.s ? ( 14 | spaMode ? '' : queryParams.s // if we are in spa mode, don't use a serviceBaseUrl 15 | ) : ( 16 | defaultServiceBaseUrl 17 | ) 18 | 19 | export const stanPlaygroundUrl = "https://stan-playground.vercel.app/api/playground" 20 | 21 | export const useWebrtc = queryParams.webrtc === '1' 22 | 23 | export let webrtcConnectionToService: WebrtcConnectionToService | undefined 24 | 25 | setTimeout(() => { 26 | // setting the timeout allows the import of postApiRequest to complete before it gets 27 | // called in the connect() method. 28 | // Otherwise, you get an (apparently ignorable, but annoying) error during connection setup. 29 | if ((useWebrtc) && (!webrtcConnectionToService)) { 30 | const peerInstance = new SimplePeer({initiator: true}) 31 | webrtcConnectionToService = new WebrtcConnectionToService(peerInstance).configurePeer() 32 | webrtcConnectionToService?.connect() 33 | } 34 | }, 0) -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client' 2 | import App from './App' 3 | // import './index.css' 4 | 5 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 6 | // 7 | 8 | // , 9 | ) 10 | -------------------------------------------------------------------------------- /src/networking/WebrtcConnectionToService.ts: -------------------------------------------------------------------------------- 1 | import SimplePeer from "simple-peer"; 2 | import { MCMCMonitorPeerRequest, MCMCMonitorRequest, MCMCMonitorResponse, WebrtcSignalingRequest, isMCMCMonitorPeerResponse } from "../../service/src/types"; 3 | import randomAlphaString from "../util/randomAlphaString"; 4 | import sleepMsec from "../util/sleepMsec"; 5 | import postApiRequest from "./postApiRequest"; 6 | 7 | type webrtcConnectionStatus = 'pending' | 'connected' | 'error' 8 | 9 | export const WEBRTC_CONNECTION_RETRY_INTERVAL_MS = 3000 10 | export const WEBRTC_CONNECTION_TIMEOUT_INTERVAL_MS = 15000 11 | export const WEBRTC_CONNECTION_PENDING_API_WAIT_INTERVAL_MS = 100 12 | 13 | type callbacksQueueType = {[requestId: string]: (response: MCMCMonitorResponse) => void} 14 | 15 | class WebrtcConnectionToService { 16 | #peer: SimplePeer.Instance | undefined 17 | #requestCallbacks: callbacksQueueType = {} 18 | #clientId = 'ID-PENDING' 19 | #status: webrtcConnectionStatus = 'pending' 20 | #timer: number | undefined 21 | constructor(peer: SimplePeer.Instance, callbacksQueue?: callbacksQueueType) { 22 | this.#clientId = randomAlphaString(10) 23 | this.#peer = peer 24 | this.#requestCallbacks = callbacksQueue ?? {} 25 | this.#timer = undefined 26 | } 27 | configurePeer() { 28 | if (this.#peer === undefined) { 29 | console.warn(`Attempt to configure uninitialized peer.`) 30 | return this 31 | } 32 | const peer = this.#peer 33 | peer.on('signal', async s => sendWebrtcSignal(this.#clientId, peer, s)) 34 | peer.on('connect', () => { 35 | console.info('Webrtc connection established') 36 | this.#status = 'connected' 37 | }) 38 | peer.on('data', d => { 39 | const dd = JSON.parse(d) 40 | if (!isMCMCMonitorPeerResponse(dd)) { 41 | console.warn(dd) 42 | throw Error('Unexpected peer response') 43 | } 44 | const cb = this.#requestCallbacks[dd.requestId] 45 | if (!cb) { 46 | console.warn('Got response, but no matching request ID callback') 47 | return 48 | } 49 | delete this.#requestCallbacks[dd.requestId] 50 | cb(dd.response) 51 | }) 52 | return this 53 | } 54 | async connect() { 55 | if (this.#peer === undefined) { 56 | console.warn("Attempt to connect using uninitialized SimplePeer instance.") 57 | return 58 | } 59 | this.#timer = this.#timer ?? Date.now() 60 | const elapsed = Date.now() - this.#timer 61 | if (elapsed > WEBRTC_CONNECTION_TIMEOUT_INTERVAL_MS) { 62 | this.#status = 'error' 63 | console.warn('Unable to establish webrtc connection.') 64 | return 65 | } 66 | if (this.#status === 'pending') { 67 | sendWebrtcSignal(this.#clientId, this.#peer, undefined) 68 | setTimeout(() => { 69 | if (this.#status === 'pending') { 70 | this.connect() 71 | } 72 | }, WEBRTC_CONNECTION_RETRY_INTERVAL_MS) 73 | } 74 | } 75 | async postApiRequest(request: MCMCMonitorRequest): Promise { 76 | if (!this.#peer) throw Error('No peer') 77 | if (this.status === 'error') { 78 | throw Error('Error in webrtc connection') 79 | } 80 | while (this.status === 'pending') { 81 | await sleepMsec(WEBRTC_CONNECTION_PENDING_API_WAIT_INTERVAL_MS) 82 | } 83 | const peer = this.#peer 84 | const requestId = randomAlphaString(10) 85 | const rr: MCMCMonitorPeerRequest = { 86 | type: 'mcmcMonitorPeerRequest', 87 | request, 88 | requestId 89 | } 90 | return new Promise((resolve) => { 91 | this.#requestCallbacks[requestId] = (resp: MCMCMonitorResponse) => { 92 | resolve(resp) 93 | } 94 | peer.send(JSON.stringify(rr)) 95 | }) 96 | } 97 | public get status() { 98 | return this.#status 99 | } 100 | public get clientId() { 101 | return this.#clientId 102 | } 103 | public setErrorStatus() { 104 | this.#status = 'error' 105 | } 106 | } 107 | 108 | export const sendWebrtcSignal = async (clientId: string, peer: SimplePeer.Instance, s: SimplePeer.SignalData | undefined) => { 109 | const request: WebrtcSignalingRequest = { 110 | type: 'webrtcSignalingRequest', 111 | clientId, 112 | signal: s === undefined ? undefined : JSON.stringify(s) 113 | } 114 | const response = await postApiRequest(request) 115 | if (response.type !== 'webrtcSignalingResponse') { 116 | console.warn(response) 117 | throw Error('Unexpected webrtc signaling response') 118 | } 119 | for (const sig0 of response.signals) { 120 | peer.signal(sig0) 121 | } 122 | } 123 | 124 | export default WebrtcConnectionToService -------------------------------------------------------------------------------- /src/networking/postApiRequest.ts: -------------------------------------------------------------------------------- 1 | import { MCMCMonitorRequest, MCMCMonitorResponse, isMCMCMonitorResponse } from "../../service/src/types" 2 | import { serviceBaseUrl, spaMode, useWebrtc, webrtcConnectionToService } from "../config" 3 | 4 | const postApiRequest = async (request: MCMCMonitorRequest): Promise => { 5 | if (spaMode) throw Error('Unexpected: cannot postApiRequest in spa mode') 6 | if (!serviceBaseUrl) throw Error('Unexpected in postApiRequest: serviceBaseUrl not set') 7 | // Note: we always use http for probe requests and webrtc signaling requests 8 | if ((useWebrtc) && (request.type !== 'probeRequest') && (request.type !== 'webrtcSignalingRequest')) { 9 | if (webrtcConnectionToService && webrtcConnectionToService.status === 'connected') { 10 | // if we have a webrtc connection, post the request via webrtc 11 | return webrtcConnectionToService.postApiRequest(request) 12 | } 13 | // if no webrtc connection, fall through to fetch via http below 14 | } 15 | const rr = await fetch( 16 | `${serviceBaseUrl}/api`, 17 | { 18 | method: 'POST', 19 | headers: { 'Content-Type': 'application/json' }, 20 | body: JSON.stringify(request) 21 | } 22 | ) 23 | const response = await rr.json() 24 | if (!isMCMCMonitorResponse(response)) { 25 | console.warn(response) 26 | throw TypeError('Unexpected api response') 27 | } 28 | return response 29 | } 30 | 31 | export default postApiRequest -------------------------------------------------------------------------------- /src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, FunctionComponent } from "react"; 2 | import ConnectionStatusWidget from "../components/ConnectionStatusWidget"; 3 | import Hyperlink from "../components/Hyperlink"; 4 | import RunsTable from "../components/RunsTable"; 5 | import { defaultServiceBaseUrl, exampleServiceBaseUrl, serviceBaseUrl } from "../config"; 6 | 7 | type Props = any 8 | 9 | const Home: FunctionComponent = () => { 10 | return ( 11 | 12 | { 13 | serviceBaseUrl !== exampleServiceBaseUrl && ( 14 |
15 | {;(window as any).location = `${window.location.protocol}//${window.location.host}${window.location.pathname}?s=${exampleServiceBaseUrl}`}} 17 | >View example data 18 |
19 | ) 20 | } 21 | { 22 | serviceBaseUrl === exampleServiceBaseUrl && ( 23 |
24 | Viewing example data.  25 | {;(window as any).location = `${window.location.protocol}//${window.location.host}${window.location.pathname}?s=${defaultServiceBaseUrl}`}} 27 | >Connect to local service 28 |
29 | ) 30 | } 31 | 32 |
33 | 34 |
35 | ) 36 | } 37 | 38 | export default Home 39 | -------------------------------------------------------------------------------- /src/pages/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | 3 | type Props = any 4 | 5 | // tricky 6 | const logoUrl = window.location.hostname.includes('github.io') ? ( 7 | `/mcmc-monitor/mcmc-monitor-logo.png` 8 | ) : ( 9 | `/mcmc-monitor-logo.png` 10 | ) 11 | 12 | const Logo: FunctionComponent = () => { 13 | return ( 14 | 15 | ) 16 | } 17 | 18 | export default Logo 19 | -------------------------------------------------------------------------------- /src/pages/MainWindow.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, FunctionComponent, PropsWithChildren, useEffect } from "react"; 2 | import { protocolVersion } from "../../service/src/types"; 3 | import MCMCDataManager from "../MCMCMonitorDataManager/MCMCMonitorDataManager"; 4 | import { useMCMCMonitor } from "../MCMCMonitorDataManager/useMCMCMonitor"; 5 | import Hyperlink from "../components/Hyperlink"; 6 | import { defaultServiceBaseUrl, exampleServiceBaseUrl, serviceBaseUrl, useWebrtc } from "../config"; 7 | import useRoute from "../util/useRoute"; 8 | import Home from "./Home"; 9 | import Logo from "./Logo"; 10 | import RunPage from "./RunPage"; 11 | import { constructSpaRunId } from "../spaInterface/util"; 12 | 13 | 14 | type Props = { 15 | dataManager: MCMCDataManager | undefined 16 | } 17 | 18 | const MainWindow: FunctionComponent = (props: Props) => { 19 | const { dataManager } = props 20 | const { route } = useRoute() 21 | const { updateRuns, serviceProtocolVersion, connectedToService } = useMCMCMonitor() 22 | useEffect(() => { 23 | updateRuns() 24 | }, [updateRuns]) 25 | 26 | if (serviceBaseUrl) { 27 | if (connectedToService === undefined) { 28 | return 29 | } 30 | 31 | if (connectedToService === false) { 32 | return ( 33 | 34 | 35 | 36 | ) 37 | } 38 | } 39 | 40 | switch (route.page) { 41 | case "home": 42 | return ( 43 | 44 | 45 | 46 | ) 47 | break 48 | case "run": 49 | return 50 | break 51 | case "spa": 52 | return 53 | default: 54 | return 55 | } 56 | } 57 | 58 | const ConnectionInProgress: FunctionComponent = () => { 59 | return ( 60 |
Connecting to service{useWebrtc ? ' using WebRTC' : ''}: {serviceBaseUrl}
61 | ) 62 | } 63 | 64 | const LogoFrame: FunctionComponent = ({children}) => { 65 | return ( 66 |
67 | 68 |

WIP

69 | {children} 70 |
71 | 72 |
73 | ) 74 | } 75 | 76 | type FailedConnectionProps = { 77 | serviceProtocolVersion: string | undefined 78 | } 79 | 80 | const FailedConnection: FunctionComponent = (props: FailedConnectionProps) => { 81 | const { serviceProtocolVersion } = props 82 | return ( 83 | 84 |
Not connected to service {serviceBaseUrl}
85 | 86 |
87 |
88 | { 89 | serviceBaseUrl !== exampleServiceBaseUrl && ( 90 | {;(window as any).location = `${window.location.protocol}//${window.location.host}${window.location.pathname}?s=${exampleServiceBaseUrl}`}} 92 | >View example data 93 | ) 94 | } 95 |
96 | { 97 | serviceBaseUrl === defaultServiceBaseUrl && ( 98 |

How to run a local service

99 | ) 100 | } 101 |
102 | ) 103 | } 104 | 105 | const GithubLink: FunctionComponent = () => { 106 | return 107 |
108 | 109 | To report issues, make suggestions, or check for updates, please visit {;(window as any).location = "https://github.com/flatironinstitute/mcmc-monitor"}} 111 | >the project Github repository. 112 | 113 |
114 |
115 | } 116 | 117 | type ProtocolCheckProps = { 118 | expectedProtocol: string 119 | serviceProtocol?: string 120 | } 121 | 122 | const ProtocolCheck: FunctionComponent = (props: ProtocolCheckProps) => { 123 | const { expectedProtocol, serviceProtocol } = props 124 | if (serviceProtocol === undefined || expectedProtocol === serviceProtocol) { 125 | return <> 126 | } 127 | return
128 |
129 |
PROTOCOL MISMATCH: Connected service is running protocol version {serviceProtocol} while we expect {expectedProtocol}. 130 | Please contact the service administrator and request that they upgrade. 131 |
132 |
133 | } 134 | 135 | export default MainWindow 136 | -------------------------------------------------------------------------------- /src/pages/RunPage.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, useEffect, useMemo } from "react"; 2 | import { MCMCChain } from "../../service/src/types"; 3 | import MCMCDataManager from "../MCMCMonitorDataManager/MCMCMonitorDataManager"; 4 | import { useMCMCMonitor } from "../MCMCMonitorDataManager/useMCMCMonitor"; 5 | import RunControlPanel from "../components/RunControlPanel"; 6 | import Splitter from "../components/Splitter"; 7 | import { AutoCorrelationTab, ConnectionTab, ExportTab, HistogramTab, RunInfoTab, ScatterplotsTab, SummaryStatsTab, TabWidget, TracePlotsTab } from "../tabs"; 8 | import { chainColorForIndex } from "../util/chainColorList"; 9 | import useWindowDimensions from "../util/useWindowDimensions"; 10 | 11 | type Props = { 12 | runId: string 13 | dataManager: MCMCDataManager | undefined 14 | } 15 | 16 | const RunPage: FunctionComponent = ({runId, dataManager}) => { 17 | const {chains, sequences, updateChainsForRun, setSelectedChainIds, generalOpts, updateKnownData, setSelectedRunId} = useMCMCMonitor() 18 | 19 | useEffect(() => { 20 | if (dataManager === undefined) return 21 | dataManager.start() 22 | return () => { dataManager.stop() } 23 | }, [dataManager]) 24 | 25 | useEffect(() => { 26 | setSelectedRunId(runId) 27 | }, [runId, setSelectedRunId]) 28 | 29 | useEffect(() => { 30 | let canceled = false 31 | function update() { 32 | if (canceled) return 33 | setTimeout(() => { 34 | if (generalOpts.dataRefreshMode === 'auto') { 35 | updateKnownData(runId) 36 | } 37 | update() 38 | }, generalOpts.dataRefreshIntervalSec * 1000) 39 | } 40 | update() 41 | return () => {canceled = true} 42 | }, [runId, generalOpts.dataRefreshMode, generalOpts.dataRefreshIntervalSec, updateKnownData]) 43 | 44 | useEffect(() => { 45 | updateChainsForRun(runId) 46 | }, [runId, updateChainsForRun]) 47 | 48 | const numDrawsForRun: number = useMemo(() => { 49 | const a = sequences.filter(s => (s.runId === runId)).map(s => ( 50 | s.data.length 51 | )) 52 | if (a.length === 0) return 0 53 | return Math.max(...a) 54 | }, [sequences, runId]) 55 | 56 | const chainsForRun = useMemo(() => { 57 | return (chains.filter(c => (c.runId === runId)) 58 | .sort((a, b) => a.chainId.localeCompare(b.chainId))) 59 | }, [chains, runId]) 60 | 61 | const chainColors = useMemo(() => { 62 | const ret: {[chainId: string]: string} = {} 63 | for (let i = 0; i < chainsForRun.length; i++) { 64 | ret[chainsForRun[i].chainId] = chainColorForIndex(i) 65 | } 66 | return ret 67 | }, [chainsForRun]) 68 | 69 | useEffect(() => { 70 | // // start with 5 chains selected 71 | // setSelectedChainIds(chainsForRun.slice(0, 5).map(c => (c.chainId))) 72 | 73 | // start with all chains selected 74 | setSelectedChainIds(chainsForRun.map(c => (c.chainId))) 75 | }, [runId, setSelectedChainIds, chainsForRun]) 76 | 77 | const {width, height} = useWindowDimensions() 78 | 79 | return ( 80 |
81 | 86 | 90 | 97 | 98 |
99 | ) 100 | } 101 | 102 | type RightContentProps = { 103 | chainsForRun: MCMCChain[] 104 | numDrawsForRun: number 105 | chainColors: {[chainId: string]: string} 106 | width: number 107 | height: number 108 | } 109 | 110 | const tabs = [ 111 | {label: 'Trace Plots', closeable: false}, 112 | {label: 'Summary Statistics', closeable: false}, 113 | {label: 'Autocorrelations', closeable: false}, 114 | {label: 'Histograms', closeable: false}, 115 | {label: 'Run Info', closeable: false}, 116 | {label: 'Scatterplots', closeable: false}, 117 | {label: 'Export', closeable: false}, 118 | {label: 'Connection', closeable: false} 119 | ] 120 | 121 | const RightContent: FunctionComponent = ({width, height, numDrawsForRun, chainColors}) => { 122 | const {selectedRunId: runId} = useMCMCMonitor() 123 | if (!runId) return
No run ID
124 | return ( 125 | 130 | 137 | 141 | 148 | 155 | 160 | 167 | 171 | 175 | 176 | ) 177 | } 178 | 179 | export default RunPage 180 | -------------------------------------------------------------------------------- /src/pages/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | /* place-items: center; */ 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/spaInterface/getSpaChainsForRun.ts: -------------------------------------------------------------------------------- 1 | import { MCMCChain } from "../../service/src/types"; 2 | import { spaOutputsForRunIds, updateSpaOutputForRun } from "./spaOutputsForRunIds"; 3 | 4 | const getSpaChainsForRun = async (runId: string): Promise => { 5 | await updateSpaOutputForRun(runId) 6 | const cachedEntry = spaOutputsForRunIds[runId] 7 | if (!cachedEntry) { 8 | console.warn('Unable to load data for run', runId) 9 | return [] 10 | } 11 | const spaOutput = cachedEntry.spaOutput 12 | const ret: MCMCChain[] = [] 13 | for (const ch of spaOutput.chains) { 14 | ret.push({ 15 | runId, 16 | chainId: ch.chainId, 17 | variableNames: Object.keys(ch.sequences), 18 | rawHeader: ch.rawHeader, 19 | rawFooter: ch.rawFooter, 20 | lastChangeTimestamp: Date.now(), 21 | excludedInitialIterationCount: ch.numWarmupDraws ?? 0, 22 | }) 23 | } 24 | return ret 25 | } 26 | 27 | export default getSpaChainsForRun -------------------------------------------------------------------------------- /src/spaInterface/getSpaSequenceUpdates.ts: -------------------------------------------------------------------------------- 1 | import { MCMCSequence, MCMCSequenceUpdate } from "../../service/src/types"; 2 | import { spaOutputsForRunIds, updateSpaOutputForRun } from "./spaOutputsForRunIds"; 3 | 4 | const getSpaSequenceUpdates = async (runId: string, sequences: MCMCSequence[]): Promise => { 5 | await updateSpaOutputForRun(runId) 6 | const cachedEntry = spaOutputsForRunIds[runId] 7 | if (!cachedEntry) { 8 | console.warn('Unable to load data for run', runId) 9 | return [] 10 | } 11 | const spaOutput = cachedEntry.spaOutput 12 | 13 | const ret: MCMCSequenceUpdate[] = [] 14 | for (const seq of sequences) { 15 | const data = spaOutput.chains.find(c => c.chainId === seq.chainId)?.sequences[seq.variableName] ?? [] 16 | ret.push({ 17 | runId, 18 | chainId: seq.chainId, 19 | variableName: seq.variableName, 20 | position: seq.data.length, 21 | data: data.slice(seq.data.length) 22 | }) 23 | } 24 | return ret 25 | } 26 | 27 | export default getSpaSequenceUpdates -------------------------------------------------------------------------------- /src/spaInterface/postStanPlaygroundRequest.ts: -------------------------------------------------------------------------------- 1 | import { stanPlaygroundUrl } from "../config" 2 | 3 | const postStanPlaygroundRequest = async (req: any): Promise => { 4 | const url = stanPlaygroundUrl 5 | 6 | const rr = { 7 | payload: req 8 | } 9 | const resp = await fetch(url, { 10 | method: 'POST', 11 | headers: { 12 | 'Content-Type': 'application/json', 13 | }, 14 | body: JSON.stringify(rr), 15 | }) 16 | const responseData = await resp.json() 17 | return responseData 18 | } 19 | 20 | export default postStanPlaygroundRequest -------------------------------------------------------------------------------- /src/spaInterface/spaOutputsForRunIds.ts: -------------------------------------------------------------------------------- 1 | import postStanPlaygroundRequest from "./postStanPlaygroundRequest"; 2 | import { parseSpaRunId } from "./util"; 3 | 4 | export type SpaOutput = { 5 | chains: { 6 | chainId: string, 7 | rawHeader: string, 8 | rawFooter: string, 9 | numWarmupDraws?: number, 10 | sequences: { 11 | [key: string]: number[] 12 | } 13 | }[] 14 | } 15 | 16 | export const spaOutputsForRunIds: {[key: string]: { 17 | sha1: string, 18 | spaOutput: SpaOutput 19 | }} = {} 20 | 21 | export const updateSpaOutputForRun = async (runId: string) => { 22 | const {projectId, fileName} = parseSpaRunId(runId) 23 | 24 | // first we need to get the sha1 of the latest file 25 | const req = { 26 | type: 'getProjectFile', 27 | timestamp: Date.now() / 1000, 28 | projectId, 29 | fileName 30 | } 31 | const resp = await postStanPlaygroundRequest(req) 32 | if (resp.type !== 'getProjectFile') { 33 | console.warn(resp) 34 | throw Error('Unexpected response from Stan Playground') 35 | } 36 | const sha1 = resp.projectFile.contentSha1 37 | 38 | const cachedEntry = spaOutputsForRunIds[runId] 39 | if ((cachedEntry && cachedEntry.sha1 === sha1)) { 40 | // we already have the latest version 41 | return 42 | } 43 | 44 | const req2 = { 45 | type: 'getDataBlob', 46 | timestamp: Date.now() / 1000, 47 | workspaceId: resp.projectFile.workspaceId, 48 | projectId, 49 | sha1 50 | } 51 | const resp2 = await postStanPlaygroundRequest(req2) 52 | if (resp2.type !== 'getDataBlob') { 53 | console.warn(resp2) 54 | throw Error('Unexpected response from Stan Playground') 55 | } 56 | const x = JSON.parse(resp2.content) 57 | const spaOutput = x as SpaOutput 58 | spaOutputsForRunIds[runId] = { 59 | sha1, 60 | spaOutput 61 | } 62 | } -------------------------------------------------------------------------------- /src/spaInterface/util.ts: -------------------------------------------------------------------------------- 1 | export const isSpaRunId = (runId: string): boolean => { 2 | return runId.startsWith('spa|') 3 | } 4 | 5 | export const constructSpaRunId = (projectId: string, fileName: string): string => { 6 | return `spa|${projectId}|${fileName}` 7 | } 8 | 9 | export const parseSpaRunId = (runId: string): {projectId: string, fileName: string} => { 10 | const a = runId.split('|') 11 | if (a.length !== 3) throw Error(`Invalid SPA runId: ${runId}`) 12 | if (a[0] !== 'spa') throw Error(`Invalid SPA runId: ${runId}`) 13 | const projectId = a[1] 14 | const fileName = a[2] 15 | return { 16 | projectId, 17 | fileName 18 | } 19 | } -------------------------------------------------------------------------------- /src/tabs/AutoCorrelationTab.tsx: -------------------------------------------------------------------------------- 1 | import { Grid } from "@mui/material"; 2 | import { Fragment, FunctionComponent, useMemo, useReducer, useState } from "react"; 3 | import { useMCMCMonitor } from "../MCMCMonitorDataManager/useMCMCMonitor"; 4 | import AutocorrelationPlot from "../components/AutocorrelationPlot"; 5 | import CollapsibleElement from "../components/CollapsibleElement"; 6 | import CollapsibleTabFrame from "./CollapsibleTabFrame"; 7 | import { CollapsibleContentTabProps, PlotSize, collapsedVariablesReducer, scaleForPlotSize, useSequenceDrawRange } from "./TabsUtility"; 8 | 9 | type AcfProps = CollapsibleContentTabProps & { 10 | selectedVariableName: string 11 | selectedChainIds: string[] 12 | samplesRange: [number, number] 13 | sizeScale: number 14 | } 15 | 16 | const AcfPlot: FunctionComponent = (props) => { 17 | const { runId, selectedVariableName, selectedChainIds, samplesRange, sizeScale, width } = props 18 | 19 | return ( 20 | 21 | { 22 | selectedChainIds.map(chainId => ( 23 | 24 | 32 | 33 | )) 34 | } 35 | 36 | ) 37 | } 38 | 39 | const AutoCorrelationTab: FunctionComponent = (props) => { 40 | const { numDrawsForRun, width, height } = props 41 | const { selectedVariableNames, selectedChainIds, effectiveInitialDrawsToExclude } = useMCMCMonitor() 42 | const [collapsedVariables, collapsedVariablesDispatch] = useReducer(collapsedVariablesReducer, {}) 43 | const samplesRange = useSequenceDrawRange(numDrawsForRun, effectiveInitialDrawsToExclude) 44 | const [plotSize, setPlotSize] = useState('medium') 45 | const sizeScale = useMemo(() => scaleForPlotSize(plotSize), [plotSize]) 46 | 47 | const plots = selectedVariableNames.map(v => { 48 | return ( 49 | 55 | 62 | 63 | ) 64 | }) 65 | 66 | return ( 67 | 73 | {plots} 74 | 75 | ) 76 | } 77 | 78 | export default AutoCorrelationTab 79 | -------------------------------------------------------------------------------- /src/tabs/CollapsibleTabFrame.tsx: -------------------------------------------------------------------------------- 1 | import { Grid } from "@mui/material"; 2 | import { Dispatch, FunctionComponent, PropsWithChildren, SetStateAction, useState } from "react"; 3 | import { ExcludeWarmups, ExcludeWarmupsSelector, PlotSize, PlotSizeSelector, initialWarmupInclusionSelection } from "./TabsUtility"; 4 | 5 | type CollapsibleTabFrameProps = { 6 | width: number, 7 | height: number, 8 | plotSize: PlotSize, 9 | setPlotSize: Dispatch> 10 | } 11 | 12 | const CollapsibleTabFrame: FunctionComponent> = (props) => { 13 | const {width, height, plotSize, setPlotSize, children} = props 14 | // The exclude-warmups state will probably also move up in a future iteration 15 | const [excludeWarmups, setExcludeWarmups] = useState(initialWarmupInclusionSelection) 16 | 17 | return ( 18 |
19 | 20 | 21 | 22 | 23 | {children} 24 |
 
25 |
26 | ) 27 | } 28 | 29 | export default CollapsibleTabFrame 30 | -------------------------------------------------------------------------------- /src/tabs/ConnectionTab.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import ConnectionStatusWidget from "../components/ConnectionStatusWidget"; 3 | 4 | type Props = any 5 | 6 | const ConnectionTab: FunctionComponent = () => { 7 | return ( 8 | 9 | ) 10 | } 11 | 12 | export default ConnectionTab 13 | -------------------------------------------------------------------------------- /src/tabs/ExportTab.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, useMemo } from "react"; 2 | import { useMCMCMonitor } from "../MCMCMonitorDataManager/useMCMCMonitor"; 3 | 4 | type Props = any 5 | 6 | const ExportTab: FunctionComponent = () => { 7 | const {selectedRunId: runId, selectedChainIds, selectedVariableNames, sequences} = useMCMCMonitor() 8 | const csvText = useMemo(() => { 9 | const lines: string[] = [] 10 | lines.push(['chain_', 'draw_', ...selectedVariableNames].join(',')) 11 | for (const chainId of selectedChainIds) { 12 | const sss: number[][] = [] 13 | let n = 0 14 | for (const variableName of selectedVariableNames) { 15 | const s = sequences.filter(s => (s.runId === runId && s.chainId === chainId && s.variableName === variableName))[0] 16 | if (s) { 17 | n = Math.max(n, s.data.length) 18 | sss.push(s.data) 19 | } 20 | else { 21 | sss.push([]) 22 | } 23 | } 24 | for (let i = 0; i < n; i++) { 25 | const vals: string[] = [] 26 | for (let j = 0; j < selectedVariableNames.length; j++) { 27 | const aa = sss[j][i] 28 | vals.push(aa === undefined ? '' : `${aa}`) 29 | } 30 | lines.push([chainId, `${i + 1}`, ...vals].join(',')) 31 | } 32 | } 33 | return lines.join('\n') 34 | }, [selectedVariableNames, sequences, selectedChainIds, runId]) 35 | return ( 36 |
37 |