diff --git a/.gitmodules b/.gitmodules index cbb756961..1ec16cc05 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,9 @@ [submodule "public/libs/converter"] path = src/libs/converter - url = https://github.com/yaptide/converter + url = ../converter branch = master shallow = true +[submodule "src/libs/geant4_web/geant-web-stubs"] + path = src/libs/geant4_web/geant-web-stubs + url = ../geant-web-stubs diff --git a/config-overrides.js b/config-overrides.js index de5808237..795e4e7b4 100644 --- a/config-overrides.js +++ b/config-overrides.js @@ -1,3 +1,5 @@ +const webpack = require('webpack'); + module.exports = function override(webpackConfig) { // react-dnd webpackConfig.module.rules.unshift({ @@ -7,6 +9,12 @@ module.exports = function override(webpackConfig) { } }); + webpackConfig.plugins.push( + new webpack.IgnorePlugin({ + resourceRegExp: /geant4_wasm\.wasm$/ + }) + ); + // react-dnd webpackConfig.resolve.alias = { ...webpackConfig.resolve.alias, diff --git a/src/WrapperApp/WrapperApp.tsx b/src/WrapperApp/WrapperApp.tsx index 3bcaf38e2..b5c773f1f 100644 --- a/src/WrapperApp/WrapperApp.tsx +++ b/src/WrapperApp/WrapperApp.tsx @@ -3,6 +3,7 @@ import { styled } from '@mui/material/styles'; import { SyntheticEvent, useEffect, useState } from 'react'; import { useConfig } from '../config/ConfigService'; +import { useDatasetDownloadManager } from '../libs/geant4_web/DatasetDownloadManager'; import { useAuth } from '../services/AuthService'; import { FullSimulationData } from '../services/ShSimulatorService'; import { useStore } from '../services/StoreService'; @@ -50,6 +51,12 @@ function WrapperApp() { const [providedInputFiles, setProvidedInputFiles] = useState(); const [highlightRunForm, setHighLightRunForm] = useState(false); + const { + managerState: geant4DownloadManagerState, + datasetStates: geant4DatasetStates, + startDownload: geant4DatasetDownload + } = useDatasetDownloadManager(); + useEffect(() => { if (Object.keys(providedInputFiles ?? {}).length > 0) { setHighLightRunForm(true); @@ -206,6 +213,9 @@ function WrapperApp() { highlight={highlightRunForm} clearInputFiles={() => setProvidedInputFiles(undefined)} runSimulation={runSimulation} + geant4DownloadManagerState={geant4DownloadManagerState} + geant4DatasetDownloadStart={geant4DatasetDownload} + geant4DatasetStates={geant4DatasetStates} /> {/* end Simulations screen */} diff --git a/src/WrapperApp/components/Simulation/Geant4Datasets.tsx b/src/WrapperApp/components/Simulation/Geant4Datasets.tsx new file mode 100644 index 000000000..e2dbb4ac9 --- /dev/null +++ b/src/WrapperApp/components/Simulation/Geant4Datasets.tsx @@ -0,0 +1,103 @@ +import CheckIcon from '@mui/icons-material/Check'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { + AccordionDetails, + AccordionSummary, + Box, + Button, + LinearProgress, + Typography, + useTheme +} from '@mui/material'; +import { useState } from 'react'; + +import { + DatasetDownloadStatus, + DatasetStatus, + DownloadManagerStatus +} from '../../../libs/geant4_web/DatasetDownloadManager'; +import StyledAccordion from '../../../shared/components/StyledAccordion'; + +export interface Geant4DatasetsProps { + geant4DownloadManagerState: DownloadManagerStatus; + geant4DatasetStates: DatasetStatus[]; + geant4DatasetDownloadStart: () => void; +} + +function DatasetCurrentStatus(props: { status: DatasetStatus }) { + const { status } = props; + + return ( + + + {status.name} + {status.status === DatasetDownloadStatus.DONE && ( + + )} + + {(status.status === DatasetDownloadStatus.DOWNLOADING || + status.status === DatasetDownloadStatus.PROCESSING) && ( + + + + {Math.round((status.done! / status.total!) * 10000) / 100}% + + + )} + {status.status === DatasetDownloadStatus.IDLE && } + + ); +} + +export function Geant4Datasets(props: Geant4DatasetsProps) { + const theme = useTheme(); + const { geant4DownloadManagerState, geant4DatasetStates, geant4DatasetDownloadStart } = props; + const [open, setOpen] = useState(true); + + return ( + + } + onClick={() => setOpen(!open)}> + + Datasets download + + + + {geant4DownloadManagerState === DownloadManagerStatus.IDLE && ( + + )} + {geant4DatasetStates.map(status => ( + + ))} + {geant4DownloadManagerState === DownloadManagerStatus.ERROR && ( + Something went wrong + )} + + + ); +} diff --git a/src/WrapperApp/components/Simulation/RunSimulationPanel.tsx b/src/WrapperApp/components/Simulation/RunSimulationPanel.tsx index 784638971..d4140b4f2 100644 --- a/src/WrapperApp/components/Simulation/RunSimulationPanel.tsx +++ b/src/WrapperApp/components/Simulation/RunSimulationPanel.tsx @@ -3,14 +3,21 @@ import CloudOff from '@mui/icons-material/CloudOff'; import { AccordionDetails, AccordionSummary, Link, Typography, useTheme } from '@mui/material'; import { useConfig } from '../../../config/ConfigService'; +import { + DatasetStatus, + DownloadManagerStatus +} from '../../../libs/geant4_web/DatasetDownloadManager'; import { useAuth } from '../../../services/AuthService'; import { useStore } from '../../../services/StoreService'; import StyledAccordion from '../../../shared/components/StyledAccordion'; import { SimulatorNames, SimulatorType } from '../../../types/RequestTypes'; +import { Geant4Datasets, Geant4DatasetsProps } from './Geant4Datasets'; import RecentSimulations from './RecentSimulations'; import { RunSimulationForm, RunSimulationFormProps } from './RunSimulationForm'; -export default function RunSimulationPanel(props: RunSimulationFormProps) { +export type RunSimulationPanelProps = RunSimulationFormProps & Geant4DatasetsProps; + +export default function RunSimulationPanel(props: RunSimulationPanelProps) { const theme = useTheme(); const { demoMode } = useConfig(); const { yaptideEditor } = useStore(); @@ -23,6 +30,9 @@ export default function RunSimulationPanel(props: RunSimulationFormProps) { return showRunForm ? ( <> + {yaptideEditor?.contextManager.currentSimulator === SimulatorType.GEANT4 && ( + + )} ) : ( diff --git a/src/libs/geant4_web/DatasetDownloadManager.ts b/src/libs/geant4_web/DatasetDownloadManager.ts new file mode 100644 index 000000000..9af872136 --- /dev/null +++ b/src/libs/geant4_web/DatasetDownloadManager.ts @@ -0,0 +1,124 @@ +import { useCallback, useEffect, useState } from 'react'; + +export enum DownloadManagerStatus { + IDLE, + WORKING, + FINISHED, + ERROR, +} + +export enum DatasetDownloadStatus { + IDLE, + DOWNLOADING, + PROCESSING, + DONE, +} + +export interface DatasetStatus { + name: string, + status: DatasetDownloadStatus, + done?: number, + total?: number, +} + +const idleRegex = /IDLE \((\w+)\)/g; +const downloadEndRegex = /END DL \((\w+)\)/g; +const downloadRegex = /DL \((\w+)\) \((\d+)\/(\d+)\)/g; +const processingRegex = /PROCESS \((\d+)\/(\d+)\)/g; + +export function useDatasetDownloadManager() { + const [managerState, setManagerState] = useState(DownloadManagerStatus.IDLE); + const [datasetStates, setDatasetStates] = useState>({}); + const [processingState, setProcessingState] = useState(undefined); + const [idle, setIdle] = useState(true); + const [worker, setWorker] = useState(); + + const startDownload = useCallback( + idle + ? () => { + worker?.postMessage({ type: 'loadDepsData' }); + setProcessingState({ name: 'Processing downloaded files', status: DatasetDownloadStatus.PROCESSING, done: 0, total: 1 }); + setManagerState(DownloadManagerStatus.WORKING); + setIdle(false); + } + : () => {}, + [worker, idle], + ); + + useEffect(() => { + setWorker(new Worker(new URL('./geantWorker.worker.ts', import.meta.url))); + }, []); + + useEffect(() => { + let done = '', total = '', dataset = ''; + const handler = (event: MessageEvent) => { + switch (event.data.type) { + case 'status': + switch (true) { + case event.data.data?.startsWith('IDLE'): + [, dataset] = Array.from(event.data.data.matchAll(idleRegex))[0] as string[]; + setDatasetStates(states => ({ + ...states, + [dataset]: { + name: dataset, + status: states[dataset] ? states[dataset].status : DatasetDownloadStatus.IDLE, + } + })); + break; + case event.data.data?.startsWith('DL'): + [, dataset, done, total] = Array.from(event.data.data.matchAll(downloadRegex))[0] as string[]; + setDatasetStates(states => ({ + ...states, + [dataset]: { + name: dataset, + status: DatasetDownloadStatus.DOWNLOADING, + done: parseInt(done), + total: parseInt(total) + } + })); + break; + case event.data.data?.startsWith('END DL'): + [, dataset] = Array.from(event.data.data.matchAll(downloadEndRegex))[0] as string[]; + console.log('end dl', dataset); + setDatasetStates(states => ({ + ...states, + [dataset]: { + name: dataset, + status: DatasetDownloadStatus.DONE, + done: parseInt(done), + total: parseInt(total) + } + })); + break; + case event.data.data?.startsWith('PROCESS'): + [, done, total] = Array.from(event.data.data.matchAll(processingRegex))[0] as string[]; + setProcessingState({ + name: 'Processing downloaded files', + status: DatasetDownloadStatus.PROCESSING, + done: parseInt(done), + total: parseInt(total) + }) + break; + case event.data.data?.startsWith('INIT END'): + setManagerState(DownloadManagerStatus.FINISHED); + setDatasetStates(states => Object.fromEntries(Object.entries(states).map(([k, v], _) => [k, { ...v, status: DatasetDownloadStatus.DONE }]))); + setProcessingState({ name: 'Processing downloaded files', status: DatasetDownloadStatus.DONE }); + break; + } + break; + case 'error': + setManagerState(DownloadManagerStatus.ERROR); + break; + } + }; + worker?.addEventListener('message', handler); + return () => worker?.removeEventListener('message', handler); + }, [worker]); + + let allStates = Object.values(datasetStates); + if (processingState) { + allStates.push(processingState); + } + + return { managerState, datasetStates: allStates, startDownload }; +} \ No newline at end of file diff --git a/src/libs/geant4_web/GeantWorkerInterface.ts b/src/libs/geant4_web/GeantWorkerInterface.ts new file mode 100644 index 000000000..f6b15daf4 --- /dev/null +++ b/src/libs/geant4_web/GeantWorkerInterface.ts @@ -0,0 +1,19 @@ + +export enum GeantWorkerMessageType { + INIT_DATA_FILES, + INIT_LAZY_FILES, + CREATE_FILE, + READ_FILE, + RUN_SIMULATION, + FILE_RESPONSE +} + +export type GeantWorkerMessageFile = { + name: string, + data: string +} + +export type GeantWorkerMessage = { + type: GeantWorkerMessageType, + data: GeantWorkerMessageFile | string +} \ No newline at end of file diff --git a/src/libs/geant4_web/geant-web-stubs b/src/libs/geant4_web/geant-web-stubs new file mode 160000 index 000000000..d7b6a4f97 --- /dev/null +++ b/src/libs/geant4_web/geant-web-stubs @@ -0,0 +1 @@ +Subproject commit d7b6a4f97fb445a7626188cf2c94595bff6ee481 diff --git a/src/libs/geant4_web/geantWorker.worker.ts b/src/libs/geant4_web/geantWorker.worker.ts new file mode 100644 index 000000000..73d4565f7 --- /dev/null +++ b/src/libs/geant4_web/geantWorker.worker.ts @@ -0,0 +1,324 @@ +import createMainModule from './geant-web-stubs/geant4_wasm' + +import { default as initG4EMLOW } from './geant-web-stubs/preload/preload_G4EMLOW8.6.1'; +import { default as initG4ENSDFSTATE } from './geant-web-stubs/preload/preload_G4ENSDFSTATE3.0'; +import { default as initG4NDL } from './geant-web-stubs/preload/preload_G4NDL4.7.1'; +import { default as initG4PARTICLEXS } from './geant-web-stubs/preload/preload_G4PARTICLEXS4.1'; +import { default as initG4SAIDDATA } from './geant-web-stubs/preload/preload_G4SAIDDATA2.0'; +import { default as initPhotoEvaporation } from './geant-web-stubs/preload/preload_PhotonEvaporation6.1'; + +import { + GeantWorkerMessage, + GeantWorkerMessageType, + GeantWorkerMessageFile +} from './GeantWorkerInterface'; + +import { TextDecoder } from 'util'; + +const s3_prefix_map: Record = { + ".wasm": "https://s3p.cloud.cyfronet.pl/geant4-wasm/", + ".data": "https://s3p.cloud.cyfronet.pl/geant4-wasm/datafiles/", + ".metadata": "https://s3p.cloud.cyfronet.pl/geant4-wasm/datafiles/", + ".json": "https://s3p.cloud.cyfronet.pl/geant4-wasm/lazy_files_metadata/", +}; + +/* eslint-disable-next-line no-restricted-globals */ +var ctx: Worker = self as any; // TypeScript type assertion to treat self as a Worker + +var preModule = { + preRun: [ ], + postRun: [], + onRuntimeInitialized: function () { + console.log("onRuntimeInitialized"); + postMessage({ type: 'init', data: "onRuntimeInitialized" }); + }, + printErr: (function () { + return function (text: any) { + if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' '); + + if (!text.includes('dependency')) { + console.error(text); + } + }; + })(), + print: (function () { + + return function (text: any) { + if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' '); + + // console.log(text); + postMessage({ type: 'print', data: text }); + + }; + })(), + last: { + time: Date.now(), + text: '' + }, + setStatus: function (text: string) { + if (!preModule.last) preModule.last = { time: Date.now(), text: '' }; + if (text === preModule.last.text) return; + var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/); + var now = Date.now(); + if (m && now - preModule.last.time < 30) return; // if this is a progress update, skip it if too soon + preModule.last.time = now; + preModule.last.text = text; + postMessage({ type: 'status', data: text }); + }, + totalDependencies: 0, + monitorRunDependencies: function (left: any) { + this.totalDependencies = Math.max(this.totalDependencies, left); + preModule.setStatus(left ? 'PROCESS (' + (this.totalDependencies - left) + '/' + this.totalDependencies + ')' : 'All downloads complete.'); + }, + locateFile: function (path: any, prefix: any) { + // if it's a mem init file, use a custom dir + const ext = path.slice(path.lastIndexOf('.')); + if (ext in s3_prefix_map) { + return s3_prefix_map[ext] + path; + } + + // otherwise, use the default, the prefix (JS file's dir) + the path + return prefix + path; + }, +}; + +var mod = createMainModule(preModule); + +ctx.onmessage = async (event: MessageEvent) => { + switch (event.data.type) { + case GeantWorkerMessageType.INIT_DATA_FILES: { + const res = await mod.then(async (module) => { + + console.log("Initializing lazy files..."); + try { + await initG4ENSDFSTATE(module); + await initG4EMLOW(module); + await initG4NDL(module); + await initG4PARTICLEXS(module); + await initG4SAIDDATA(module); + await initPhotoEvaporation(module); + } catch (error: unknown) { + console.error("Error initializing lazy files:", (error as Error).message); + } + postMessage({ type: 'status', data: 'INIT END' }); + }); + break; + } + case GeantWorkerMessageType.INIT_LAZY_FILES: { + const res = await mod.then(async (module) => { + module.FS_createPath('/', 'data', true, true); + module.FS_createPath('/data', 'G4EMLOW8.6.1', true, true); + module.FS_createPath('/data', 'G4ENSDFSTATE3.0', true, true); + module.FS_createPath('/data', 'G4NDL4.7.1', true, true); + module.FS_createPath('/data', 'G4PARTICLEXS4.1', true, true); + module.FS_createPath('/data', 'G4SAIDDATA2.0', true, true); + module.FS_createPath('/data', 'PhotonEvaporation6.1', true, true); + + const jsonFiles = [ + "load_G4EMLOW8.6.1.json", + "load_G4ENSDFSTATE3.0.json", + "load_G4NDL4.7.1.json", + "load_G4PARTICLEXS4.1.json", + "load_G4SAIDDATA2.0.json", + "load_PhotonEvaporation6.1.json" + ]; + + for (const jsonFile of jsonFiles) { + const path = s3_prefix_map[".json"] + jsonFile; + await fetch(path) + .then(response => { + if (!response.ok) { + throw new Error("HTTP error " + response.status); + } + return response.json(); + }) + .then((data: any) => { + for (const file of data) { + if (file.type === 'file') { + module.FS_createLazyFile(file.parent, file.name, file.url, true, true); + } else if (file.type === 'path') { + module.FS_createPath(file.parent, file.name, true, true); + } + } + }); + console.log(`Loaded lazy files from ${jsonFile}`); + } + }); + break; + } + + case GeantWorkerMessageType.CREATE_FILE: + await mod.then((module) => { + const data = event.data.data as GeantWorkerMessageFile; + + module.FS.createFile("/", data.name, null, true, true); + module.FS.writeFile(data.name, data.data); + }); + break; + case GeantWorkerMessageType.READ_FILE: + await mod.then((module) => { + const fileName = event.data.data as string; + + const fileConent = module.FS.readFile(fileName, { encoding: "utf8" }); + + ctx.postMessage({ + type: GeantWorkerMessageType.FILE_RESPONSE, + data: { + name: fileName, + data: new TextDecoder().decode(fileConent) + } as GeantWorkerMessageFile + } as GeantWorkerMessage) + }); + break; + case GeantWorkerMessageType.RUN_SIMULATION: + try { + console.log("Running GDML simulation..."); + const gdmlResult = await mod.then((module) => { + module.FS.createFile("/", "geom.gdml", null, true, true); + module.FS.createFile("/", "init.mac", null, true, true); + + module.FS.writeFile('geom.gdml', +` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +` + ); + + module.FS.writeFile('init.mac', +`/run/initialize + +########################################## +####### Particle Source definition ####### +########################################## + +/gps/verbose 0 +/gps/particle proton +/gps/position 0 0 -10.5 cm + +/gps/pos/type Beam +/gps/direction 0 0 1 + +/gps/ene/type Gauss +/gps/ene/mono 150 MeV +/gps/ene/sigma 1.5 MeV +/gps/ene/max 1000 MeV + +########################################## +################ Scoring ################# +########################################## + +/score/create/cylinderMesh CylZ_Mesh +/score/mesh/cylinderSize 5 10 cm +/score/mesh/nBin 1 400 1 +/score/quantity/energyDeposit eDep +/score/filter/particle protonFilter proton +/score/quantity/cellFlux fluence +/score/filter/particle protonFilter proton +/score/close + +/score/create/boxMesh YZ_Mesh +/score/mesh/boxSize 0.5 4. 10. cm +/score/mesh/nBin 1 80 400 +/score/quantity/energyDeposit eDep +/score/filter/particle protonFilter proton +/score/quantity/cellFlux fluence +/score/filter/particle protonFilter proton +/score/close + +/score/create/probe Pr 2. cm +/score/probe/locate 0. 0. -5. cm +/score/quantity/cellFlux fluxdiff +/score/filter/particle protonFilter proton +/score/close + +/analysis/h1/create fluxdiff Pr_differential 100 0. 200. MeV + +/score/fill1D 0 Pr fluxdiff + +########################################## +################## Run ################### +########################################## + +/run/beamOn 10000 + +########################################## +############ Collect results ############# +########################################## + +/particle/dump +/score/dumpQuantityToFile CylZ_Mesh eDep cylz_edep.txt +/score/dumpQuantityToFile CylZ_Mesh fluence cylz_fluence.txt +/score/dumpQuantityToFile YZ_Mesh eDep yz_edep.txt +/score/dumpQuantityToFile YZ_Mesh fluence yz_fluence.txt +/score/dumpQuantityToFile Pr fluxdiff diff.txt +` + ); + return module.Geant4GDMRun("geom.gdml", "init.mac"); + }); + + console.log("GDML run result:", gdmlResult); + + const result_data = await mod.then((module) => { + return module.FS.readFile("cylz_fluence.txt", { encoding: "utf8" }); + }); + + ctx.postMessage({ + type: "gdmlResult", + result: gdmlResult + }); + } catch (error: unknown) { + ctx.postMessage({ + type: "error", + message: (error as Error).message, + error: error + }); + } + break; + default: + console.warn("Unknown message type:", event.data); + } +} \ No newline at end of file