Skip to content

Commit ef52102

Browse files
maglandjsoules
andauthored
support loading of stan playground runs (#85)
* support loading of stan playground runs * handle s=spa, empty service URL * address js review of #85 * include spaMode in tests config * set test env to jsdom in vite.config.ts * Revert to node.js default environment. Mock import to avoid error. --------- Co-authored-by: Jeff Soules <jsoules@flatironinstitute.org>
1 parent 0f79b0b commit ef52102

File tree

15 files changed

+266
-41
lines changed

15 files changed

+266
-41
lines changed
Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,42 @@
1-
import { GetSequencesRequest, isGetSequencesResponse } from "../../service/src/types";
1+
import { GetSequencesRequest, isGetSequencesResponse, MCMCSequenceUpdate } from "../../service/src/types";
22
import postApiRequest from "../networking/postApiRequest";
3+
import getSpaSequenceUpdates from "../spaInterface/getSpaSequenceUpdates";
4+
import { isSpaRunId } from "../spaInterface/util";
35
import { MCMCMonitorAction, MCMCMonitorData } from "./MCMCMonitorDataTypes";
46

5-
67
export default async function updateSequences(data: MCMCMonitorData, dispatch: (a: MCMCMonitorAction) => void) {
78
const X = data.sequences.filter(s => (s.updateRequested))
89
if (X.length > 0) {
9-
const req: GetSequencesRequest = {
10-
type: 'getSequencesRequest',
11-
sequences: X.map(s => ({
12-
runId: s.runId, chainId: s.chainId, variableName: s.variableName, position: s.data.length
13-
}))
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
1434
}
15-
const resp = await postApiRequest(req)
16-
if (!isGetSequencesResponse(resp)) {
17-
console.warn(resp)
18-
throw Error('Unexpected getSequences response')
35+
if (sequenceUpdates) {
36+
dispatch({
37+
type: "updateSequenceData",
38+
sequences: sequenceUpdates
39+
})
1940
}
20-
dispatch({
21-
type: "updateSequenceData",
22-
sequences: resp.sequences
23-
})
2441
}
2542
}

src/MCMCMonitorDataManager/useMCMCMonitor.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { useCallback, useContext, useMemo } from 'react'
22
import { GetChainsForRunRequest, GetRunsRequest, MCMCChain, MCMCRun, isGetChainsForRunResponse, isGetRunsResponse } from '../../service/src/types'
3+
import { serviceBaseUrl, spaMode } from '../config'
34
import postApiRequest from '../networking/postApiRequest'
5+
import getSpaChainsForRun from '../spaInterface/getSpaChainsForRun'
6+
import { isSpaRunId } from '../spaInterface/util'
47
import { MCMCMonitorContext, detectedWarmupIterationCount } from './MCMCMonitorData'
58
import { GeneralOpts } from './MCMCMonitorDataTypes'
69
import updateChains from './updateChains'
@@ -32,6 +35,10 @@ export const useMCMCMonitor = () => {
3235

3336
const updateRuns = useCallback(() => {
3437
; (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+
}
3542
const req: GetRunsRequest = {
3643
type: 'getRunsRequest'
3744
}
@@ -46,16 +53,25 @@ export const useMCMCMonitor = () => {
4653

4754
const updateChainsForRun = useCallback((runId: string) => {
4855
; (async () => {
49-
const req: GetChainsForRunRequest = {
50-
type: 'getChainsForRunRequest',
51-
runId
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)
5260
}
53-
const resp = await postApiRequest(req)
54-
if (!isGetChainsForRunResponse(resp)) {
55-
console.warn(JSON.stringify(resp))
56-
throw Error('Unexpected getChainsForRun response')
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
5773
}
58-
setChainsForRun(runId, resp.chains)
74+
setChainsForRun(runId, chains)
5975
})()
6076
}, [setChainsForRun])
6177

src/config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@ export const defaultServiceBaseUrl = 'http://localhost:61542'
88

99
export const exampleServiceBaseUrl = 'https://lit-bayou-76056.herokuapp.com'
1010

11+
export const spaMode = queryParams.s === 'spa'
12+
1113
export const serviceBaseUrl = queryParams.s ? (
12-
queryParams.s
14+
spaMode ? '' : queryParams.s // if we are in spa mode, don't use a serviceBaseUrl
1315
) : (
1416
defaultServiceBaseUrl
1517
)
1618

19+
export const stanPlaygroundUrl = "https://stan-playground.vercel.app/api/playground"
20+
1721
export const useWebrtc = queryParams.webrtc === '1'
1822

1923
export let webrtcConnectionToService: WebrtcConnectionToService | undefined

src/networking/postApiRequest.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { MCMCMonitorRequest, MCMCMonitorResponse, isMCMCMonitorResponse } from "../../service/src/types"
2-
import { serviceBaseUrl, useWebrtc, webrtcConnectionToService } from "../config"
2+
import { serviceBaseUrl, spaMode, useWebrtc, webrtcConnectionToService } from "../config"
33

44
const postApiRequest = async (request: MCMCMonitorRequest): Promise<MCMCMonitorResponse> => {
5+
if (spaMode) throw Error('Unexpected: cannot postApiRequest in spa mode')
6+
if (!serviceBaseUrl) throw Error('Unexpected in postApiRequest: serviceBaseUrl not set')
57
// Note: we always use http for probe requests and webrtc signaling requests
68
if ((useWebrtc) && (request.type !== 'probeRequest') && (request.type !== 'webrtcSignalingRequest')) {
79
if (webrtcConnectionToService && webrtcConnectionToService.status === 'connected') {

src/pages/MainWindow.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import useRoute from "../util/useRoute";
88
import Home from "./Home";
99
import Logo from "./Logo";
1010
import RunPage from "./RunPage";
11+
import { constructSpaRunId } from "../spaInterface/util";
1112

1213

1314
type Props = {
@@ -22,17 +23,19 @@ const MainWindow: FunctionComponent<Props> = (props: Props) => {
2223
updateRuns()
2324
}, [updateRuns])
2425

25-
if (connectedToService === undefined) {
26-
return <ConnectionInProgress />
27-
}
26+
if (serviceBaseUrl) {
27+
if (connectedToService === undefined) {
28+
return <ConnectionInProgress />
29+
}
2830

29-
if (connectedToService === false) {
30-
return (
31-
<LogoFrame>
32-
<FailedConnection serviceProtocolVersion={serviceProtocolVersion} />
33-
</LogoFrame>
34-
)
35-
}
31+
if (connectedToService === false) {
32+
return (
33+
<LogoFrame>
34+
<FailedConnection serviceProtocolVersion={serviceProtocolVersion} />
35+
</LogoFrame>
36+
)
37+
}
38+
}
3639

3740
switch (route.page) {
3841
case "home":
@@ -45,6 +48,8 @@ const MainWindow: FunctionComponent<Props> = (props: Props) => {
4548
case "run":
4649
return <RunPage runId={route.runId} dataManager={dataManager} />
4750
break
51+
case "spa":
52+
return <RunPage runId={constructSpaRunId(route.projectId, route.fileName)} dataManager={dataManager} />
4853
default:
4954
return <span />
5055
}

src/pages/RunPage.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FunctionComponent, useEffect, useMemo } from "react";
2-
import { MCMCChain, MCMCRun } from "../../service/src/types";
2+
import { MCMCChain } from "../../service/src/types";
33
import MCMCDataManager from "../MCMCMonitorDataManager/MCMCMonitorDataManager";
44
import { useMCMCMonitor } from "../MCMCMonitorDataManager/useMCMCMonitor";
55
import RunControlPanel from "../components/RunControlPanel";
@@ -14,7 +14,7 @@ type Props = {
1414
}
1515

1616
const RunPage: FunctionComponent<Props> = ({runId, dataManager}) => {
17-
const {runs, chains, sequences, updateChainsForRun, setSelectedChainIds, generalOpts, updateKnownData, setSelectedRunId} = useMCMCMonitor()
17+
const {chains, sequences, updateChainsForRun, setSelectedChainIds, generalOpts, updateKnownData, setSelectedRunId} = useMCMCMonitor()
1818

1919
useEffect(() => {
2020
if (dataManager === undefined) return
@@ -53,8 +53,6 @@ const RunPage: FunctionComponent<Props> = ({runId, dataManager}) => {
5353
return Math.max(...a)
5454
}, [sequences, runId])
5555

56-
const run: MCMCRun | undefined = useMemo(() => (runs.filter(r => (r.runId === runId))[0]), [runs, runId])
57-
5856
const chainsForRun = useMemo(() => {
5957
return (chains.filter(c => (c.runId === runId))
6058
.sort((a, b) => a.chainId.localeCompare(b.chainId)))
@@ -78,7 +76,6 @@ const RunPage: FunctionComponent<Props> = ({runId, dataManager}) => {
7876

7977
const {width, height} = useWindowDimensions()
8078

81-
if (!run) return <span>Run not found: {runId}</span>
8279
return (
8380
<div style={{position: 'absolute', width: width - 40, height: height - 40, margin: 20, overflow: 'hidden'}}>
8481
<Splitter
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { MCMCChain } from "../../service/src/types";
2+
import { spaOutputsForRunIds, updateSpaOutputForRun } from "./spaOutputsForRunIds";
3+
4+
const getSpaChainsForRun = async (runId: string): Promise<MCMCChain[]> => {
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
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { MCMCSequence, MCMCSequenceUpdate } from "../../service/src/types";
2+
import { spaOutputsForRunIds, updateSpaOutputForRun } from "./spaOutputsForRunIds";
3+
4+
const getSpaSequenceUpdates = async (runId: string, sequences: MCMCSequence[]): Promise<MCMCSequenceUpdate[] | undefined> => {
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
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { stanPlaygroundUrl } from "../config"
2+
3+
const postStanPlaygroundRequest = async (req: any): Promise<any> => {
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
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
}

0 commit comments

Comments
 (0)