diff --git a/.env.example b/.env.example index 230425e..d4067a7 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,2 @@ TILED_SINGLE_USER_API_KEY= -REACT_APP_WEBSOCKET_URL=ws://localhost:8001/simImages +REACT_APP_WEBSOCKET_URL=ws://localhost:8021/viz diff --git a/.flake8 b/.flake8 index 13bbc9d..eda9efd 100644 --- a/.flake8 +++ b/.flake8 @@ -4,5 +4,5 @@ exclude = __pycache__, build, dist -max-line-length = 115 +max-line-length = 145 extend-ignore = E203, W503 diff --git a/Dockerfile_frontend b/Dockerfile_frontend index 2a0bc09..173c50f 100644 --- a/Dockerfile_frontend +++ b/Dockerfile_frontend @@ -5,6 +5,8 @@ COPY ./frontend/package*.json /frontend/ RUN npm ci COPY ./frontend /frontend/ COPY .env_frontend /frontend/.env + + RUN npm run build FROM docker.io/nginx:1.26-alpine diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2926a81..aee3ed0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,17 +12,19 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "dayjs": "^1.11.13", + "mermaid": "^11.4.1", "msgpack-lite": "^0.1.26", "plotly.js": "^2.12.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-plotly.js": "^2.5.1", + "react-router": "^7.1.5", "react-scripts": "5.0.1", "react-tooltip": "^5.28.0", "web-vitals": "^2.1.4" }, "devDependencies": { - "tailwindcss": "^3.4.3" + "tailwindcss": "^3.4.17" } }, "node_modules/@adobe/css-tools": { @@ -53,6 +55,26 @@ "node": ">=6.0.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.0.0.tgz", + "integrity": "sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==", + "dependencies": { + "package-manager-detector": "^0.2.8", + "tinyexec": "^0.3.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/utils": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.0.tgz", + "integrity": "sha512-XPR7Jfwp0FFl/dFYPX8ZjpmU4/1mIXTjnZ1ba48BLMyKOV62/tiRjdsFcPs2hsYcSud4tzk7w3a3LjX8Fu3huA==", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@babel/code-frame": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", @@ -2028,6 +2050,45 @@ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", + "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==" + }, "node_modules/@choojs/findup": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@choojs/findup/-/findup-0.2.1.tgz", @@ -2459,6 +2520,37 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==" }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==" + }, + "node_modules/@iconify/utils": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz", + "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==", + "dependencies": { + "@antfu/install-pkg": "^1.0.0", + "@antfu/utils": "^8.1.0", + "@iconify/types": "^2.0.0", + "debug": "^4.4.0", + "globals": "^15.14.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.0.0", + "mlly": "^1.7.4" + } + }, + "node_modules/@iconify/utils/node_modules/globals": { + "version": "15.14.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", + "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3303,6 +3395,14 @@ "node": ">=6.0.0" } }, + "node_modules/@mermaid-js/parser": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.3.0.tgz", + "integrity": "sha512-HsvL6zgE5sUPGgkIDlmAWR1HTNHz2Iy11BAWPTa4Jjabkpguy4Ze2gzfLrg6pdRuBvFwgUYyxiaNqZwrEEXepA==", + "dependencies": { + "langium": "3.0.0" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -4252,6 +4352,233 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/eslint": { "version": "8.56.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", @@ -4297,6 +4624,11 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -5138,9 +5470,9 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "bin": { "acorn": "bin/acorn" }, @@ -6367,6 +6699,30 @@ "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", "integrity": "sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==" }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -6715,6 +7071,11 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==" + }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -6802,6 +7163,14 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -7249,6 +7618,49 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/cytoscape": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.31.0.tgz", + "integrity": "sha512-zDGn1K/tfZwEnoGOcHc0H4XazqAAXAuDpcYw9mUnUjATjqljyCNGJv8uEvbvxGaGHaVshxMecyl6oc6uKzRfbw==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==" + }, "node_modules/d": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", @@ -7261,11 +7673,85 @@ "node": ">=0.12" } }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-array": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-collection": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", @@ -7279,11 +7765,107 @@ "node": ">=12" } }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-dispatch": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==" }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-force": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", @@ -7308,81 +7890,321 @@ "d3-array": "1" } }, - "node_modules/d3-geo-projection": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-2.9.0.tgz", - "integrity": "sha512-ZULvK/zBn87of5rWAfFMc9mJOipeSo57O+BBitsKIXmU4rTVAnX1kSsJkE0R+TxY8pGNoM1nbyRRE7GYHhdOEQ==", + "node_modules/d3-geo-projection": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-2.9.0.tgz", + "integrity": "sha512-ZULvK/zBn87of5rWAfFMc9mJOipeSo57O+BBitsKIXmU4rTVAnX1kSsJkE0R+TxY8pGNoM1nbyRRE7GYHhdOEQ==", + "dependencies": { + "commander": "2", + "d3-array": "1", + "d3-geo": "^1.12.0", + "resolve": "^1.1.10" + }, + "bin": { + "geo2svg": "bin/geo2svg", + "geograticule": "bin/geograticule", + "geoproject": "bin/geoproject", + "geoquantize": "bin/geoquantize", + "geostitch": "bin/geostitch" + } + }, + "node_modules/d3-geo-projection/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==" + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", + "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==" + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale/node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + }, + "node_modules/d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "dependencies": { + "d3-time": "1" + } + }, + "node_modules/d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", "dependencies": { - "commander": "2", - "d3-array": "1", - "d3-geo": "^1.12.0", - "resolve": "^1.1.10" + "d3-array": "2.5.0 - 3" }, - "bin": { - "geo2svg": "bin/geo2svg", - "geograticule": "bin/geograticule", - "geoproject": "bin/geoproject", - "geoquantize": "bin/geoquantize", - "geostitch": "bin/geostitch" + "engines": { + "node": ">=12" } }, - "node_modules/d3-geo-projection/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "node_modules/d3/node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } }, - "node_modules/d3-hierarchy": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", - "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==" + "node_modules/d3/node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } }, - "node_modules/d3-interpolate": { + "node_modules/d3/node_modules/d3-quadtree": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "dependencies": { - "d3-color": "1 - 3" + "d3-path": "^3.1.0" }, "engines": { "node": ">=12" } }, - "node_modules/d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" - }, - "node_modules/d3-quadtree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", - "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==" + "node_modules/d3/node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } }, - "node_modules/d3-shape": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "node_modules/d3/node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", "dependencies": { - "d3-path": "1" + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" } }, - "node_modules/d3-time": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", - "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + "node_modules/d3/node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } }, - "node_modules/d3-time-format": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", - "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "node_modules/dagre-d3-es": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz", + "integrity": "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==", "dependencies": { - "d3-time": "1" + "d3": "^7.9.0", + "lodash-es": "^4.17.21" } }, - "node_modules/d3-timer": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", - "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" - }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -7455,11 +8277,11 @@ "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -7583,6 +8405,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -7785,6 +8615,14 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", + "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -10256,6 +11094,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==" + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -10783,6 +11626,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -13399,9 +14250,9 @@ } }, "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "bin": { "jiti": "bin/jiti.js" } @@ -13570,6 +14421,21 @@ "node": ">=4.0" } }, + "node_modules/katex": { + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, "node_modules/kdbush": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", @@ -13583,6 +14449,11 @@ "json-buffer": "3.0.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -13607,6 +14478,26 @@ "node": ">= 8" } }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==" + }, + "node_modules/langium": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.0.0.tgz", + "integrity": "sha512-+Ez9EoiByeoTu/2BXmEaZ06iPNXM6thWJp02KfBO/raSMyCJ4jw7AkWWa+zBCTm0+Tw1Fj9FOxdqSskyN5nAwg==", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -13632,6 +14523,11 @@ "shell-quote": "^1.8.1" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -13686,6 +14582,21 @@ "node": ">=8.9.0" } }, + "node_modules/local-pkg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.0.0.tgz", + "integrity": "sha512-bbgPw/wmroJsil/GgL4qjDzs5YLTBMQ99weRsok1XCDccQeehbHA/I1oRvk2NPtr7KGZgT/Y5tPRnAtMqeG2Kg==", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.3.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -13702,6 +14613,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -13849,6 +14765,17 @@ "node": ">=6.4.0" } }, + "node_modules/marked": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz", + "integrity": "sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/math-log2": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/math-log2/-/math-log2-1.0.1.tgz", @@ -13899,6 +14826,45 @@ "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.4.1.tgz", + "integrity": "sha512-Mb01JT/x6CKDWaxigwfZYuYmDZ6xtrNwNlidKZwkSrDaY9n90tdrJTV5Umk+wP1fZscGptmKFXHsXMDEVZ+Q6A==", + "dependencies": { + "@braintree/sanitize-url": "^7.0.1", + "@iconify/utils": "^2.1.32", + "@mermaid-js/parser": "^0.3.0", + "@types/d3": "^7.4.3", + "cytoscape": "^3.29.2", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.11", + "dayjs": "^1.11.10", + "dompurify": "^3.2.1", + "katex": "^0.16.9", + "khroma": "^2.1.0", + "lodash-es": "^4.17.21", + "marked": "^13.0.2", + "roughjs": "^4.6.6", + "stylis": "^4.3.1", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.1" + } + }, + "node_modules/mermaid/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -13908,9 +14874,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -14027,6 +14993,17 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mlly": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", + "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", + "dependencies": { + "acorn": "^8.14.0", + "pathe": "^2.0.1", + "pkg-types": "^1.3.0", + "ufo": "^1.5.4" + } + }, "node_modules/mouse-change": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/mouse-change/-/mouse-change-1.4.0.tgz", @@ -14056,9 +15033,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/msgpack-lite": { "version": "0.1.26", @@ -14116,9 +15093,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -14579,6 +15556,11 @@ "node": ">=6" } }, + "node_modules/package-manager-detector": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.9.tgz", + "integrity": "sha512-+vYvA/Y31l8Zk8dwxHhL3JfTuHPm6tlxM2A3GeQyl7ovYnSp1+mzAxClxaOr0qO1TtPxbQxetI7v5XqKLJZk7Q==" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -14661,6 +15643,11 @@ "tslib": "^2.0.3" } }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -14726,6 +15713,11 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", + "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==" + }, "node_modules/pbf": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", @@ -14749,9 +15741,9 @@ "integrity": "sha512-ESj2+eBxhGrcA1azgHs7lARG5+5iLakc/6nlfbpjcLl00HuuUOIuORhYXN4D1HfvMSKuVtFQjAlnwi1JHEeDIw==" }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -14791,6 +15783,16 @@ "node": ">=8" } }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, "node_modules/pkg-up": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", @@ -14905,6 +15907,20 @@ "resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.1.0.tgz", "integrity": "sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==" }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/polybooljs": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/polybooljs/-/polybooljs-1.2.2.tgz", @@ -14919,9 +15935,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", "funding": [ { "type": "opencollective", @@ -14937,9 +15953,9 @@ } ], "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -15621,19 +16637,25 @@ } }, "node_modules/postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "postcss-selector-parser": "^6.0.11" + "postcss-selector-parser": "^6.1.1" }, "engines": { "node": ">=12.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": "^8.2.14" } @@ -16019,9 +17041,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", - "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -16619,6 +17641,37 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.5.tgz", + "integrity": "sha512-8BUF+hZEU4/z/JD201yK6S+UYhsf58bzYIDq2NS1iGpwxSXDu7F+DeGSkIXMFBuHZB21FSiCzEcUb18cQNdRkA==", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "engines": { + "node": ">=18" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -17130,6 +18183,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/rollup": { "version": "2.79.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", @@ -17199,6 +18257,17 @@ "node": ">=8" } }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -17462,11 +18531,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -17559,6 +18623,11 @@ "node": ">= 0.8.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -17690,9 +18759,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } @@ -18207,6 +19276,11 @@ "postcss": "^8.2.15" } }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -18472,32 +19546,32 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, "node_modules/tailwindcss": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", - "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.3.0", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.21.0", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", @@ -18507,6 +19581,17 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -18731,6 +19816,11 @@ "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==" + }, "node_modules/tinyqueue": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", @@ -18837,6 +19927,14 @@ "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "engines": { + "node": ">=6.10" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -18896,6 +19994,11 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==" + }, "node_modules/type": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", @@ -19047,6 +20150,11 @@ "node": ">=4.2.0" } }, + "node_modules/ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==" + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -19265,6 +20373,49 @@ "node": ">= 0.8" } }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" + }, "node_modules/vt-pbf": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index ea23b88..147f26a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,11 +7,13 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "dayjs": "^1.11.13", + "mermaid": "^11.4.1", "msgpack-lite": "^0.1.26", "plotly.js": "^2.12.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-plotly.js": "^2.5.1", + "react-router": "^7.1.5", "react-scripts": "5.0.1", "react-tooltip": "^5.28.0", "web-vitals": "^2.1.4" @@ -41,6 +43,6 @@ ] }, "devDependencies": { - "tailwindcss": "^3.4.3" + "tailwindcss": "^3.4.17" } } diff --git a/frontend/public/images/bnl_logo.png b/frontend/public/images/bnl_logo.png new file mode 100644 index 0000000..4c2b44b Binary files /dev/null and b/frontend/public/images/bnl_logo.png differ diff --git a/frontend/public/images/illumine_logo.png b/frontend/public/images/illumine_logo.png new file mode 100644 index 0000000..1936035 Binary files /dev/null and b/frontend/public/images/illumine_logo.png differ diff --git a/frontend/public/images/mlexchange_logo.png b/frontend/public/images/mlexchange_logo.png new file mode 100644 index 0000000..d3e45e2 Binary files /dev/null and b/frontend/public/images/mlexchange_logo.png differ diff --git a/frontend/public/images/mwet_logo.png b/frontend/public/images/mwet_logo.png new file mode 100644 index 0000000..aebd2d2 Binary files /dev/null and b/frontend/public/images/mwet_logo.png differ diff --git a/frontend/public/images/xrayIcon/gif0.png b/frontend/public/images/xrayIcon/gif0.png new file mode 100644 index 0000000..6aee7a2 Binary files /dev/null and b/frontend/public/images/xrayIcon/gif0.png differ diff --git a/frontend/public/images/xrayIcon/gif1.png b/frontend/public/images/xrayIcon/gif1.png new file mode 100644 index 0000000..9c23aa3 Binary files /dev/null and b/frontend/public/images/xrayIcon/gif1.png differ diff --git a/frontend/public/images/xrayIcon/gif2.png b/frontend/public/images/xrayIcon/gif2.png new file mode 100644 index 0000000..dd40729 Binary files /dev/null and b/frontend/public/images/xrayIcon/gif2.png differ diff --git a/frontend/public/images/xrayIcon/gif3.png b/frontend/public/images/xrayIcon/gif3.png new file mode 100644 index 0000000..f6541f9 Binary files /dev/null and b/frontend/public/images/xrayIcon/gif3.png differ diff --git a/frontend/public/images/xrayIcon/gif4.png b/frontend/public/images/xrayIcon/gif4.png new file mode 100644 index 0000000..6c4be57 Binary files /dev/null and b/frontend/public/images/xrayIcon/gif4.png differ diff --git a/frontend/public/index.html b/frontend/public/index.html index d6d62e5..841602a 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -24,9 +24,12 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - TR-AP-XPS + SMI Data Viewer +
framelisten(Frame Listener); + + subgraph Mars1Group["mars1.nsls2.bnl.gov"]; + framelisten --zmq frame--> viz_op(Viz Operator); + framelisten --zmq frame + tiledurl--> lse_op(LSE Operator); + lse_op --websocket--> lse(Latent Space Explorer dash); + + lse <--http--> browser(Browser); + viz_op --http 1D reductions--> tiled(Tiled); + viz_op --websocket 2 tiled urls + data--> browser; + lse_op <--zmq raw frame--> lse_worker1(LSE Worker 1); + lse_op <--zmq raw frame--> lse_worker2(LSE Worker 2); + lse_op <--zmq raw frame--> lse_worker3(LSE Worker N); + lse_op --http feature vec and url--> tiled; + + browser <--http get data--> tiled; + tiled <----> postgres; + wf_viz(Workflow Viz) <--http--> browser; + wf_viz --> redis(Redis); + redis --> viz_op; + + + end; + ` + + + const attachEventHandler = () => { + const callBackFunction = (event) => { + console.log("Clicked element:", event.currentTarget); + }; + + // Select all node elements + const nodes = document.querySelectorAll("g.node"); + + // Attach the event handler to each node + nodes.forEach(node => { + node.addEventListener("click", callBackFunction); + }); + }; + + const changeColorTest = () => { + const element = getRectByNodeLabel("LSE Operator"); + if (element) changeBackgroundColor(element, "red"); + setSampleText('made it red') + const newElement = getRectByParentId('lse'); + console.log({newElement}) + changeBackgroundColor(newElement, "blue") + }; + + const getRectByParentId = (text) => { + // Select the parent element by matching its ID pattern + const parent = document.querySelector(`[id^="flowchart-${text}-"]`); + if (parent) { + const rect = parent.querySelector("rect"); + if (rect) return rect; + } + + return null; + }; + + const getRectByNodeLabel = (searchText='') => { + //The rectangles will have a sibling that contains a child with the name in parenthesis of the item + //ex) for: framelisten --zmq frame + tiledurl--> lse_op(LSE Operator); + // the search text should be "LSE Operator" + const labelText = searchText; + const labelElement = Array.from(document.querySelectorAll(".nodeLabel")) + .find(el => el.innerText.trim() === labelText); + + // The HTML structure is g->rect, where rect has sibling g->foreignObject->div->span => searchText + if (labelElement) { + const nodeGroup = labelElement.closest("g.node"); + if (nodeGroup) { + const rect = nodeGroup.querySelector("rect"); + if (rect) { + return rect; + } else { + return false; + } + } else { + return false; + } + } else { + return false; + } + }; + + const changeBackgroundColor = (element, color="red") => { + element.style.fill = color; + } + + const getLineByEndpoints = (endpoint1, endpoint2) => { + const examplePathId = "L-redis-viz_op-0" // connects redis to viz_op + // the pattern is 'L- endpoint1 - endpoint2 -0' + const idA = `#L-${endpoint1}-${endpoint2}-0`; + const idB = `#L-${endpoint2}-${endpoint1}-0`; + let path = false; + path = document.querySelector(idA); + if (path) return path; + path = document.querySelector(idB); + if (path) { + return path; + } else { + return false; + } + + } + + + function new_script(src) { + return new Promise(function(resolve, reject){ + if (typeof window !== "undefined") { + var script = window.document.createElement('script'); + script.src = src; + script.addEventListener('load', function () { + resolve(); + }); + script.addEventListener('error', function (e) { + reject(e); + }); + window.document.body.appendChild(script); + } + }) + }; + + useEffect(() => { + if (!window.mermaid) { + var my_script = new_script('https://cdnjs.cloudflare.com/ajax/libs/mermaid/9.3.0/mermaid.min.js'); + my_script.then(() => { + window.mermaid.mermaidAPI.initialize({ + securityLevel: 'loose', + }); + window.mermaid.contentLoaded(); + attachEventHandler(); + }); + } + }, []); + + + + return ( + <> +
+ {graph} +
+
+
+ + + ) +} \ No newline at end of file diff --git a/frontend/src/components/PlotlyHeatMap.jsx b/frontend/src/components/PlotlyHeatMap.jsx index aee8871..324ebaa 100644 --- a/frontend/src/components/PlotlyHeatMap.jsx +++ b/frontend/src/components/PlotlyHeatMap.jsx @@ -5,16 +5,18 @@ const plotlyColorScales = ['Viridis', 'Plasma', 'Inferno', 'Magma', 'Cividis']; export default function PlotlyHeatMap({ array = [], + linecutYPosition=50, + linecutThickness = 2, title = '', xAxisTitle = '', yAxisTitle = '', colorScale = 'Viridis', - verticalScaleFactor = 0.1, // Scale factor for content growth + verticalScaleFactor = 1, // Scale factor for content growth width = 'w-full', - height='h-full', + height = 'h-full', showTicks = false, - tickStep = 100, - fixPlotHeightToParent=false + tickStep = 10, + fixPlotHeightToParent = false }) { const plotContainer = useRef(null); const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); @@ -33,7 +35,8 @@ export default function PlotlyHeatMap({ return () => resizeObserver.disconnect(); }, []); - const data = [ + // Create the heatmap data + var data = [ { z: array, type: 'heatmap', @@ -44,8 +47,14 @@ export default function PlotlyHeatMap({ }, ]; - // Calculate the height based on the number of rows in the array - const dynamicHeight = Math.max(array.length * verticalScaleFactor, 0); // Minimum height is 200px + // Calculate the y position for the horizontal line + let lineY = null; + if (linecutYPosition !== undefined && array.length > 0) { + lineY = array.length - linecutYPosition; // Convert from bottom index to y-axis coordinate + } + + // Calculate the height dynamically based on the number of rows in the array + const dynamicHeight = Math.max(array.length * verticalScaleFactor, 200); // Minimum height is 200px return (
@@ -57,15 +66,14 @@ export default function PlotlyHeatMap({ }, xaxis: { title: xAxisTitle, - //scaleanchor: "y", // Ensure squares remain proportional }, yaxis: { title: yAxisTitle, - range: [0, array.length], // Dynamically adjust y-axis range + range: [-0.5, array.length - 0.5], // Dynamically adjust y-axis range autorange: false, - tickmode: showTicks ? 'linear' : '', // tick marks should only appear when - tick0: 0, // Starting tick - dtick: showTicks ? tickStep : 10000, // Tick step, + tickmode: showTicks ? 'linear' : '', + tick0: 0, + dtick: showTicks ? tickStep : 10000, showticklabels: showTicks }, autosize: true, @@ -77,11 +85,26 @@ export default function PlotlyHeatMap({ t: 0, b: 0, }, + shapes: lineY !== null + ? [ + { + type: 'line', + x0: 0, + x1: array[0]?.length - 1 || 1, // Assuming non-empty array, width of heatmap + y0: lineY, + y1: lineY, + line: { + color: 'red', + width: linecutThickness, + }, + }, + ] + : [], }} config={{ responsive: true }} className="rounded-b-md" /> -
+
{title}
diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 5fb385c..9f63fc1 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -4,10 +4,11 @@ const hamburgerIcon = -export default function Sidebar({ children }) { +export default function Sidebar({isSidebarClosed=()=>{}, children }) { const [isCollapsed, setIsCollapsed] = useState(false); const toggleSidebar = () => { + isSidebarClosed(!isCollapsed); setIsCollapsed(!isCollapsed); }; diff --git a/frontend/src/components/SidebarItem.jsx b/frontend/src/components/SidebarItem.jsx index 21ae0fe..3a8e9a9 100644 --- a/frontend/src/components/SidebarItem.jsx +++ b/frontend/src/components/SidebarItem.jsx @@ -34,7 +34,7 @@ const chevronUp = ( ); -export default function SidebarItem({ children, title, icon='', pulse }) { +export default function SidebarItem({ children, title, icon='', pulse, rotate }) { const [isExpanded, setIsExpanded] = useState(true); const toggleExpand = () => { @@ -48,7 +48,7 @@ export default function SidebarItem({ children, title, icon='', pulse }) { onClick={toggleExpand} >
-
{icon}
+
{icon}
{title}
{isExpanded ? chevronUp : chevronDown} diff --git a/frontend/src/components/Status.jsx b/frontend/src/components/Status.jsx new file mode 100644 index 0000000..28ac3a9 --- /dev/null +++ b/frontend/src/components/Status.jsx @@ -0,0 +1,31 @@ +const images = [ + "images/xrayIcon/gif0.png", + "images/xrayIcon/gif1.png", + "images/xrayIcon/gif2.png", + "images/xrayIcon/gif3.png", + "images/xrayIcon/gif4.png", + ]; + + export default function Status({height='h-14', cycleTime=5, slideshow=true}) { + return ( +
+
+ { + slideshow ? + images.map((src, index) => ( + xray icon + )) + : + + } +
+
+ ); + } + \ No newline at end of file diff --git a/frontend/src/components/TimelineHeatmap.jsx b/frontend/src/components/TimelineHeatmap.jsx new file mode 100644 index 0000000..bd69fd8 --- /dev/null +++ b/frontend/src/components/TimelineHeatmap.jsx @@ -0,0 +1,57 @@ +import { useState } from "react"; +import PlotlyHeatMap from "./PlotlyHeatMap"; +import JSONPrinter from "./JSONPrinter"; +import InputSlider from "./InputSlider"; + +import { generateEggData } from "../utils/plotHelper"; + +export default function TimelineHeatmap ({cumulativeArrayData=[], demo=false}) { + //to do - see if we can wrap anything in usecallback or usememo since every index change rerenders the plot + const [ index, setIndex ] = useState(0); + + //handle updates where cumulativeArrayData goes to 1 and existing large index would be out of bounds + if (cumulativeArrayData.length > 0 && index > cumulativeArrayData.length - 1) setIndex(0); + + + if (!demo && cumulativeArrayData.length > 0) { + return ( +
+
+
+ +
+
+ +
+ i)}/> +
+ +
+ ) + } + + if (demo) { + var sampleCumulativeData = []; + const samples = 7; + for (let i=0; i +
+
+ +
+
+ +
+ i)}/> +
+ +
+ ) + } + +} + + \ No newline at end of file diff --git a/frontend/src/components/TimelineHeatmapScatter.jsx b/frontend/src/components/TimelineHeatmapScatter.jsx new file mode 100644 index 0000000..89b3606 --- /dev/null +++ b/frontend/src/components/TimelineHeatmapScatter.jsx @@ -0,0 +1,93 @@ +import { useState } from "react"; +import PlotlyHeatMap from "./PlotlyHeatMap"; +import PlotlyScatterMultiple from "./PlotlyScatterMultiple"; +import JSONPrinter from "./JSONPrinter"; +import InputSlider from "./InputSlider"; + +import { generateEggData } from "../utils/plotHelper"; + +const sampleData = [ + { + x: [1, 2, 3], + y: [2, 6, 3], + type: 'scatter', + mode: 'lines+markers', + marker: {color: 'red'}, + }, +]; + +const generateSampleScatterPlots = (n=1) => { + var scatterPlots = []; + for (let i = 0; i < n; i++) { + scatterPlots.push({ + x: [1, 2, 3], + y: [2*i, 6*i - 2*i*i, 3*i], + type: 'scatter', + mode: 'lines+markers', + marker: {color: 'red'}, + }); + } + return scatterPlots; +} + +export default function TimelineHeatmapScatter ({arrayData=[], scatterData=[], demo=false}) { + //to do - see if we can wrap anything in usecallback or usememo since every index change rerenders the plot + const [ index, setIndex ] = useState(0); + + //handle updates where arrayData goes to 1 and existing large index would be out of bounds + if (arrayData.length > 0 && index > arrayData.length - 1) setIndex(0); + + + if (!demo && arrayData.length > 0 && scatterData.length === arrayData.length) { + return ( +
+
+
+ +
+
+ +
+
+ +
+ i)}/> +
+ +
+ ) + } else { + if (!demo) { + return

Waiting for data

+ } + } + + if (demo) { + var sampleCumulativeData = []; + const samples = 7; + for (let i=0; i +
+
+ +
+
+ +
+
+ +
+ i)}/> +
+ + + ) + } + +} + + \ No newline at end of file diff --git a/frontend/src/components/Widget.jsx b/frontend/src/components/Widget.jsx index eb96595..0e7c280 100644 --- a/frontend/src/components/Widget.jsx +++ b/frontend/src/components/Widget.jsx @@ -8,8 +8,10 @@ export default function Widget({ defaultHeight='h-1/4', maxHeight='max-h-3/4', width='w-1/4', + minWidth="min-w-64", maxWidth='max-w-full', expandedWidth='w-full', + expandedHeight='h-full', contentStyles='' }) { const [isExpanded, setIsExpanded] = useState(false); @@ -27,7 +29,7 @@ export default function Widget({ return ( -
+
{/* Title */}
diff --git a/frontend/src/hooks/useGISAXS.js b/frontend/src/hooks/useGISAXS.js new file mode 100644 index 0000000..2007762 --- /dev/null +++ b/frontend/src/hooks/useGISAXS.js @@ -0,0 +1,247 @@ +import { useEffect, useRef, useState } from 'react'; +import msgpack from 'msgpack-lite'; +import dayjs from 'dayjs'; +import { getWsUrl } from '../utils/connectionHelper'; +import { processAndDownsampleArrayData, processJSONPlot, updateCumulativePlot } from '../utils/plotHelper'; + +const defaultWsUrl = getWsUrl(); +const defaultHeatmapSettings = { + tickStep: { + label: 'Tick Step', + type: 'float', + value: '10', + description: 'Factor to scale the vertical axis of Raw, VFFT, and IFFT images in the heatmap. Larger number will increase the vertical height.' + }, + showTicks: { + label: 'Tick Marks', + type: 'boolean', + value: false, + description: 'Toggles the display of tickmarks on the heatmap graphs, where tickmarks represent the frame count at that row.' + } +}; + +export const useGISAXS = ({}) => { + const [ messages, setMessages ] = useState([]); + const [ currentArrayData, setCurrentArayData ] = useState([]); + const [ cumulativeArrayData, setCumulativeArrayData ] = useState([]); + const [ currentScatterPlot, setCurrentScatterPlot ] = useState([]); + const [ cumulativeScatterPlots, setCumulativeScatterPlots ] = useState([]); + const [ isExperimentRunning, setIsExperimentRunning ] = useState(false); + const [ isReductionTest, setIsReductionTest ] = useState(false); + const [ linecutYPosition, setLinecutYPosition ] = useState(50); //using 50 as a test, change default later + + const [ wsUrl, setWsUrl ] = useState(defaultWsUrl); + const [ socketStatus, setSocketStatus ] = useState('closed'); + const [ frameNumber, setFrameNumber ] = useState(); + const [ warningMessage, setWarningMessage ] = useState(''); + const [ heatmapSettings, setHeatmapSettings ] = useState(defaultHeatmapSettings); + const [ metadata, setMetadata ] = useState(''); + + const ws = useRef(null); + const isUserClosed = useRef(false); + + + + const handleHeatmapSettingChange = (newValue, key) => { + setHeatmapSettings((prevState) => ({ + ...prevState, + [key]: { + ...prevState[key], + value: newValue + } + })); + }; + + const handleNewWebsocketMessages = async (event) => { + //process with webpack + try { + let newMessage; + let timestamp = dayjs().format('h:m:s a'); + + if (event.data instanceof Blob) { + // Convert Blob to ArrayBuffer for binary processing + const arrayBuffer = await event.data.arrayBuffer(); + newMessage = msgpack.decode(new Uint8Array(arrayBuffer)); + console.log({newMessage}) + + } else if (event.data instanceof ArrayBuffer) { + // Process ArrayBuffer directly + newMessage = msgpack.decode(new Uint8Array(event.data)); + + } else { + // Assume JSON string for non-binary data + newMessage = JSON.parse(event.data); + console.log(newMessage); + } + var keyList = ''; + for (const key in newMessage) { + keyList = keyList.concat(', ', key); + }; + + setMessages((prevMessages) => [...prevMessages, keyList]); + + if ('frame_number' in newMessage) { + setFrameNumber(newMessage.frame_number); + } + + if ('curve' in newMessage) { + const newPlot = processJSONPlot(newMessage['curve'], newMessage?.frame_number); + setCurrentScatterPlot(newPlot); + setCumulativeScatterPlots((prevState) => { + var newState = [...prevState]; + newState.push(newPlot[0]); + return newState; + }); + //updateCumulativePlot(newPlot, setCumulativeScatterPlots); //for use when showing all scatter plots on a single graph only + } + + if ('raw_frame' in newMessage) { + let newPlot = processAndDownsampleArrayData(newMessage.raw_frame, newMessage.width, newMessage.height, 1); + setCurrentArayData(newPlot); + setCumulativeArrayData((prevState) => { + var newState = [...prevState]; + var newPlotObject = { + data: newPlot, + }; + try { + newPlotObject.metadata= { + timestamp: timestamp, + height: newMessage.height, + width: newMessage.width, + tiledUrl: newMessage.tiled_url, + dataType: newMessage.data_type, + } + } catch (e) { + console.error('Check keys in raw frame message: ', e); + } + newState.push(newPlotObject); + return newState; + }); + } + + if ('msg_type' in newMessage) { + if (newMessage.msg_type === 'start') { + resetAllData(); + setIsExperimentRunning(true); + + } + if (newMessage.msg_type === 'stop') { + setIsExperimentRunning(false); + } + if (newMessage.scan_type === 'reduction') { + setIsReductionTest(true); + } else { + setIsReductionTest(false); + } + setMetadata(newMessage); + } + } catch (error) { + console.error('Error processing WebSocket message:', error); + } + }; + + const resetAllData = () => { + setCumulativeArrayData([]); + setCumulativeScatterPlots([]); + setCurrentArayData([]); + setCurrentScatterPlot([]); + } + + const handleWebsocketClose = (event) => { + ws.current = false; + if (isUserClosed.current === true) { + //do nothing, the user forced the websocket to close + console.log('user closed websocket'); + return; + } else { + //if websocket closed due to external reason, send in warning and attempt reconnection + const maxAttempt = 2; + const time = 5; //time in seconds + //alert(`Websocket ${event.currentTarget.url} closed at ${dayjs().format('h:mm:ss A')} `) + console.log({event}) + + // Attempt to reconnect + setWarningMessage("WebSocket closed unexpectedly. Attempting to reconnect..."); + console.log(`WebSocket ${event.currentTarget.url} closed unexpectedly at ${dayjs().format('h:mm:ss A')}`); + + // Reconnection logic + const maxAttempts = 2; // Number of attempts to reconnect + let attempts = 0; + + const tryReconnect = () => { + if (attempts >= maxAttempts) { + setWarningMessage("Failed to reconnect to WebSocket after multiple attempts."); + return; + } + if (ws.current !== false) { + //ws has restarted + return; + } else { + attempts++; + console.log(`Reconnection attempt ${attempts}`); + startWebSocket(); + } + }; + + setTimeout(tryReconnect, time*1000); + } + }; + + const startWebSocket = () => { + setWarningMessage(''); + + ws.current = new WebSocket(wsUrl); + + ws.current.onopen = (event) => { + setSocketStatus('Open'); + isUserClosed.current = false; + } + + ws.current.onerror = (error) => { + console.error('Error with ws: ' + error); + setWarningMessage("Error connecting websocket: Check port/path and verify processor running"); + } + + ws.current.onmessage = (event) => { + handleNewWebsocketMessages(event); + }; + + ws.current.onclose = (event) => { + handleWebsocketClose(event); + } + }; + + const closeWebSocket = () => { + try { + ws.current.close(); + } catch (error) { + console.log({error}); + return; + } + setSocketStatus('closed'); + isUserClosed.current = true; //this function is only able to be called by the user + }; + + + + return { + messages, + currentArrayData, + currentScatterPlot, + cumulativeScatterPlots, + cumulativeArrayData, + isExperimentRunning, + linecutYPosition, + wsUrl, + setWsUrl, + frameNumber, + socketStatus, + startWebSocket, + closeWebSocket, + heatmapSettings, + handleHeatmapSettingChange, + warningMessage, + isReductionTest, + metadata, + } +} \ No newline at end of file diff --git a/frontend/src/index.js b/frontend/src/index.js index 3f31261..ca928fb 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -3,11 +3,13 @@ import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; +import { BrowserRouter } from 'react-router' const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - - + + + ); diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx new file mode 100644 index 0000000..da55d22 --- /dev/null +++ b/frontend/src/pages/Home.jsx @@ -0,0 +1,148 @@ +import { useEffect, useState } from 'react'; + +import Main from "../components/Main"; +import Header from "../components/Header"; +import Sidebar from "../components/Sidebar"; +import SidebarItem from "../components/SidebarItem"; +import Widget from "../components/Widget"; +import PlotlyHeatMap from "../components/PlotlyHeatMap"; +import PlotlyScatterSingle from "../components/PlotlyScatterSingle"; +import PlotlyScatterMultiple from "../components/PlotlyScatterMultiple"; +import ConsoleViewer from "../components/ConsoleViewer"; +import TimelineHeatmap from '../components/TimelineHeatmap'; +import TimelineHeatmapScatter from '../components/TimelineHeatmapScatter'; +import Button from "../component_library/Button"; +import TextField from "../component_library/TextField"; +import ScanMetadata from "../components/ScanMetadata"; +import Settings from "../components/Settings"; +import FormContainer from "../component_library/FormContainer"; +import Status from '../components/Status'; +import { phosphorIcons } from '../assets/icons'; + +import { useAPXPS } from "../hooks/useAPXPS"; +import { useGISAXS } from '../hooks/useGISAXS'; +export default function Home() { + const [ isSidebarClosed, setIsSidebarClosed ] = useState(false) + + const { + messages, + currentArrayData, + currentScatterPlot, + cumulativeScatterPlots, + cumulativeArrayData, + isExperimentRunning, + isReductionTest, + wsUrl, + setWsUrl, + frameNumber, + socketStatus, + startWebSocket, + closeWebSocket, + heatmapSettings, + handleHeatmapSettingChange, + warningMessage, + metadata, + } = useGISAXS({}); + + var statusMessage; + if (isExperimentRunning && isReductionTest) { + statusMessage = "Running Reduction Test"; + } else { + if (isReductionTest && !isExperimentRunning) { + statusMessage = "Reduction Test Completed" + } else { + if (isExperimentRunning) { + statusMessage = "Running Scans"; + } else { + statusMessage = "Inactive" + } + } + } + + + //Automatically start the websocket connection on page load + useEffect(() => { + startWebSocket(); + return closeWebSocket; + }, []); + + return ( +
+ +
+
+
+ +
+ + +
+ +

{statusMessage}

+
+
+ +
  • + {warningMessage.length > 0 ?

    {warningMessage}

    : ''} + + {socketStatus === 'closed' ?
  • +
    + + + + + +
    + +
    +
    + + {/* Current Scan */} + +
    +
    + +
    +
    + +
    +
    +
    + + {/* Timeline of All Scans */} + + + + + + +{/* + + + + + + + + + + + + + + + + + + + */} +
    +
    +
    +
    + + ) +} diff --git a/frontend/src/pages/Services.jsx b/frontend/src/pages/Services.jsx new file mode 100644 index 0000000..b936ea0 --- /dev/null +++ b/frontend/src/pages/Services.jsx @@ -0,0 +1,8 @@ +import Mermaid from "../components/Mermaid" +export default function Services(){ + return ( +
    + +
    + ) +} \ No newline at end of file diff --git a/frontend/src/utils/connectionHelper.js b/frontend/src/utils/connectionHelper.js index 6674e66..f49cf72 100644 --- a/frontend/src/utils/connectionHelper.js +++ b/frontend/src/utils/connectionHelper.js @@ -4,8 +4,8 @@ */ const getWsUrl = () => { const currentWebsiteIP = window.location.hostname; - const pathname = "/simImages"; - const port = ":8001"; + const pathname = "/viz"; + const port = ":8021"; var wsUrl; if (process.env.REACT_APP_WEBSOCKET_URL) { diff --git a/frontend/src/utils/plotHelper.js b/frontend/src/utils/plotHelper.js new file mode 100644 index 0000000..9a2b17b --- /dev/null +++ b/frontend/src/utils/plotHelper.js @@ -0,0 +1,163 @@ +const sampleScatterData = [ + { + x: [1, 2, 3], + y: [2, 6, 3], + type: 'scatter', + mode: 'lines+markers', + marker: {color: 'red'}, + }, +]; + +export const processJSONPlot = (rawJSONData, frameNumber='N/A') => { + //receives a json from + //"1D": message.one_d_reduction.df.to_json(), + //one_d_reduction=DataFrameModel(df=one_d_reduction), + const parsedData = JSON.parse(rawJSONData); + //console.log({parsedData}) + //we receive an array of 'q' and an array of 'qy' + + var xValues = []; + var yValues = []; + for (var key in parsedData) { + xValues.push(key); + yValues.push(parsedData[key]['0']) + } + const newPlot = [ + { + x: xValues, + y: yValues, + type: 'scatter', + mode: 'lines+markers', + marker: {color: 'red'}, + name: `frame ${frameNumber}` + } + ] + return newPlot; +} + +export const processPeakData = (peakDataArray=[{x:0, h:0, fwhm: 0}], singlePlotCallback=()=>{}, multiPlotCallback=()=>{}) => { + + var recentPlots = []; + peakDataArray.forEach(data => { + //receives an array of objects + var y_peak = data.h; + var x_peak = data.x; + + // Calculate sigma and define x range + var sigma = data.fwhm / (2 * Math.sqrt(2 * Math.log(2))); + var x_min = x_peak - 5 * sigma; + var x_max = x_peak + 5 * sigma; + var step = (x_max - x_min) / 100; + + // Generate x and y values for the single plot + var xValues = []; + var yValues = []; + for (let x = x_min; x <= x_max; x += step) { + var y = y_peak * Math.exp(-Math.pow(x - x_peak, 2) / (2 * Math.pow(sigma, 2))); + xValues.push(x); + yValues.push(y); + } + + // Create single plot object + recentPlots.push({ x: xValues, y: yValues, type: 'scatter', mode: 'lines' }); + }) + + + //update state + singlePlotCallback(recentPlots); + multiPlotCallback(recentPlots); +}; + +export const updateCumulativePlot = (recentPlots=[], cb=()=>{}) => { + //console.log({frameNumber}) + cb((data) => { + var oldArrayData = Array.from(data); + var newArrayData = []; + let totalFrames = oldArrayData.length; + let colorNumber = 255; //the lightest color for the oldest entries + + //TO DO: refactor this if its slowing the app down + oldArrayData.forEach((plot, index) => { + let colorWeight = (totalFrames - index) / totalFrames * colorNumber; //scale color based on index relative to total frames + plot.line = { + color: `rgb(${colorWeight}, ${colorWeight}, ${colorWeight})`, + width: 1, + }; + plot.mode = 'lines'; + newArrayData.push(plot); + }) + var newestData = []; + recentPlots.forEach((plot) => { + var newPlot = { + x: plot.x, + y: plot.y, + line: { + color: 'rgb(0, 94, 245)', + width: 2, + }, + name: plot.name, + mode: 'lines+markers' + }; + newestData.push(newPlot); + }) + return [...newArrayData, ...newestData]; + }) + }; + +export const processAndDownsampleArrayData = (data = [], width, height, scaleFactor = 1) => { + if (scaleFactor < 1) throw new Error("Scale factor must be 1 or greater."); + + const downsampledHeight = Math.floor(height / scaleFactor); + const downsampledWidth = Math.floor(width / scaleFactor); + const newData = []; + + for (let row = 0; row < downsampledHeight; row++) { + const newRow = []; + for (let col = 0; col < downsampledWidth; col++) { + let sum = 0; + let count = 0; + + // Sum up values within the scaleFactor x scaleFactor block + for (let i = 0; i < scaleFactor; i++) { + for (let j = 0; j < scaleFactor; j++) { + const originalRow = row * scaleFactor + i; + const originalCol = col * scaleFactor + j; + const index = originalRow * width + originalCol; + + if (originalRow < height && originalCol < width) { + sum += data[index]; + count++; + } + } + } + // Calculate the average value and add to the downsampled row + newRow.push(sum / count); + } + newData.push(newRow); + } + //cb(newData); + return newData; +}; + + +export function generateEggData(size, maxVal=255, offset=2) { + const center = size / offset; // Center of the Egg + const data = []; + + for (let y = 0; y < size; y++) { + const row = []; + for (let x = 0; x < size; x++) { + // Calculate distance from the center + const dx = x - center; + const dy = y - center; + const distance = Math.sqrt(dx * dx + dy * dy); + + // Egg-like function: exponential decay from center + const intensity = maxVal * Math.exp(-distance * distance / (2 * (center / 2) ** 2)); + row.push(Math.round(intensity)); // Normalize to integer + } + data.push(row); + } + + return data; + } \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..7ff36fa --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,38 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./src/**/*.{js,jsx,ts,tsx}", + ], + theme: { + extend: { + animation: { + fadeCycle: "fadeCycle 5s infinite", // Adjust duration as needed + fadeCycleSmooth: "fadeCycleSmooth 5s infinite", + }, + keyframes: { + fadeCycle: { + "0%, 100%": { opacity: "0" }, + "10%": { opacity: "1" }, // Fades in + "20%": { opacity: "1" }, // Fades in + "30%": { opacity: "0.5" }, // Fades in + "40%": {opacity: "0"}, + "50%": {opacity: "0"}, + "60%": { opacity: "0" }, // Fades out before switching + }, + fadeCycleSmooth: { + "0%": { opacity: "0" }, + "10%": { opacity: "0.5" }, // Fades in + "20%": { opacity: "1" }, // Fades in + "30%": { opacity: "1" }, // Fades in + "40%": {opacity: "1"}, + "50%": {opacity: "0.8"}, + "60%": { opacity: "0.5" }, // Fades out before switching + "70%": { opacity: "0" }, + "100%": {opacity: "0"} + }, + }, + }, + }, + plugins: [], +} + diff --git a/models/cnn_autoencoder/model.py b/models/cnn_autoencoder/model.py new file mode 100644 index 0000000..9bad82f --- /dev/null +++ b/models/cnn_autoencoder/model.py @@ -0,0 +1,59 @@ +import torch.nn as nn + + +class CNNAutoencoder(nn.Module): + def __init__(self, params=None, **kwargs): + super(CNNAutoencoder, self).__init__() + if params is None: + self.latent_dim = 1000 + else: + self.latent_dim = params.latent_dim + + # Encoder + self.encoder = nn.Sequential( + nn.Conv2d(1, 32, 3, stride=2, padding=1), + nn.ReLU(), + nn.Conv2d(32, 64, 3, stride=2, padding=1), + nn.ReLU(), + nn.Conv2d(64, 128, 3, stride=2, padding=1), + nn.ReLU(), + nn.Conv2d(128, 256, 3, stride=2, padding=1), + nn.ReLU(), + nn.Conv2d(256, 512, 7), # Last convolution + nn.Flatten(), # Flatten to vector for Linear Layer + nn.Linear( + 2 * 2 * 512, self.latent_dim + ), # Linear layer to map to latent space + ) + + # Decoder + self.decoder = nn.Sequential( + nn.ConvTranspose2d(512, 256, 7), + nn.ReLU(), + nn.ConvTranspose2d(256, 128, 3, stride=2, padding=1, output_padding=1), + nn.ReLU(), + nn.ConvTranspose2d(128, 64, 3, stride=2, padding=1, output_padding=1), + nn.ReLU(), + nn.ConvTranspose2d(64, 32, 3, stride=2, padding=1, output_padding=1), + nn.ReLU(), + nn.ConvTranspose2d(32, 1, 3, stride=2, padding=1, output_padding=1), + nn.Tanh(), + ) + + self.linear_layer = nn.Linear(self.latent_dim, 2 * 2 * 512) + + def forward(self, x): + latent = self.encoder(x) + latent_matrix = self.linear_layer(latent) + reshaped_latent = latent_matrix.reshape(x.shape[0], 512, 2, 2) + decoded = self.decoder(reshaped_latent) + return decoded + + def embed_imgs(self, x): + return self.encoder(x) # Get the latent representation (no decoding) + + +# Function to create the Autoencoder model using params +def simple_autoencoder(params=None, **kwargs): + model = CNNAutoencoder(params, **kwargs) + return model diff --git a/models/cnn_autoencoder/model_state_dict.npz b/models/cnn_autoencoder/model_state_dict.npz new file mode 100644 index 0000000..66f17c1 Binary files /dev/null and b/models/cnn_autoencoder/model_state_dict.npz differ diff --git a/models/umap/quick_test.joblib b/models/umap/quick_test.joblib new file mode 100644 index 0000000..0902b07 Binary files /dev/null and b/models/umap/quick_test.joblib differ diff --git a/settings.yaml b/settings.yaml index 952b54e..7f4d3a8 100644 --- a/settings.yaml +++ b/settings.yaml @@ -4,37 +4,46 @@ logging_level: INFO tiled_poller: publish_address: tcp://0.0.0.0:5000 #only safe in containers -viz_operator: - zmq_listen_address: tcp://0.0.0.0:5000 #only safe in containers - zmq_hwm: 100000 - redis_host: redis - redis_port: 6379 +lse_reducer: models: - name: CNNAutoencoder - checkpoint_dir: /torch/cnnautoencoder + state_dict: ./models/cnn_autoencoder/model_state_dict.npz python_class: CNNAutoencoder + python_file: ./models/cnn_autoencoder/model.py type: torch - proceosors: gpu - - name: PCA - dir: /dim_reduction/pca - current: PCA + - name: UMAP + file: ./models/umap/quick_test.joblib type: joblib - processor: cpu - current_latent_space: CNNAutoencoder - current_dim_reduction: CNNAutoencoder - # - name: UMAP - # directory: /dim_reduction/umap - current_reduction: PCA - -lse: - zmq_router_address: tcp://*:5555 - zmq_router_hwm: 100000 - zmq_dealer_address: tcp://*:5556 - websocket_publish_host: 0.0.0.0 - websocket_publish_port: 8021 - zmq_publish_address: tcp://*:5557 + current_dim_reduction: UMAP + +lse_operator: + ws_publisher: + host: 0.0.0.0 + port: 8765 + zmq_publisher: # ??? + zmq_address: tcp://0.0.0.0:5557 + listener: + zmq_address: tcp://0.0.0.0:5000 + zmq_broker: + router_address: tcp://0.0.0.0:5555 + router_hwm: 100000 + dealer_address: tcp://0.0.0.0:5556 +lse_publisher: + ws_publisher: + host: 0.0.0.0 + port: 8021 + +viz_operator: + ws_publisher: + host: 0.0.0.0 + port: 8021 + listener: + zmq_address: tcp://0.0.0.0:5000 #only safe in containers + redis: + host: kvrocks + port: 6666 diff --git a/src/arroyogisaxs/app/__init__.py b/src/arroyogisaxs/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/arroyogisaxs/app/tiled_poller_sim_cli.py b/src/arroyogisaxs/app/frame_listener_sim_cli.py similarity index 82% rename from src/arroyogisaxs/app/tiled_poller_sim_cli.py rename to src/arroyogisaxs/app/frame_listener_sim_cli.py index c7aa86e..16f9596 100644 --- a/src/arroyogisaxs/app/tiled_poller_sim_cli.py +++ b/src/arroyogisaxs/app/frame_listener_sim_cli.py @@ -11,8 +11,8 @@ from ..config import settings from ..schemas import ( GISAXSRawEvent, - GISAXSRawStart, - GISAXSRawStop, + GISAXSStart, + GISAXSStop, SerializableNumpyArrayModel, ) @@ -21,9 +21,8 @@ onto ZMQ, taking care of pydantic messages, serialization and msgpack """ - -FRAME_WIDTH = 100 -FRAME_HEIGHT = 100 +FRAME_WIDTH = 1475 +FRAME_HEIGHT = 619 DATA_TYPE = "float32" app = typer.Typer() @@ -33,7 +32,7 @@ async def process_images( socket: zmq.asyncio.Socket, cycles: int, frames: int, pause: float ): for cycle_num in range(cycles): - start = GISAXSRawStart( + start = GISAXSStart( width=FRAME_WIDTH, height=FRAME_HEIGHT, data_type=DATA_TYPE, @@ -45,7 +44,7 @@ async def process_images( for frame_num in range(frames): # Create a test pattern image that changes slightly each time frame_number = int(time.time()) % 100 # Change pattern every second - image = np.zeros((FRAME_WIDTH, FRAME_HEIGHT), dtype=DATA_TYPE) + image = np.random.rand(FRAME_WIDTH, FRAME_HEIGHT).astype(DATA_TYPE) np.fill_diagonal(image, frame_number % 255) event = GISAXSRawEvent( @@ -55,7 +54,7 @@ async def process_images( ) print("event") await socket.send(msgpack.packb(event.model_dump())) - stop = GISAXSRawStop(num_frames=frames) + stop = GISAXSStop(num_frames=frames) print("stop") await socket.send(msgpack.packb(stop.model_dump())) await asyncio.sleep(pause) @@ -66,11 +65,11 @@ async def process_images( @app.command() -def main(cycles: int = 10000, frames: int = 5, pause: float = 5): +def main(cycles: int = 10000, frames: int = 50, pause: float = 5): async def run(): context = zmq.asyncio.Context() socket = context.socket(zmq.PUB) - address = f"tcp://{settings.tiled_poller.publish_address}:{settings.tiled_poller.publish_port}" + address = settings.tiled_poller.publish_address print(f"Connecting to {address}") socket.bind(address) await process_images(socket, cycles, frames, pause) diff --git a/src/arroyogisaxs/app/lse_broker_cli.py b/src/arroyogisaxs/app/lse_broker_cli.py new file mode 100644 index 0000000..ee3712b --- /dev/null +++ b/src/arroyogisaxs/app/lse_broker_cli.py @@ -0,0 +1,27 @@ +import asyncio +import logging + +import typer + +from ..config import settings +from ..log_utils import setup_logger +from ..zmq import ZMQBroker + +app = typer.Typer() +logger = logging.getLogger("arroyogisaxs") +setup_logger(logger) + + +@app.command() +async def start() -> None: + app_settings = settings.lse_operator + logger.info("Getting settings") + logger.info(f"{settings.lse_operator}") + logger.info("Starting Broker") + # we may consider starting this in its own process + broker = ZMQBroker.from_settings(app_settings.zmq_broker) + await asyncio.gather(broker.start()) + + +if __name__ == "__main__": + asyncio.run(start()) diff --git a/src/arroyogisaxs/app/lse_operator_cli.py b/src/arroyogisaxs/app/lse_operator_cli.py index b23e46a..bbf71e5 100644 --- a/src/arroyogisaxs/app/lse_operator_cli.py +++ b/src/arroyogisaxs/app/lse_operator_cli.py @@ -6,12 +6,12 @@ from ..config import settings from ..log_utils import setup_logger from ..lse.lse_operator import LatentSpaceOperator -from ..websockets import OneDWSResultPublisher -from ..zmq import ZMQBroker, ZMQFramePublisher, ZMQPubSubListener +from ..lse.lse_ws_publisher import LSEWSResultPublisher +from ..zmq import ZMQFrameListener app = typer.Typer() logger = logging.getLogger("arroyogisaxs") -setup_logger(logger) +setup_logger(logger, settings.logging_level) @app.command() @@ -22,19 +22,14 @@ async def start() -> None: logger.info("Starting ZMQ PubSub Listener") logger.info(f"ZMQPubSubListener settings: {app_settings}") - operator = LatentSpaceOperator() + operator = LatentSpaceOperator.from_settings(app_settings) - ws_publisher = OneDWSResultPublisher.from_settings(app_settings.ws_publisher) - zmq_publisher = ZMQFramePublisher.from_settings(app_settings.zmq_publisher) + # ws_publisher = OneDWSResultPublisher.from_settings(app_settings.ws_publisher) + ws_publisher = LSEWSResultPublisher.from_settings(app_settings.ws_publisher) operator.add_publisher(ws_publisher) - operator.add_publisher(zmq_publisher) - listener = ZMQPubSubListener.from_settings(app_settings.listener, operator) - - # we may consider starting this in its own process - broker = ZMQBroker.from_settings(app_settings.router) - - await asyncio.gather(listener.start(), ws_publisher.start(), broker.start()) + listener = ZMQFrameListener.from_settings(app_settings.listener, operator) + await asyncio.gather(listener.start(), ws_publisher.start()) if __name__ == "__main__": diff --git a/src/arroyogisaxs/app/lse_worker_cli.py b/src/arroyogisaxs/app/lse_worker_cli.py index 0c98c32..20a45d5 100644 --- a/src/arroyogisaxs/app/lse_worker_cli.py +++ b/src/arroyogisaxs/app/lse_worker_cli.py @@ -1,13 +1,18 @@ -import asyncio import logging import msgpack import typer import zmq +import zmq.asyncio from ..config import settings from ..log_utils import setup_logger from ..lse.lse_reducer import LatentSpaceReducer +from ..schemas import ( + GISAXSLatentSpaceEvent, + GISAXSRawEvent, + SerializableNumpyArrayModel, +) app = typer.Typer() logger = logging.getLogger("arroyogisaxs") @@ -15,29 +20,48 @@ @app.command() -async def start() -> None: - app_settings = settings.lse +def start() -> None: logger.info("Getting settings") - logger.info(f"{settings.lse}") + logger.info(f"{settings}") context = zmq.Context() client_socket = context.socket(zmq.REP) # worker to the broker - client_socket.connect(app_settings.zmq_router_address) - logger.info(f"Connected to broker at {app_settings.zmq_router_address}") - reducer = LatentSpaceReducer().with_models_loaded() - + client_socket.connect(settings.lse_operator.zmq_broker.dealer_address) + logger.info( + f"Connected to broker dealer at {settings.lse_operator.zmq_broker.dealer_address}" + ) + reducer = LatentSpaceReducer.from_settings(settings.lse_reducer) + logger.info("Listening for messages") while True: + response_sent = False try: - raw_msg = await client_socket.recv() + raw_msg = client_socket.recv() message = msgpack.unpackb(raw_msg, raw=False) message_type = message.get("msg_type") if message_type != "event": continue - latent_space = await reducer.reduce(message) - await client_socket.send(msgpack.packb(latent_space, use_bin_type=True)) + image = SerializableNumpyArrayModel.deserialize_array(message["image"]) + message["image"] = image + event = GISAXSRawEvent(**message) + # logger.debug("calculating latent space") + + latent_space = reducer.reduce(event) + # logger.debug("latent space returned") + return_message = GISAXSLatentSpaceEvent( + tiled_url="foo", + feature_vector=latent_space[0].tolist(), + index=message.get("frame_number"), + ) + client_socket.send( + msgpack.packb(return_message.model_dump(), use_bin_type=True) + ) + response_sent = True + # logger.debug("LSE returned") except Exception as e: logger.error(f"Error processing message: {e}") + if not response_sent: + client_socket.send(b"ERROR") if __name__ == "__main__": - asyncio.run(start()) + start() diff --git a/src/arroyogisaxs/app/viz_operator_cli.py b/src/arroyogisaxs/app/viz_operator_cli.py index dd78d16..dc97214 100644 --- a/src/arroyogisaxs/app/viz_operator_cli.py +++ b/src/arroyogisaxs/app/viz_operator_cli.py @@ -3,7 +3,7 @@ import typer -from arroyogisaxs.zmq import ZMQPubSubListener +from arroyogisaxs.zmq import ZMQFrameListener from ..config import settings from ..kv_store import KVStore @@ -22,11 +22,10 @@ async def start(): logger.info("Starting Tiled Poller") logger.info("Getting settings") logger.info(f"{settings.viz_operator}") - operator = OneDReductionOperator.create(KVStore.from_settings(app_settings.redis)) ws_publisher = OneDWSResultPublisher.from_settings(app_settings.ws_publisher) operator.add_publisher(ws_publisher) - listener = ZMQPubSubListener.from_settings(app_settings.listener, operator) + listener = ZMQFrameListener.from_settings(app_settings.listener, operator) await asyncio.gather(listener.start(), ws_publisher.start()) diff --git a/src/arroyogisaxs/kv_store.py b/src/arroyogisaxs/kv_store.py index ccfc460..866a842 100644 --- a/src/arroyogisaxs/kv_store.py +++ b/src/arroyogisaxs/kv_store.py @@ -2,18 +2,16 @@ import redis -KEY_CURRENT_REDUCTION_PARAMS = "current_reduction_params" - class KVStore: - def __init__(self, redis_server: redis.Redis): - self.redis_server = redis.Redis(host="localhost", port=6379, db=0) + def __init__(self, redis_conn: redis.Redis): + self.redis_conn = redis_conn def get(self, key: str): - return self.redis_server.get(key) + return self.redis_conn.get(key) def set(self, key: str, value): - self.redis_server.set(key, value) + self.redis_conn.set(key, value) def get_json(self, key: str): value_s = self.get(key) @@ -29,5 +27,8 @@ def set_json(self, key: str, value: dict): @classmethod def from_settings(cls, settings: dict) -> "KVStore": - redis_server = redis.Redis(host=settings.host, port=settings.port) - return cls(redis_server) + pool = redis.ConnectionPool( + host=settings.host, port=settings.port, decode_responses=True + ) + redis_conn = redis.Redis(connection_pool=pool) + return cls(redis_conn) diff --git a/src/arroyogisaxs/log_utils.py b/src/arroyogisaxs/log_utils.py index 4cc133f..81beb7f 100644 --- a/src/arroyogisaxs/log_utils.py +++ b/src/arroyogisaxs/log_utils.py @@ -6,6 +6,6 @@ def setup_logger(logger: logging.Logger, log_level: str = "INFO"): handler = logging.StreamHandler() handler.setFormatter(formatter) logger.addHandler(handler) - logger.setLevel(logging.DEBUG) + # logger.setLevel(log_level) logger.setLevel(log_level.upper()) logger.debug("DEBUG LOGGING SET") diff --git a/src/arroyogisaxs/lse/lse_operator.py b/src/arroyogisaxs/lse/lse_operator.py index 01955e9..33be142 100644 --- a/src/arroyogisaxs/lse/lse_operator.py +++ b/src/arroyogisaxs/lse/lse_operator.py @@ -1,27 +1,62 @@ import logging +import msgpack +import zmq from arroyopy.operator import Operator -from ..schemas import GISAXSMessage, GISAXSRawEvent, GISAXSRawStart, GISAXSRawStop +from ..schemas import ( + GISAXSLatentSpaceEvent, + GISAXSMessage, + GISAXSRawEvent, + GISAXSStart, + GISAXSStop, +) logger = logging.getLogger(__name__) class LatentSpaceOperator(Operator): - def __init__(self): + def __init__(self, proxy_socket: zmq.Socket): super().__init__() + self.proxy_socket = proxy_socket async def process(self, message: GISAXSMessage) -> None: - if isinstance(message, GISAXSRawStart): + logger.debug("message recvd") + if isinstance(message, GISAXSStart): + logger.info("Received Start Message") await self.publish(message) elif isinstance(message, GISAXSRawEvent): - await self.publish(message) - elif isinstance(message, GISAXSRawStop): + result = await self.dispatch(message) + await self.publish(result) + elif isinstance(message, GISAXSStop): + logger.info("Received Stop Message") await self.publish(message) else: logger.warning(f"Unknown message type: {type(message)}") return None + async def dispatch(self, message: GISAXSRawEvent) -> GISAXSLatentSpaceEvent: + try: + message = message.model_dump() + message = msgpack.packb(message, use_bin_type=True) + await self.proxy_socket.send(message) + # logger.debug("sent frame to broker") + response = await self.proxy_socket.recv() + if response == b"ERROR": + logger.debug("Worker reported an error") + return None + # logger.debug("response from broker") + return GISAXSLatentSpaceEvent(**msgpack.unpackb(response)) + except Exception as e: + logger.error(f"Error sending message to broker {e}") + @classmethod def from_settings(cls, settings): - return cls() + # Connect to the ZMQ Router/Dealer as a client + context = zmq.asyncio.Context() + socket = context.socket(zmq.REQ) + socket.setsockopt(zmq.SNDHWM, 10000) # Allow up to 10,000 messages + socket.setsockopt(zmq.RCVHWM, 10000) + socket.connect(settings.zmq_broker.router_address) + logger.info(f"Connected to broker at {settings.zmq_broker.router_address}") + return cls(socket) diff --git a/src/arroyogisaxs/lse/lse_reducer.py b/src/arroyogisaxs/lse/lse_reducer.py index 38d391f..c06b262 100644 --- a/src/arroyogisaxs/lse/lse_reducer.py +++ b/src/arroyogisaxs/lse/lse_reducer.py @@ -11,12 +11,18 @@ import torchvision.transforms as transforms from PIL import Image -from ..config import settings as default_settings from ..schemas import GISAXSRawEvent logger = logging.getLogger(__name__) +# message = { +# "tiled_uri": DATA_TILED_URI, +# "index": index, +# "feature_vector": latent_vector.tolist(), +# } + + class LatentSpaceReducer: """ Responsible for taking an image, encoding it into a @@ -27,7 +33,12 @@ class LatentSpaceReducer: agorithm to a 2D space. The results are saved to a Tiled dataset. """ - def __init__(self, settings): + def __init__( + self, current_latent_space: str, current_dim_reduction: str, models_config + ): + self.current_latent_space = current_latent_space + self.current_dim_reduction = current_dim_reduction + self.models_config = models_config # Check for CUDA else use CPU # needs to be members of the reducer class if torch.cuda.is_available(): @@ -37,20 +48,21 @@ def __init__(self, settings): device = torch.device("cpu") logger.info("Using CPU") self.device = device - self.settings = settings self.model_cache = {} - self.current_ls_model = self.get_ls_model() + + # Load the models now + self.current_torch_model = self.get_ls_model() self.curent_dim_reduction_model = self.get_dim_reduction_model() self.current_transform = self.get_transform() - def reduce(self, message: GISAXSRawEvent): + def reduce(self, message: GISAXSRawEvent) -> np.ndarray: # 1. Encode the image into a latent space. For now we assume pil = Image.fromarray(message.image.array) tensor = self.current_transform(pil) ls_is_on_gpu = False if ( self.device == torch.device("cuda") - and self.current_ls_model["config"].type == "torch" + and self.current_latent_space["config"].type == "torch" ): ls_is_on_gpu = True @@ -70,30 +82,37 @@ def reduce(self, message: GISAXSRawEvent): return f_vec def get_ls_model(self): - current_name = self.settings.current_latent_space - return self.get_model(current_name) + ls_model_name = self.current_latent_space + logger.info(f"Loading Latent Space model {self.current_latent_space}") + model = self.get_model(ls_model_name) + logger.info("Latent Space Model Loaded") + return model def get_dim_reduction_model(self): - current_name = self.settings.current_dim_reduction - return self.get_model(current_name) + dim_reduction_name = self.current_dim_reduction + logger.info(f"Loading Dimensionality Reduction Model {dim_reduction_name}") + model = self.get_model(dim_reduction_name) + logger.info("Dimensionality Reduction model loaded") + return model def get_model(self, name: str): # check for model in configured models - for model_config in self.settings.models: + current_model_config = None + for model_config in self.models_config: if model_config.name == name: - current_model = model_config + current_model_config = model_config break - if model_config is None: + if current_model_config is None: raise ValueError(f"Current model {name} is not found in models") # check if model is in cache. If not, load it and add to cache - if current_model.name not in self.model_cache: + if current_model_config.name not in self.model_cache: loaded_model = self.load_model(model_config) - self.model_cache[current_model.name] = { + self.model_cache[current_model_config.name] = { "model": loaded_model, "config": model_config, } - return self.model_cache[current_model.name] + return self.model_cache[current_model_config.name] def load_model(self, model_config): if model_config.type == "torch": @@ -159,18 +178,16 @@ def import_torch_models(self): return torch_models @classmethod - def with_models_loaded(cls, settings) -> "LatentSpaceReducer": - if settings is None: - settings = default_settings - reducer = cls(settings) - # Load the models now - reducer.current_torch_model = reducer.get_ls_model() - reducer.curent_dim_reduction_model = reducer.get_dim_reduction_model() - reducer.current_transform = reducer.get_transform() + def from_settings(cls, settings) -> "LatentSpaceReducer": + reducer = cls( + settings.current_latent_space, + settings.current_dim_reduction, + settings.models, + ) return reducer -if __name__ == "__main__": - reducer = LatentSpaceReducer.with_models_loaded(default_settings.lse) - reducer.reduce() +# if __name__ == "__main__": +# reducer = LatentSpaceReducer.from_settings(default_settings.lse) +# reducer.reduce() diff --git a/src/arroyogisaxs/lse/lse_ws_publisher.py b/src/arroyogisaxs/lse/lse_ws_publisher.py new file mode 100644 index 0000000..76f2e9c --- /dev/null +++ b/src/arroyogisaxs/lse/lse_ws_publisher.py @@ -0,0 +1,138 @@ +import asyncio +import json +import logging +from typing import Union + +# import msgpack +# import numpy as np +import websockets +from arroyopy.publisher import Publisher + +from ..schemas import GISAXSLatentSpaceEvent, GISAXSStart, GISAXSStop + +logger = logging.getLogger(__name__) + + +class LSEWSResultPublisher(Publisher): + """ + A publisher class for sending dimensionality reduction information + + """ + + websocket_server = None + connected_clients = set() + current_start_message = None + + def __init__(self, host: str = "localhost", port: int = 8001): + super().__init__() + self.host = host + self.port = port + + async def start( + self, + ): + # Use partial to bind `self` while matching the expected handler signature + server = await websockets.serve( + self.websocket_handler, + self.host, + self.port, + ) + logger.info(f"Websocket server started at ws://{self.host}:{self.port}") + await server.wait_closed() + + async def publish(self, message: GISAXSLatentSpaceEvent) -> None: + if self.connected_clients: # Only send if there are clients connected + asyncio.gather( + *(self.publish_ws(client, message) for client in self.connected_clients) + ) + + async def publish_ws( + self, + client: websockets.ServerConnection, + message: Union[GISAXSLatentSpaceEvent | GISAXSStart | GISAXSStop], + ) -> None: + if isinstance(message, GISAXSStop): + logger.info(f"WS Sending Stop {message}") + self.current_start_message = None + await client.send(json.dumps(message.model_dump())) + return + + if isinstance(message, GISAXSStart): + self.current_start_message = message + logger.info(f"WS Sending Start {message}") + await client.send(json.dumps(message.model_dump())) + return + + if isinstance(message, GISAXSLatentSpaceEvent): + # send image data separately to client memory issues + message = { + "tiled_uri": "foo", + "index": 0, + "feature_vector": message.feature_vector, + } + await client.send(json.dumps(message)) + + async def websocket_handler(self, websocket): + logger.info(f"New connection from {websocket.remote_address}") + # if websocket.request.path != "/viz": + # logger.info(f"Invalid path: {websocket.request.path}, we only support /viz") + # return + self.connected_clients.add(websocket) + try: + # Keep the connection open and do nothing until the client disconnects + await websocket.wait_closed() + finally: + # Remove the client when it disconnects + self.connected_clients.remove(websocket) + logger.info("Client disconnected") + + @classmethod + def from_settings(cls, settings: dict) -> "LSEWSResultPublisher": + return cls(settings.host, settings.port) + + +# async def test_client(publisher: OneDWSResultPublisher, num_frames: int = 10): +# import time + +# import pandas as pd +# from arroyopy.schemas import DataFrameModel, NumpyArrayModel + +# from arroyogisaxs.schemas import GISAXSLatentSpaceEvent, GISAXSStart, GISAXSStop + +# await asyncio.sleep(2) +# for y in range(100): +# await publisher.publish(GISAXSStart()) +# for x in range(num_frames): +# await asyncio.sleep(1) +# # Create a test pattern image that changes slightly each time +# frame_number = int(time.time()) % 100 # Change pattern every second +# image = np.zeros((100, 100), dtype=np.float32) +# np.fill_diagonal(image, frame_number % 255) + +# # Create a 1D sine wave pattern +# x = np.linspace(0, 2 * np.pi, 100) +# one_d_reduction = pd.DataFrame( +# {"q": x, "qy": np.sin(x + frame_number * 0.1)} +# ) +# image_info = { +# "frame_number": frame_number, +# "width": image.shape[1], +# "height": image.shape[0], +# "data_type": "uint8", +# } + +# # Create GISAXSResult message +# message = GISAXSLatentSpaceEvent() + +# await publisher.publish(message) +# await publisher.publish(GISAXSStop(num_frames=num_frames)) + + +# async def main(publisher: OneDWSResultPublisher): +# await asyncio.gather(publisher.start(), test_client(publisher)) + + +# if __name__ == "__main__": +# logging.basicConfig(level=logging.INFO) +# publisher = GISAXSWSResultPublisher(host="0.0.0.0", port=8001) +# asyncio.run(main(publisher)) diff --git a/src/arroyogisaxs/one_d_reduction/detector.py b/src/arroyogisaxs/one_d_reduction/detector.py new file mode 100644 index 0000000..7a3b188 --- /dev/null +++ b/src/arroyogisaxs/one_d_reduction/detector.py @@ -0,0 +1,448 @@ +import numpy as np +from pyFAI import detectors +from pyFAI.detectors import ( + Eiger1M, + Eiger500k, + Pilatus, + Pilatus1M, + Pilatus100k, + Pilatus300k, + Pilatus300kw, +) +from pyFAI.detectors._common import Detector + + +class Pilatus900k(Pilatus): + """ + Pilatus 900k detector, assembly of 3x3 modules + Available at NSLS-II 12-ID. + + This is different from the "Pilatus CdTe 900kw" available at ESRF ID06-LVP which is 1x9 modules + """ + + MAX_SHAPE = (619, 1475) + aliases = ["Pilatus 900k"] + + +class Pilatus100k_OPLS(Pilatus100k): + """ + Pilatus 100k class inherited from the pyFAI Pilatus1M class + This class is used to add a specific masking for the Pilatus 100k of OPLS beamline at BNL + """ + + def calc_mask(self, bs=None, bs_kind=None, optional_mask=None): + """ + :param bs: (string) This is the beamstop position on teh detctor (teh pixels behind will be mask inherently) + :param bs_kind: (string) What beamstop is in: Only need to be defined if pindiode which have a different shape) + :param optional_mask: (string) This is usefull for tender x-ray energy and will add extra max at the chips junction + :return: (a 2D array) A mask array with 0 and 1 with 0s where the image will be masked + """ + mask = np.logical_not(detectors.Pilatus100k().calc_mask()) + mask[:, :5], mask[:, -5:], mask[:5, :], mask[-5:, :] = ( + False, + False, + False, + False, + ) + + # Hot pixels needs to be defines + # mask[20, 884], mask[56, 754], mask[111, 620], mask[145, 733], mask[178, 528], mask[ + # 189, 571] = False, False, False, False, False, False + + # Beamstop + if bs == [0, 0]: + return np.logical_not(mask) + else: + mask[bs[1] :, bs[0] - 8 : bs[0] + 8] = False + return np.logical_not(mask) + + +class Pilatus300k_OPLS(Pilatus300k): + """ + Pilatus 100k class inherited from the pyFAI Pilatus1M class + This class is used to add a specific masking for the Pilatus 100k of OPLS beamline at BNL + """ + + MAX_SHAPE = (619, 487) + + def calc_mask(self, bs=None, bs_kind=None, optional_mask=None): + """ + :param bs: (string) This is the beamstop position on teh detctor (teh pixels behind will be mask inherently) + :param bs_kind: (string) What beamstop is in: Only need to be defined if pindiode which have a different shape) + :param optional_mask: (string) This is usefull for tender x-ray energy and will add extra max at the chips junction + :return: (a 2D array) A mask array with 0 and 1 with 0s where the image will be masked + """ + mask = np.logical_not(np.zeros(self.MAX_SHAPE)) + mask[:, :5], mask[:, -5:], mask[:5, :], mask[-5:, :] = ( + False, + False, + False, + False, + ) + + # Hot pixels needs to be defines + # mask[20, 884], mask[56, 754], mask[111, 620], mask[145, 733], mask[178, 528], mask[ + # 189, 571] = False, False, False, False, False, False + + # Beamstop + if bs == [0, 0]: + return np.logical_not(mask) + else: + mask[bs[1] :, bs[0] - 8 : bs[0] + 8] = False + return np.logical_not(mask) + + +class Pilatus1M_SMI(Pilatus1M): + """ + Pilatus 1M class inherited from the pyFAI Pilatus1M class + This class is used to add a specific masking for the Pilatus 1M of SMI beamline at BNL + """ + + def calc_mask(self, bs=None, bs_kind=None, optional_mask=None): + """ + :param bs: (string) This is the beamstop position on teh detctor (teh pixels behind will be mask inherently) + :param bs_kind: (string) What beamstop is in: Only need to be defined if pindiode which have a different shape) + :param optional_mask: (string) This is usefull for tender x-ray energy and will add extra max at the chips junction + :return: (a 2D array) A mask array with 0 and 1 with 0s where the image will be masked + """ + mask = np.logical_not(detectors.Pilatus1M().calc_mask()) + mask[:, :5], mask[:, -5:], mask[:5, :], mask[-5:, :] = ( + False, + False, + False, + False, + ) + + # Hot pixels + ( + mask[20, 884], + mask[56, 754], + mask[111, 620], + mask[145, 733], + mask[178, 528], + mask[189, 571], + ) = (False, False, False, False, False, False) + ( + mask[372, 462], + mask[454, 739], + mask[657, 947], + mask[869, 544], + mask[870, 546], + mask[870, 547], + ) = (False, False, False, False, False, False) + mask[870, 544], mask[871, 545], mask[871, 546], mask[871, 547] = ( + False, + False, + False, + False, + ) + + # For tender x-rays + if optional_mask == "tender": + i = 60 + while i < np.shape(mask)[0]: + if 480 < i < 530: + i = 554 + mask[:, i - 2 : i + 2] = False + i += 61 + + j = 97 + while j < np.shape(mask)[0]: + if 150 < j < 250: + j = 310 + elif 380 < j < 420: + j = 520 + elif 600 < j < 700: + j = 734 + elif 790 < j < 890: + j = 945 + mask[j - 2 : j + 2, :] = False + j += 100 + + # Beamstop + if bs == [0, 0]: + return np.logical_not(mask) + else: + mask[bs[1] :, bs[0] - 11 : bs[0] + 11] = False + if bs_kind == "pindiode": + mask[bs[1] : bs[1] + 40, bs[0] - 22 : bs[0] + 22] = False + return np.logical_not(mask) + + +class VerticalPilatus300kw(Pilatus300kw): + """ + VerticalPilatus300kw class inherited from the pyFAI Pilatus300kw class but rotated by 90 deg to fit the position of the WAXS detector at SMI + This class is used to add a specific masking for the Pilatus 300KW of SMI beamline at BNL + """ + + MAX_SHAPE = (1475, 195) + MODULE_SIZE = (487, 195) + MODULE_GAP = (7, 17) + aliases = ["Pilatus 300kw (Vertical)"] + + def calc_mask(self, bs, bs_kind=None, optional_mask=None): + """ + :param bs: (string) This is the beamstop position on teh detctor (teh pixels behind will be mask inherently) + :param bs_kind: (string) Not used for now but can be used if different beamstop are used + :param optional_mask: (string) This is usefull for tender x-ray energy and will add extra max at the chips junction + :return: (a 2D array) A mask array with 0 and 1 with 0s where the image will be masked + """ + mask = np.rot90(np.logical_not(detectors.Pilatus300kw().calc_mask()), 1) + + # border of the detector and chips + mask[:, :5], mask[:, -5:], mask[:5, :], mask[-5:, :] = ( + False, + False, + False, + False, + ) + mask[486, :], mask[494, :], mask[980, :], mask[988, :] = ( + False, + False, + False, + False, + ) + + # Dead pixel + dead_pix_x = [ + 1386, + 1387, + 1386, + 1387, + 228, + 307, + 733, + 733, + 792, + 1211, + 1211, + 1231, + 1232, + 1276, + 1321, + 1366, + 1405, + 1467, + 1355, + 1372, + 1356, + ] + dead_pix_y = [ + 96, + 96, + 97, + 97, + 21, + 67, + 170, + 171, + 37, + 109, + 110, + 74, + 74, + 57, + 81, + 181, + 46, + 188, + 85, + 89, + 106, + ] + for d_x, d_y in zip(dead_pix_x, dead_pix_y): + mask[d_x, d_y] = False + + # Hot pixels + mask[1314, 81] = False + mask[732, 7], mask[732, 8], mask[733, 8], mask[733, 7], mask[733, 9] = ( + False, + False, + False, + False, + False, + ) + mask[1314, 82], mask[1315, 81] = False, False + + mask[674, 133], mask[674, 134], mask[1130, 20], mask[1239, 50] = ( + False, + False, + False, + False, + ) + + # For tender x-rays + if optional_mask == "tender": + mask[:, 92:102] = False + i = 59 + while i < np.shape(mask)[0]: + if 450 < i < 550: + i = 553 + elif 970 < i < 1000: + i = 1047 + mask[1475 - i - 6 : 1475 - i, :] = False + i += 61 + + # Beamstop + if bs == [0, 0]: + return np.logical_not(mask) + else: + mask[bs[1] :, bs[0] - 8 : bs[0] + 8] = False + return np.logical_not(mask) + + +class VerticalPilatus900kw(Pilatus900k): + """ + VerticalPilatus900kw class inherited from the pyFAI Pilatus300k class but rotated by 90 deg to fit the position of the WAXS detector at SMI + This class is used to add a specific masking for the Pilatus 900KW of SMI beamline at BNL + """ + + MAX_SHAPE = (1475, 195) + MODULE_SIZE = (487, 195) + + aliases = ["Pilatus 900kw (Vertical)"] + + def calc_mask(self, bs, bs_kind=None, module=0, optional_mask=None): + """ + :param bs: (string) This is the beamstop position on teh detector (teh pixels behind will be mask inherently) + :param bs_kind: (string) Not used for now but can be used if different beamstop are used + :param optional_mask: (string) This is useful for tender x-ray energy and will add extra max at the chips junction + :return: (a 2D array) A mask array with 0 and 1 with 0s where the image will be masked + """ + mask = np.rot90(np.logical_not(Pilatus900k().calc_mask()), 1) + + # Border of detector + mask[:, :5], mask[:, -5:], mask[:5, :], mask[-5:, :] = ( + False, + False, + False, + False, + ) + + # Hot pixels + mask[15:19, 281:285] = False + mask[305:309, 566:570] = False + mask[292:296, 571:575] = False + mask[305:309, 1108:1113] = False + mask[401:403, 579:581] = False + mask[182:184, 259:261] = False + mask[19:21, 287:289] = False + mask[1291:1294, 303:305] = False + mask[1254, 259] = False + + if optional_mask == "tender": + # vertical gaps of pilatus for each module + mask[:, 92:102] = False + mask[:, 304:314] = False + mask[:, 516:526] = False + + # horizontal gaps of pilatus for each module + i = 59 + while i < np.shape(mask)[0]: + if 450 < i < 550: + i = 553 + elif 970 < i < 1000: + i = 1047 + mask[1475 - i - 6 : 1475 - i, :] = False + i += 61 + + # Beamstop + mask[bs[1] :, bs[0] - 8 : bs[0] + 8] = False + mask = np.logical_not(mask) + return mask + + +# TODO: define rayonix class + + +class rayonix(Detector): + """ + Rayonix detector: generic description containing mask algorithm + + Nota: 1920x1920 pixels, 0.109mm pixel size + """ + + def __init__(self, pixel1=109e-6, pixel2=109e-6, max_shape=None): + Detector.__init__(self, pixel1=pixel1, pixel2=pixel2, max_shape=max_shape) + + def __repr__(self): + return "Detector %s\t PixelSize= %.3e, %.3e m" % ( + self.name, + self._pixel1, + self._pixel2, + ) + + +class Rayonix(rayonix): + MAX_SHAPE = (1920, 1920) + aliases = ["rayonix"] + + def calc_mask(self, bs, bs_kind=None, img=None, threshold=15): + if img is None: + mask = True + else: + mask = np.ones_like(img, dtype=bool) + mask[:, :5], mask[:, -5:], mask[:5, :], mask[-5:, :] = ( + False, + False, + False, + False, + ) + mask[np.where(img < threshold)] = False + + return np.logical_not(mask) + + +class Eiger1M_xeuss(Eiger1M): + """ + Eiger1M class inherited from the pyFAI Eiger1M class + This class is used to add a specific masking for the Eiger1M of the xeuss instru at CEA + """ + + aliases = ["Eiger1M_xeuss"] + + def calc_mask(self, img): + """ + :return: (a 2D array) A mask array with 0 and 1 with 0s where the image will be masked + """ + + mask = np.logical_not(np.zeros(np.shape(img))) + self.shape = np.shape(img) + mask[np.where(img < -0.5)] = False + + mask[:, :5], mask[:, -5:], mask[:5, :], mask[-5:, :] = ( + False, + False, + False, + False, + ) + + return np.logical_not(mask) + + +class Eiger500k_xeuss(Eiger500k): + """ + Eiger1M class inherited from the pyFAI Eiger1M class + This class is used to add a specific masking for the Eiger1M of the xeuss instru at CEA + """ + + aliases = ["Eiger500k_xeuss"] + MAX_SHAPE = (1030, 514) + + def calc_mask(self, img): + """ + :return: (a 2D array) A mask array with 0 and 1 with 0s where the image will be masked + """ + + mask = np.logical_not(np.zeros(np.shape(img))) + self.shape = np.shape(img) + mask[np.where(img < -0.5)] = False + + mask[:, :5], mask[:, -5:], mask[:5, :], mask[-5:, :] = ( + False, + False, + False, + False, + ) + + return np.logical_not(mask) diff --git a/src/arroyogisaxs/one_d_reduction/operator.py b/src/arroyogisaxs/one_d_reduction/operator.py index cac7cf3..56893b9 100644 --- a/src/arroyogisaxs/one_d_reduction/operator.py +++ b/src/arroyogisaxs/one_d_reduction/operator.py @@ -3,43 +3,62 @@ from arroyopy.operator import Operator from ..kv_store import KVStore -from ..schemas import GISAXS1DReduction, GISAXSRawEvent, GISAXSRawStart, GISAXSRawStop +from ..schemas import GISAXS1DReduction, GISAXSRawEvent, GISAXSStart, GISAXSStop +from .detector import VerticalPilatus900kw from .reduce import pixel_roi_horizontal_cut logger = logging.getLogger(__name__) +REDUCTION_CONFIG_KEY = "reduction_config" + class OneDReductionOperator(Operator): def __init__(self, kv_store: KVStore): super().__init__() self.kv_store = kv_store + self.current_scan_metadata = None + self.mask = None async def process(self, message): - if isinstance(message, GISAXSRawStart): + if isinstance(message, GISAXSStart): logger.info(f"Processing Start {message}") - self.current_reduction_settings = get_reduction_settings() - logger.info("") + self.current_scan_metadata = message + logger.info("Calculating mask") + reduction_settings = self.kv_store.get_json(REDUCTION_CONFIG_KEY) + beamstop = ( + reduction_settings.get("beamcenter_x"), + reduction_settings.get("beamcenter_y"), + ) + self.mask = VerticalPilatus900kw().calc_mask(beamstop).astype(int) await self.publish(message) - if isinstance(message, GISAXSRawStop): + if isinstance(message, GISAXSStop): logger.info(f"Processing Stop {message}") + self.current_scan_metadata = None + self.current_reduction_settings = None await self.publish(message) if isinstance(message, GISAXSRawEvent): - # For now - reduction = pixel_roi_horizontal_cut(**self.current_reduction_settings) + reduction_settings = self.kv_store.get_json(REDUCTION_CONFIG_KEY) + if reduction_settings is None: + logger.error("No reduction settings found") + return + reduction_settings.pop("input_uri_data") + reduction_settings.pop("input_uri_mask") + masked_image = message.image.array + self.mask + reduction_settings["masked_image"] = masked_image + reduction = pixel_roi_horizontal_cut(**reduction_settings) + # reduction_msg = GISAXS1DReduction( - curve=reduction, + curve=reduction[ + 0 + ], # just the qparrallel, not the cut_average or errors curve_tiled_url=self.current_run_url, raw_frame=message.image, - raw_frame_tiled_url=self.current_run_url, + raw_frame_tiled_url=message.tiled_url, ) await self.publish(reduction_msg) @classmethod def create(cls, kv_store: KVStore) -> "OneDReductionOperator": return cls(kv_store) - - -def get_reduction_settings() -> dict: - return dict() diff --git a/src/arroyogisaxs/schemas.py b/src/arroyogisaxs/schemas.py index 619263d..bb45304 100644 --- a/src/arroyogisaxs/schemas.py +++ b/src/arroyogisaxs/schemas.py @@ -54,7 +54,7 @@ class GISAXSMessage(Message): pass -class GISAXSRawStart(Start, GISAXSMessage): +class GISAXSStart(Start, GISAXSMessage): msg_type: str = "start" width: int height: int @@ -75,9 +75,16 @@ class GISAXSRawEvent(Event, GISAXSMessage): msg_type: str = "event" image: SerializableNumpyArrayModel frame_number: int + tiled_url: str + + +class GISAXSLatentSpaceEvent(Event, GISAXSMessage): + tiled_url: str + feature_vector: list[float] + index: int -class GISAXSRawStop(Stop, GISAXSMessage): +class GISAXSStop(Stop, GISAXSMessage): """ { "msg_type": "stop", diff --git a/src/arroyogisaxs/tiled.py b/src/arroyogisaxs/tiled.py index 66552f6..b42496e 100644 --- a/src/arroyogisaxs/tiled.py +++ b/src/arroyogisaxs/tiled.py @@ -7,7 +7,7 @@ from arroyopy.publisher import Publisher from tiled.client.container import Container -from .schemas import GISAXS1DReduction, GISAXSRawEvent, GISAXSRawStart, GISAXSRawStop +from .schemas import GISAXS1DReduction, GISAXSRawEvent, GISAXSStart, GISAXSStop class Tiled1DPublisher(Publisher): @@ -39,7 +39,7 @@ async def start(self): # current and send GISAXSStart message if current_tiled_run is None: current_tiled_run = most_recent_run(self.beamline_runs_tiled) - start_message = GISAXSRawStart( + start_message = GISAXSStart( width=current_tiled_run.width, height=current_tiled_run.height, data_type=current_tiled_run.data_type, @@ -49,7 +49,7 @@ async def start(self): # If run has stop document, send GISAXSStop message and # set_current_run to None, sent_frames to [] and continue if current_tiled_run.has_stop_document(): - stop_message = GISAXSRawStop(num_frames=len(sent_frames)) + stop_message = GISAXSStop(num_frames=len(sent_frames)) await self.operator.process(stop_message) current_tiled_run = None sent_frames = [] diff --git a/src/arroyogisaxs/websockets.py b/src/arroyogisaxs/websockets.py index 6f1d99a..f0bbe86 100644 --- a/src/arroyogisaxs/websockets.py +++ b/src/arroyogisaxs/websockets.py @@ -8,7 +8,7 @@ import websockets from arroyopy.publisher import Publisher -from .schemas import GISAXSRawEvent, GISAXSRawStart, GISAXSRawStop +from .schemas import GISAXSRawEvent, GISAXSStart, GISAXSStop logger = logging.getLogger(__name__) @@ -58,15 +58,15 @@ async def publish_ws( self, # client: websockets.client.ClientConnection, client, - message: Union[GISAXSRawEvent | GISAXSRawStart | GISAXSRawStop], + message: Union[GISAXSRawEvent | GISAXSStart | GISAXSStop], ) -> None: - if isinstance(message, GISAXSRawStop): + if isinstance(message, GISAXSStop): logger.info(f"WS Sending Stop {message}") self.current_start_message = None await client.send(json.dumps(message.model_dump())) return - if isinstance(message, GISAXSRawStart): + if isinstance(message, GISAXSStart): self.current_start_message = message logger.info(f"WS Sending Start {message}") await client.send(json.dumps(message.model_dump())) @@ -189,7 +189,7 @@ async def main(publisher: OneDWSResultPublisher): await asyncio.gather(publisher.start(), test_client(publisher)) -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - publisher = OneDWSResultPublisher(host="localhost", port=8001) - asyncio.run(main(publisher)) +# if __name__ == "__main__": +# logging.basicConfig(level=logging.INFO) +# publisher = GISAXSWSResultPublisher(host="0.0.0.0", port=8001) +# asyncio.run(main(publisher)) diff --git a/src/arroyogisaxs/zmq.py b/src/arroyogisaxs/zmq.py index 613123e..3fc44f4 100644 --- a/src/arroyogisaxs/zmq.py +++ b/src/arroyogisaxs/zmq.py @@ -11,15 +11,15 @@ from .schemas import ( GISAXSMessage, GISAXSRawEvent, - GISAXSRawStart, - GISAXSRawStop, + GISAXSStart, + GISAXSStop, SerializableNumpyArrayModel, ) logger = logging.getLogger(__name__) -class ZMQPubSubListener(Listener): +class ZMQFrameListener(Listener): """ Takes messages from ZQM and deserializes them into GISAXSMessage objects """ @@ -37,8 +37,9 @@ async def start(self): message_type = message.get("msg_type") if message_type == "start": logger.info(f"Received Start {message}") - message = GISAXSRawStart(**message) + message = GISAXSStart(**message) elif message_type == "event": + # logger.debug("Received event") image = SerializableNumpyArrayModel.deserialize_array( message["image"] ) @@ -46,23 +47,26 @@ async def start(self): message = GISAXSRawEvent(**message) elif message_type == "stop": logger.info(f"Received Stop {message}") - message = GISAXSRawStop(**message) + message = GISAXSStop(**message) else: logger.error(f"Unknown message type {message_type}") continue await self.operator.process(message) except Exception as e: - logger.error(f"Error processing message: {e}") + logger.exception(f"Error processing message: {e}") async def stop(self): pass @classmethod - def from_settings(cls, settings: dict, operator: Operator) -> "ZMQPubSubListener": + def from_settings(cls, settings: dict, operator: Operator) -> "ZMQFrameListener": context = Context() zmq_socket = context.socket(zmq.SUB) zmq_socket.connect(settings.zmq_address) zmq_socket.setsockopt_string(zmq.SUBSCRIBE, "") + zmq_socket.setsockopt(zmq.SNDHWM, 10000) # Allow up to 10,000 messages + zmq_socket.setsockopt(zmq.RCVHWM, 10000) + logger.info(f"Listening for frames on {settings.zmq_address}") return cls(operator, zmq_socket) @@ -71,7 +75,7 @@ def __init__(self, zmq_socket: Socket): self.zmq_socket = zmq_socket async def publish(self, message: GISAXSMessage) -> None: - if isinstance(message, GISAXSRawStart) or isinstance(message, GISAXSRawStop): + if isinstance(message, GISAXSStart) or isinstance(message, GISAXSStop): message = msgpack.packb(message, use_bin_type=True) await self.zmq_socket.send(message) if isinstance(message, GISAXSRawEvent): @@ -112,12 +116,12 @@ async def start(self): logger.info(f"Dealer address: {self.zmq_dealer_address}") logger.info(f"Router address: {self.zmq_router_address}") context = zmq.asyncio.Context() - frontend_router = context.socket(zmq.ROUTER) - backend_dealer = context.socket(zmq.DEALER) - backend_dealer.bind(self.zmq_dealer_address) # Accept request from clients - frontend_router.bind(self.zmq_router_address) # Distribute requests to workers - zmq.proxy(frontend_router, backend_dealer) - logger.info("Proxy started") + router_socket = context.socket(zmq.ROUTER) + dealte_socket = context.socket(zmq.DEALER) + dealte_socket.bind(self.zmq_dealer_address) # Accept request from clients + router_socket.bind(self.zmq_router_address) # Distribute requests to workers + logger.info("Starting Proxy") + zmq.proxy(router_socket, dealte_socket) @classmethod def from_settings(cls, settings: dict) -> "ZMQBroker":