diff --git a/.github/workflows/ci-pages.yml b/.github/workflows/ci-pages.yml index 00e7a96f4..fbecb3498 100644 --- a/.github/workflows/ci-pages.yml +++ b/.github/workflows/ci-pages.yml @@ -50,7 +50,7 @@ jobs: with: fetch-depth: 0 # otherwise, you will fail to push refs to dest repo - - uses: prefix-dev/setup-pixi@v0.9.6 # https://github.com/prefix-dev/setup-pixi + - uses: prefix-dev/setup-pixi@v0.10.0 # https://github.com/prefix-dev/setup-pixi with: pixi-version: v0.68.0 environments: docs @@ -79,4 +79,4 @@ jobs: publish_dir: ./docs/_build/html/ # Rewrite gh-pages as a single orphan commit each publish. The site # is multi-MB; without this the branch history grows unbounded. - force_orphan: true \ No newline at end of file + force_orphan: true diff --git a/.github/workflows/pr-profiling.yml b/.github/workflows/pr-profiling.yml index 627671883..a11f65f6b 100644 --- a/.github/workflows/pr-profiling.yml +++ b/.github/workflows/pr-profiling.yml @@ -1,7 +1,7 @@ name: PR Profiling on: - pull_request_target: + pull_request: branches: - main paths: @@ -19,8 +19,8 @@ jobs: name: Profile and Track Performance runs-on: ubuntu-latest permissions: - pull-requests: write contents: read + pull-requests: write steps: - uses: actions/checkout@v7 with: @@ -29,7 +29,7 @@ jobs: persist-credentials: false - name: Setup Pixi - uses: prefix-dev/setup-pixi@v0.9.6 + uses: prefix-dev/setup-pixi@v0.10.0 with: pixi-version: v0.68.0 environments: profile @@ -53,7 +53,7 @@ jobs: - name: Save Profiling History uses: actions/cache/save@v6 - if: always() + if: always() && github.event.pull_request.head.repo.full_name == github.repository with: path: output/profiling_history.json key: profiling-history-${{ github.event.pull_request.number }}-${{ github.run_id }} @@ -72,9 +72,10 @@ jobs: - name: Post Comment uses: thollander/actions-comment-pull-request@v3 + if: github.event.pull_request.head.repo.full_name == github.repository with: file-path: output/profiling_comment.md comment-tag: profiling-results mode: upsert create-if-not-exists: true - github-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index a0e5f6997..4ef4fd105 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -19,7 +19,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha || github.ref }} - - uses: prefix-dev/setup-pixi@v0.9.6 # https://github.com/prefix-dev/setup-pixi + - uses: prefix-dev/setup-pixi@v0.10.0 # https://github.com/prefix-dev/setup-pixi with: pixi-version: v0.68.0 cache: true @@ -41,11 +41,11 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha || github.ref }} - - uses: prefix-dev/setup-pixi@v0.9.6 + - uses: prefix-dev/setup-pixi@v0.10.0 with: pixi-version: v0.68.0 environments: tests cache: true - name: Runs Tests - run: pixi run -e tests test \ No newline at end of file + run: pixi run -e tests test diff --git a/CHANGELOG.md b/CHANGELOG.md index b2c7e3b48..a7301efc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # CHANGELOG +## Unreleased + +### Feature + +* feat: add collapsible DNV capacity results sidecar to the viewer + + ## v0.28.0 (2026-07-03) diff --git a/deploy/helm/adapy-viewer/README.md b/deploy/helm/adapy-viewer/README.md index e53a3fad6..628359ec5 100644 --- a/deploy/helm/adapy-viewer/README.md +++ b/deploy/helm/adapy-viewer/README.md @@ -1,4 +1,4 @@ -# adapy-viewer Helm chart + # adapy-viewer Helm chart Deploys the hosted ada-py viewer: a single-pod web app that loads 3D model files (FEA, CAD, GLB) from S3-compatible storage and renders @@ -59,6 +59,53 @@ helm install viewer ./deploy/helm/adapy-viewer \ --set storage.local.existingClaim=my-pvc ``` +## DNV-RP-C201 Capacity Result Sidecars + +The viewer can visualize DNV-RP-C201 capacity results for SIN-derived FEA +models when a capacity sidecar is available. The normal SIN bake still provides +the geometry and FE result fields; the sidecar adds capacity model membership, +usage factors, check details, and per-element visual fields. + +Preferred colocated artefacts: + +```text +model.SIN +_derived/model.SIN.fea/fea.manifest.json +_derived/model.SIN.fea/capacity.results.json +``` + +The manifest may also advertise a sidecar explicitly: + +```json +{ + "capacity": { + "version": 1, + "results_url": "capacity.results.json", + "default_run_id": "run-001", + "field_strategy": "json" + } +} +``` + +If the manifest has no `capacity` section, the frontend tries these fallback +locations: + +- `_derived/.fea/capacity.results.json` +- `.c201.json` +- `.capacity.json` +- `capacity.results.json` next to the source + +Generate the sidecar from the DNV-RP-C201 package: + +```bash +run-codecheck --sin model.SIN --group Mini_area_dbl_btm --export-viewer out/ +``` + +When the sidecar is found, the Capacity panel appears in the simulation +controls. It supports Definition and Results modes, result-case and metric +selection, failed-only filtering, usage-factor coloring, capacity boundary +outlines, and element-pick to capacity-model selection. + ## Ingress ```yaml diff --git a/pixi.lock b/pixi.lock index 53af9b383..bdc0b66b0 100644 --- a/pixi.lock +++ b/pixi.lock @@ -1014,11 +1014,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.19.1-hf2c6c5f_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.1.0-hd936e49_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.5-haf901d7_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h8455456_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h0adab6e_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-1.88.0-h9dfe17d_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-devel-1.88.0-h1c1089f_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-headers-1.88.0-h57928b3_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a8eebe_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.20.0-h8206538_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_0.conda @@ -1030,13 +1030,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libgfortran5-15.2.0-h44d81a7_19.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.88.1-h7ce1215_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h8ee18e1_19.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.13.0-default_h049141e_1000.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.4.1-hfd05255_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hd232482_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libmed-4.2-nompi_hbaf3de0_28.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libopenblas-0.3.33-pthreads_h877e47f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libraw-0.22.1-h345c428_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.22-h6a83c73_1.conda @@ -1055,11 +1055,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.1-py312h2f35c63_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mpfr-4.2.2-h883a981_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.4.6-py312ha3f287d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/occt-7.9.3-novtk_h1b358ef_103.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openexr-3.4.12-h7faf11f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h0e57b4f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjph-0.27.3-hf13a347_0.conda @@ -1089,7 +1087,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/shapely-2.1.2-py312h37f46ab_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/simplejson-4.1.1-py312he06e257_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-hd3d4ead_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.6-py312he06e257_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda @@ -1962,11 +1959,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.19.1-hf2c6c5f_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.1.0-hd936e49_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.5-haf901d7_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h8455456_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h0adab6e_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-1.88.0-h9dfe17d_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-devel-1.88.0-h1c1089f_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-headers-1.88.0-h57928b3_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a8eebe_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.20.0-h8206538_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_0.conda @@ -1978,13 +1975,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libgfortran5-15.2.0-h44d81a7_19.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.88.1-h7ce1215_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h8ee18e1_19.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.13.0-default_h049141e_1000.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.4.1-hfd05255_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hd232482_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libmed-4.2-nompi_hbaf3de0_28.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libopenblas-0.3.33-pthreads_h877e47f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libraw-0.22.1-h345c428_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.22-h6a83c73_1.conda @@ -2003,11 +2000,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.1-py312h2f35c63_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mpfr-4.2.2-h883a981_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.4.6-py312ha3f287d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/occt-7.9.3-novtk_h1b358ef_103.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openexr-3.4.12-h7faf11f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h0e57b4f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjph-0.27.3-hf13a347_0.conda @@ -2036,7 +2031,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/shapely-2.1.2-py312h37f46ab_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/simplejson-4.1.1-py312he06e257_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-hd3d4ead_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.6-py312he06e257_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda @@ -2620,11 +2614,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.19.1-hf2c6c5f_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.1.0-hd936e49_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.5-haf901d7_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h8455456_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h0adab6e_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-1.88.0-h9dfe17d_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-devel-1.88.0-h1c1089f_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-headers-1.88.0-h57928b3_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a8eebe_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.20.0-h8206538_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_0.conda @@ -2636,13 +2630,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libgfortran5-15.2.0-h44d81a7_19.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.88.1-h7ce1215_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h8ee18e1_19.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.13.0-default_h049141e_1000.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.4.1-hfd05255_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hd232482_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libmed-4.2-nompi_hbaf3de0_28.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libopenblas-0.3.33-pthreads_h877e47f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libraw-0.22.1-h345c428_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.1-hf5d6505_0.conda @@ -2660,11 +2654,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.1-py312h2f35c63_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mpfr-4.2.2-h883a981_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.4.6-py312ha3f287d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/occt-7.9.3-novtk_h1b358ef_103.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openexr-3.4.12-h7faf11f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h0e57b4f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjph-0.27.3-hf13a347_0.conda @@ -2689,7 +2681,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/shapely-2.1.2-py312h37f46ab_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/simplejson-4.1.1-py312he06e257_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-hd3d4ead_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_38.conda @@ -3864,11 +3855,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.19.1-hf2c6c5f_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.1.0-hd936e49_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.5-haf901d7_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h8455456_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h0adab6e_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-1.88.0-h9dfe17d_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-devel-1.88.0-h1c1089f_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-headers-1.88.0-h57928b3_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a8eebe_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.20.0-h8206538_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_0.conda @@ -3878,14 +3869,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.2.0-h8ee18e1_19.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.88.1-h7ce1215_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h8ee18e1_19.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.13.0-default_h049141e_1000.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.4.1-hfd05255_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hd232482_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libmed-4.2-nompi_hbaf3de0_28.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libnetcdf-4.10.0-nompi_h3948bcf_104.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libopenblas-0.3.33-pthreads_h877e47f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libraw-0.22.1-h345c428_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.22-h6a83c73_1.conda @@ -3904,12 +3895,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.1-py312h2f35c63_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mpfr-4.2.2-h883a981_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/netcdf4-1.7.4-nompi_py311h7adff93_107.conda - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.4.6-py312ha3f287d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/occt-7.9.3-novtk_h1b358ef_103.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openexr-3.4.12-h7faf11f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h0e57b4f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjph-0.27.3-hf13a347_0.conda @@ -3934,7 +3923,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/shapely-2.1.2-py312h37f46ab_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/simplejson-4.1.1-py312he06e257_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-hd3d4ead_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.6-py312he06e257_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda @@ -4792,11 +4780,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.19.1-hf2c6c5f_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.1.0-hd936e49_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.5-haf901d7_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h8455456_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h0adab6e_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-1.88.0-h9dfe17d_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-devel-1.88.0-h1c1089f_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-headers-1.88.0-h57928b3_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a8eebe_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.20.0-h8206538_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_0.conda @@ -4806,14 +4794,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.2.0-h8ee18e1_19.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.88.1-h7ce1215_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h8ee18e1_19.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.13.0-default_h049141e_1000.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.4.1-hfd05255_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hd232482_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libmed-4.2-nompi_hbaf3de0_28.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libnetcdf-4.10.0-nompi_h3948bcf_104.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libopenblas-0.3.33-pthreads_h877e47f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libraw-0.22.1-h345c428_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.22-h6a83c73_1.conda @@ -4832,12 +4820,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.1-py312h2f35c63_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mpfr-4.2.2-h883a981_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/netcdf4-1.7.4-nompi_py311h7adff93_107.conda - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.4.6-py312ha3f287d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/occt-7.9.3-novtk_h1b358ef_103.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openexr-3.4.12-h7faf11f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h0e57b4f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjph-0.27.3-hf13a347_0.conda @@ -4866,7 +4852,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/shapely-2.1.2-py312h37f46ab_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/simplejson-4.1.1-py312he06e257_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-hd3d4ead_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.6-py312he06e257_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda @@ -5623,11 +5608,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.19.1-hf2c6c5f_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.1.0-hd936e49_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.5-haf901d7_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h8455456_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h0adab6e_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-1.88.0-h9dfe17d_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-devel-1.88.0-h1c1089f_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-headers-1.88.0-h57928b3_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a8eebe_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.20.0-h8206538_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_0.conda @@ -5637,13 +5622,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.2.0-h8ee18e1_19.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.88.1-h7ce1215_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h8ee18e1_19.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.13.0-default_h049141e_1000.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.4.1-hfd05255_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hd232482_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libmed-4.2-nompi_hbaf3de0_28.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libopenblas-0.3.33-pthreads_h877e47f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libraw-0.22.1-h345c428_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.22-h6a83c73_1.conda @@ -5661,11 +5646,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.1-py312h2f35c63_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mpfr-4.2.2-h883a981_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.4.6-py312ha3f287d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/occt-7.9.3-novtk_h1b358ef_103.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openexr-3.4.12-h7faf11f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h0e57b4f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjph-0.27.3-hf13a347_0.conda @@ -5690,7 +5673,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/shapely-2.1.2-py312h37f46ab_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/simplejson-4.1.1-py312he06e257_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-hd3d4ead_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.6-py312he06e257_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda @@ -6471,11 +6453,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.19.1-hf2c6c5f_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.1.0-hd936e49_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.5-haf901d7_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h8455456_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h0adab6e_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-1.88.0-h9dfe17d_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-devel-1.88.0-h1c1089f_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-headers-1.88.0-h57928b3_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a8eebe_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.20.0-h8206538_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_0.conda @@ -6485,13 +6467,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.2.0-h8ee18e1_19.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.88.1-h7ce1215_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h8ee18e1_19.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.13.0-default_h049141e_1000.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.4.1-hfd05255_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hd232482_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libmed-4.2-nompi_hbaf3de0_28.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libopenblas-0.3.33-pthreads_h877e47f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libraw-0.22.1-h345c428_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.22-h6a83c73_1.conda @@ -6509,11 +6491,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.1-py312h2f35c63_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mpfr-4.2.2-h883a981_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.4.6-py312ha3f287d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/occt-7.9.3-novtk_h1b358ef_103.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openexr-3.4.12-h7faf11f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h0e57b4f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjph-0.27.3-hf13a347_0.conda @@ -6539,7 +6519,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/shapely-2.1.2-py312h37f46ab_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/simplejson-4.1.1-py312he06e257_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-hd3d4ead_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.6-py312he06e257_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda @@ -7436,11 +7415,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.19.1-hf2c6c5f_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.1.0-hd936e49_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.5-haf901d7_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h8455456_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h0adab6e_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-1.88.0-h9dfe17d_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-devel-1.88.0-h1c1089f_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-headers-1.88.0-h57928b3_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a8eebe_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.20.0-h8206538_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_0.conda @@ -7450,13 +7429,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.2.0-h8ee18e1_19.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.88.1-h7ce1215_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h8ee18e1_19.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.13.0-default_h049141e_1000.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.4.1-hfd05255_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hd232482_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libmed-4.2-nompi_hbaf3de0_28.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libopenblas-0.3.33-pthreads_h877e47f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libraw-0.22.1-h345c428_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.22-h6a83c73_1.conda @@ -7474,11 +7453,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.1-py312h2f35c63_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mpfr-4.2.2-h883a981_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.4.6-py312ha3f287d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/occt-7.9.3-novtk_h1b358ef_103.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openexr-3.4.12-h7faf11f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h0e57b4f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjph-0.27.3-hf13a347_0.conda @@ -7507,7 +7484,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/shapely-2.1.2-py312h37f46ab_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/simplejson-4.1.1-py312he06e257_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-hd3d4ead_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.6-py312he06e257_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda @@ -8632,14 +8608,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.19.1-hf2c6c5f_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.1.0-hd936e49_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.5-haf901d7_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h8455456_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h0adab6e_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-1.88.0-h9dfe17d_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-devel-1.88.0-h1c1089f_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-headers-1.88.0-h57928b3_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.2.0-hfd05255_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.2.0-hfd05255_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.2.0-hfd05255_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a8eebe_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.20.0-h8206538_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_0.conda @@ -8655,12 +8631,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.4.1-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjxl-0.11.2-h932607e_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hd232482_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-devel-5.8.3-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libmed-4.2-nompi_hbaf3de0_28.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libnetcdf-4.10.0-nompi_h3948bcf_104.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libogg-1.3.5-h2466b09_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libopenblas-0.3.33-pthreads_h877e47f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libopus-1.6.1-h6a83c73_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libraw-0.22.1-h345c428_0.conda @@ -8687,14 +8664,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/matplotlib-base-3.10.9-py312h0ebf65c_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mesalib-25.0.5-hf8ad13a_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mpfr-4.2.2-h883a981_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/msgpack-python-1.1.2-py312hf90b1b7_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/multidict-6.7.1-py312h05f76fc_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/nlohmann_json-3.12.0-h5112557_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.4.6-py312ha3f287d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/occt-7.9.3-all_hf10db9b_203.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openexr-3.4.12-h7faf11f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openh264-2.6.0-hb17fa0b_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h0e57b4f_0.conda @@ -9871,14 +9846,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.19.1-hf2c6c5f_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.1.0-hd936e49_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.5-haf901d7_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h8455456_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h0adab6e_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-1.88.0-h9dfe17d_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-devel-1.88.0-h1c1089f_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-headers-1.88.0-h57928b3_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.2.0-hfd05255_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.2.0-hfd05255_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.2.0-hfd05255_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a8eebe_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.20.0-h8206538_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_0.conda @@ -9894,12 +9869,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.4.1-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjxl-0.11.2-h932607e_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hd232482_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-devel-5.8.3-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libmed-4.2-nompi_hbaf3de0_28.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libnetcdf-4.10.0-nompi_h3948bcf_104.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libogg-1.3.5-h2466b09_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libopenblas-0.3.33-pthreads_h877e47f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libopus-1.6.1-h6a83c73_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libraw-0.22.1-h345c428_0.conda @@ -9926,14 +9902,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/matplotlib-base-3.10.9-py312h0ebf65c_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mesalib-25.0.5-hf8ad13a_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mpfr-4.2.2-h883a981_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/msgpack-python-1.1.2-py312hf90b1b7_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/multidict-6.7.1-py312h05f76fc_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/nlohmann_json-3.12.0-h5112557_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.4.6-py312ha3f287d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/occt-7.9.3-all_hf10db9b_203.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openexr-3.4.12-h7faf11f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openh264-2.6.0-hb17fa0b_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h0e57b4f_0.conda @@ -10803,11 +10777,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.19.1-hf2c6c5f_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.1.0-hd936e49_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.5-haf901d7_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h8455456_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h0adab6e_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-1.88.0-h9dfe17d_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-devel-1.88.0-h1c1089f_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-headers-1.88.0-h57928b3_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a8eebe_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.20.0-h8206538_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_0.conda @@ -10817,13 +10791,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.2.0-h8ee18e1_19.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.88.1-h7ce1215_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h8ee18e1_19.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.13.0-default_h049141e_1000.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.4.1-hfd05255_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hd232482_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libmed-4.2-nompi_hbaf3de0_28.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libopenblas-0.3.33-pthreads_h877e47f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libraw-0.22.1-h345c428_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.1-hf5d6505_0.conda @@ -10840,12 +10814,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.1-py312h2f35c63_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mpfr-4.2.2-h883a981_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.4.6-py312ha3f287d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/obstore-0.10.0-py312hb0142fd_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/occt-7.9.3-novtk_h1b358ef_103.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openexr-3.4.12-h7faf11f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h0e57b4f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjph-0.27.3-hf13a347_0.conda @@ -10865,7 +10837,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.15.15-h45713df_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/shapely-2.1.2-py312h37f46ab_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-hd3d4ead_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_38.conda @@ -11743,11 +11714,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.19.1-hf2c6c5f_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.1.0-hd936e49_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.5-haf901d7_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h8455456_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h0adab6e_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-1.88.0-h9dfe17d_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-devel-1.88.0-h1c1089f_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-headers-1.88.0-h57928b3_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a8eebe_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.20.0-h8206538_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_0.conda @@ -11757,13 +11728,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.2.0-h8ee18e1_19.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.88.1-h7ce1215_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h8ee18e1_19.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.13.0-default_h049141e_1000.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.4.1-hfd05255_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hd232482_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libmed-4.2-nompi_hbaf3de0_28.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libopenblas-0.3.33-pthreads_h877e47f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libraw-0.22.1-h345c428_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.1-hf5d6505_0.conda @@ -11780,12 +11751,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.1-py312h2f35c63_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mpfr-4.2.2-h883a981_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.4.6-py312ha3f287d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/obstore-0.10.0-py312hb0142fd_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/occt-7.9.3-novtk_h1b358ef_103.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openexr-3.4.12-h7faf11f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h0e57b4f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjph-0.27.3-hf13a347_0.conda @@ -11812,7 +11781,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/shapely-2.1.2-py312h37f46ab_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/simplejson-4.1.1-py312he06e257_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-hd3d4ead_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_38.conda @@ -12431,11 +12399,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.19.1-hf2c6c5f_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.1.0-hd936e49_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.5-haf901d7_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h8455456_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h0adab6e_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-1.88.0-h9dfe17d_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-devel-1.88.0-h1c1089f_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-headers-1.88.0-h57928b3_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a8eebe_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.20.0-h8206538_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_0.conda @@ -12445,13 +12413,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.2.0-h8ee18e1_19.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.88.1-h7ce1215_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h8ee18e1_19.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.13.0-default_h049141e_1000.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.4.1-hfd05255_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hd232482_openblas.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libmed-4.2-nompi_hbaf3de0_28.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libopenblas-0.3.33-pthreads_h877e47f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libraw-0.22.1-h345c428_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.1-hf5d6505_0.conda @@ -12468,12 +12436,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.1-py312h2f35c63_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mpfr-4.2.2-h883a981_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.4.6-py312ha3f287d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/obstore-0.10.0-py312hb0142fd_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/occt-7.9.3-novtk_h1b358ef_103.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_908.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openexr-3.4.12-h7faf11f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h0e57b4f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjph-0.27.3-hf13a347_0.conda @@ -12500,7 +12466,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/shapely-2.1.2-py312h37f46ab_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/simplejson-4.1.1-py312he06e257_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-hd3d4ead_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_38.conda @@ -25621,6 +25586,7 @@ packages: - ucrt >=10.0.20348.0 license: BSD-3-Clause license_family: BSD + purls: [] size: 162939 timestamp: 1736084372191 - conda: https://conda.anaconda.org/conda-forge/win-64/asyncpg-0.31.0-py312he5662c2_1.conda @@ -25793,6 +25759,7 @@ packages: - liblapack >=3.9.0,<4.0a0 license: GPL-2.0-or-later license_family: GPL + purls: [] size: 2852768 timestamp: 1777053176511 - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py312he06e257_1.conda @@ -26665,6 +26632,30 @@ packages: purls: [] size: 34463 timestamp: 1769221960556 +- conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h0adab6e_openblas.conda + build_number: 8 + sha256: 4f1b32dbaf68bcf0aa54bf9a218d2bb99fcb70070a0ca7031548855004b0d545 + md5: a5ed6575e8cc7028c8327f1b5167caa6 + depends: + - libopenblas >=0.3.33,<0.3.34.0a0 + - libopenblas >=0.3.33,<1.0a0 + constrains: + - mkl <2027 + - libcblas 3.11.0 8*_openblas + - blas 2.308 openblas + - liblapack 3.11.0 8*_openblas + - liblapacke 3.11.0 8*_openblas + track_features: + - blas_openblas + - blas_openblas_2 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libblas >=3.11.0,<4.0a0 + size: 68058 + timestamp: 1779859607236 - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h8455456_mkl.conda build_number: 8 sha256: 43a87b59e6d4c68d80b2e4de487b1b54d66fe1f9a06636909b5a5ab9eae27269 @@ -26774,6 +26765,26 @@ packages: purls: [] size: 68443 timestamp: 1779859701498 +- conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a8eebe_openblas.conda + build_number: 8 + sha256: 15ed9a069e722471c159573b299d94762a516dde6ac9cd44e24b05dcde7d2416 + md5: 91b2c920695d5f69081279743858ecef + depends: + - libblas 3.11.0 8_h0adab6e_openblas + constrains: + - blas 2.308 openblas + - liblapack 3.11.0 8*_openblas + - liblapacke 3.11.0 8*_openblas + track_features: + - blas_openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libcblas >=3.11.0,<4.0a0 + size: 68309 + timestamp: 1779859619968 - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.20.0-h8206538_0.conda sha256: f4ce5aa835a698532feaa368e804365a7e45a9edebe006a8e1c80505d893c24e md5: 7bee27a8f0a295117ccb864f30d2d87e @@ -26876,6 +26887,7 @@ packages: - libgfortran-ng ==15.2.0=*_19 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL + purls: [] size: 51402 timestamp: 1778273257466 - conda: https://conda.anaconda.org/conda-forge/win-64/libgfortran5-15.2.0-h44d81a7_19.conda @@ -26888,6 +26900,7 @@ packages: - libgfortran 15.2.0 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL + purls: [] size: 2792696 timestamp: 1778273047082 - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.88.1-h7ce1215_2.conda @@ -26998,6 +27011,26 @@ packages: purls: [] size: 1194926 timestamp: 1777065171989 +- conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hd232482_openblas.conda + build_number: 8 + sha256: db0174b9786c13bf1c10c743252c1bad67ea9521ac8c6f65060f66a80102de31 + md5: 7363b5f4caa1312e2eff8a99b95e0ede + depends: + - libblas 3.11.0 8_h0adab6e_openblas + constrains: + - libcblas 3.11.0 8*_openblas + - blas 2.308 openblas + - liblapacke 3.11.0 8*_openblas + track_features: + - blas_openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - liblapack >=3.11.0,<3.12.0a0 + size: 80600 + timestamp: 1779859632574 - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hf9ab0e9_mkl.conda build_number: 8 sha256: 44999ed04bc0a56de44ee0ac8bd5b3702efd411a8b29491c0e3d3deb8619c94e @@ -27105,6 +27138,23 @@ packages: purls: [] size: 35040 timestamp: 1745826086628 +- conda: https://conda.anaconda.org/conda-forge/win-64/libopenblas-0.3.33-pthreads_h877e47f_0.conda + sha256: 104bba89eaed090ab7dd6de17376ba7c14be7f4d44807346012e50a493f18ec9 + md5: 51d4c0bc3695f63375d095004dc2597c + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - openblas >=0.3.33,<0.3.34.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libopenblas >=0.3.33,<1.0a0 + size: 4046868 + timestamp: 1776999773412 - conda: https://conda.anaconda.org/conda-forge/win-64/libopus-1.6.1-h6a83c73_0.conda sha256: c3678f111866235b44fa65265966abae7d90b6387178f1459afaedcee8b4a997 md5: 0ed21da5b6e3a0393e05762b3cce2878 @@ -27207,6 +27257,7 @@ packages: - libstdcxx-ng ==15.2.0=*_19 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL + purls: [] size: 6461114 timestamp: 1778273060138 - conda: https://conda.anaconda.org/conda-forge/win-64/libtheora-1.1.1-hc70643c_1006.conda diff --git a/pixi.toml b/pixi.toml index 47aa0572a..a5eba4e69 100644 --- a/pixi.toml +++ b/pixi.toml @@ -186,6 +186,10 @@ test-all = { cmd = "pytest tests --ignore=tests/comms/rest --ignore=tests/fem/vi python = "3.12.*" bcf-client = "*" numpy = "*" +# Force the OpenBLAS BLAS/LAPACK backend. The MKL 2026 preview that the +# solver otherwise picks (mkl_avx10.3) SEH-crashes (0xc06d007f) on np.linalg +# (svd/gemm) on Windows here, taking down the FEA bake and any svd path. +libblas = { version = "*", build = "*openblas" } hdf5 = "*" h5py = "*" ifcopenshell = "*" @@ -318,6 +322,13 @@ audit-repro = { cmd = "python scripts/audit_repro.py", env = { "PYTHONPATH" = "$ viewer-upload = { cmd = "python scripts/viewer_upload.py", env = { "PYTHONPATH" = "$PIXI_PROJECT_ROOT/src" } } glb-face-classify = { cmd = "python scripts/glb_face_classify.py", env = { "PYTHONPATH" = "$PIXI_PROJECT_ROOT/src" } } +# Windows: PYTHONPATH uses ';' as separator, so the ':'-joined overlay prefix above turns the whole +# value into one bogus path and `python -m ada.comms.rest` dies with ModuleNotFoundError. The +# /opt/adacpp-overlay prefix is a Linux-deploy concern only, so the win-64 override drops it. +[feature.viewer-api.target.win-64.tasks] +viewer-api = { cmd = "python -m ada.comms.rest", env = { "PYTHONPATH" = "$PIXI_PROJECT_ROOT/src" } } +viewer-worker = { cmd = "python -m ada.comms.rest.worker", env = { "PYTHONPATH" = "$PIXI_PROJECT_ROOT/src" } } + # NOTE: meshopt GLB compression (ada.visit.gltf.meshopt) no longer pulls the PyPI # `meshoptimizer` package — it is sdist-only (no wheels) so every viewer/worker image # tried to compile it from source and failed (no g++). The EXT_meshopt_compression diff --git a/src/ada/fem/capacity/__init__.py b/src/ada/fem/capacity/__init__.py new file mode 100644 index 000000000..93ae64af5 --- /dev/null +++ b/src/ada/fem/capacity/__init__.py @@ -0,0 +1,50 @@ +"""Capacity Manager — reconstruct DNV-RP-C201 capacity models from FE results. + +This subpackage reproduces what DNV Genie's *Capacity Manager* does: from a +meshed Sesam model (read via :mod:`ada.fem.formats.sesam.results.read_sin`) it +assembles *capacity models* (panel groups of plates + stiffeners), derives each +plate field's geometry / material / stiffener profile, and resolves the FE +membrane stresses into the design variables a DNV-RP-C201 buckling check needs. + +The output is a *neutral*, serializable :class:`~ada.fem.capacity.model.CapacityModel` +(plus per-result-case :class:`~ada.fem.capacity.model.ResolvedCase`). adapy does +**not** run the code check itself and has **no dependency** on any code-check +package; a downstream consumer (e.g. ``codecheck.dnv_rp_c201``) reads the neutral +output. A Genie-compatible ``model.json`` mirror can also be emitted for +field-by-field validation. + +See ``further_work/capacity_manager_adapy_stiffened_plate.md`` in the +``codecheck`` repo for the design and progress tracker. +""" + +from __future__ import annotations + +from ada.fem.capacity.manager import CapacityManager +from ada.fem.capacity.model import ( + CapacityModel, + CapMaterial, + CapPlate, + CapSection, + CapStiffener, + ResolvedCase, +) +from ada.fem.capacity.sources import ( + ModelJsonSource, + PanelGroupSource, + PanelGroupSpec, + SinSource, +) + +__all__ = [ + "CapacityManager", + "CapacityModel", + "CapMaterial", + "CapPlate", + "CapSection", + "CapStiffener", + "ResolvedCase", + "PanelGroupSource", + "PanelGroupSpec", + "ModelJsonSource", + "SinSource", +] diff --git a/src/ada/fem/capacity/extract.py b/src/ada/fem/capacity/extract.py new file mode 100644 index 000000000..c58049bc0 --- /dev/null +++ b/src/ada/fem/capacity/extract.py @@ -0,0 +1,465 @@ +"""Low-level FE extraction helpers for the capacity manager. + +These read the few records the high-level :class:`~ada.fem.results.common.Mesh` +does not surface (plate thickness ``GELTH``, eccentricity ``GECCEN``) directly +from the SIN via the low-level ``sin_reader`` — keeping ``ada`` core unchanged — +and provide geometry helpers (element node coordinates, edge spans) used to +derive plate-field dimensions and stiffener spans. +""" + +from __future__ import annotations + +import pathlib +from dataclasses import dataclass, field + +import numpy as np + +from ada.fem.capacity.model import CapSection, dominant_flange +from ada.fem.results.common import Mesh + + +@dataclass +class AuxRecords: + """SIN records not surfaced by :class:`Mesh`, keyed by ``geono``/id. + + Carries the plate thicknesses (``GELTH``), shell pressures (``BEUSLO``), + and, as a compatibility fallback, the stiffener cross-sections parsed + straight from the raw section cards (``GIORH`` / ``GBOX`` / ``GLSEC``). + """ + + thickness_by_geono: dict[int, float] = field(default_factory=dict) + section_by_geono: dict[int, CapSection] = field(default_factory=dict) + result_point_coords_by_element: dict[int, dict[int, np.ndarray]] = field(default_factory=dict) + element_transform_by_element: dict[int, np.ndarray] = field(default_factory=dict) + concept_name_by_element: dict[int, str] = field(default_factory=dict) + pressure_by_case_element: dict[int, dict[int, float]] = field(default_factory=dict) + + @classmethod + def from_sin(cls, sin_path: str | pathlib.Path) -> "AuxRecords": + from ada.fem.formats.sesam.results.sin_reader import open_sin + + sin = open_sin(sin_path) + try: + thickness: dict[int, float] = {} + if "GELTH" in sin.type_blocks: + # GELTH record: (GEONO, TH, ...) — GELREF1.geono of a shell + # element references this thickness entry. + for rec in sin.iter_records("GELTH"): + if len(rec) >= 2: + thickness[int(rec[0])] = float(rec[1]) + names = _section_names(sin) + sections = _parse_sections(sin, names) + result_points, transforms = _parse_rdpoints(sin) + return cls( + thickness_by_geono=thickness, + section_by_geono=sections, + result_point_coords_by_element=result_points, + element_transform_by_element=transforms, + concept_name_by_element=_parse_concept_names(sin), + pressure_by_case_element=_parse_shell_pressures(sin), + ) + finally: + sin.close() + + +# Genie SectionType code for Holland-profile / bulb-flat sections. +_BULB = 7 + + +def _section_names(sin) -> dict[int, str]: + out: dict[int, str] = {} + if "TDSECT" not in sin.type_blocks: + return out + for prefix, text in sin.iter_text_records("TDSECT"): + if prefix and text: + out[int(prefix[0])] = text + return out + + +def _parse_sections(sin, names: dict[int, str]) -> dict[int, CapSection]: + """Stiffener cross-sections by ``geono`` from the raw beam-section cards. + + * ``GIORH`` [geono, H, tw, Bt, Tt, Bb, Tb] — I/T girders. Genie encodes a + missing flange as a dummy (width == ``tw``, token thickness), and a + T-girder is an unsymmetrical I whose real flange can sit in *either* + slot, so the governing flange is picked via + :func:`~ada.fem.capacity.model.dominant_flange`. + * ``GLSEC`` [geono, H, tw, b, tf] — L-sections, incl. idealized bulb flats + (``HP*`` names → tagged as bulb so the consumer applies the bulb→angle + web-height rule). + * ``GBOX`` [geono, H, ...] — box; carried with zero flange. + """ + out: dict[int, CapSection] = {} + for rec in sin.iter_records("GIORH") if "GIORH" in sin.type_blocks else []: + g = int(rec[0]) + h, tw = float(rec[1]), float(rec[2]) + top = (float(rec[3]), float(rec[4])) + bottom = (float(rec[5]), float(rec[6])) if len(rec) >= 7 else (None, None) + bf, tf = dominant_flange(tw, top, bottom) + out[g] = CapSection(names.get(g, ""), 0, h, tw, bf, tf) + for rec in sin.iter_records("GLSEC") if "GLSEC" in sin.type_blocks else []: + g = int(rec[0]) + h, tw, b, tf = float(rec[1]), float(rec[2]), float(rec[3]), float(rec[4]) + is_bulb = names.get(g, "").upper().startswith("HP") + out[g] = CapSection(names.get(g, ""), _BULB if is_bulb else 0, h, tw, b, tf) + for rec in sin.iter_records("GBOX") if "GBOX" in sin.type_blocks else []: + g = int(rec[0]) + out[g] = CapSection(names.get(g, ""), 0, float(rec[1]), float(rec[2]), 0.0, 0.0) + return out + + +def _parse_rdpoints(sin) -> tuple[dict[int, dict[int, np.ndarray]], dict[int, np.ndarray]]: + """Result-point coordinates and element transforms by element id. + + SIN ``RDPOINTS`` stores absolute coordinates in the variable-width bulk + payload as ``point id, x, y, z`` groups, with optional ``-1`` separators, + followed by the element transform matrix. The public result field keeps only + the point number, so the capacity resolver surfaces the coordinates and + local-to-global stress basis here. + """ + if "RDPOINTS" not in sin.type_blocks: + return {}, {} + + out: dict[int, dict[int, np.ndarray]] = {} + transforms: dict[int, np.ndarray] = {} + for rec in sin.iter_records("RDPOINTS"): + if len(rec) < 9: + continue + elno = int(rec[1]) + nsptra = int(rec[6]) + transform_len = nsptra * 9 + bulk = rec[8:-transform_len] if transform_len else rec[8:] + if transform_len: + transform = np.array(rec[-transform_len:], dtype=float).reshape((nsptra, 3, 3)) + if len(transform): + transforms[elno] = transform[0] + points: dict[int, np.ndarray] = {} + i = 0 + while i < len(bulk): + point_id = int(bulk[i]) + i += 1 + if point_id == -1: + continue + if i + 2 >= len(bulk): + break + points[point_id] = np.array((float(bulk[i]), float(bulk[i + 1]), float(bulk[i + 2])), dtype=float) + i += 3 + if points: + out[elno] = points + return out, transforms + + +def _parse_concept_names(sin) -> dict[int, str]: + """Map FEM element ids to Sesam concept names from TDSCONC/SCONCEPT/SCONMESH.""" + if not all(name in sin.type_blocks for name in ("TDSCONC", "SCONCEPT", "SCONMESH")): + return {} + + names = {int(prefix[0]): text for prefix, text in sin.iter_text_records("TDSCONC") if prefix and text} + concept_to_mesh = { + int(rec[0]): int(rec[-1]) for rec in sin.iter_records("SCONCEPT") if len(rec) >= 7 and int(rec[1]) == 7 + } + mesh_to_elements = { + int(rec[0]): tuple(int(e) for e in rec[4:]) for rec in sin.iter_records("SCONMESH") if len(rec) >= 5 + } + + out: dict[int, str] = {} + for concept_id, mesh_id in concept_to_mesh.items(): + name = names.get(concept_id) + if not name: + continue + for element_id in mesh_to_elements.get(mesh_id, ()): + out[element_id] = name + return out + + +def _parse_shell_pressures(sin) -> dict[int, dict[int, float]]: + """Mean signed shell pressure from ``BEUSLO`` records by case and element. + + Observed SIN records store ``load case, ..., element id, n values, ...`` + followed by one pressure value per shell node. Values are in SI pressure + units and retain the Sesam sign convention; the capacity resolver maps them + to the DNV line-load input without changing that sign. + """ + if "BEUSLO" not in sin.type_blocks: + return {} + + out: dict[int, dict[int, float]] = {} + for rec in sin.iter_records("BEUSLO"): + if len(rec) < 9: + continue + case = int(rec[0]) + element_id = int(rec[4]) + n_values = int(rec[5]) if len(rec) > 5 and rec[5] > 0 else len(rec) - 8 + values = [float(x) for x in rec[8 : 8 + n_values]] + if not values: + continue + out.setdefault(case, {})[element_id] = float(np.mean(values)) + return out + + +def geono_of(mesh: Mesh, element_id: int) -> int: + """GELREF1 geometry id (geono) of an element.""" + row = mesh.elem_data[np.where(mesh.elem_data[:, 0] == element_id)[0]] + if row.size == 0: + raise KeyError(f"element {element_id} not in mesh") + return int(row[0][2]) + + +def matno_of(mesh: Mesh, element_id: int) -> int: + row = mesh.elem_data[np.where(mesh.elem_data[:, 0] == element_id)[0]] + if row.size == 0: + raise KeyError(f"element {element_id} not in mesh") + return int(row[0][1]) + + +@dataclass +class _NodeIndex: + """One-pass connectivity index for a mesh, cached on the mesh. + + Element/node lookups via :func:`element_node_ids` / ``element_node_coords`` + and the tributary search used to scan every block (``np.where`` per call, + O(n_elements) each), making panel reconstruction O(n^2) — ~23 h on a 128k- + element topside. This builds the maps once (O(n_elements)) so every lookup + is O(1) and tributary is O(node degree). + """ + + elem_nodes: dict[int, tuple[int, ...]] + node_coord: dict[int, np.ndarray] + node_elems: dict[int, list[int]] + area: dict[int, float] = field(default_factory=dict) + + +def element_area(mesh: Mesh, element_id: int) -> float: + """Planar polygon area of a shell element (cached). + + Frame-invariant, so the rectangularity test can sum cached areas instead of + re-projecting and shoelacing every plate on every merge attempt. + """ + idx = _ensure_index(mesh) + cached = idx.area.get(element_id) + if cached is not None: + return cached + coords = element_node_coords(mesh, element_id) + if len(coords) < 3: + a = 0.0 + else: + normal_sum = np.zeros(3) + for i in range(1, len(coords) - 1): + normal_sum = normal_sum + np.cross(coords[i] - coords[0], coords[i + 1] - coords[0]) + a = 0.5 * float(np.linalg.norm(normal_sum)) + idx.area[element_id] = a + return a + + +def _ensure_index(mesh: Mesh) -> _NodeIndex: + idx = getattr(mesh, "_cap_node_index", None) + if idx is not None: + return idx + from collections import defaultdict + + elem_nodes: dict[int, tuple[int, ...]] = {} + node_coord: dict[int, np.ndarray] = {} + node_elems: dict[int, list[int]] = defaultdict(list) + for block in mesh.elements: + ids = [int(x) for x in block.identifiers] + refs = block.node_refs + if block.node_refs_are_indices: + flat = sorted({int(r) for row in refs for r in row}) + nodes = mesh.nodes.get_node_by_index(flat) + id_by_ref = {ref: int(n.id) for ref, n in zip(flat, nodes)} + coord_by_ref = {ref: np.asarray(n.p, dtype=float) for ref, n in zip(flat, nodes)} + rows = [[id_by_ref[int(r)] for r in row] for row in refs] + for row_refs, row_ids in zip(refs, rows): + for ref, nid in zip(row_refs, row_ids): + node_coord[nid] = coord_by_ref[int(ref)] + else: + flat = sorted({int(r) for row in refs for r in row}) + coord_by_id = {int(n.id): np.asarray(n.p, dtype=float) for n in mesh.nodes.get_node_by_id(flat)} + rows = [[int(r) for r in row] for row in refs] + node_coord.update(coord_by_id) + for element_id, row in zip(ids, rows): + elem_nodes[element_id] = tuple(row) + for nid in row: + node_elems[nid].append(element_id) + idx = _NodeIndex(elem_nodes=elem_nodes, node_coord=node_coord, node_elems=dict(node_elems)) + try: + mesh._cap_node_index = idx # type: ignore[attr-defined] + except Exception: + pass + return idx + + +def element_node_ids(mesh: Mesh, element_id: int) -> list[int]: + """Node ids of an element, in connectivity order.""" + idx = _ensure_index(mesh) + nodes = idx.elem_nodes.get(element_id) + if nodes is None: + raise KeyError(f"element {element_id} not in mesh") + return list(nodes) + + +def tributary_plate_ids(mesh: Mesh, beam_element_ids: tuple[int, ...], candidate_plate_ids: list[int]) -> list[int]: + """Plate elements that border a stiffener = those sharing all the beam's nodes. + + A stiffener beam edge is shared by the (up to two) adjacent plate elements; + those plates carry the stiffener's tributary membrane stresses. + """ + idx = _ensure_index(mesh) + beam_nodes: set[int] = set() + for be in beam_element_ids: + beam_nodes.update(idx.elem_nodes.get(be, ())) + if not beam_nodes: + return [] + # Candidate plates are those sharing at least one of the beam's nodes; a + # node→element map makes this O(degree) instead of scanning every plate. + candidates: set[int] = set() + for nid in beam_nodes: + candidates.update(idx.node_elems.get(nid, ())) + allowed = candidates.intersection(candidate_plate_ids) + return [pe for pe in candidate_plate_ids if pe in allowed and beam_nodes.issubset(idx.elem_nodes.get(pe, ()))] + + +def _point_segment_dist2(p: np.ndarray, a: np.ndarray, b: np.ndarray) -> float: + """Squared distance from point ``p`` to segment ``a``-``b`` (no BLAS).""" + ab = b - a + ab2 = float((ab * ab).sum()) + if ab2 <= 1e-20: + d = p - a + return float((d * d).sum()) + t = float(((p - a) * ab).sum()) / ab2 + t = 0.0 if t < 0.0 else 1.0 if t > 1.0 else t + d = p - (a + t * ab) + return float((d * d).sum()) + + +def partition_plates_by_nearest_stiffener( + mesh: Mesh, + stiffener_lines: list[tuple[str, tuple[int, ...]]], + plate_ids: list[int], +) -> dict[str, list[int]]: + """Assign every plate element to its nearest stiffener (full-coverage tributary). + + Unlike :func:`tributary_plate_ids` (only the plates sharing a stiffener's full + edge), this partitions *all* the panel's plate elements — quads and triangles + alike — among the stiffeners by centroid distance to each stiffener segment, + so no element is left uncoloured in the per-stiffener ("individual UF") view. + Pure indexing/elementwise maths — crash-safe on any BLAS backend. + """ + segments: list[tuple[str, np.ndarray, np.ndarray]] = [] + for name, eids in stiffener_lines: + ids = tuple(int(e) for e in eids) + if not ids: + continue + first = element_node_coords(mesh, ids[0]) + last = element_node_coords(mesh, ids[-1]) + segments.append((name, np.asarray(first[0], float), np.asarray(last[-1], float))) + + out: dict[str, list[int]] = {name: [] for name, _ in stiffener_lines} + if not segments: + return out + for pe in plate_ids: + centroid = element_node_coords(mesh, int(pe)).mean(axis=0) + best_name = segments[0][0] + best_d2 = _point_segment_dist2(centroid, segments[0][1], segments[0][2]) + for name, a, b in segments[1:]: + d2 = _point_segment_dist2(centroid, a, b) + if d2 < best_d2: + best_d2 = d2 + best_name = name + out.setdefault(best_name, []).append(int(pe)) + return out + + +def element_node_coords(mesh: Mesh, element_id: int) -> np.ndarray: + """(n_nodes, 3) node coordinates of an element, in connectivity order.""" + idx = _ensure_index(mesh) + nodes = idx.elem_nodes.get(element_id) + if nodes is None: + raise KeyError(f"element {element_id} not in mesh") + return np.array([idx.node_coord[n] for n in nodes], dtype=float) + + +def beam_axis_and_span(mesh: Mesh, element_ids: tuple[int, ...]) -> tuple[np.ndarray, float]: + """Unit axis vector and total length of a (chain of) beam element(s). + + Span = sum of element lengths; axis = unit vector from the first node to the + last along the chain (good enough for straight stiffeners). + """ + coords = [element_node_coords(mesh, e) for e in element_ids] + span = sum(float(np.linalg.norm(c[-1] - c[0])) for c in coords) + start = coords[0][0] + end = coords[-1][-1] + axis = end - start + n = np.linalg.norm(axis) + axis = axis / n if n else np.array([0.0, 0.0, 1.0]) + return axis, span + + +def stiffener_stations(mesh: Mesh, element_ids: tuple[int, ...]) -> list[list[float]]: + """The 3 Section-5 stations [start, mid, end] along a stiffener span. + + These are the points where the resolved design stresses/forces are sampled + (positions 1/2/3): ``start`` = the first node of the chain, ``end`` = the + last node, ``mid`` = their midpoint. Returns ``[]`` for an empty chain. + Pure indexing/averaging — no BLAS, so it stays crash-safe on any backend. + """ + ids = tuple(int(e) for e in element_ids) + if not ids: + return [] + coords = [element_node_coords(mesh, e) for e in ids] + start = np.asarray(coords[0][0], dtype=float) + end = np.asarray(coords[-1][-1], dtype=float) + mid = (start + end) / 2.0 + return [start.tolist(), mid.tolist(), end.tolist()] + + +def stiffener_discretization(mesh: Mesh, element_ids: tuple[int, ...]) -> dict[str, int | bool]: + """Mesh discretization of a stiffener vs DNV-RP-C201 [6.4.3] (SCM2). + + [6.4.3] requires a minimum of four (4) elements along the stiffener length for + first-order elements, and a minimum of one (1) for second-order elements. + Element order is inferred from the node count of the line elements (2 nodes → + first order, 3 nodes → second order LINE3). Returns + ``{elements_along, element_order, min_required, ok}``. + """ + ids = tuple(int(e) for e in element_ids) + n = len(ids) + order = 1 + if ids: + order = 2 if len(element_node_ids(mesh, ids[0])) >= 3 else 1 + min_required = 4 if order == 1 else 1 + return { + "elements_along": n, + "element_order": order, + "min_required": min_required, + "ok": n >= min_required, + } + + +def plate_dimensions(mesh: Mesh, element_ids: tuple[int, ...], stiffener_axis: np.ndarray) -> tuple[float, float]: + """(length, width) of a plate field, oriented by the stiffener axis. + + ``length`` is the extent along the stiffener (span direction); ``width`` is + the extent perpendicular to it (spacing direction). Computed as the bounding + extent of the field's element nodes projected onto the stiffener axis and an + in-plane perpendicular. + """ + pts = np.vstack([element_node_coords(mesh, e) for e in element_ids]) + pts = pts - pts.mean(axis=0) + + axis = np.asarray(stiffener_axis, dtype=float) + axis = axis / (np.linalg.norm(axis) or 1.0) + + # Plate normal from the first element's first three nodes. + first = element_node_coords(mesh, element_ids[0]) + normal = np.cross(first[1] - first[0], first[2] - first[0]) + nn = np.linalg.norm(normal) + normal = normal / nn if nn else np.array([1.0, 0.0, 0.0]) + + perp = np.cross(normal, axis) + pn = np.linalg.norm(perp) + perp = perp / pn if pn else axis + + length = float(pts @ axis).real if pts.ndim == 1 else float(np.ptp(pts @ axis)) + width = float(np.ptp(pts @ perp)) + return length, width diff --git a/src/ada/fem/capacity/girder.py b/src/ada/fem/capacity/girder.py new file mode 100644 index 000000000..d4ae4778c --- /dev/null +++ b/src/ada/fem/capacity/girder.py @@ -0,0 +1,1185 @@ +"""Girder capacity models + DNV-RP-C201 Section-7 load resolve from a SIN. + +The stiffened-panel pipeline identifies the *secondary* stiffeners and builds +panel groups around them (:mod:`.sources`); the girders are the plate-bordering +beam profiles it filters out. This module turns those primary beam runs into +girder capacity models — one model per girder *bay* (a colinear run between +crossings with other girders / perpendicular plate junctions) — and resolves the +Section-7 design loads (N_G, M_G, V, tau, sigma_x, p) for them per result case. + +Everything is SI (m, Pa, N). The DNV-RP-C201 check itself lives downstream (the +``structural-codecheck`` package); this module only reconstructs its inputs from +the FE model + results, mirroring what :mod:`.stress_resolve` does for the +stiffened-panel checks. +""" + +from __future__ import annotations + +import hashlib +import json +import pathlib +from collections.abc import Callable +from dataclasses import asdict, dataclass, field +from types import SimpleNamespace +from typing import Any + +import numpy as np + +from ada.config import logger +from ada.fem.capacity import extract +from ada.fem.capacity.model import CapacityModel, CapMaterial, CapSection +from ada.fem.capacity.sources import ( + SinSource, + _connected_colinear_runs, + _split_runs_at_supports, +) +from ada.fem.capacity.stiffened_plate import _cap_material, _section_of +from ada.fem.capacity.stress_resolve import ( + _accum_blocks, + _area_weighted_element_mean, + _area_weighted_pressure, + _element_area, + _element_membrane_point_records, + _point_tuples, + _recovered_stress_blocks, + _rotation_cossin, + _rows_for_element, + _round_source_value, + _station_stress_provenance, + _station_values, + _superpose_into, + _term, +) +from ada.fem.results.common import Mesh + +#: A flanking panel's stiffeners must run roughly perpendicular to the girder +#: (|cos| below this) for the panel to feed the girder's l / s / As / Is. +_PERPENDICULAR_COS_TOL = 0.35 +#: The girder axis must lie in a flanking panel's plane (|cos(axis, normal)|). +_PLANE_CONTAINS_AXIS_TOL = 0.2 +#: Max out-of-plane offset of the girder line from a flanking panel's plane [m]. +_COPLANAR_TOL = 0.02 +#: A flanking panel must share nodes with the girder line over at least this +#: fraction of the bay span — a corner touch is not support. +_MIN_LINE_CONTACT_FRACTION = 0.5 + +# FORCES block value columns (after the two id columns elem, pos): +# [NXX, NXY, NXZ, MXX, MXY, MXZ] — see read_sif.FORCE_MAP. +_FORCE_AXIAL = 0 # NXX +_FORCE_SHEAR_Z = 2 # NXZ — shear normal to the plate (girder web plane) +_FORCE_MOMENT_Y = 4 # MXY — bending in the plate-normal plane +_MAX_PROVENANCE_SOURCES = 8 + + +@dataclass(frozen=True) +class GirderCapacityModel: + """One girder bay and everything the Section-7 check needs from the model. + + ``l``/``s``/``t``/``As``/``Is`` come from the *flanking* stiffened panels + (the panels whose stiffeners the girder supports); ``notes`` records the + aggregation choices so a report can surface them. + """ + + name: str + id: str + element_ids: tuple[int, ...] + tributary_plate_ids: tuple[int, ...] + section: CapSection + material: CapMaterial + LG: float # girder span (this bay) [m] + l: float # effective stiffener span = 0.5*(l1 + l2) [m] # noqa: E741 + l1: float # adjacent stiffener span on one side of the girder [m] + l2: float # adjacent stiffener span on the other side of the girder [m] + s: float # stiffener spacing of the flanking panels [m] + t: float # plate thickness [m] + As: float # aggregate stiffener area (excl. plate) [m^2] + Is: float # stiffener inertia with full plate width s as flange [m^4] + stiffener_section: CapSection | None = None + continuous: bool = True + stiffener_continuous_through: bool = True + stations: tuple[tuple[float, float, float], ...] = () + #: Supported-stiffener lines (2-point polylines, clipped to the tributary + #: strip) — viewer decoration showing which stiffeners the girder carries. + stiffener_stations: tuple[tuple[tuple[float, float, float], ...], ...] = () + notes: tuple[str, ...] = () + + +@dataclass(frozen=True) +class ResolvedGirderCase: + """Section-7 design loads for one (result case, girder bay). + + ``vectors`` hold the 3 along-span positions (end 1 / mid / end 2, [5.3]): + ``NG`` axial force [N] (compression positive), ``MG`` bending moment [Nm] + (tension in plate flange positive), ``Tau`` plate shear [Pa]. ``variables`` + carry ``SigmaXSd`` (stress in the stiffener direction, compression + positive), ``PSd`` (lateral pressure) and ``VSd`` (max web shear force). + """ + + result_case: int + girder: str + capacity_model_id: str = "" + variables: dict[str, float] = field(default_factory=dict) + vectors: dict[str, list[float]] = field(default_factory=dict) + provenance: dict[str, Any] = field(default_factory=dict) + + +# --------------------------------------------------------------------------- # +# Girder identification +# --------------------------------------------------------------------------- # +def girder_runs( + mesh: Mesh, + aux: extract.AuxRecords | None = None, + *, + group: str | list[str] | None = None, +) -> list[tuple[str, tuple[int, ...]]]: + """Identify girder bays: primary plate-bordering beam runs between supports. + + Returns ``(name, ordered element ids)`` per bay. Primary beams are the + plate-bordering profiles that are *not* the dominant (secondary-stiffener) + profile; colinear same-profile chains are split at crossings with other + primaries and at perpendicular plate junctions, so each run is one + DNV-RP-C201 girder span L_G. + """ + from ada.fem.shapes.definitions import LineShapes, ShellShapes + + beam_ids: list[int] = [] + shell_ids: list[int] = [] + for block in mesh.elements: + etype = block.elem_info.type + ids = [int(x) for x in block.identifiers] + if isinstance(etype, LineShapes): + beam_ids.extend(ids) + elif isinstance(etype, ShellShapes): + shell_ids.extend(ids) + + all_line_ids = set(beam_ids) + all_shell_ids = set(shell_ids) + + if group is not None: + members = SinSource._scoped_set_members(mesh, group) + beam_ids = [b for b in beam_ids if b in members] + # A plate-only scope still has its girders on the plate boundary; pull in + # the beams bordering the scoped plates. + plate_members = members.intersection(all_shell_ids) + if plate_members: + from ada.fem.capacity.sources import _beams_bordering_plates + + beam_ids = sorted(set(beam_ids) | _beams_bordering_plates(mesh, sorted(all_line_ids), plate_members)) + + bordering = [b for b in beam_ids if extract.tributary_plate_ids(mesh, (b,), shell_ids)] + secondary = set(SinSource._secondary_stiffener_ids(mesh, bordering)) + primary = [b for b in bordering if b not in secondary] + if not primary: + return [] + + runs = _connected_colinear_runs(mesh, primary) + # Split each chain at its supports. ``secondary_ids`` excludes the + # stiffeners from the support test, so a stiffener crossing the girder does + # NOT end the bay while a perpendicular girder / plate junction does. + runs = _split_runs_at_supports(mesh, runs, secondary, all_line_ids, all_shell_ids) + + concept_names = getattr(aux, "concept_name_by_element", {}) if aux is not None else {} + from ada.fem.capacity.sources import _concept_base + + named: list[tuple[str, tuple[int, ...]]] = [] + counts: dict[str, int] = {} + for run in runs: + base = _concept_base(concept_names.get(run[0], f"el{run[0]}")) + counts[base] = counts.get(base, 0) + 1 + named.append((base, run)) + seen: dict[str, int] = {} + out: list[tuple[str, tuple[int, ...]]] = [] + for base, run in named: + if counts[base] > 1: + seen[base] = seen.get(base, 0) + 1 + out.append((f"Girder_{base}@{seen[base]}", run)) + else: + out.append((f"Girder_{base}", run)) + return out + + +# --------------------------------------------------------------------------- # +# Girder capacity models +# --------------------------------------------------------------------------- # +def build_girder_models( + mesh: Mesh, + aux: extract.AuxRecords, + panel_models: list[CapacityModel], + *, + group: str | list[str] | None = None, + on_progress: Callable[[int, int], None] | None = None, +) -> list[GirderCapacityModel]: + """Build one :class:`GirderCapacityModel` per girder bay. + + ``panel_models`` are the stiffened-panel capacity models already built for + this mesh; the panels flanking a girder supply its stiffener span ``l``, + stiffener spacing ``s``, plate thickness ``t`` and the aggregate stiffener + properties ``As`` / ``Is``. Girder bays without any perpendicular flanking + panel are skipped (logged) — there is nothing for the girder to support. + """ + runs = girder_runs(mesh, aux, group=group) + idx = extract._ensure_index(mesh) + + # Per panel: plate nodes (line-contact test), stiffener axis + # (perpendicularity test) and plate plane (coplanarity test), computed once. + panel_info: list[_PanelInfo] = [] + for pm in panel_models: + if not pm.stiffeners or not pm.plates: + continue + st = pm.stiffeners[0] + if not st.element_ids: + continue + st_axis, _ = extract.beam_axis_and_span(mesh, st.element_ids) + nodes = {n for p in pm.plates for e in p.element_ids for n in idx.elem_nodes.get(e, ())} + first_plate_elem = next((e for p in pm.plates for e in p.element_ids), None) + if first_plate_elem is None: + continue + coords = extract.element_node_coords(mesh, first_plate_elem) + if len(coords) < 3: + continue + normal = np.cross(coords[1] - coords[0], coords[2] - coords[0]) + norm = float(np.linalg.norm(normal)) + if norm <= 0.0: + continue + panel_info.append( + _PanelInfo( + model=pm, + nodes=nodes, + stiffener_axis=st_axis, + plane_normal=normal / norm, + plane_point=coords[0], + ) + ) + + # Phase 1: per bay, the flanking panels + section/bay dims (no tributary yet). + prepared: list[_PreparedGirder] = [] + total = len(runs) + for i, (name, run) in enumerate(runs, start=1): + pre = _prepare_girder(mesh, aux, name, run, panel_info, idx) + if pre is not None: + prepared.append(pre) + if on_progress is not None: + on_progress(i, total) + + # Phase 2: partition the flanking plate elements between girders. An + # element within a girder's bay window and half-span (l_i/2) is a claim; + # among *parallel* claimants the nearest girder wins (so parallel girders' + # strips never overlap, also with mixed bay widths), while *perpendicular* + # claimants each keep the element — the corner square at a girder crossing + # genuinely serves both, and dropping it would carve the rectangles. + claims: dict[int, list[tuple[float, int]]] = {} # element -> [(distance, prepared idx)] + centroid_cache: dict[int, np.ndarray] = {} + for gi, pre in enumerate(prepared): + seen: set[int] = set() + for pm in pre.strip_panels: + for p in pm.plates: + for e in p.element_ids: + e = int(e) + if e in seen: + continue + seen.add(e) + centroid = centroid_cache.get(e) + if centroid is None: + coords = [idx.node_coord[n] for n in idx.elem_nodes.get(e, ()) if n in idx.node_coord] + if not coords: + continue + centroid = np.mean(np.asarray(coords), axis=0) + centroid_cache[e] = centroid + rel = centroid - pre.origin + along = float(np.dot(rel, pre.axis)) + if along < -1e-3 or along > pre.a_max + 1e-3: + continue + signed = float(np.dot(rel, pre.perp_dir)) + half = pre.half_by_side.get(1 if signed >= 0.0 else -1, 0.0) + if abs(signed) > half + 1e-3: + continue + claims.setdefault(e, []).append((abs(signed), gi)) + + trib_by_girder: dict[int, list[int]] = {} + for e, entries in claims.items(): + for distance, gi in entries: + axis = prepared[gi].axis + beaten = any( + other_gi != gi + # Ties (element exactly mid-way between two parallel girders) + # break on the girder index so exactly one side keeps it. + and (other_d, other_gi) < (distance, gi) and abs(float(np.dot(prepared[other_gi].axis, axis))) > 0.7 + for other_d, other_gi in entries + ) + if not beaten: + trib_by_girder.setdefault(gi, []).append(e) + + # Phase 3: assemble the models. + out: list[GirderCapacityModel] = [] + for gi, pre in enumerate(prepared): + trib = sorted(trib_by_girder.get(gi, [])) + if not trib: + logger.info("capacity girder: empty tributary strip for %s - skipped", pre.name) + continue + stations = extract.stiffener_stations(mesh, pre.run) + l1, l2 = _adjacent_stiffener_spans(pre.half_by_side, pre.l_span) + out.append( + GirderCapacityModel( + name=pre.name, + id=_girder_model_id(pre.name, pre.run), + element_ids=tuple(pre.run), + tributary_plate_ids=tuple(trib), + section=pre.section, + material=_cap_material(mesh, pre.run[0]), + LG=float(pre.span), + l=pre.l_span, + l1=l1, + l2=l2, + s=pre.s_spacing, + t=pre.t, + As=pre.As, + Is=pre.Is, + stiffener_section=pre.st_section, + stations=tuple(tuple(p) for p in stations or ()), + stiffener_stations=_supported_stiffener_lines(mesh, pre), + notes=tuple(pre.notes), + ) + ) + return out + + +def _adjacent_stiffener_spans(half_by_side: dict[int, float], fallback: float) -> tuple[float, float]: + """Return the two adjacent stiffener spans carried by a girder. + + The capacity equations consume the effective width ``l``. Stipla DNV-G and + DNV-RP-C201 Figure 5-4 keep the two neighbouring panel spans separately as + L1/L2, with the effective plate flange width equal to their mean. For an + edge/single-sided girder, mirror the one available side so the effective + width stays unchanged and both exported spans remain positive. + """ + spans = [2.0 * half_by_side[side] for side in sorted(half_by_side) if half_by_side[side] > 0.0] + if len(spans) >= 2: + return float(spans[0]), float(spans[1]) + if len(spans) == 1: + return float(spans[0]), float(spans[0]) + return float(fallback), float(fallback) + + +def _supported_stiffener_lines(mesh: Mesh, pre: _PreparedGirder) -> tuple[tuple[tuple[float, float, float], ...], ...]: + """The supported stiffeners as 2-point lines clipped to the tributary strip. + + Viewer decoration: each strip panel's stiffener line, cut to this bay's + along-extent and to the per-side rectangle, so the girder capacity model can + show the stiffeners it carries without spilling into the neighbours. + """ + half_plus = pre.half_by_side.get(1, 0.0) + half_minus = pre.half_by_side.get(-1, 0.0) + out: list[tuple[tuple[float, float, float], ...]] = [] + for pm in pre.strip_panels: + for st in pm.stiffeners: + if not st.element_ids: + continue + stations = extract.stiffener_stations(mesh, st.element_ids) + if not stations or len(stations) < 2: + continue + p0 = np.asarray(stations[0], dtype=float) + p1 = np.asarray(stations[-1], dtype=float) + clipped = _clip_segment_to_strip(p0, p1, pre, half_plus, half_minus) + if clipped is not None: + out.append((tuple(float(v) for v in clipped[0]), tuple(float(v) for v in clipped[1]))) + return tuple(out) + + +def _clip_segment_to_strip( + p0: np.ndarray, + p1: np.ndarray, + pre: _PreparedGirder, + half_plus: float, + half_minus: float, +) -> tuple[np.ndarray, np.ndarray] | None: + """Clip a segment to along in [0, a_max] and signed perp in [-half-, +half+].""" + r0 = p0 - pre.origin + r1 = p1 - pre.origin + a0, a1 = float(np.dot(r0, pre.axis)), float(np.dot(r1, pre.axis)) + s0, s1 = float(np.dot(r0, pre.perp_dir)), float(np.dot(r1, pre.perp_dir)) + t_lo, t_hi = 0.0, 1.0 + for value0, value1, lo, hi in ( + (a0, a1, -1e-3, pre.a_max + 1e-3), + (s0, s1, -half_minus - 1e-3, half_plus + 1e-3), + ): + dv = value1 - value0 + if abs(dv) < 1e-12: + if value0 < lo or value0 > hi: + return None + continue + t_at_lo = (lo - value0) / dv + t_at_hi = (hi - value0) / dv + t_min, t_max = min(t_at_lo, t_at_hi), max(t_at_lo, t_at_hi) + t_lo, t_hi = max(t_lo, t_min), min(t_hi, t_max) + if t_hi - t_lo < 1e-3: + return None + return p0 + t_lo * (p1 - p0), p0 + t_hi * (p1 - p0) + + +@dataclass(frozen=True) +class _PanelInfo: + """Case-invariant flanking-candidate facts for one stiffened panel.""" + + model: CapacityModel + nodes: frozenset[int] | set[int] + stiffener_axis: np.ndarray + plane_normal: np.ndarray + plane_point: np.ndarray + + +@dataclass(frozen=True) +class _PreparedGirder: + """One girder bay with its flanking panels resolved (tributary pending).""" + + name: str + run: tuple[int, ...] + axis: np.ndarray + origin: np.ndarray + a_max: float + span: float + flanking: list[CapacityModel] + strip_panels: list[CapacityModel] + perp_dir: np.ndarray + half_by_side: dict[int, float] # {+1/-1: rectangle half-width on that side} + l_span: float + s_spacing: float + t: float + As: float + Is: float + st_section: CapSection + section: CapSection + notes: list[str] + + +def _prepare_girder( + mesh: Mesh, + aux: extract.AuxRecords, + name: str, + run: tuple[int, ...], + panel_info: list[_PanelInfo], + idx, +) -> _PreparedGirder | None: + axis, span = extract.beam_axis_and_span(mesh, run) + if span <= 0.0: + return None + run_nodes = {n for e in run for n in idx.elem_nodes.get(e, ()) if n in idx.node_coord} + if not run_nodes: + return None + ref_coord = idx.node_coord[next(iter(run_nodes))] + run_alongs = [float(np.dot(idx.node_coord[n] - ref_coord, axis)) for n in run_nodes] + origin = ref_coord + min(run_alongs) * axis # bay start on the girder line + a_max = max(run_alongs) - min(run_alongs) + + # A flanking panel supports the girder when (a) its stiffeners run + # perpendicular to it, (b) the girder line lies IN the panel's plane (a + # deck girder must not adopt wall panels it merely touches at a corner), + # and (c) it shares at least an edge with the girder line. A girder that + # only carries parallel-stiffener panels (a primary supporting secondary + # girders) gets no flanking panels here and is skipped: the check targets + # girders that directly support stiffeners. + candidates: list[tuple[_PanelInfo, tuple[float, float, float], tuple[float, float]]] = [] + for info in panel_info: + if abs(float(np.dot(info.stiffener_axis, axis))) > _PERPENDICULAR_COS_TOL: + continue + if abs(float(np.dot(info.plane_normal, axis))) > _PLANE_CONTAINS_AXIS_TOL: + continue + if abs(float(np.dot(info.plane_normal, origin - info.plane_point))) > _COPLANAR_TOL: + continue + shared = run_nodes & info.nodes + if len(shared) < 2: + continue + alongs = [float(np.dot(idx.node_coord[n] - origin, axis)) for n in shared if n in idx.node_coord] + if len(alongs) < 2: + continue + interval = (min(alongs), max(alongs)) + plane_key = tuple(float(v) for v in np.round(np.abs(info.plane_normal), 1)) + candidates.append((info, plane_key, interval)) + if not candidates: + logger.info("capacity girder: no perpendicular flanking panel for %s - skipped", name) + return None + + # A girder on a plane junction (deck edge) can be flanked in two planes; + # the capacity model must live in one. Keep the plane with the most line + # contact and note what was dropped. The girder qualifies only when the + # union of its in-plane contact intervals covers a real fraction of the + # bay — corner touches alone are not support. + notes: list[str] = [] + contact_by_plane: dict[tuple[float, float, float], float] = {} + for _info, plane_key, interval in candidates: + contact_by_plane[plane_key] = contact_by_plane.get(plane_key, 0.0) + (interval[1] - interval[0]) + best_plane = max(contact_by_plane, key=lambda k: contact_by_plane[k]) + if len(contact_by_plane) > 1: + notes.append( + "girder lies on a plane junction; capacity model uses the dominant " + f"plane (normal ~{best_plane}), panels in the other plane(s) dropped" + ) + in_plane = [(info, interval) for info, plane_key, interval in candidates if plane_key == best_plane] + coverage = _interval_union_length([iv for _, iv in in_plane]) + if coverage < _MIN_LINE_CONTACT_FRACTION * span: + logger.info( + "capacity girder: %s supported over only %.0f%% of the bay - skipped", name, 100.0 * coverage / span + ) + return None + # The tributary strip takes every in-plane panel touching the girder line + # (partial-bay panels fill the strip to the bay ends); the *support + # properties* (l, s, t, As, Is) come from the panels with substantial line + # contact — a small edge touch says nothing about what the girder carries. + strip_panels = [info.model for info, _ in in_plane] + flanking = [ + info.model for info, interval in in_plane if interval[1] - interval[0] >= _MIN_LINE_CONTACT_FRACTION * span + ] + if not flanking: + # No single panel dominates the bay (e.g. two half-bay panels): use + # every in-plane panel for the dims too. + flanking = strip_panels + + # Per-side half-widths of the tributary rectangle: the dominant (largest + # line contact) panel on each side of the girder line sets that side's + # reach as its half-span l_side/2. Clipping every candidate element to + # these two explicit rectangles is what keeps the strip rectangular even + # when panels of different widths meet along the girder. + perp_dir = np.cross(in_plane[0][0].plane_normal, axis) + perp_norm = float(np.linalg.norm(perp_dir)) + perp_dir = perp_dir / perp_norm if perp_norm > 0.0 else perp_dir + best_contact_by_side: dict[int, float] = {} + half_by_side: dict[int, float] = {} + for info, interval in in_plane: + coords = [idx.node_coord[n] for n in info.nodes if n in idx.node_coord] + if not coords: + continue + centroid = np.mean(np.asarray(coords), axis=0) + side = 1 if float(np.dot(centroid - origin, perp_dir)) >= 0.0 else -1 + contact = interval[1] - interval[0] + l_i = info.model.plates[0].length if info.model.plates else 0.0 + if l_i > 0.0 and contact > best_contact_by_side.get(side, 0.0): + best_contact_by_side[side] = contact + half_by_side[side] = l_i / 2.0 + + plates = [pm.plates[0] for pm in flanking if pm.plates] + spans = [p.length for p in plates if p.length > 0.0] + spacings = [p.width for p in plates if p.width > 0.0] + thicknesses = [p.thickness for p in plates if p.thickness > 0.0] + if not spans or not spacings or not thicknesses: + logger.info("capacity girder: flanking panels of %s carry no usable plate dims - skipped", name) + return None + l1_span, l2_span = _adjacent_stiffener_spans(half_by_side, float(np.mean(spans))) + l_span = 0.5 * (l1_span + l2_span) + s_spacing = float(np.mean(spacings)) + if span <= s_spacing: + # A bay shorter than the stiffener spacing supports no stiffener — a stub + # member (bracket/reinforcement), not a girder ([7.8.3] needs s < L_G). + logger.info("capacity girder: %s span %.3f m <= stiffener spacing %.3f m - skipped", name, span, s_spacing) + return None + t = float(min(thicknesses)) + if len(set(np.round(thicknesses, 6))) > 1: + notes.append(f"flanking panels have mixed plate thickness; min used (t={t:.4f} m)") + if len(flanking) == 1: + notes.append("girder flanked by a panel on one side only; l taken from that side") + + st_section = flanking[0].stiffeners[0].section + As, Is = _stiffener_As_Is(st_section, s_spacing, t) + if st_section.is_bulb: + notes.append("stiffener As/Is from raw bulb dims (no bulb->angle idealization)") + + section = _section_of(mesh, aux, run[0]) + if section.height <= 0.0 or section.web_thickness <= 0.0: + logger.info("capacity girder: no usable girder section for %s - skipped", name) + return None + + return _PreparedGirder( + name=name, + run=run, + axis=axis, + origin=origin, + a_max=a_max, + span=float(span), + flanking=flanking, + strip_panels=strip_panels, + perp_dir=perp_dir, + half_by_side=half_by_side, + l_span=l_span, + s_spacing=s_spacing, + t=t, + As=As, + Is=Is, + st_section=st_section, + section=section, + notes=notes, + ) + + +def _interval_union_length(intervals: list[tuple[float, float]]) -> float: + """Total length covered by the union of 1-D intervals.""" + if not intervals: + return 0.0 + merged = 0.0 + current_start, current_end = None, None + for start, end in sorted(intervals): + if current_end is None or start > current_end: + if current_end is not None: + merged += current_end - current_start + current_start, current_end = start, end + else: + current_end = max(current_end, end) + merged += current_end - current_start + return merged + + +def _girder_model_id(name: str, run: tuple[int, ...]) -> str: + payload = json.dumps(sorted(int(e) for e in run), separators=(",", ":")) + digest = hashlib.blake2s(payload.encode("ascii"), digest_size=6).hexdigest() + return f"{name}#{digest}" + + +def _stiffener_As_Is(section: CapSection, s: float, t: float) -> tuple[float, float]: + """Aggregate stiffener properties for the girder check. + + ``As`` — stiffener area excluding the plate. ``Is`` — moment of inertia of + the stiffener with the full plate width ``s`` acting as flange (used in + DNV-RP-C201 eqs. (7.40)/(6.51)/(7.3)), from the composite plate + web + + flange section about its own neutral axis (z measured from the plate + mid-plane). + """ + h = float(section.height) + tw = float(section.web_thickness) + bf = float(section.flange_width) + tf = float(section.flange_thickness) + if h <= 0.0 or tw <= 0.0: + return 0.0, 0.0 + hw = max(h - tf, 0.0) if (bf > 0.0 and tf > 0.0) else h + + parts = [(s * t, 0.0, s * t**3 / 12.0)] # (A, z_centroid, I_local) + parts.append((hw * tw, t / 2.0 + hw / 2.0, tw * hw**3 / 12.0)) + if bf > 0.0 and tf > 0.0: + parts.append((bf * tf, t / 2.0 + hw + tf / 2.0, bf * tf**3 / 12.0)) + + area = sum(a for a, _, _ in parts) + if area <= 0.0: + return 0.0, 0.0 + zc = sum(a * z for a, z, _ in parts) / area + inertia = sum(i_local + a * (z - zc) ** 2 for a, z, i_local in parts) + a_stiffener = sum(a for a, _, _ in parts[1:]) + return a_stiffener, inertia + + +def _girder_section_area(section: CapSection) -> float: + h = float(section.height) + tw = float(section.web_thickness) + bf = float(section.flange_width) + tf = float(section.flange_thickness) + if h <= 0.0 or tw <= 0.0: + return 0.0 + hw = max(h - tf, 0.0) if (bf > 0.0 and tf > 0.0) else h + return hw * tw + max(bf, 0.0) * max(tf, 0.0) + + +# --------------------------------------------------------------------------- # +# Section-7 load resolve +# --------------------------------------------------------------------------- # +@dataclass +class _GirderGeom: + """Case-invariant per-girder geometry (mirrors ``_StiffGeom``).""" + + element_ids: tuple[int, ...] + axis: np.ndarray + origin: np.ndarray + trib: list[int] + cs_by_element: dict[int, tuple[float, float]] + area_by_element: dict[int, float] + elem_along: dict[int, tuple[float, float]] # element -> (along start, along end) + + +def _build_girder_geom(mesh: Mesh, aux: extract.AuxRecords, gm: GirderCapacityModel) -> _GirderGeom: + axis, _ = extract.beam_axis_and_span(mesh, gm.element_ids) + origin = extract.element_node_coords(mesh, gm.element_ids[0])[0] + + elem_along: dict[int, tuple[float, float]] = {} + for e in gm.element_ids: + coords = extract.element_node_coords(mesh, e) + a0 = float(np.dot(coords[0] - origin, axis)) + a1 = float(np.dot(coords[-1] - origin, axis)) + elem_along[e] = (min(a0, a1), max(a0, a1)) + + cs_by_element: dict[int, tuple[float, float]] = {} + area_by_element: dict[int, float] = {} + for pe in gm.tributary_plate_ids: + transform = aux.element_transform_by_element.get(pe) + cs_by_element[pe] = _rotation_cossin(mesh, pe, axis, transform) + area_by_element[pe] = _element_area(mesh, pe) + + return _GirderGeom( + element_ids=gm.element_ids, + axis=axis, + origin=origin, + trib=list(gm.tributary_plate_ids), + cs_by_element=cs_by_element, + area_by_element=area_by_element, + elem_along=elem_along, + ) + + +def _beam_run_stations( + force_blocks, + geom: _GirderGeom, + component: int, + *, + sign: float = 1.0, +) -> list[float]: + """Force component at the run's end 1 / mid / end 2 ([5.3] positions). + + Unlike the per-element station average used for single-bay stiffeners, a + girder bay is a chain of beam elements: each force sample is placed at its + along-run coordinate (element start / mid / end for result-point positions + 1/2/3) and the three stations read the samples nearest the run ends and + midpoint. + """ + samples: list[tuple[float, float]] = [] + for el in geom.element_ids: + a0, a1 = geom.elem_along.get(el, (0.0, 0.0)) + for sub in _rows_for_element(force_blocks, el): + for row in sub: + pos = int(row[1]) + along = a0 if pos == 1 else (a1 if pos == 3 else 0.5 * (a0 + a1)) + samples.append((along, float(row[2 + component]) * sign)) + if not samples: + return [0.0, 0.0, 0.0] + along = np.array([a for a, _ in samples]) + values = np.array([v for _, v in samples]) + span = float(np.ptp(along)) + if span <= 1e-12: + mean = float(values.mean()) + return [mean, mean, mean] + tol = max(span * 1e-6, 1e-6) + start = float(values[along <= along.min() + tol].mean()) + end = float(values[along >= along.max() - tol].mean()) + mid_coord = 0.5 * (float(along.min()) + float(along.max())) + mid = float(values[np.abs(along - mid_coord) == np.abs(along - mid_coord).min()].mean()) + return [start, mid, end] + + +def _beam_position_nodes(node_ids: list[int], position: int) -> list[int]: + if not node_ids: + return [] + if position == 1: + return [int(node_ids[0])] + if position == 3: + return [int(node_ids[-1])] + if len(node_ids) >= 3: + return [int(node_ids[2])] + return [int(n) for n in node_ids] + + +def _beam_run_station_data( + mesh: Mesh, + force_blocks, + geom: _GirderGeom, + component: int, + *, + sign: float = 1.0, + unit: str, + component_label: str, +) -> tuple[list[float], dict[int, dict[str, Any]]]: + samples: list[dict[str, Any]] = [] + for el in geom.element_ids: + a0, a1 = geom.elem_along.get(el, (0.0, 0.0)) + nodes = extract.element_node_ids(mesh, el) + for sub in _rows_for_element(force_blocks, el): + for row in sub: + pos = int(row[1]) + along = a0 if pos == 1 else (a1 if pos == 3 else 0.5 * (a0 + a1)) + raw = float(row[2 + component]) + samples.append( + { + "element_id": int(el), + "node_ids": _beam_position_nodes(nodes, pos), + "force_position": pos, + "along_m": _round_source_value(along), + "_along_exact": along, + "raw_value": _round_source_value(raw), + "value": _round_source_value(raw * sign), + "_exact": raw * sign, + "unit": unit, + } + ) + if not samples: + empty = { + pos: { + "label": f"{component_label} station {pos}", + "position": pos, + "calculation": "No matching beam FORCE rows were found; value resolved to zero.", + "formula": "0", + "source_sets": [], + } + for pos in (1, 2, 3) + } + return [0.0, 0.0, 0.0], empty + + along = np.array([float(s["_along_exact"]) for s in samples]) + values = np.array([float(s["_exact"]) for s in samples]) + span = float(np.ptp(along)) + if span <= 1e-12: + value_sets = {pos: samples for pos in (1, 2, 3)} + values_out = [float(values.mean())] * 3 + else: + tol = max(span * 1e-6, 1e-6) + start_samples = [s for s, a in zip(samples, along) if a <= along.min() + tol] + end_samples = [s for s, a in zip(samples, along) if a >= along.max() - tol] + mid_coord = 0.5 * (float(along.min()) + float(along.max())) + mid_dist = np.abs(along - mid_coord) + min_mid_dist = float(mid_dist.min()) + mid_samples = [s for s, d in zip(samples, mid_dist) if d == min_mid_dist] + value_sets = {1: start_samples, 2: mid_samples, 3: end_samples} + values_out = [ + float(np.mean([float(s["_exact"]) for s in value_sets[pos]])) if value_sets[pos] else 0.0 + for pos in (1, 2, 3) + ] + + provenance = {} + for pos in (1, 2, 3): + src = [ + {k: v for k, v in s.items() if k not in {"_exact", "_along_exact"}} + for s in value_sets[pos] + ] + provenance[pos] = { + "label": f"{component_label} station {pos}", + "position": pos, + "calculation": ( + "FORCES rows are placed at their along-girder coordinate " + "(element start/mid/end for force positions 1/2/3). Station 1 and 3 " + "use rows at the run ends; station 2 uses rows nearest the run midpoint." + ), + "formula": f"mean({component_label} rows selected by along-girder station)", + "source_sets": [ + { + "label": "beam FORCE result rows", + "source_count": len(src), + "element_ids": sorted({int(s["element_id"]) for s in src}), + "sources": src[:_MAX_PROVENANCE_SOURCES], + "truncated_source_count": max(0, len(src) - _MAX_PROVENANCE_SOURCES), + } + ], + } + return values_out, provenance + + +def _beam_run_max_abs(force_blocks, geom: _GirderGeom, component: int) -> float: + values = [ + float(row[2 + component]) + for el in geom.element_ids + for sub in _rows_for_element(force_blocks, el) + for row in sub + ] + return max((abs(v) for v in values), default=0.0) + + +def _beam_run_max_abs_data( + mesh: Mesh, + force_blocks, + geom: _GirderGeom, + component: int, + *, + unit: str, + component_label: str, +) -> tuple[float, dict[str, Any]]: + sources: list[dict[str, Any]] = [] + values: list[float] = [] + for el in geom.element_ids: + nodes = extract.element_node_ids(mesh, el) + for sub in _rows_for_element(force_blocks, el): + for row in sub: + pos = int(row[1]) + raw = float(row[2 + component]) + values.append(raw) + sources.append( + { + "element_id": int(el), + "node_ids": _beam_position_nodes(nodes, pos), + "force_position": pos, + "raw_value": _round_source_value(raw), + "value": _round_source_value(abs(raw)), + "unit": unit, + } + ) + value = max((abs(v) for v in values), default=0.0) + return value, { + "label": component_label, + "calculation": "Maximum absolute value over all girder beam FORCE rows.", + "formula": f"max(abs({component_label} FORCES rows))", + "source_sets": [ + { + "label": "beam FORCE result rows", + "source_count": len(sources), + "element_ids": sorted({int(s["element_id"]) for s in sources}), + "sources": sources[:_MAX_PROVENANCE_SOURCES], + "truncated_source_count": max(0, len(sources) - _MAX_PROVENANCE_SOURCES), + } + ], + } + + +def _resolve_girder( + mesh: Mesh, + aux: extract.AuxRecords, + gm: GirderCapacityModel, + case: int, + stress_blocks, + force_blocks, + geom: _GirderGeom, +) -> ResolvedGirderCase: + # Membrane stresses over the tributary plates, rotated into the girder frame + # (xx along the girder, yy = stiffener direction, xy shear). Negated to the + # compression-positive design convention, as in the stiffened-panel resolve. + point_records_by_element = { + pe: _element_membrane_point_records(mesh, aux, stress_blocks, pe, geom.axis, geom.cs_by_element.get(pe)) + for pe in geom.trib + } + points_by_element = {pe: _point_tuples(records) for pe, records in point_records_by_element.items()} + points = [pt for pts in points_by_element.values() for pt in pts] + point_records = [record for records in point_records_by_element.values() for record in records] + weighted = _area_weighted_element_mean(mesh, points_by_element, geom.area_by_element) + overall = -weighted if weighted is not None else np.zeros(3) + + sigma_x_pos = [-x for x in _station_values(points, geom.origin, geom.axis, 1, -float(overall[1]))] + tau_pos = [abs(x) for x in _station_values(points, geom.origin, geom.axis, 2, -float(overall[2]))] + # [7.8.5]: maximum compressive linearized value within 0.25*L_G of midspan. + sigma_x_sd = max( + sigma_x_pos[1] + 0.5 * (sigma_x_pos[0] - sigma_x_pos[1]), + sigma_x_pos[1], + sigma_x_pos[1] + 0.5 * (sigma_x_pos[2] - sigma_x_pos[1]), + 0.0, + ) + + # Girder beam force resultants. Axial: compression positive (sign -1 from the + # FE tension-positive NXX). Moment: MXY sign-flipped to tension-in-plate- + # flange positive — the same convention the stiffened-panel resolve is + # calibrated to; verify per model (see further_work girder plan, item 4h). + n_g, n_g_provenance = _beam_run_station_data( + mesh, + force_blocks, + geom, + _FORCE_AXIAL, + sign=-1.0, + unit="N", + component_label="girder axial force NXX", + ) + m_g, m_g_provenance = _beam_run_station_data( + mesh, + force_blocks, + geom, + _FORCE_MOMENT_Y, + sign=-1.0, + unit="Nm", + component_label="girder moment MXY", + ) + v_sd, v_sd_provenance = _beam_run_max_abs_data( + mesh, + force_blocks, + geom, + _FORCE_SHEAR_Z, + unit="N", + component_label="girder web shear NXZ", + ) + p_sd = _area_weighted_pressure(mesh, aux, case, geom.trib, geom.area_by_element) + + axial_area = _girder_section_area(gm.section) + gm.l * gm.t + provenance: dict[str, Any] = { + "V_Sd": v_sd_provenance, + "p_Sd": { + "label": "Lateral pressure", + "calculation": "Area-weighted mean shell pressure over the girder tributary plate elements.", + "formula": "p_Sd = area-weighted mean(BEUSLO pressure)", + "source_sets": [ + { + "label": "tributary plate elements", + "source_count": len(geom.trib), + "element_ids": [int(e) for e in geom.trib], + "sources": [], + "truncated_source_count": 0, + } + ], + }, + } + for pos in (1, 2, 3): + i = pos - 1 + provenance[f"sigma_x_{pos}"] = _station_stress_provenance( + records=point_records, + origin=geom.origin, + axis=geom.axis, + position=pos, + component=1, + label=f"Stiffener-direction stress position {pos}", + calculation=( + "Shell top/bottom STRESS result points on the girder tributary plate " + "elements are averaged to membrane stress, rotated into the girder frame, " + "and converted to compression-positive stiffener-direction stress." + ), + formula="sigma_x,i = - rotated tributary membrane stress component yy at station i", + sign=-1.0, + ) + provenance[f"tau_{pos}"] = _station_stress_provenance( + records=point_records, + origin=geom.origin, + axis=geom.axis, + position=pos, + component=2, + label=f"Shear stress position {pos}", + calculation=( + "Shell membrane shear on the girder tributary plate elements is rotated " + "into the girder frame and reported as an absolute design shear stress." + ), + formula="tau_i = abs(rotated tributary membrane shear at station i)", + absolute=True, + ) + provenance[f"N_G{pos}"] = n_g_provenance[pos] + provenance[f"M_G{pos}"] = m_g_provenance[pos] + provenance[f"sigma_y_{pos}"] = { + "label": f"Girder membrane stress position {pos}", + "position": pos, + "calculation": ( + "Displayed girder-direction membrane stress is back-calculated from the " + "resolved girder axial force and the girder-plus-plate-flange area." + ), + "formula": "sigma_y,i = N_Gi / (A_G + l*t)", + "terms": [ + _term("N_Gi", n_g[i], "N"), + _term("A_G + l*t", axial_area, "m^2"), + _term("sigma_y,i", n_g[i] / axial_area if axial_area else 0.0, "Pa"), + ], + "source_sets": n_g_provenance[pos]["source_sets"], + } + + return ResolvedGirderCase( + result_case=case, + girder=gm.name, + capacity_model_id=gm.id, + variables={ + "SigmaXSd": sigma_x_sd, + "PSd": float(p_sd), + "VSd": float(v_sd), + }, + vectors={ + "NG": [float(x) for x in n_g], + "MG": [float(x) for x in m_g], + "Tau": [float(x) for x in tau_pos], + "AverageStiffenerDirectionMembraneStresses": [float(x) for x in sigma_x_pos], + }, + provenance=provenance, + ) + + +def resolve_girder_cases( + sin_path: str | pathlib.Path, + models: list[GirderCapacityModel], + result_cases: list[int] | None = None, + *, + on_progress: Callable[[int, int], None] | None = None, +) -> list[ResolvedGirderCase]: + """Resolve Section-7 design loads for every (result case, girder bay). + + Accepts basic result cases and RDRESCMB combinations, superposing the FE + field blocks exactly like the stiffened-panel + :func:`~ada.fem.capacity.stress_resolve.resolve_cases` (the two share the + same streaming/superposition machinery). + """ + from ada.fem.formats.sesam.results.read_sin import ( + iter_sin_step_results, + read_sin_metadata, + ) + + meta = read_sin_metadata(sin_path) + available = set(meta.steps) + combinations = meta.combinations + if result_cases is None: + result_cases = meta.steps + + direct: list[int] = [] + combo_plan: dict[int, dict[int, float]] = {} + for c in result_cases: + ci = int(c) + if ci in combinations: + comps = combinations[ci] + if not comps: + raise ValueError(f"result combination {ci} lists no basic cases") + combo_plan[ci] = comps + elif ci in available: + direct.append(ci) + else: + raise ValueError( + f"requested result case {ci} is not in the SIN. " + f"Available basic cases: {sorted(available)}; " + f"available combinations: {sorted(combinations)}" + ) + needed_combo_steps = {s for comps in combo_plan.values() for s in comps} + missing = sorted(needed_combo_steps - available) + if missing: + raise ValueError( + f"result combination(s) reference basic case(s) {missing} that are not " + f"stored in the SIN. Available basic cases: {sorted(available)}" + ) + direct_set = set(direct) + needed_steps = sorted(direct_set | needed_combo_steps) + + aux = extract.AuxRecords.from_sin(sin_path) + forces_elements = {int(e) for gm in models for e in gm.element_ids} + material_by_element: dict[int, tuple[float, float]] | None = None + + out: list[ResolvedGirderCase] = [] + total = len(direct) + len(combo_plan) + done = 0 + accums: dict[int, dict] = {case: {} for case in combo_plan} + geom_cache: dict[str, _GirderGeom] = {} + + def _resolve_step_girders(mesh, case: int, results) -> None: + nonlocal material_by_element + stress_blocks = [r for r in results if r.name == "STRESS"] + force_blocks = [r for r in results if r.name == "FORCES"] + if material_by_element is None: + material_by_element = {} + for gm in models: + for element_id in gm.tributary_plate_ids: + mat = _cap_material(mesh, int(element_id)) + material_by_element[int(element_id)] = (mat.E, mat.poisson) + if not stress_blocks: + shim = SimpleNamespace(results=list(results)) + stress_blocks = _recovered_stress_blocks( + mesh, aux, shim, case, material_by_element, log=done == 0 + ) + for gm in models: + geom = geom_cache.get(gm.id) + if geom is None: + geom = _build_girder_geom(mesh, aux, gm) + geom_cache[gm.id] = geom + out.append(_resolve_girder(mesh, aux, gm, case, stress_blocks, force_blocks, geom)) + + mesh = None + for step, res in iter_sin_step_results(sin_path, needed_steps, forces_elements=forces_elements): + if mesh is None: + mesh = res.mesh + for case, comps in combo_plan.items(): + factor = comps.get(step) + if factor: + _superpose_into(accums[case], res.results, float(factor)) + if step in direct_set: + _resolve_step_girders(mesh, step, res.results) + done += 1 + if on_progress is not None: + on_progress(done, total) + + for case in combo_plan: + _resolve_step_girders(mesh, case, _accum_blocks(accums[case])) + done += 1 + if on_progress is not None: + on_progress(done, total) + return out + + +# --------------------------------------------------------------------------- # +# Neutral serialization +# --------------------------------------------------------------------------- # +def girders_to_neutral(models: list[GirderCapacityModel], cases: list[ResolvedGirderCase]) -> dict: + """Serialize girder capacity models + resolved cases to a neutral dict.""" + return { + "format": "adapy-capacity-girder/1", + "girders": [asdict(m) for m in models], + "girder_cases": [asdict(c) for c in cases], + } diff --git a/src/ada/fem/capacity/manager.py b/src/ada/fem/capacity/manager.py new file mode 100644 index 000000000..7af6dffdd --- /dev/null +++ b/src/ada/fem/capacity/manager.py @@ -0,0 +1,164 @@ +"""CapacityManager — orchestrate capacity-model reconstruction from a SIN. + +Public entry point. Reads a Sesam ``.SIN`` via the existing +:func:`~ada.fem.formats.sesam.results.read_sin.read_sin_file`, assembles panel +groups from a :class:`~ada.fem.capacity.sources.PanelGroupSource`, and builds +neutral :class:`~ada.fem.capacity.model.CapacityModel` objects with a +:class:`~ada.fem.capacity.stiffened_plate.CapacityModelBuilder`. +""" + +from __future__ import annotations + +import pathlib +from collections.abc import Callable + +from ada.fem.capacity import serialize +from ada.fem.capacity.extract import AuxRecords +from ada.fem.capacity.model import CapacityModel, ResolvedCase, write_neutral_json +from ada.fem.capacity.sources import PanelGroupSource +from ada.fem.capacity.stiffened_plate import CapacityModelBuilder, StiffenedPlateBuilder + + +class CapacityManager: + def __init__( + self, + sin_path: str | pathlib.Path, + source: PanelGroupSource, + *, + builder: CapacityModelBuilder | None = None, + ) -> None: + self.sin_path = pathlib.Path(sin_path) + self.source = source + self.builder = builder or StiffenedPlateBuilder() + self._aux: AuxRecords | None = None + self._mesh = None + self._groups = None + self._models: list[CapacityModel] | None = None + self._girder_models = None + + # ── construction ────────────────────────────────────────────────── + @classmethod + def from_sin( + cls, + sin_path: str | pathlib.Path, + source: PanelGroupSource, + *, + builder: CapacityModelBuilder | None = None, + ) -> "CapacityManager": + return cls(sin_path, source, builder=builder) + + @property + def mesh(self): + if self._mesh is None: + from ada.fem.formats.sesam.results.read_sin import read_sin_file + + # Step 1 is enough for geometry; the mesh is step-invariant. + self._mesh = read_sin_file(self.sin_path, step=1).mesh + return self._mesh + + @property + def aux(self) -> AuxRecords: + if self._aux is None: + self._aux = AuxRecords.from_sin(self.sin_path) + return self._aux + + # ── capacity models ─────────────────────────────────────────────── + def panel_groups(self): + """Identify (and cache) the panel-group membership. + + Exposed separately from :meth:`capacity_models` because on a large SIN + the grouping is the slow part (mesh read + geometric panel + reconstruction); a caller can report it as its own progress phase. + """ + if self._groups is None: + self._groups = list(self.source.groups(self.mesh, self.aux)) + return self._groups + + def capacity_models( + self, + *, + on_progress: Callable[[int, int], None] | None = None, + ) -> list[CapacityModel]: + """Build (and cache) the capacity models. + + ``on_progress(completed, total)`` is called once per built model so a + caller can drive a progress bar without this package depending on it. + """ + if self._models is None: + groups = self.panel_groups() + total = len(groups) + models: list[CapacityModel] = [] + for index, group in enumerate(groups, start=1): + models.append(self.builder.build(self.mesh, self.aux, group)) + if on_progress is not None: + on_progress(index, total) + self._models = models + elif on_progress is not None: + count = len(self._models) + on_progress(count, count) + return self._models + + # ── resolved design variables (Phase 3) ─────────────────────────── + def resolve_cases( + self, + result_cases: list[int] | None = None, + *, + on_progress: Callable[[int, int], None] | None = None, + ) -> list[ResolvedCase]: + from ada.fem.capacity.stress_resolve import resolve_cases + + return resolve_cases( + self.sin_path, + self.capacity_models(), + result_cases=result_cases, + on_progress=on_progress, + ) + + # ── girder capacity models (DNV-RP-C201 Section 7) ───────────────── + def girder_models( + self, + *, + on_progress: Callable[[int, int], None] | None = None, + ): + """Build (and cache) the girder capacity models. + + The stiffened-panel models are built first (the girders derive their + flanking-panel geometry — l, s, t, As, Is — from them). + """ + if self._girder_models is None: + from ada.fem.capacity.girder import build_girder_models + + group = getattr(self.source, "group", None) + self._girder_models = build_girder_models( + self.mesh, + self.aux, + self.capacity_models(), + group=group, + on_progress=on_progress, + ) + elif on_progress is not None: + count = len(self._girder_models) + on_progress(count, count) + return self._girder_models + + def resolve_girder_cases( + self, + result_cases: list[int] | None = None, + *, + on_progress: Callable[[int, int], None] | None = None, + ): + from ada.fem.capacity.girder import resolve_girder_cases + + return resolve_girder_cases( + self.sin_path, + self.girder_models(), + result_cases=result_cases, + on_progress=on_progress, + ) + + # ── serialization ───────────────────────────────────────────────── + def to_genie_json(self, path: str | pathlib.Path) -> None: + serialize.write_genie_json(path, self.capacity_models()) + + def to_neutral_json(self, path: str | pathlib.Path, result_cases: list[int] | None = None) -> None: + write_neutral_json(path, self.capacity_models(), self.resolve_cases(result_cases)) diff --git a/src/ada/fem/capacity/model.py b/src/ada/fem/capacity/model.py new file mode 100644 index 000000000..2e27f5d44 --- /dev/null +++ b/src/ada/fem/capacity/model.py @@ -0,0 +1,169 @@ +"""Neutral, serializable capacity-model data structures. + +These mirror the semantics of Genie's ``model.json`` (BucklingModels → Plates, +Stiffeners) and ``*__CriteriaResults.json`` (resolved design Variables / +VariableVectors), so that: + +* a Genie-compatible ``model.json`` mirror is a trivial serialization, and +* a downstream code-check adapter can consume the neutral JSON 1:1. + +All quantities are SI (m, Pa, N). Frozen dataclasses keep models hashable and +safe to share across result cases. +""" + +from __future__ import annotations + +import json +import pathlib +from dataclasses import asdict, dataclass, field +from typing import Any + +# Genie SectionType enum value for Holland-profile / bulb-flat sections. +SECTION_TYPE_BULB = 7 + + +@dataclass(frozen=True) +class CapMaterial: + """Plate/stiffener material. ``E``, ``fy``, ``G`` in Pa.""" + + E: float + fy: float + poisson: float = 0.3 + gamma_m: float = 1.15 + G: float | None = None + name: str = "steel" + + +@dataclass(frozen=True) +class CapSection: + """Stiffener cross-section, described the way Genie's ``model.json`` does. + + ``section_type`` is Genie's ``SectionType`` code (``7`` = bulb/Holland). The + web/flange dimensions are the *raw* profile dimensions; the bulb→angle + idealization for the actual check is the consumer's responsibility (it lives + in the code-check package, not here). + """ + + name: str + section_type: int + height: float + web_thickness: float + flange_width: float = 0.0 + flange_thickness: float = 0.0 + + @property + def is_bulb(self) -> bool: + return self.section_type == SECTION_TYPE_BULB + + +def dominant_flange( + web_thickness: float, + top: tuple[float | None, float | None], + bottom: tuple[float | None, float | None] = (None, None), +) -> tuple[float, float]: + """Pick the governing (real) flange of an I/T profile description, in m. + + Genie models T-girders as *unsymmetrical I-sections* with one dummy flange: + width equal to the web thickness (the GIORH "no flange when B == tw" + convention) and a token thickness (~0.1 mm). Taking whichever flange happens + to sit in the "top" slot therefore silently drops the real flange (e.g. + TG850x300x16x20 checked with a 16 x 0.1 mm flange). Treat a dummy flange as + absent and return the larger of the remaining flanges as ``(width, + thickness)``; ``(0, 0)`` if the profile genuinely has none. + """ + + def real(flange: tuple[float | None, float | None]) -> tuple[float, float] | None: + b, t = flange + if b is None or t is None: + return None + b, t = float(b), float(t) + if b <= 0.0 or t <= 0.0: + return None + if b <= web_thickness * (1.0 + 1e-6): # Genie dummy: width == tw + return None + if t < 5e-4: # token thickness (0.1 mm dummy flanges) + return None + return b, t + + candidates = [f for f in (real(top), real(bottom)) if f is not None] + if not candidates: + return 0.0, 0.0 + return max(candidates, key=lambda f: f[0] * f[1]) + + +@dataclass(frozen=True) +class CapPlate: + """One plate field of a capacity model.""" + + name: str + thickness: float + length: float # span direction (along stiffener) [m] + width: float # stiffener spacing direction [m] + material: CapMaterial + element_ids: tuple[int, ...] = () + + +@dataclass(frozen=True) +class CapStiffener: + """One stiffener of a capacity model.""" + + name: str + section: CapSection + material: CapMaterial + span: float # stiffener length l [m] + element_ids: tuple[int, ...] = () + eccentricity: float = 0.0 # working-point offset from plate mid-plane [m] + continuous: bool = True + + +@dataclass(frozen=True) +class CapacityModel: + """A capacity model = panel group of plates + stiffeners (one Genie ``BucklingModel``).""" + + name: str + plates: tuple[CapPlate, ...] = () + stiffeners: tuple[CapStiffener, ...] = () + id: str = "" + + def stiffener(self, name: str) -> CapStiffener: + for s in self.stiffeners: + if s.name == name: + return s + raise KeyError(name) + + +@dataclass(frozen=True) +class ResolvedCase: + """Resolved design variables for one (result case, stiffener). + + Keys in ``variables`` / ``vectors`` mirror Genie's ``Variables`` / + ``VariableVectors`` (``SigmaXSd``, ``TauSd``, ``Qdir``, + ``AverageTransverseMembraneStresses`` …) so the downstream adapter reads + them without translation. ``vectors`` hold the 3 along-span positions + (start, mid, end). + """ + + result_case: int + stiffener: str + panel_group: str = "" + capacity_model_id: str = "" + continuous: bool = True + variables: dict[str, float] = field(default_factory=dict) + vectors: dict[str, list[float]] = field(default_factory=dict) + provenance: dict[str, Any] = field(default_factory=dict) + + +# --------------------------------------------------------------------------- # +# Neutral JSON (de)serialization +# --------------------------------------------------------------------------- # +def to_neutral_dict(models: list[CapacityModel], cases: list[ResolvedCase]) -> dict: + """Serialize capacity models + resolved cases to a neutral dict.""" + return { + "format": "adapy-capacity/1", + "models": [asdict(m) for m in models], + "cases": [asdict(c) for c in cases], + } + + +def write_neutral_json(path: str | pathlib.Path, models: list[CapacityModel], cases: list[ResolvedCase]) -> None: + pathlib.Path(path).write_text(json.dumps(to_neutral_dict(models, cases), indent=2), encoding="utf-8") diff --git a/src/ada/fem/capacity/serialize.py b/src/ada/fem/capacity/serialize.py new file mode 100644 index 000000000..6a3fd3f03 --- /dev/null +++ b/src/ada/fem/capacity/serialize.py @@ -0,0 +1,71 @@ +"""Serialize a :class:`CapacityModel` to a Genie-compatible ``model.json`` mirror. + +The mirror mimics the subset of Genie's ``model.json`` schema needed for +field-by-field validation (geometry, material, section, element ids), so a +derived model can be diffed against the reference ``Cc2.run1/model.json``. +""" + +from __future__ import annotations + +import json +import pathlib + +from ada.fem.capacity.model import CapacityModel, CapMaterial, CapSection + + +def _material(m: CapMaterial) -> dict: + return { + "E": m.E, + "G": m.G if m.G is not None else m.E / (2.0 * (1.0 + m.poisson)), + "fy": m.fy, + "GammaM": m.gamma_m, + "PoissonRatio": m.poisson, + } + + +def _section(s: CapSection) -> dict: + return { + "SectionName": s.name, + "SectionType": s.section_type, + "SectionParameters": { + "Height": s.height, + "WebThickness": s.web_thickness, + "FlangeWidth": s.flange_width, + "FlangeThickness": s.flange_thickness, + }, + } + + +def to_genie_dict(models: list[CapacityModel]) -> dict: + buckling_models = [] + for i, m in enumerate(models, start=1): + plates = [ + { + "Id": j, + "Name": p.name, + "Type": 0, + "Material": _material(p.material), + "Geometry": {"Thickness": p.thickness, "Length": p.length, "Width": p.width}, + "FiniteElements": list(p.element_ids), + } + for j, p in enumerate(m.plates, start=1) + ] + stiffeners = [ + { + "Id": j, + "Name": s.name, + "Type": 1, + "Material": _material(s.material), + "Sections": [_section(s.section)], + "FiniteElements": list(s.element_ids), + "SupportAtFirstCrossSection": 0 if s.continuous else 1, + "SupportAtSecondCrossSection": 0 if s.continuous else 1, + } + for j, s in enumerate(m.stiffeners, start=1) + ] + buckling_models.append({"Id": i, "Name": m.name, "Type": 1, "Plates": plates, "Stiffeners": stiffeners}) + return {"BucklingModels": buckling_models} + + +def write_genie_json(path: str | pathlib.Path, models: list[CapacityModel]) -> None: + pathlib.Path(path).write_text(json.dumps(to_genie_dict(models), indent=2), encoding="utf-8") diff --git a/src/ada/fem/capacity/sources.py b/src/ada/fem/capacity/sources.py new file mode 100644 index 000000000..518a49940 --- /dev/null +++ b/src/ada/fem/capacity/sources.py @@ -0,0 +1,1231 @@ +"""Panel-group *grouping* sources. + +A capacity model is a panel group = a set of plate elements bound by a set of +stiffener (beam) elements. Genie's Capacity Manager identifies these groups from +the concept model and mesh. ``SinSource`` uses the SIN concept records when +available and falls back to mesh/profile geometry when they are not; the source +stays pluggable behind the :class:`PanelGroupSource` interface. + +``ModelJsonSource`` reads the element-id grouping from a Genie ``model.json``. +It remains useful as an oracle source for validating the SIN-native path against +the matched reference dataset. +""" + +from __future__ import annotations + +import itertools +import json +import pathlib +import re +from abc import ABC, abstractmethod +from collections import defaultdict +from dataclasses import dataclass, field + +import numpy as np + +from ada.config import logger +from ada.fem.capacity.model import CapSection + + +@dataclass(frozen=True) +class PlateSpec: + name: str + element_ids: tuple[int, ...] + + +@dataclass(frozen=True) +class StiffenerSpec: + name: str + element_ids: tuple[int, ...] + continuous: bool = True + # Section params the SIN reader cannot recover for bulb flats. ``None`` means + # "derive from the SIN section" (works for I/box profiles adapy parses). + section: CapSection | None = None + eccentricity: float | None = None + + +@dataclass(frozen=True) +class PanelGroupSpec: + """A panel group's membership: which plate/stiffener elements belong to it.""" + + name: str + plates: tuple[PlateSpec, ...] = () + stiffeners: tuple[StiffenerSpec, ...] = () + + +class PanelGroupSource(ABC): + """Yields the panel-group membership for a model. + + ``mesh`` is the SIN :class:`~ada.fem.results.common.Mesh`; a source may use it + (e.g. :class:`SinSource`) or ignore it (e.g. :class:`ModelJsonSource`). + """ + + @abstractmethod + def groups(self, mesh, aux=None) -> list[PanelGroupSpec]: # pragma: no cover - interface + ... + + +# --------------------------------------------------------------------------- # +# Genie model.json grouping source (milestone 1) +# --------------------------------------------------------------------------- # +_DBL = -1.7976931348623157e308 # Genie's "unset" sentinel for section params + + +def _section_from_genie(sec: dict) -> CapSection: + params = sec.get("SectionParameters", {}) + + def _val(key: str) -> float: + v = params.get(key, 0.0) + return 0.0 if (v is None or v <= _DBL) else float(v) + + return CapSection( + name=sec.get("SectionName", ""), + section_type=int(sec.get("SectionType", -1)), + height=_val("Height"), + web_thickness=_val("WebThickness"), + flange_width=_val("FlangeWidth"), + flange_thickness=_val("FlangeThickness"), + ) + + +# Genie criterion-id → continuous? +_CONTINUOUS_BY_SUPPORT = {0: True, 1: False} + + +@dataclass +class ModelJsonSource(PanelGroupSource): + """Read panel-group membership from a Genie ``model.json``.""" + + model_json: str | pathlib.Path + _data: dict = field(default_factory=dict, init=False, repr=False) + + def __post_init__(self) -> None: + self._data = json.loads(pathlib.Path(self.model_json).read_text(encoding="utf-8")) + + def groups(self, mesh=None, aux=None) -> list[PanelGroupSpec]: + out: list[PanelGroupSpec] = [] + for bm in self._data.get("BucklingModels", []): + plates = tuple( + PlateSpec( + name=p.get("Name", ""), + element_ids=tuple(int(e) for e in p.get("FiniteElements", [])), + ) + for p in bm.get("Plates", []) + ) + stiffeners = tuple( + StiffenerSpec( + name=s.get("Name", ""), + element_ids=tuple(int(e) for e in s.get("FiniteElements", [])), + continuous=self._is_continuous(s), + section=_section_from_genie((s.get("Sections") or [{}])[0]), + ) + for s in bm.get("Stiffeners", []) + ) + out.append(PanelGroupSpec(name=bm.get("Name", ""), plates=plates, stiffeners=stiffeners)) + return out + + @staticmethod + def _is_continuous(stiffener: dict) -> bool: + # A stiffener supported at both ends is treated as simply supported + # ([6.10.1]); otherwise continuous ([6.10.2]). Genie encodes support at + # each cross-section; default to continuous when unknown. + s1 = int(stiffener.get("SupportAtFirstCrossSection", 0) or 0) + s2 = int(stiffener.get("SupportAtSecondCrossSection", 0) or 0) + return not (s1 and s2) + + +# --------------------------------------------------------------------------- # +# SIN-native grouping source (no Genie model.json required) +# --------------------------------------------------------------------------- # +@dataclass +class SinSource(PanelGroupSource): + """Identify panel groups straight from the SIN mesh. + + Beam candidates are taken from the scoped mesh/set, filtered to the + secondary-stiffener profile when primary girders are present, and grouped + with the plate elements that border them. With SIN concept names available + this reproduces Genie's panel-group names, stiffener names, and plate-field + element tuples, so the stiffened-plate check runs without a Genie capacity + run. Section dimensions come from the parsed mesh sections, with raw section + cards in :class:`~ada.fem.capacity.extract.AuxRecords` as fallback. + + ``group`` optionally restricts the stiffeners to a named Sesam set/group + present in the SIN; bordering plates are always included. ``continuous`` + sets the support assumption ([6.10.2] vs [6.10.1]) for every stiffener. + + When concept names are absent, stiffener/plate names are synthesised from + element ids and multi-element stiffener chains are treated one beam element + at a time. + + Genie's Capacity Manager does not keep one panel group per concept plate + cell: it fuses adjacent cells into the *maximal rectangular stiffened field* + a DNV-RP-C201 check operates on (e.g. the double bottom, where girders run in + two directions, is split by both girder families into cells that are then + re-merged across the longitudinal girders into full-width panels). With + ``merge_panels`` (default) the concept cells are merged the same way, by a + geometric rule rather than a per-region special case: coplanar, parallel, + same-profile, regularly-spaced stiffener strips that form one rectangle are + one panel. ``include_unstiffened`` additionally emits the plate fields that + carry no secondary stiffener as stiffener-less panel groups, ready for a + future unstiffened-plate check. + """ + + group: str | list[str] | None = None # a SIN set name, or several (their union) + continuous: bool = True + classify_secondary: bool = True + merge_panels: bool = True + include_unstiffened: bool = False + + def groups(self, mesh, aux=None) -> list[PanelGroupSpec]: + from ada.fem.shapes.definitions import LineShapes, ShellShapes + + beam_ids: list[int] = [] + shell_ids: list[int] = [] + for block in mesh.elements: + etype = block.elem_info.type + ids = [int(x) for x in block.identifiers] + if isinstance(etype, LineShapes): + beam_ids.extend(ids) + elif isinstance(etype, ShellShapes): + shell_ids.extend(ids) + + # Full, model-wide element sets — kept before any scoping so the support + # split can see transverse girders/frames and bulkhead plates that lie + # outside the scoped plate set but still terminate a stiffener's span. + all_line_ids = set(beam_ids) + all_shell_ids = set(shell_ids) + + if self.group is not None: + members = self._scoped_set_members(mesh, self.group) + scoped = [b for b in beam_ids if b in members] + # A scope may be a plate set (e.g. ``Pl_T100`` — plates only, no + # stiffener beams). Pull in the stiffeners that border those plates + # so a plate set still produces its stiffened panels. + plate_members = members.intersection(shell_ids) + if plate_members: + scoped = sorted(set(scoped) | _beams_bordering_plates(mesh, beam_ids, plate_members)) + beam_ids = scoped + + from ada.fem.capacity.extract import tributary_plate_ids + + trib_by_beam: dict[int, list[int]] = {} + for beam in beam_ids: + plate_els = tributary_plate_ids(mesh, (beam,), shell_ids) + if not plate_els: + continue # a free beam (no bordering plate) is not a stiffener + trib_by_beam[beam] = plate_els + + if self.group is not None: + # Restrict the plate side to the scoped region (its plate members plus + # the stiffeners' tributary plates). The plate-field bounding and the + # merge then work over the scope, not every shell in the model. + relevant = members.intersection(shell_ids) + relevant.update(p for plates in trib_by_beam.values() for p in plates) + shell_ids = sorted(relevant) + + concept_names = getattr(aux, "concept_name_by_element", {}) if aux is not None else {} + + if self.classify_secondary: + beam_ids = self._secondary_stiffener_ids(mesh, list(trib_by_beam)) + else: + beam_ids = list(trib_by_beam) + + stiffened = self._geometric_groups( + mesh, beam_ids, shell_ids, trib_by_beam, concept_names, all_line_ids, all_shell_ids + ) + + if self.merge_panels: + stiffened = _merge_panels(mesh, stiffened, aux) + + # Subdivide each panel's plate into one field per inter-stiffener strip, + # the way Genie does (#fields = #stiffeners + 1). Done once on the final + # panel so the bays span its full width regardless of how the concept + # cells were merged. + stiffened = [_subdivide_plate_fields(mesh, spec) for spec in stiffened] + + out = list(stiffened) + if self.include_unstiffened: + out.extend(_unstiffened_panels(mesh, shell_ids, stiffened, concept_names)) + return out + + @staticmethod + def _set_members(mesh, group: str) -> set[int]: + sets = mesh.sets or {} + fs = sets.get(group) + if fs is None: + if not sets: + raise KeyError( + f"set/group {group!r} cannot be scoped: this SIN carries no set membership " + "(GSETMEMB) records - it is a concept-model export with set names only. " + "Run without a group to check the whole model." + ) + available = ", ".join(sorted(sets)) + raise KeyError(f"set/group {group!r} not found in SIN. Available: {available}") + ids = getattr(fs, "_member_ids", None) + if ids: + return {int(x) for x in ids} + return {int(getattr(m, "id", m)) for m in getattr(fs, "members", []) or []} + + @staticmethod + def _scoped_set_members(mesh, group: str | list[str]) -> set[int]: + if not isinstance(group, str): + out: set[int] = set() + for one in group: + out |= SinSource._scoped_set_members(mesh, one) + return out + members = SinSource._set_members(mesh, group) + prefix, _, area = group.partition("_area_") + if not area: + return members + + # Sesam concept models often carry broad area sets plus grid-plane sets. + # Genie's Capacity Manager scopes a named area to the active x-grid + # plane; in the Mini reference this is Mini_area_* ∩ Mini_grid_x100. + grid_sets = sorted( + name for name in (mesh.sets or {}) if name.startswith(f"{prefix}_grid_x") and name != f"{prefix}_grid_x" + ) + for grid in grid_sets: + scoped = members & SinSource._set_members(mesh, grid) + if scoped: + return scoped + return members + + @staticmethod + def _secondary_stiffener_ids(mesh, beam_ids: list[int]) -> list[int]: + """Filter out primary girders when several beam profiles share a set. + + Purely geometric — independent of any concept-naming convention. The + secondary stiffener is the *dominant* plate-bordering profile: there are + far more stiffeners than girders or stub members, so the most frequent + section (plus any of essentially the same depth) is the stiffener. This + replaces the old ``*_sbmN`` concept-role test and the "shallowest + profile" rule (which mis-fired on models with tiny stub beams below the + real stiffeners). + """ + if not beam_ids: + return [] + + from ada.fem.capacity.extract import geono_of + + beams_by_geono: dict[int, list[int]] = {} + for beam in beam_ids: + beams_by_geono.setdefault(geono_of(mesh, beam), []).append(beam) + if len(beams_by_geono) == 1: + return beam_ids + + dominant = max(beams_by_geono, key=lambda g: len(beams_by_geono[g])) + dominant_depth = SinSource._section_depth(mesh, dominant) + if dominant_depth <= 0.0: + return beam_ids + min_count = max(2, int(0.02 * len(beams_by_geono[dominant]))) + keep_geonos = { + geono + for geono, beams in beams_by_geono.items() + if ( + abs(SinSource._section_depth(mesh, geono) - dominant_depth) <= 0.05 * dominant_depth + or ( + len(beams) >= min_count + and 0.25 * dominant_depth <= SinSource._section_depth(mesh, geono) <= 1.25 * dominant_depth + ) + ) + } + return [beam for beam in beam_ids if geono_of(mesh, beam) in keep_geonos] + + @staticmethod + def _section_depth(mesh, geono: int) -> float: + sec = mesh.sections.get(geono) + name_depth = _flatbar_depth_from_name(str(getattr(sec, "name", ""))) + if name_depth is not None: + return name_depth + for attr in ("h", "r", "w_top", "w_btn"): + value = getattr(sec, attr, None) + if value: + scale = 2.0 if attr == "r" else 1.0 + return float(value) * scale + return 0.0 + + def _geometric_groups( + self, + mesh, + beam_ids: list[int], + shell_ids: list[int], + trib_by_beam: dict[int, list[int]], + concept_names: dict[int, str], + all_line_ids: set[int] | None = None, + all_shell_ids: set[int] | None = None, + ) -> list[PanelGroupSpec]: + """Atomic panels from geometry plus optional SIN concept-run identity. + + Each stiffener run (a colinear, node-connected, same-profile chain of + beam elements, broken at girder lines) plus its bounded rectangular plate + strip is one atomic panel; :func:`_merge_panels` then fuses adjacent + strips into Genie's maximal panels. Concept names, when present, are used + only to *label* the panels/stiffeners — never to decide grouping. + """ + # Concept names, when present, define support-aware multi-element + # stiffener runs; unnamed meshes keep a one-beam fallback. + runs = _stiffener_runs(mesh, beam_ids, concept_names) + # A SIN concept name spans the *whole physical stiffener*, crossing every + # transverse girder/frame/bulkhead it passes. The DNV-RP-C201 span is the + # bay between those supports, so split each named run there: a stiffener + # carried over a 19.5 m deck by five frames is six ~3.9 m spans, not one + # 19.5 m girder-scale span. Without it the check sees girder spans. + runs = _split_runs_at_supports(mesh, runs, set(beam_ids), all_line_ids, all_shell_ids) + shell_coords = _shell_coords(mesh, shell_ids) + + out: list[PanelGroupSpec] = [] + used_plate_ids: set[int] = set() + for run in sorted(runs, key=min): + edge_plate_ids = sorted({p for beam in run for p in trib_by_beam.get(beam, [])}) + plate_ids = _bounded_plate_ids(mesh, shell_ids, shell_coords, list(run), edge_plate_ids) + unique_plate_ids = [plate_id for plate_id in plate_ids if plate_id not in used_plate_ids] + if not unique_plate_ids: + continue + if not _is_rectangular_plate_field(mesh, shell_coords, unique_plate_ids, list(run)): + continue + used_plate_ids.update(unique_plate_ids) + base = _run_label_base(run, concept_names) + out.append( + PanelGroupSpec( + name=f"panelGroup({base})", + # One raw field here; ``_subdivide_plate_fields`` splits it into + # per-stiffener-bay fields once the panel is final (post-merge). + plates=(PlateSpec(name=f"Plate({base})", element_ids=tuple(sorted(unique_plate_ids))),), + stiffeners=( + StiffenerSpec( + name=_run_stiffener_label(run, concept_names), + element_ids=tuple(run), + continuous=self.continuous, + ), + ), + ) + ) + return out + + +# Stiffener beam-segment suffix. Used ONLY to derive human-readable panel/ +# stiffener *labels* from concept names — never for a grouping decision. +_BEAM_SUFFIX_RE = re.compile(r"_[sgr]bm\d+$", re.IGNORECASE) + + +def _concept_base(name: str) -> str: + return _BEAM_SUFFIX_RE.sub("", name) + + +def _stiffener_name(name: str) -> str: + if name.startswith("Stiffener_") or name.startswith("el"): + return name + return f"Stiffener_{name}" + + +def _run_label_base(run: tuple[int, ...], concept_names: dict[int, str]) -> str: + """Label base for an atomic panel — the (alphabetically first) concept cell + its stiffener belongs to, or a synthetic ``elN`` when concepts are absent. + Cosmetic only; merged-panel names union these bases via ``_combine_specs``.""" + bases = sorted({_concept_base(concept_names.get(beam, f"el{beam}")) for beam in run}) + return bases[0] if bases else f"el{run[0]}" + + +def _run_stiffener_label(run: tuple[int, ...], concept_names: dict[int, str]) -> str: + return _stiffener_name(concept_names.get(run[0], f"el{run[0]}")) + + +def _stiffener_runs( + mesh, + beam_ids: list[int], + concept_names: dict[int, str] | None = None, +) -> list[tuple[int, ...]]: + """Return support-aware stiffener runs when concept names are available. + + The atomic unit is a single beam; :func:`_merge_panels` then fuses the + bordering strips into Genie's panels. Chaining colinear beams into a single + multi-element stiffener is deliberately *not* done here: a physical stiffener + is split by Genie at every support — girders **and** the perpendicular-plate + floors/bulkheads that the FE mesh does not expose as beams — so a purely + geometric chain (which can only see girder beams) over-merges across those + floor lines and changes the panel reconstruction. Per-beam keeps the panels + correct and the grouping fully naming-independent. + """ + # The paragraph above documents the fallback. Named SIN beams carry a better + # support-aware identity, so group connected colinear segments by exact name. + if not concept_names: + return [(beam,) for beam in beam_ids] + + by_name: dict[str, list[int]] = defaultdict(list) + unnamed: list[int] = [] + for beam in beam_ids: + name = concept_names.get(beam) + if name: + by_name[name].append(beam) + else: + unnamed.append(beam) + + runs: list[tuple[int, ...]] = [] + for beams in by_name.values(): + runs.extend(_connected_colinear_runs(mesh, beams)) + runs.extend((beam,) for beam in unnamed) + return sorted(runs, key=lambda run: min(run)) + + +_FLATBAR_NAME_RE = re.compile(r"^(?:fbar|fb)(\d+(?:[._]\d+)?)x(\d+(?:[._]\d+)?)$", re.IGNORECASE) + + +def _flatbar_depth_from_name(name: str) -> float | None: + match = _FLATBAR_NAME_RE.match(name.strip()) + if match is None: + return None + return float(match.group(1).replace("_", ".")) / 1000.0 + + +def _connected_colinear_runs(mesh, beam_ids: list[int]) -> list[tuple[int, ...]]: + if len(beam_ids) <= 1: + return [tuple(beam_ids)] if beam_ids else [] + + from ada.fem.capacity.extract import _ensure_index, element_node_coords, geono_of + + idx = _ensure_index(mesh) + axes: dict[int, np.ndarray] = {} + endpoints: dict[int, tuple[int, int]] = {} + by_node: dict[int, list[int]] = defaultdict(list) + geono: dict[int, int] = {} + for beam in beam_ids: + coords = element_node_coords(mesh, beam) + axis = coords[-1] - coords[0] + norm = np.linalg.norm(axis) + if norm <= 0.0: + continue + axes[beam] = axis / norm + nodes = idx.elem_nodes.get(beam) + if not nodes: + continue + endpoints[beam] = (nodes[0], nodes[-1]) + by_node[nodes[0]].append(beam) + by_node[nodes[-1]].append(beam) + geono[beam] = geono_of(mesh, beam) + + adjacent: dict[int, set[int]] = defaultdict(set) + for beam, nodes in endpoints.items(): + for node in nodes: + for other in by_node[node]: + if other == beam: + continue + if geono.get(other) != geono[beam]: + continue + if abs(float(axes[beam] @ axes[other])) < _COLINEAR_TOL: + continue + adjacent[beam].add(other) + adjacent[other].add(beam) + + seen: set[int] = set() + out: list[tuple[int, ...]] = [] + for beam in sorted(endpoints): + if beam in seen: + continue + stack = [beam] + seen.add(beam) + component: list[int] = [] + while stack: + current = stack.pop() + component.append(current) + for other in adjacent[current]: + if other not in seen: + seen.add(other) + stack.append(other) + out.append(_order_colinear_run(mesh, component)) + return out + + +# --------------------------------------------------------------------------- # +# Support split — cut a named stiffener run into its per-bay spans +# --------------------------------------------------------------------------- # +# A Sesam concept name labels the *whole physical stiffener*, so a name-grouped, +# colinear run runs the full length of the member and crosses every transverse +# support it passes (girders, frames, and the floors/bulkheads the FE mesh +# carries as perpendicular plates rather than beams). The DNV-RP-C201 stiffened- +# plate span ``l`` is the distance between those supports, not the full member +# length, so each run is cut at its interior support nodes. This works whether a +# region has girders in one direction, in two, or terminates at a bulkhead — the +# rule is geometric (a perpendicular member at the node), not naming-based. + + +def _split_runs_at_supports( + mesh, + runs: list[tuple[int, ...]], + secondary_ids: set[int], + all_line_ids: set[int] | None, + all_shell_ids: set[int] | None, +) -> list[tuple[int, ...]]: + if not all_line_ids and not all_shell_ids: + return runs + line_ids = all_line_ids or set() + shell_ids = all_shell_ids or set() + out: list[tuple[int, ...]] = [] + for run in runs: + out.extend(_split_run_at_supports(mesh, run, secondary_ids, line_ids, shell_ids)) + return sorted(out, key=min) + + +def _split_run_at_supports( + mesh, + run: tuple[int, ...], + secondary_ids: set[int], + line_ids: set[int], + shell_ids: set[int], +) -> list[tuple[int, ...]]: + if len(run) <= 1: + return [run] + from ada.fem.capacity.extract import _ensure_index, element_node_coords + + idx = _ensure_index(mesh) + beams = list(_order_colinear_run(mesh, list(run))) + start = element_node_coords(mesh, beams[0])[0] + end = element_node_coords(mesh, beams[-1])[-1] + axis = end - start + norm = np.linalg.norm(axis) + if norm <= 0.0: + return [run] + axis = axis / norm + + subs: list[tuple[int, ...]] = [] + current: list[int] = [beams[0]] + for prev, beam in zip(beams, beams[1:]): + shared = set(idx.elem_nodes.get(prev, ())) & set(idx.elem_nodes.get(beam, ())) + node = next(iter(shared), None) + if node is not None and _is_support_node(mesh, node, axis, secondary_ids, line_ids, shell_ids): + subs.append(tuple(current)) + current = [beam] + else: + current.append(beam) + subs.append(tuple(current)) + return subs + + +def _is_support_node( + mesh, + node: int, + run_axis: np.ndarray, + secondary_ids: set[int], + line_ids: set[int], + shell_ids: set[int], +) -> bool: + """Does a transverse support terminate the stiffener span at this node? + + A support is either a perpendicular *girder/frame* (a line element that is + not itself a secondary stiffener, crossing the run roughly at right angles) + or a perpendicular *plate junction* — a floor/bulkhead the mesh exposes as + shells in a second plane meeting the panel plate here. + """ + from ada.fem.capacity.extract import _ensure_index, element_node_coords + + idx = _ensure_index(mesh) + attached = idx.node_elems.get(node, ()) + + for elem in attached: + if elem in line_ids and elem not in secondary_ids: + coords = element_node_coords(mesh, elem) + vec = coords[-1] - coords[0] + n = np.linalg.norm(vec) + if n > 0.0 and abs(float((vec / n) @ run_axis)) < _COLINEAR_TOL: + return True + + normals: set[tuple] = set() + for elem in attached: + if elem in shell_ids: + normal = _plane_normal(element_node_coords(mesh, elem)) + if normal is not None: + normals.add(tuple(np.round(normal, 1))) + if len(normals) > 1: + return True + return False + + +def _order_colinear_run(mesh, beam_ids: list[int]) -> tuple[int, ...]: + if len(beam_ids) <= 1: + return tuple(beam_ids) + + from ada.fem.capacity.extract import element_node_coords + + first = element_node_coords(mesh, beam_ids[0]) + axis = first[-1] - first[0] + axis = axis / (np.linalg.norm(axis) or 1.0) + origin = first[0] + + def key(beam: int) -> float: + coords = element_node_coords(mesh, beam) + return float(np.min((coords - origin) @ axis)) + + return tuple(sorted(beam_ids, key=key)) + + +_STIFFENER_PERP_TOL = 0.02 # m, cluster colinear beam elements onto one stiffener line + + +def _subdivide_plate_fields(mesh, spec: PanelGroupSpec) -> PanelGroupSpec: + """Split a panel's plate into one field per inter-stiffener strip. + + Genie's plate fields are the strips a DNV-RP-C201 check averages over: one + between each pair of adjacent stiffeners, plus the two edge strips, so + ``#fields == #stiffeners + 1``. The split is geometric — a plate element + belongs to the bay its perpendicular centroid falls in — which works for any + orientation and mesh density without per-region rules. + """ + plate_ids = [e for p in spec.plates for e in p.element_ids] + beams = [e for s in spec.stiffeners for e in s.element_ids] + if not plate_ids or not beams: + return spec + from ada.fem.capacity.extract import element_node_coords + + coords = {e: element_node_coords(mesh, e) for e in plate_ids + beams} + fields = _plate_fields_by_bay(mesh, coords, plate_ids, beams) + match = _PANEL_NAME_RE.match(spec.name) + base = match.group(1).split(",")[0].strip() if match else spec.name + plates = tuple(PlateSpec(name=f"Plate({base}, {i})", element_ids=group) for i, group in enumerate(fields, start=1)) + return PanelGroupSpec(name=spec.name, plates=plates, stiffeners=spec.stiffeners) + + +def _plate_fields_by_bay( + mesh, coords: dict[int, np.ndarray], plate_ids: list[int], beams: list[int] +) -> list[tuple[int, ...]]: + axes = _plate_field_axes(mesh, coords, plate_ids, beams) + if axes is None: + return [tuple(sorted(plate_ids))] + origin, _axis, perp, _normal = axes + + # Distinct stiffener lines by perpendicular position (colinear beam segments + # of one stiffener collapse to a single line). + raw = sorted(float(((coords[b] - origin) @ perp).mean()) for b in beams) + lines: list[float] = [] + for value in raw: + if not lines or value - lines[-1] > _STIFFENER_PERP_TOL: + lines.append(value) + if not lines: + return [tuple(sorted(plate_ids))] + + bays: dict[int, list[int]] = defaultdict(list) + for plate_id in plate_ids: + position = float(((coords[plate_id] - origin) @ perp).mean()) + bay = sum(1 for line in lines if line < position) + bays[bay].append(plate_id) + return [tuple(sorted(bays[bay])) for bay in sorted(bays)] + + +def _shell_coords(mesh, shell_ids: list[int]) -> dict[int, np.ndarray]: + from ada.fem.capacity.extract import element_node_coords + + return {element_id: element_node_coords(mesh, element_id) for element_id in shell_ids} + + +def _bounded_plate_ids( + mesh, + shell_ids: list[int], + shell_coords: dict[int, np.ndarray], + beams: list[int], + edge_plate_ids: list[int], +) -> list[int]: + """Fill a concept panel from its adjacent plate edges in local geometry. + + The previous SIN-native fallback used a numeric element-id range between the + stiffeners and their edge plates. That happens to work for small scoped + references, but on larger sets it pulls in unrelated shell elements whose ids + are interleaved. Here the adjacent plates define the actual rectangular + panel envelope, and candidate shells are accepted only when all their nodes + lie on the same plane and inside that local envelope. + """ + if not beams or not edge_plate_ids: + return sorted(edge_plate_ids) + + axes = _plate_field_axes(mesh, shell_coords, edge_plate_ids, beams) + if axes is None: + return sorted(edge_plate_ids) + origin, axis, perp, normal = axes + + edge_points = np.vstack([shell_coords[element_id] for element_id in edge_plate_ids]) + rel = edge_points - origin + x = rel @ axis + y = rel @ perp + tol = 1e-6 + xmin, xmax = float(x.min()) - tol, float(x.max()) + tol + ymin, ymax = float(y.min()) - tol, float(y.max()) + tol + plane_tol = 1e-4 + + out: list[int] = [] + for element_id in shell_ids: + coords = shell_coords[element_id] + rel = coords - origin + if float(np.max(np.abs(rel @ normal))) > plane_tol: + continue + xe = rel @ axis + ye = rel @ perp + if float(xe.min()) >= xmin and float(xe.max()) <= xmax and float(ye.min()) >= ymin and float(ye.max()) <= ymax: + out.append(element_id) + return sorted(out) + + +def _is_rectangular_plate_field( + mesh, + shell_coords: dict[int, np.ndarray], + plate_ids: list[int], + beams: list[int], + *, + min_area_ratio: float = 0.95, +) -> bool: + if not plate_ids or not beams: + return False + from ada.fem.capacity.extract import element_area + + axes = _plate_field_axes(mesh, shell_coords, plate_ids, beams) + if axes is None: + return False + origin, axis, perp, _normal = axes + + # Plate area is frame-invariant → sum the cached per-plate areas instead of + # re-shoelacing every plate on every merge attempt. Only the bbox needs the + # in-plane projection. + area = sum(element_area(mesh, element_id) for element_id in plate_ids) + rel = np.vstack([shell_coords[element_id] for element_id in plate_ids]) - origin + # Elementwise dot (not ``@``) — the local pixi env's threaded BLAS faults + # (0xc06d007f) on batched matmuls of this shape. + bbox_area = float(np.ptp((rel * axis).sum(axis=1)) * np.ptp((rel * perp).sum(axis=1))) + if bbox_area <= 0.0: + return False + return area / bbox_area >= min_area_ratio + + +def _plate_field_axes( + mesh, + shell_coords: dict[int, np.ndarray], + plate_ids: list[int], + beams: list[int], +) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] | None: + from ada.fem.capacity.extract import beam_axis_and_span + + if not plate_ids or not beams: + return None + axis, _span = beam_axis_and_span(mesh, (beams[0],)) + axis = np.asarray(axis, dtype=float) + axis_norm = np.linalg.norm(axis) + if axis_norm <= 0.0: + return None + axis = axis / axis_norm + + first = shell_coords[plate_ids[0]] + if len(first) < 3: + return None + normal = np.cross(first[1] - first[0], first[2] - first[0]) + normal_norm = np.linalg.norm(normal) + if normal_norm <= 0.0: + return None + normal = normal / normal_norm + + perp = np.cross(normal, axis) + perp_norm = np.linalg.norm(perp) + if perp_norm <= 0.0: + return None + perp = perp / perp_norm + + points = np.vstack([shell_coords[element_id] for element_id in plate_ids]) + origin = points.mean(axis=0) + return origin, axis, perp, normal + + +def _polygon_area_2d(xy: np.ndarray) -> float: + x = xy[:, 0] + y = xy[:, 1] + return 0.5 * abs(float(np.dot(x, np.roll(y, -1)) - np.dot(y, np.roll(x, -1)))) + + +def _beams_bordering_plates(mesh, beam_ids: list[int], plate_members: set[int]) -> set[int]: + """Stiffener beams sharing an edge (>=2 nodes) with one of ``plate_members``. + + Lets a plate-only scope (e.g. ``Pl_T100``) pull in the stiffeners that border + its plates so the capacity models for that region can still be built. Uses the + cached node index, so it is O(plate nodes), not a per-beam scan. + """ + from ada.fem.capacity.extract import _ensure_index + + idx = _ensure_index(mesh) + beam_set = set(beam_ids) + out: set[int] = set() + for plate in plate_members: + plate_nodes = idx.elem_nodes.get(plate) + if not plate_nodes: + continue + plate_node_set = set(plate_nodes) + for nid in plate_nodes: + for elem in idx.node_elems.get(nid, ()): + if ( + elem in beam_set + and elem not in out + and len(plate_node_set.intersection(idx.elem_nodes.get(elem, ()))) >= 2 + ): + out.add(elem) + return out + + +# --------------------------------------------------------------------------- # +# Geometric panel merge — reproduce Genie's maximal rectangular stiffened panels +# --------------------------------------------------------------------------- # +# Genie's concept model splits a plate at every supporting member, so a region +# crossed by girders in two directions becomes a grid of small cells. A +# DNV-RP-C201 capacity model is the *maximal rectangular stiffened field*, so the +# cells are merged back together. Two cells are one panel when they are +# coplanar, their stiffeners are parallel and of the same profile, and their +# union is a rectangle reinforced at a regular spacing sitting side by side in +# the same span bay (a lateral merge across a girder that runs parallel to the +# stiffeners). A girder line that interrupts the field shows up as a doubled +# lateral gap and stops the merge — which is why the double-bottom webs and the +# deck cells separated by frames stay split. Cells stacked end to end along the +# stiffener (different span bays) are deliberately *not* merged: whether the +# stiffener is continuous over the intervening transverse girder ([6.10.2]) or +# simply supported ([6.10.1]) is a property of Genie's concept model that the +# shared FE mesh does not expose, and over-splitting is the conservative choice. + +_PANEL_NAME_RE = re.compile(r"^panelGroup\((.*)\)$") +_COLINEAR_TOL = 0.999 # |u·v| above which two unit vectors count as parallel +_PLANE_NORMAL_DECIMALS = 2 +_PLANE_OFFSET_DECIMALS = 2 +_WINDOW_TOL = 0.05 # m, span-window coincidence → strips share a bay +_GAP_RATIO = 1.6 # a lateral gap above this × the regular spacing = a girder line / edge +_BBOX_GAP = 1.0 # m, bbox-adjacency slack for the merge spatial prune + + +def _bbox_adjacent(a: tuple[np.ndarray, np.ndarray], b: tuple[np.ndarray, np.ndarray]) -> bool: + """Do two plate bounding boxes overlap or touch within ``_BBOX_GAP`` on every axis? + + A necessary (cheap) condition for two panels to be mergeable into one + rectangle; lets the merge skip the expensive geometry test on far pairs. + """ + return bool(np.all(a[0] - _BBOX_GAP <= b[1]) and np.all(b[0] - _BBOX_GAP <= a[1])) + + +def _merge_panels(mesh, specs: list[PanelGroupSpec], aux=None) -> list[PanelGroupSpec]: + """Fuse adjacent stiffened cells into maximal rectangular panels (Genie-style). + + A plate-thickness change is a panel boundary: a buckling panel is a region of + constant plate thickness ([4]/[6]), and the stiffener axial force integrates + that thickness over the tributary (eq (5.1)). Cells of different plate + thickness are therefore put in different buckets and never merged, so a panel + never spans (e.g.) 8/20/40 mm plate regions. ``aux`` supplies the GELTH + thickness per geono; when absent, thickness is not gated (legacy behaviour). + """ + from ada.fem.capacity.extract import ( + beam_axis_and_span, + element_node_coords, + geono_of, + ) + + def _plate_thickness(pids: list[int]) -> float: + if aux is None or not pids: + return 0.0 + return round(float(aux.thickness_by_geono.get(geono_of(mesh, pids[0]), 0.0)), 6) + + specs = list(specs) + coords: dict[int, np.ndarray] = {} + for spec in specs: + for e in _spec_plate_ids(spec) + _spec_beam_ids(spec): + if e not in coords: + coords[e] = element_node_coords(mesh, e) + + axis_by_spec: dict[int, np.ndarray] = {} + bucket_of: dict[int, tuple] = {} + for i, spec in enumerate(specs): + pids, bids = _spec_plate_ids(spec), _spec_beam_ids(spec) + if not pids or not bids: + continue # not a stiffened panel (e.g. already an unstiffened field) — leave as is + normal = _plane_normal(coords[pids[0]]) + if normal is None: + continue + axis, _ = beam_axis_and_span(mesh, (bids[0],)) + axis = np.asarray(axis, float) + norm = np.linalg.norm(axis) + if norm <= 0.0: + continue + axis_by_spec[i] = axis / norm + offset = float(np.vstack([coords[e] for e in pids]).mean(0) @ normal) + bucket_of[i] = ( + geono_of(mesh, bids[0]), + tuple(np.round(normal, _PLANE_NORMAL_DECIMALS)), + round(offset, _PLANE_OFFSET_DECIMALS), + _plate_thickness(pids), # a thickness change is a panel boundary + ) + + # Plate bounding box per spec (global coords) — a merge only ever joins + # spatially adjacent panels, so a cheap bbox-adjacency test prunes the vast + # majority of pairs before the expensive rectangularity / spacing checks. + pbbox: dict[int, tuple[np.ndarray, np.ndarray]] = {} + for i in bucket_of: + pts = np.vstack([coords[e] for e in _spec_plate_ids(specs[i])]) + pbbox[i] = (pts.min(axis=0), pts.max(axis=0)) + + parent = list(range(len(specs))) + + def find(x: int) -> int: + while parent[x] != x: + parent[x] = parent[parent[x]] + x = parent[x] + return x + + buckets: dict[tuple, list[int]] = defaultdict(list) + for i, key in bucket_of.items(): + buckets[key].append(i) + + for members in buckets.values(): + changed = True + while changed: + changed = False + by_root: dict[int, list[int]] = defaultdict(list) + for i in members: + by_root[find(i)].append(i) + roots = list(by_root) + group_bbox = { + r: ( + np.min([pbbox[i][0] for i in by_root[r]], axis=0), + np.max([pbbox[i][1] for i in by_root[r]], axis=0), + ) + for r in roots + } + for ra, rb in itertools.combinations(roots, 2): + a, b = find(ra), find(rb) + if a == b: + continue + if not _bbox_adjacent(group_bbox[a], group_bbox[b]): + continue + if _can_merge( + mesh, coords, axis_by_spec[a], _union_ids(specs, by_root[a]), _union_ids(specs, by_root[b]) + ): + parent[a] = b + by_root[b] = by_root.pop(a) + by_root[b] + group_bbox[b] = ( + np.minimum(group_bbox[a][0], group_bbox[b][0]), + np.maximum(group_bbox[a][1], group_bbox[b][1]), + ) + changed = True + + by_root = defaultdict(list) + for i in range(len(specs)): + by_root[find(i)].append(i) + + out: list[PanelGroupSpec] = [] + for root in sorted(by_root, key=lambda r: min(by_root[r])): # stable order by first constituent + idxs = sorted(by_root[root]) + if len(idxs) == 1: + out.append(specs[idxs[0]]) + else: + out.append(_combine_specs([specs[i] for i in idxs])) + return out + + +def _spec_plate_ids(spec: PanelGroupSpec) -> list[int]: + return [e for p in spec.plates for e in p.element_ids] + + +def _spec_beam_ids(spec: PanelGroupSpec) -> list[int]: + return [e for s in spec.stiffeners for e in s.element_ids] + + +def _union_ids(specs: list[PanelGroupSpec], idxs: list[int]) -> tuple[list[int], list[int]]: + pids: list[int] = [] + bids: list[int] = [] + for i in idxs: + pids.extend(_spec_plate_ids(specs[i])) + bids.extend(_spec_beam_ids(specs[i])) + return pids, bids + + +def _plane_normal(elem_coords: np.ndarray) -> np.ndarray | None: + if len(elem_coords) < 3: + return None + normal = np.cross(elem_coords[1] - elem_coords[0], elem_coords[2] - elem_coords[0]) + n = np.linalg.norm(normal) + if n <= 0.0: + return None + normal = normal / n + if normal[int(np.argmax(np.abs(normal)))] < 0: # canonical sign so coplanar normals compare equal + normal = -normal + return normal + + +def _can_merge( + mesh, + coords: dict[int, np.ndarray], + axis: np.ndarray, + a: tuple[list[int], list[int]], + b: tuple[list[int], list[int]], +) -> bool: + pids = sorted(set(a[0]) | set(b[0])) + bids = sorted(set(a[1]) | set(b[1])) + axes = _plate_field_axes(mesh, coords, pids, bids) + if axes is None: + return False + origin, ax, perp, _normal = axes + if abs(float(ax @ axis)) < _COLINEAR_TOL: + return False + if not _is_rectangular_plate_field(mesh, coords, pids, bids, min_area_ratio=0.95): + return False + + # Only side-by-side strips in the *same* span bay are one panel. Two bays end + # to end along the stiffener (separated by a transverse girder) are kept apart + # even if the stiffener lines align: whether such a stiffener is continuous + # ([6.10.2]) or terminates ([6.10.1]) is a property of Genie's concept model, + # not recoverable from the mesh (the FE nodes are shared at the girder line + # either way). Over-splitting there is the safe, conservative direction. + if not _windows_match(_axis_window(coords, a[0], origin, ax), _axis_window(coords, b[0], origin, ax)): + return False + + # A doubled lateral gap means a girder line was swallowed → not one panel. + lat = np.array( + sorted(_lateral_positions(coords, a[1], origin, perp) + _lateral_positions(coords, b[1], origin, perp)) + ) + if len(lat) >= 2: + gaps = np.diff(lat) + gmin = float(gaps.min()) + if gmin > 1e-9 and float(gaps.max()) > _GAP_RATIO * gmin: + return False + return True + + +def _lateral_positions(coords: dict[int, np.ndarray], bids: list[int], origin, perp) -> list[float]: + return [float(((coords[e] - origin) @ perp).mean()) for e in bids] + + +def _axis_window(coords: dict[int, np.ndarray], pids: list[int], origin, axis) -> tuple[float, float]: + lo = hi = None + for e in pids: + proj = (coords[e] - origin) @ axis + emin, emax = float(proj.min()), float(proj.max()) + lo = emin if lo is None else min(lo, emin) + hi = emax if hi is None else max(hi, emax) + return (lo or 0.0, hi or 0.0) + + +def _windows_match(a: tuple[float, float], b: tuple[float, float]) -> bool: + return abs(a[0] - b[0]) < _WINDOW_TOL and abs(a[1] - b[1]) < _WINDOW_TOL + + +def _combine_specs(specs: list[PanelGroupSpec]) -> PanelGroupSpec: + bases: set[str] = set() + for spec in specs: + match = _PANEL_NAME_RE.match(spec.name) + if match: + bases.update(part.strip() for part in match.group(1).split(",") if part.strip()) + else: + bases.add(spec.name) + name = f"panelGroup({', '.join(sorted(bases))})" + plates = tuple(p for spec in specs for p in spec.plates) + stiffeners = tuple(s for spec in specs for s in spec.stiffeners) + return PanelGroupSpec(name=name, plates=plates, stiffeners=stiffeners) + + +# --------------------------------------------------------------------------- # +# Unstiffened plate fields — bounded plate that carries no secondary stiffener +# --------------------------------------------------------------------------- # +# Emitted opt-in (``SinSource(include_unstiffened=True)``) so a future +# unstiffened-plate check ([DNV-RP-C201 Sec. 6 / Sec. 5]) has its panels. A field +# is a maximal run of unclaimed shell elements connected across *plate* edges +# only: a shared edge that carries a beam (girder or stiffener) bounds the field, +# so a field never grows past a girder line into the next bay. Rectangular fields +# are emitted; irregular remnants (around openings, brackets) are skipped with a +# warning rather than emitted as malformed panels. + + +def _unstiffened_panels( + mesh, + shell_ids: list[int], + stiffened: list[PanelGroupSpec], + concept_names: dict[int, str], +) -> list[PanelGroupSpec]: + from ada.fem.capacity.extract import element_node_coords, element_node_ids + from ada.fem.shapes.definitions import LineShapes + + claimed = {e for spec in stiffened for e in _spec_plate_ids(spec)} + remaining = [e for e in shell_ids if e not in claimed] + if not remaining: + return [] + + # An edge (unordered node pair) that a beam runs along bounds a plate field. + beam_edges: set[frozenset] = set() + for block in mesh.elements: + if not isinstance(block.elem_info.type, LineShapes): + continue + for e in block.identifiers: + ns = element_node_ids(mesh, int(e)) + if len(ns) >= 2: + beam_edges.add(frozenset((ns[0], ns[-1]))) + + coords = {e: element_node_coords(mesh, e) for e in remaining} + nodes_by_elem = {e: element_node_ids(mesh, e) for e in remaining} + edge_to_elems: dict[frozenset, list[int]] = defaultdict(list) + for e, ns in nodes_by_elem.items(): + for a, b in zip(ns, ns[1:] + ns[:1]): + edge_to_elems[frozenset((a, b))].append(e) + + adjacent: dict[int, set[int]] = defaultdict(set) + for edge, elems in edge_to_elems.items(): + if len(elems) == 2 and edge not in beam_edges: + adjacent[elems[0]].add(elems[1]) + adjacent[elems[1]].add(elems[0]) + + seen: set[int] = set() + out: list[PanelGroupSpec] = [] + for start in remaining: + if start in seen: + continue + stack = [start] + seen.add(start) + component: list[int] = [] + while stack: + e = stack.pop() + component.append(e) + for nb in adjacent[e]: + if nb not in seen: + seen.add(nb) + stack.append(nb) + if not _is_coplanar_rectangle(coords, component): + logger.warning("capacity: skipped unstiffened plate field of %d elements (not rectangular)", len(component)) + continue + name = _unstiffened_name(component, concept_names) + out.append( + PanelGroupSpec( + name=name, + plates=(PlateSpec(name=f"Plate({name})", element_ids=tuple(sorted(component))),), + stiffeners=(), + ) + ) + return out + + +def _is_coplanar_rectangle(coords: dict[int, np.ndarray], elems: list[int], *, min_area_ratio: float = 0.95) -> bool: + first = coords[elems[0]] + normal = _plane_normal(first) + if normal is None: + return False + # In-plane axis from the first element edge (mesh is grid-aligned, so this + # hugs the rectangle without a PCA/SVD that is fragile on some LAPACK builds). + axis = first[1] - first[0] + axis = axis - (axis @ normal) * normal + an = np.linalg.norm(axis) + if an <= 0.0: + return False + axis = axis / an + perp = np.cross(normal, axis) + + centre = np.vstack([coords[e] for e in elems]).mean(0) + area = 0.0 + xy_all: list[np.ndarray] = [] + for e in elems: + rel = coords[e] - centre + xy = np.column_stack((rel @ axis, rel @ perp)) + xy_all.append(xy) + area += _polygon_area_2d(xy) + xy = np.vstack(xy_all) + bbox = float(np.ptp(xy[:, 0]) * np.ptp(xy[:, 1])) + if bbox <= 0.0: + return False + return area / bbox >= min_area_ratio + + +def _unstiffened_name(elems: list[int], concept_names: dict[int, str]) -> str: + bases = sorted({_concept_base(concept_names[e]) for e in elems if e in concept_names}) + if bases: + return f"unstiffenedPanel({', '.join(bases)})" + return f"unstiffenedPanel(el{min(elems)})" diff --git a/src/ada/fem/capacity/stiffened_plate.py b/src/ada/fem/capacity/stiffened_plate.py new file mode 100644 index 000000000..84aff62a6 --- /dev/null +++ b/src/ada/fem/capacity/stiffened_plate.py @@ -0,0 +1,199 @@ +"""Build a stiffened-plate :class:`CapacityModel` from a mesh + panel-group spec. + +This is the stiffened-plate specialization of the abstract +:class:`CapacityModelBuilder`. A future girder builder implements the same +interface and slots into :class:`~ada.fem.capacity.manager.CapacityManager` +without touching the grouping or extraction layers. +""" + +from __future__ import annotations + +import hashlib +import json +import re +from abc import ABC, abstractmethod + +import numpy as np + +from ada.config import logger +from ada.fem.capacity import extract +from ada.fem.capacity.model import ( + CapacityModel, + CapMaterial, + CapPlate, + CapSection, + CapStiffener, + dominant_flange, +) +from ada.fem.capacity.sources import PanelGroupSpec +from ada.fem.results.common import Mesh + + +def _cap_material(mesh: Mesh, element_id: int, gamma_m: float = 1.15) -> CapMaterial: + mat = mesh.materials.get(extract.matno_of(mesh, element_id)) + model = getattr(mat, "model", None) + E = float(getattr(model, "E", 2.1e11)) + fy = float(getattr(model, "sig_y", 355e6)) + poisson = float(getattr(model, "poisson", None) or 0.3) + G = E / (2.0 * (1.0 + poisson)) + name = getattr(mat, "name", "steel") + return CapMaterial(E=E, fy=fy, poisson=poisson, gamma_m=gamma_m, G=G, name=name) + + +class CapacityModelBuilder(ABC): + """Turns a panel-group membership spec into a :class:`CapacityModel`.""" + + @abstractmethod + def build(self, mesh: Mesh, aux: extract.AuxRecords, group: PanelGroupSpec) -> CapacityModel: ... + + +class StiffenedPlateBuilder(CapacityModelBuilder): + def build(self, mesh: Mesh, aux: extract.AuxRecords, group: PanelGroupSpec) -> CapacityModel: + # Stiffener axis (used to orient plate length vs width). Use the first + # stiffener with mesh elements; for an unstiffened field there is no + # stiffener, so take an in-plane edge of the first plate instead (length + # and width then become the two in-plane extents of the field); fall back + # to global z. + axis = np.array([0.0, 0.0, 1.0]) + for st in group.stiffeners: + if st.element_ids: + axis, _ = extract.beam_axis_and_span(mesh, st.element_ids) + break + else: + inplane = _plate_inplane_axis(mesh, group) + if inplane is not None: + axis = inplane + + plates: list[CapPlate] = [] + for p in group.plates: + if not p.element_ids: + continue + geono = extract.geono_of(mesh, p.element_ids[0]) + thickness = aux.thickness_by_geono.get(geono, 0.0) + if not thickness: + logger.warning("capacity: no GELTH thickness for plate %s (geono %s)", p.name, geono) + length, width = extract.plate_dimensions(mesh, p.element_ids, axis) + plates.append( + CapPlate( + name=p.name, + thickness=thickness, + length=length, + width=width, + material=_cap_material(mesh, p.element_ids[0]), + element_ids=p.element_ids, + ) + ) + + stiffeners: list[CapStiffener] = [] + for s in group.stiffeners: + if not s.element_ids: + continue + _, span = extract.beam_axis_and_span(mesh, s.element_ids) + section = s.section if s.section is not None else _section_of(mesh, aux, s.element_ids[0]) + stiffeners.append( + CapStiffener( + name=s.name, + section=section, + material=_cap_material(mesh, s.element_ids[0]), + span=span, + element_ids=s.element_ids, + eccentricity=s.eccentricity or 0.0, + continuous=s.continuous, + ) + ) + + return CapacityModel( + name=group.name, + plates=tuple(plates), + stiffeners=tuple(stiffeners), + id=_capacity_model_id(group.name, plates, stiffeners), + ) + + +def _plate_inplane_axis(mesh: Mesh, group: PanelGroupSpec) -> np.ndarray | None: + """In-plane edge direction of a group's first plate (for unstiffened fields).""" + for plate in group.plates: + if not plate.element_ids: + continue + coords = extract.element_node_coords(mesh, plate.element_ids[0]) + if len(coords) < 3: + continue + normal = np.cross(coords[1] - coords[0], coords[2] - coords[0]) + norm = np.linalg.norm(normal) + if norm <= 0.0: + continue + normal = normal / norm + axis = coords[1] - coords[0] + axis = axis - (axis @ normal) * normal + an = np.linalg.norm(axis) + if an > 0.0: + return axis / an + return None + + +def _capacity_model_id(name: str, plates: list[CapPlate], stiffeners: list[CapStiffener]) -> str: + plate_ids = sorted({int(element_id) for plate in plates for element_id in plate.element_ids}) + stiffener_ids = sorted({int(element_id) for stiffener in stiffeners for element_id in stiffener.element_ids}) + if not plate_ids and not stiffener_ids: + return name + payload = json.dumps({"p": plate_ids, "s": stiffener_ids}, separators=(",", ":")) + digest = hashlib.blake2s(payload.encode("ascii"), digest_size=6).hexdigest() + return f"{name}#{digest}" + + +def _section_of(mesh: Mesh, aux: extract.AuxRecords, element_id: int) -> CapSection: + """CapSection for a stiffener from adapy's parsed Section, with raw-card fallback.""" + section = _section_from_mesh(mesh, element_id) + if section.height > 0.0 and section.web_thickness > 0.0: + return section + geono = extract.geono_of(mesh, element_id) + if geono in aux.section_by_geono: + return aux.section_by_geono[geono] + return section + + +def _section_from_mesh(mesh: Mesh, element_id: int) -> CapSection: + """Best-effort CapSection from adapy's parsed Section.""" + sec = mesh.sections.get(extract.geono_of(mesh, element_id)) + name = getattr(sec, "name", "") + flatbar = _flatbar_from_name(str(name)) + if flatbar is not None: + height, thickness = flatbar + return CapSection( + name=name or "", + section_type=0, + height=height, + web_thickness=thickness, + flange_width=0.0, + flange_thickness=0.0, + ) + h = getattr(sec, "h", None) + t_w = getattr(sec, "t_w", None) + # A Genie T-girder arrives as an unsymmetrical I whose real flange can sit + # in either slot (the other is a width==tw dummy) — pick the governing one. + bf, tf = dominant_flange( + float(t_w) if t_w else 0.0, + (getattr(sec, "w_top", None), getattr(sec, "t_ftop", None)), + (getattr(sec, "w_btn", None), getattr(sec, "t_fbtn", None)), + ) + is_bulb = str(name).upper().startswith("HP") + return CapSection( + name=name or "", + section_type=7 if is_bulb else 0, + height=float(h) if h else 0.0, + web_thickness=float(t_w) if t_w else 0.0, + flange_width=bf, + flange_thickness=tf, + ) + + +_FLATBAR_NAME_RE = re.compile(r"^(?:fbar|fb)(\d+(?:[._]\d+)?)x(\d+(?:[._]\d+)?)$", re.IGNORECASE) + + +def _flatbar_from_name(name: str) -> tuple[float, float] | None: + match = _FLATBAR_NAME_RE.match(name.strip()) + if match is None: + return None + height = float(match.group(1).replace("_", ".")) / 1000.0 + thickness = float(match.group(2).replace("_", ".")) / 1000.0 + return height, thickness diff --git a/src/ada/fem/capacity/stress_recovery.py b/src/ada/fem/capacity/stress_recovery.py new file mode 100644 index 000000000..7a0d17626 --- /dev/null +++ b/src/ada/fem/capacity/stress_recovery.py @@ -0,0 +1,299 @@ +"""Recover shell element membrane stresses from nodal displacements. + +Some Sesam SIN files are written without element stresses (SESTRA ``RSEL +ISEL4=-1`` — the "smart load combination" / forces-only workflow), and often +without shell force resultants too, leaving only nodal displacements for the +plate (shell) elements. The DNV-RP-C201 stiffened-plate check still needs the +plate membrane stresses, so we reconstruct them the way SESTRA's postprocessing +pass does: from the element's nodal in-plane displacements, + + eps = B(xi, eta) . u_local sigma = D . eps (plane stress, membrane) + +for 3-node (TRI) and 4-node (QUAD) shells. Only the *membrane* part is recovered +(translational DOF); bending (rotations/curvature) is not needed by the +membrane-based capacity check. + +Membrane stress is evaluated **at each element corner** (not just the centre) so +the resolver keeps the along-span stress gradient it relies on for the Section-5 +start/mid/end station values — recovering only a single centroid value flattens +that gradient and underestimates the governing utilisations. + +Each returned tensor ``(sigma_xx, sigma_yy, tau_xy)`` is in the element local +frame :func:`ada.fem.capacity.stress_resolve._rotate_to_stiffener_frame` +assumes — ``ex`` = ``transform`` row 0 (else the first element edge), ``ez`` the +element normal, ``ey = ez x ex`` — tension-positive like ``RVSTRESS``. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np + +from ada.fem.capacity import extract +from ada.fem.results.common import Mesh + +# QUAD4 corner natural coordinates, CCW (matches the connectivity node order). +_QUAD_CORNERS = ((-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)) + +# NB: this module deliberately avoids ``@`` / ``np.dot`` / ``np.linalg.*``. Those +# dispatch to native BLAS/LAPACK, which crashes (Windows SEH 0xC06D007F) on some +# Sesam-env BLAS backends when called per element across a large model. All the +# operands here are tiny (2x2 Jacobian, 3-vectors), so closed-form scalar / +# elementwise arithmetic is both crash-safe and faster. + + +def _norm(v: np.ndarray) -> float: + return float(np.sqrt((v * v).sum())) + + +def _dot1(a: np.ndarray, b: np.ndarray) -> float: + return float((a * b).sum()) + + +def _element_frame(coords: np.ndarray, transform: np.ndarray | None) -> tuple[np.ndarray, np.ndarray]: + """Return orthonormal (ex, ey) of the element membrane plane. + + Mirrors ``_rotate_to_stiffener_frame``: use the supplied Sesam element basis + when available (row 0 = local x, row 2 = normal), else fall back to the first + element edge and the triangle normal. + """ + if transform is not None and np.shape(transform) == (3, 3): + basis = np.asarray(transform, dtype=float) + ex = basis[0] + ez = basis[2] + if _norm(ex) > 0 and _norm(ez) > 0: + ex = ex / _norm(ex) + ez = ez / _norm(ez) + ey = np.cross(ez, ex) + ny = _norm(ey) + if ny > 0: + return ex, ey / ny + ex = coords[1] - coords[0] + ez = np.cross(coords[1] - coords[0], coords[2] - coords[0]) + ex = ex / (_norm(ex) or 1.0) + ez = ez / (_norm(ez) or 1.0) + ey = np.cross(ez, ex) + return ex, ey / (_norm(ey) or 1.0) + + +def _plane_stress(eps: np.ndarray, E: float, nu: float) -> np.ndarray: + """Plane-stress membrane stress (sxx, syy, txy) from strain (exx, eyy, gxy).""" + f = E / (1.0 - nu * nu) + exx, eyy, gxy = float(eps[0]), float(eps[1]), float(eps[2]) + return np.array([f * (exx + nu * eyy), f * (nu * exx + eyy), f * (1.0 - nu) / 2.0 * gxy]) + + +def _tri_strain(xy: np.ndarray, uv: np.ndarray) -> np.ndarray | None: + """Constant membrane strain of a 3-node triangle. ``xy``/``uv`` are (3, 2).""" + (x1, y1), (x2, y2), (x3, y3) = xy + two_a = (x2 - x1) * (y3 - y1) - (x3 - x1) * (y2 - y1) + if abs(two_a) < 1e-20: + return None + b = np.array([y2 - y3, y3 - y1, y1 - y2]) / two_a + c = np.array([x3 - x2, x1 - x3, x2 - x1]) / two_a + u = uv[:, 0] + v = uv[:, 1] + return np.array([_dot1(b, u), _dot1(c, v), _dot1(c, u) + _dot1(b, v)]) + + +def _quad_strain_at(xy: np.ndarray, uv: np.ndarray, xi: float, eta: float) -> np.ndarray | None: + """Bilinear quad membrane strain at natural coords (xi, eta). ``xy``/``uv`` (4, 2).""" + dn_dxi = np.array([-(1 - eta), (1 - eta), (1 + eta), -(1 + eta)]) / 4.0 + dn_deta = np.array([-(1 - xi), -(1 + xi), (1 + xi), (1 - xi)]) / 4.0 + # 2x2 Jacobian, closed-form determinant + inverse (no LAPACK). + j11 = _dot1(dn_dxi, xy[:, 0]) + j12 = _dot1(dn_dxi, xy[:, 1]) + j21 = _dot1(dn_deta, xy[:, 0]) + j22 = _dot1(dn_deta, xy[:, 1]) + det = j11 * j22 - j12 * j21 + if abs(det) < 1e-20: + return None + # inv(J) = 1/det [[j22, -j12], [-j21, j11]] -> d/dx, d/dy rows + dn_dx = (j22 * dn_dxi - j12 * dn_deta) / det + dn_dy = (-j21 * dn_dxi + j11 * dn_deta) / det + u = uv[:, 0] + v = uv[:, 1] + return np.array([_dot1(dn_dx, u), _dot1(dn_dy, v), _dot1(dn_dy, u) + _dot1(dn_dx, v)]) + + +@dataclass +class _RecoveryOp: + """Step-invariant displacement→corner-stress operator for one shell element. + + The element frame, the planar node coordinates, and the shape-function + derivative operators (tri ``b``/``c`` or per-corner quad ``dn_dx``/``dn_dy``) + depend only on geometry, so they are built once and cached on the mesh. + Recovery for a result case is then just projecting that case's nodal + displacements and applying the cached operators — no Jacobian inverse, frame + construction, or coordinate projection per case. + """ + + node_ids: list[int] + corner_coords: list[np.ndarray] + ex: np.ndarray + ey: np.ndarray + E: float + nu: float + is_tri: bool + b: np.ndarray | None = None + c: np.ndarray | None = None + dn_dx: list[np.ndarray] | None = None + dn_dy: list[np.ndarray] | None = None + + +def _build_recovery_op( + mesh: Mesh, + element_id: int, + E: float, + nu: float, + transform: np.ndarray | None, +) -> _RecoveryOp | None: + """Build the cached recovery operator, or ``None`` for unsupported shapes / + degenerate geometry (the element then resolves to ``[]`` every case).""" + node_ids = extract.element_node_ids(mesh, element_id) + if len(node_ids) not in (3, 4): + return None + coords = extract.element_node_coords(mesh, element_id) + if len(coords) != len(node_ids): + return None + + ex, ey = _element_frame(coords, transform) + origin = coords[0] + rel = coords - origin + xy = np.column_stack([(rel * ex).sum(axis=1), (rel * ey).sum(axis=1)]) + + if len(node_ids) == 3: + (x1, y1), (x2, y2), (x3, y3) = xy + two_a = (x2 - x1) * (y3 - y1) - (x3 - x1) * (y2 - y1) + if abs(two_a) < 1e-20: + return None + b = np.array([y2 - y3, y3 - y1, y1 - y2]) / two_a + c = np.array([x3 - x2, x1 - x3, x2 - x1]) / two_a + return _RecoveryOp(node_ids, [coords[i] for i in range(3)], ex, ey, E, nu, True, b=b, c=c) + + dn_dx_l: list[np.ndarray] = [] + dn_dy_l: list[np.ndarray] = [] + corner_coords: list[np.ndarray] = [] + for i, (xi, eta) in enumerate(_QUAD_CORNERS): + dn_dxi = np.array([-(1 - eta), (1 - eta), (1 + eta), -(1 + eta)]) / 4.0 + dn_deta = np.array([-(1 - xi), -(1 + xi), (1 + xi), (1 - xi)]) / 4.0 + j11 = _dot1(dn_dxi, xy[:, 0]) + j12 = _dot1(dn_dxi, xy[:, 1]) + j21 = _dot1(dn_deta, xy[:, 0]) + j22 = _dot1(dn_deta, xy[:, 1]) + det = j11 * j22 - j12 * j21 + if abs(det) < 1e-20: + return None + dn_dx_l.append((j22 * dn_dxi - j12 * dn_deta) / det) + dn_dy_l.append((-j21 * dn_dxi + j11 * dn_deta) / det) + corner_coords.append(coords[i]) + return _RecoveryOp(node_ids, corner_coords, ex, ey, E, nu, False, dn_dx=dn_dx_l, dn_dy=dn_dy_l) + + +def _apply_recovery_op(op: _RecoveryOp, disp_by_node: dict[int, np.ndarray]): + try: + disp = np.array([disp_by_node[n][:3] for n in op.node_ids], dtype=float) + except KeyError: + return [] + u = (disp * op.ex).sum(axis=1) + v = (disp * op.ey).sum(axis=1) + if op.is_tri: + strain = np.array([_dot1(op.b, u), _dot1(op.c, v), _dot1(op.c, u) + _dot1(op.b, v)]) + tensor = _plane_stress(strain, op.E, op.nu) # constant over a CST + return [(op.corner_coords[i], tensor) for i in range(3)] + out: list[tuple[np.ndarray, np.ndarray]] = [] + for i in range(4): + dn_dx = op.dn_dx[i] + dn_dy = op.dn_dy[i] + strain = np.array([_dot1(dn_dx, u), _dot1(dn_dy, v), _dot1(dn_dy, u) + _dot1(dn_dx, v)]) + out.append((op.corner_coords[i], _plane_stress(strain, op.E, op.nu))) + return out + + +def recover_membrane_corner_points( + mesh: Mesh, + element_id: int, + disp_by_node: dict[int, np.ndarray], + E: float, + nu: float, + transform: np.ndarray | None = None, +) -> list[tuple[np.ndarray, np.ndarray]]: + """Per-corner ``(xyz, (sxx, syy, txy))`` membrane stress for one shell element. + + Returns ``[]`` for unsupported shapes, degenerate geometry, or missing nodal + displacements. The step-invariant operator is built once and cached on the + mesh, so repeated cases only project displacements (see :class:`_RecoveryOp`). + """ + cache = getattr(mesh, "_cap_recovery_ops", None) + if cache is None: + cache = {} + try: + mesh._cap_recovery_ops = cache # type: ignore[attr-defined] + except Exception: + pass + if element_id in cache: + op = cache[element_id] + else: + op = _build_recovery_op(mesh, element_id, E, nu, transform) + cache[element_id] = op + if op is None: + return [] + return _apply_recovery_op(op, disp_by_node) + + +def build_recovered_stress( + mesh: Mesh, + disp_by_node: dict[int, np.ndarray], + material_by_element: dict[int, tuple[float, float]], + step: int, + transform_by_element: dict[int, np.ndarray] | None = None, +): + """Synthetic ``STRESS`` block + result-point coords for a stress-less step. + + Each corner is emitted as a duplicated bottom/top pair (the membrane value on + both surfaces) so the resolver's top/bottom pairing yields one membrane point + per corner at the corner coordinate. Returns ``(ElementFieldData, + coords_by_element)`` where ``coords_by_element[eid]`` maps point id → xyz, to + merge into ``AuxRecords.result_point_coords_by_element``. + """ + from ada.fem.results.field_data import ElementFieldData, FieldPosition + + transform_by_element = transform_by_element or {} + rows: list[list[float]] = [] + coords_by_element: dict[int, dict[int, np.ndarray]] = {} + for element_id, (E, nu) in material_by_element.items(): + pts = recover_membrane_corner_points( + mesh, element_id, disp_by_node, E, nu, transform_by_element.get(element_id) + ) + if not pts: + continue + n = len(pts) + cmap: dict[int, np.ndarray] = {} + for k, (xyz, tensor) in enumerate(pts): + bot, top = k + 1, k + 1 + n + rows.append([float(element_id), float(bot), float(tensor[0]), float(tensor[1]), float(tensor[2])]) + rows.append([float(element_id), float(top), float(tensor[0]), float(tensor[1]), float(tensor[2])]) + cmap[bot] = np.asarray(xyz, dtype=float) + cmap[top] = np.asarray(xyz, dtype=float) + coords_by_element[element_id] = cmap + + block = ElementFieldData( + "STRESS", + int(step), + ["SIGXX", "SIGYY", "TAUXY"], + np.array(rows, dtype=float) if rows else np.empty((0, 5), dtype=float), + field_pos=FieldPosition.INT, + ) + return block, coords_by_element + + +def displacements_by_node(disp_block) -> dict[int, np.ndarray]: + """Map node id → translation 3-vector from an ``RVNODDIS`` field block. + + The block's ``values`` rows are ``[node_id, U1, U2, U3, U4, U5, U6]``. + """ + out: dict[int, np.ndarray] = {} + for row in disp_block.values: + out[int(row[0])] = np.asarray(row[1:4], dtype=float) + return out diff --git a/src/ada/fem/capacity/stress_resolve.py b/src/ada/fem/capacity/stress_resolve.py new file mode 100644 index 000000000..90a685741 --- /dev/null +++ b/src/ada/fem/capacity/stress_resolve.py @@ -0,0 +1,1265 @@ +"""Resolve FE element membrane stresses → DNV-RP-C201 design variables. + +This is the core step Genie performs *upstream* (reporting only the resolved +values). For each (result case, stiffener) it produces the design variables a +Section 6 check needs, keyed exactly as Genie emits them so a downstream adapter +consumes them unchanged. + +Calibration status (validated against the Mini-topside ``*__CriteriaResults.json``): + +* ``AverageTransverseMembraneStresses`` (mid / field value) — **matches to ~1e-5 + relative** across every stiffener and result case. +* ``AverageShearStresses`` / ``TauSd`` — **matches to ~1e-5 relative**. +* Axial force ``N`` = ``AvgLongitudinalMembrane·(t·s) + beam axial`` — reproduces + Genie's ``AxialLoads`` (and hence ``Nsd = max(N, 0)``). The longitudinal + membrane aggregation still carries a few-percent residual under calibration + (see :func:`calibration_report`); the transverse/shear path is exact. + +Remaining calibration: the along-span variation of the transverse stress (the +two *end* positions; the *mid* is exact) needs result-point binning — currently +the 3-position vectors carry the field value at all three positions. + +Update: the resolver now emits Section-5 position vectors for axial force and +moment resultants, and midpoint membrane values are area-weighted by element. +The remaining tail is the irregular multi-element cut-plane interpolation. + +Sign convention: Genie is compression-positive, so normal membrane stresses and +the plate axial contribution are negated relative to the FE tension-positive +convention. +""" + +from __future__ import annotations + +import dataclasses +import pathlib +from collections.abc import Callable +from dataclasses import dataclass +from types import SimpleNamespace +from typing import Any + +import numpy as np + +from ada.fem.capacity import extract +from ada.fem.capacity.model import CapacityModel, CapSection, ResolvedCase +from ada.fem.results.common import Mesh + + +_MAX_PROVENANCE_SOURCES = 8 + + +def _block_elem_index(block) -> dict[int, np.ndarray]: + """Cache + return ``{element_id: rows}`` for a field block (column 0 = id). + + ``ElementFieldData.get_by_element_id`` scans the whole block, so calling it + once per element across a model is quadratic. Resolving a multi-hundred-model + SIN that way dominates the runtime; index each block's rows by element id + once (cached on the block — blocks are rebuilt per step, so it stays fresh). + """ + cached = getattr(block, "_cap_elem_index", None) + if cached is not None: + return cached + vals = block.values + built: dict[int, np.ndarray] = {} + if vals.size: + elem_col = vals[:, 0].astype(np.int64) + order = np.argsort(elem_col, kind="stable") + sorted_ids = elem_col[order] + uniq, starts = np.unique(sorted_ids, return_index=True) + bounds = np.append(starts, len(sorted_ids)) + for k, eid in enumerate(uniq): + built[int(eid)] = vals[order[bounds[k] : bounds[k + 1]]] + block._cap_elem_index = built + return built + + +def _rows_for_element(blocks, element_id: int) -> list[np.ndarray]: + out = [] + for b in blocks: + sub = _block_elem_index(b).get(element_id) + if sub is not None and sub.size: + out.append(sub) + return out + + +def _element_membrane_tensor(stress_blocks, element_id: int) -> np.ndarray | None: + """Mean membrane (SIGXX, SIGYY, TAUXY) of an element over its result points, + in the *element* coordinate frame.""" + rows = [sub[:, 2:5] for sub in _rows_for_element(stress_blocks, element_id)] # SIGXX, SIGYY, TAUXY + if not rows: + return None + return np.vstack(rows).mean(axis=0) + + +def _element_stress_rows(stress_blocks, element_id: int) -> dict[int, np.ndarray]: + """Element stress tensor rows keyed by 1-based result-point id.""" + rows: dict[int, list[np.ndarray]] = {} + for sub in _rows_for_element(stress_blocks, element_id): + for row in sub: + rows.setdefault(int(row[1]), []).append(np.asarray(row[2:5], dtype=float)) + return {point_id: np.vstack(values).mean(axis=0) for point_id, values in rows.items()} + + +@dataclass(frozen=True) +class _MembranePoint: + element_id: int + node_ids: tuple[int, ...] + result_points: tuple[int, ...] + xyz: np.ndarray + tensor: np.ndarray + calculation: str + + +def _rotation_cossin( + mesh: Mesh, + element_id: int, + axis: np.ndarray, + transform: np.ndarray | None = None, +) -> tuple[float, float]: + """Cosine/sine of the rotation from the element frame to the stiffener frame. + + This depends only on element geometry (or its Sesam basis) and the stiffener + axis — all step-invariant — so it is computed once per (stiffener, element) + and reused across every result case. The actual stress rotation is the cheap + closed form in :func:`_apply_rotation`. + """ + if transform is not None and np.shape(transform) != (3, 3): + transform = None + if transform is not None: + basis = np.asarray(transform, dtype=float) + lx = basis[0] + normal = basis[2] + if np.linalg.norm(lx) <= 0.0 or np.linalg.norm(normal) <= 0.0: + transform = None + if transform is None: + coords = extract.element_node_coords(mesh, element_id) + lx = coords[1] - coords[0] + normal = np.cross(coords[1] - coords[0], coords[2] - coords[0]) + lx = lx / (np.linalg.norm(lx) or 1.0) + normal = normal / (np.linalg.norm(normal) or 1.0) + ax = axis - normal * float(np.dot(axis, normal)) # project stiffener axis into plate plane + ax = ax / (np.linalg.norm(ax) or 1.0) + c = float(np.dot(lx, ax)) + s = float(np.dot(np.cross(lx, ax), normal)) + return c, s + + +def _apply_rotation(cs: tuple[float, float], tensor: np.ndarray) -> np.ndarray: + """Rotate an element-frame membrane ``(sxx, syy, txy)`` into the stiffener + frame given the precomputed ``(cos, sin)`` from :func:`_rotation_cossin`. + + Returns ``(sigma_long, sigma_trans, tau)`` where *long* is along the + stiffener axis and *trans* is perpendicular (in the plate plane). + """ + c, s = cs + sxx, syy, txy = tensor + s_long = sxx * c * c + syy * s * s + 2 * txy * s * c + s_trans = sxx * s * s + syy * c * c - 2 * txy * s * c + tau = (syy - sxx) * s * c + txy * (c * c - s * s) + return np.array([s_long, s_trans, tau]) + + +def _element_membrane_point_records( + mesh: Mesh, + aux: extract.AuxRecords, + stress_blocks, + element_id: int, + axis: np.ndarray, + cs: tuple[float, float] | None = None, +) -> list[_MembranePoint]: + rows = _element_stress_rows(stress_blocks, element_id) + if not rows: + return [] + + if cs is None: + transform = aux.element_transform_by_element.get(element_id) + cs = _rotation_cossin(mesh, element_id, axis, transform) + + node_ids = tuple(extract.element_node_ids(mesh, element_id)) + coords = aux.result_point_coords_by_element.get(element_id, {}) + point_ids = sorted(rows) + if coords and len(point_ids) % 2 == 0: + half = len(point_ids) // 2 + paired: list[_MembranePoint] = [] + for a, b in zip(point_ids[:half], point_ids[half:]): + if a not in coords or b not in coords: + continue + tensor = (rows[a] + rows[b]) / 2.0 + xyz = (coords[a] + coords[b]) / 2.0 + paired.append( + _MembranePoint( + element_id=element_id, + node_ids=node_ids, + result_points=(int(a), int(b)), + xyz=xyz, + tensor=_apply_rotation(cs, tensor), + calculation=( + "paired shell top/bottom STRESS result points averaged to membrane " + "stress, then rotated into the stiffener frame" + ), + ) + ) + if paired: + return paired + + tensor = _element_membrane_tensor(stress_blocks, element_id) + if tensor is None: + return [] + xyz = extract.element_node_coords(mesh, element_id).mean(axis=0) + return [ + _MembranePoint( + element_id=element_id, + node_ids=node_ids, + result_points=tuple(int(p) for p in point_ids), + xyz=xyz, + tensor=_apply_rotation(cs, tensor), + calculation=( + "mean of available shell STRESS result rows at the element centroid, " + "then rotated into the stiffener frame" + ), + ) + ] + + +def _point_tuples(records: list[_MembranePoint]) -> list[tuple[np.ndarray, np.ndarray]]: + return [(r.xyz, r.tensor) for r in records] + + +def _rotate_to_stiffener_frame( + mesh: Mesh, + element_id: int, + tensor: np.ndarray, + axis: np.ndarray, + transform: np.ndarray | None = None, +) -> np.ndarray: + """Backward-compatible one-shot rotate (basis + apply), for callers that do + not have a cached ``(cos, sin)``.""" + return _apply_rotation(_rotation_cossin(mesh, element_id, axis, transform), tensor) + + +def _element_membrane_points( + mesh: Mesh, + aux: extract.AuxRecords, + stress_blocks, + element_id: int, + axis: np.ndarray, + cs: tuple[float, float] | None = None, +) -> list[tuple[np.ndarray, np.ndarray]]: + """Membrane stress points as ``(xyz, rotated_tensor)``. + + Shell stress output is top/bottom surface data. Pair the two surfaces by + result-point id order and average them into membrane values at the midpoint + coordinate before rotating into the stiffener frame. + + ``cs`` is the precomputed ``(cos, sin)`` rotation for this element in the + stiffener frame (step-invariant); pass it to skip recomputing the element + basis on every result case. When ``None`` it is computed here. + """ + return _point_tuples(_element_membrane_point_records(mesh, aux, stress_blocks, element_id, axis, cs)) + + +def _element_area(mesh: Mesh, element_id: int) -> float: + coords = extract.element_node_coords(mesh, element_id) + if len(coords) < 3: + return 0.0 + origin = coords[0] + area = 0.0 + for i in range(1, len(coords) - 1): + area += float(np.linalg.norm(np.cross(coords[i] - origin, coords[i + 1] - origin))) / 2.0 + return area + + +def _area_weighted_element_mean( + mesh: Mesh, + points_by_element: dict[int, list[tuple[np.ndarray, np.ndarray]]], + area_by_element: dict[int, float] | None = None, +) -> np.ndarray | None: + values = [] + weights = [] + for element_id, points in points_by_element.items(): + if not points: + continue + values.append(np.array([tensor for _, tensor in points]).mean(axis=0)) + weights.append(_elem_area(mesh, element_id, area_by_element)) + if not values: + return None + return np.average(np.array(values), axis=0, weights=np.array(weights)) + + +def _elem_area(mesh: Mesh, element_id: int, area_by_element: dict[int, float] | None) -> float: + """Element area, from the per-stiffener cache when available (step-invariant).""" + if area_by_element is not None: + cached = area_by_element.get(element_id) + if cached is not None: + return cached + return _element_area(mesh, element_id) + + +def _area_weighted_pressure( + mesh: Mesh, + aux: extract.AuxRecords, + result_case: int, + element_ids: list[int], + area_by_element: dict[int, float] | None = None, +) -> float: + """Mean signed shell pressure over the adjacent plate field.""" + pressures = aux.pressure_by_case_element.get(result_case, {}) + values = [] + weights = [] + for element_id in element_ids: + if element_id not in pressures: + continue + values.append(float(pressures[element_id])) + weights.append(_elem_area(mesh, element_id, area_by_element)) + if not values: + return 0.0 + return float(np.average(np.array(values), weights=np.array(weights))) + + +def _adjacent_plate_field_ids(model: CapacityModel, edge_plate_ids: list[int]) -> list[int]: + """Expand edge-sharing plate elements to their full adjacent plate fields.""" + if not edge_plate_ids: + return [] + edge = set(edge_plate_ids) + out: list[int] = [] + seen: set[int] = set() + for plate in model.plates: + if not edge.intersection(plate.element_ids): + continue + for element_id in plate.element_ids: + if element_id not in seen: + out.append(element_id) + seen.add(element_id) + return out or edge_plate_ids + + +def _beam_origin(mesh: Mesh, element_ids: tuple[int, ...]) -> np.ndarray: + return extract.element_node_coords(mesh, element_ids[0])[0] + + +def _edge_points( + points: list[tuple[np.ndarray, np.ndarray]], + origin: np.ndarray, + axis: np.ndarray, +) -> list[tuple[np.ndarray, np.ndarray]]: + """Points closest to the stiffener line.""" + if not points: + return [] + ax = np.asarray(axis, dtype=float) + ax = ax / (np.linalg.norm(ax) or 1.0) + distances = [] + for xyz, _ in points: + offset = xyz - origin + line_vec = offset - ax * float(np.dot(offset, ax)) + distances.append(float(np.linalg.norm(line_vec))) + min_dist = min(distances) + tol = max(1e-6, min_dist + 1e-5) + return [point for point, distance in zip(points, distances) if distance <= tol] + + +def _station_values( + points: list[tuple[np.ndarray, np.ndarray]], + origin: np.ndarray, + axis: np.ndarray, + component: int, + mid_value: float, +) -> list[float]: + """Start/mid/end values from result-point coordinates along the stiffener.""" + if not points: + return [mid_value] * 3 + ax = np.asarray(axis, dtype=float) + ax = ax / (np.linalg.norm(ax) or 1.0) + along = np.array([float(np.dot(xyz - origin, ax)) for xyz, _ in points]) + values = np.array([float(v[component]) for _, v in points]) + span = float(np.ptp(along)) + if span <= 1e-12: + return [float(values.mean()), mid_value, float(values.mean())] + tol = max(span * 1e-6, 1e-6) + start = float(values[along <= along.min() + tol].mean()) + end = float(values[along >= along.max() - tol].mean()) + return [start, mid_value, end] + + +def _station_records( + records: list[_MembranePoint], + origin: np.ndarray, + axis: np.ndarray, + position: int, +) -> list[_MembranePoint]: + if not records: + return [] + if position == 2: + return records + ax = np.asarray(axis, dtype=float) + ax = ax / (np.linalg.norm(ax) or 1.0) + along = np.array([float(np.dot(r.xyz - origin, ax)) for r in records]) + span = float(np.ptp(along)) + if span <= 1e-12: + return records + tol = max(span * 1e-6, 1e-6) + if position == 1: + return [r for r, a in zip(records, along) if a <= along.min() + tol] + return [r for r, a in zip(records, along) if a >= along.max() - tol] + + +def _edge_point_records( + records: list[_MembranePoint], + origin: np.ndarray, + axis: np.ndarray, +) -> list[_MembranePoint]: + if not records: + return [] + ax = np.asarray(axis, dtype=float) + ax = ax / (np.linalg.norm(ax) or 1.0) + distances = [] + for record in records: + offset = record.xyz - origin + line_vec = offset - ax * float(np.dot(offset, ax)) + distances.append(float(np.linalg.norm(line_vec))) + min_dist = min(distances) + tol = max(1e-6, min_dist + 1e-5) + return [record for record, distance in zip(records, distances) if distance <= tol] + + +def _round_source_value(value: float) -> float: + return float(f"{float(value):.8g}") + + +def _source_payload( + record: _MembranePoint, + component: int, + *, + sign: float = 1.0, + absolute: bool = False, + unit: str = "Pa", +) -> dict[str, Any]: + value = float(record.tensor[component]) * sign + if absolute: + value = abs(value) + return { + "element_id": int(record.element_id), + "node_ids": [int(n) for n in record.node_ids], + "result_points": [int(p) for p in record.result_points], + "value": _round_source_value(value), + "unit": unit, + } + + +def _source_set_payload( + label: str, + records: list[_MembranePoint], + component: int, + *, + sign: float = 1.0, + absolute: bool = False, + unit: str = "Pa", +) -> dict[str, Any]: + shown = records[:_MAX_PROVENANCE_SOURCES] + return { + "label": label, + "source_count": len(records), + "element_ids": sorted({int(r.element_id) for r in records}), + "sources": [ + _source_payload(r, component, sign=sign, absolute=absolute, unit=unit) + for r in shown + ], + "truncated_source_count": max(0, len(records) - len(shown)), + } + + +def _station_stress_provenance( + *, + records: list[_MembranePoint], + origin: np.ndarray, + axis: np.ndarray, + position: int, + component: int, + label: str, + calculation: str, + formula: str, + sign: float = 1.0, + absolute: bool = False, +) -> dict[str, Any]: + source_records = _station_records(records, origin, axis, position) + return { + "label": label, + "position": position, + "calculation": calculation, + "formula": formula, + "source_sets": [ + _source_set_payload("contributing shell membrane result points", source_records, component, sign=sign, absolute=absolute) + ], + } + + +def _panel_sigma_x_provenance( + *, + all_records: list[_MembranePoint], + edge_records: list[_MembranePoint], + origin: np.ndarray, + axis: np.ndarray, + position: int, +) -> dict[str, Any]: + return { + "label": f"Axial membrane stress position {position}", + "position": position, + "calculation": ( + "Shell top/bottom STRESS result points are averaged to membrane stress, " + "rotated into the stiffener frame, converted to compression-positive " + "stress, then sampled at the Section 5 position. The displayed value is " + "the mean of the edge-tributary station value and the closest-to-stiffener " + "station value." + ), + "formula": "sigma_x,i = 0.5 * (all edge-tributary station mean + closest-edge station mean)", + "source_sets": [ + _source_set_payload( + "edge-tributary shell membrane result points", + _station_records(all_records, origin, axis, position), + 0, + sign=-1.0, + ), + _source_set_payload( + "closest-to-stiffener shell membrane result points", + _station_records(edge_records, origin, axis, position), + 0, + sign=-1.0, + ), + ], + } + + +def _beam_position_nodes(node_ids: list[int], position: int) -> list[int]: + if not node_ids: + return [] + if position == 1: + return [int(node_ids[0])] + if position == 3: + return [int(node_ids[-1])] + if len(node_ids) >= 3: + return [int(node_ids[2])] + return [int(n) for n in node_ids] + + +def _beam_component_position_data( + mesh: Mesh, + force_blocks, + element_ids: tuple[int, ...], + component: int, + *, + sign: float = 1.0, + unit: str, + component_label: str, +) -> tuple[list[float], dict[int, dict[str, Any]]]: + by_pos: dict[int, list[dict[str, Any]]] = {1: [], 2: [], 3: []} + exact_by_pos: dict[int, list[float]] = {1: [], 2: [], 3: []} + for el in element_ids: + nodes = extract.element_node_ids(mesh, el) + for sub in _rows_for_element(force_blocks, el): + for row in sub: + pos = int(row[1]) + if pos not in by_pos: + continue + raw = float(row[2 + component]) + signed = raw * sign + exact_by_pos[pos].append(signed) + by_pos[pos].append( + { + "element_id": int(el), + "node_ids": _beam_position_nodes(nodes, pos), + "force_position": pos, + "raw_value": _round_source_value(raw), + "value": _round_source_value(signed), + "unit": unit, + } + ) + + values = [float(np.mean(exact_by_pos[pos])) for pos in (1, 2, 3) if exact_by_pos[pos]] + fallback = float(np.mean(values)) if values else 0.0 + out = [float(np.mean(exact_by_pos[pos])) if exact_by_pos[pos] else fallback for pos in (1, 2, 3)] + provenance = { + pos: { + "label": f"{component_label} position {pos}", + "position": pos, + "calculation": ( + "Mean of matching Sesam beam FORCES rows at Section 5 result position " + f"{pos}. If that position has no rows, the mean of available positions is used." + ), + "formula": f"mean({component_label} FORCES rows at position {pos})", + "source_sets": [ + { + "label": "beam FORCE result rows", + "source_count": len(by_pos[pos]), + "element_ids": sorted({int(s["element_id"]) for s in by_pos[pos]}), + "sources": by_pos[pos][:_MAX_PROVENANCE_SOURCES], + "truncated_source_count": max(0, len(by_pos[pos]) - _MAX_PROVENANCE_SOURCES), + } + ], + } + for pos in (1, 2, 3) + } + return out, provenance + + +def _term(label: str, value: float, unit: str) -> dict[str, Any]: + return {"label": label, "value": _round_source_value(value), "unit": unit} + + +def _beam_component_positions( + force_blocks, + element_ids: tuple[int, ...], + component: int, + *, + sign: float = 1.0, +) -> list[float]: + """Mean beam force component at Section-5 positions 1/2/3.""" + by_pos: dict[int, list[float]] = {1: [], 2: [], 3: []} + for el in element_ids: + for sub in _rows_for_element(force_blocks, el): + for row in sub: + pos = int(row[1]) + if pos in by_pos: + by_pos[pos].append(float(row[2 + component]) * sign) + + values = [float(np.mean(by_pos[pos])) for pos in (1, 2, 3) if by_pos[pos]] + if not values: + return [0.0, 0.0, 0.0] + mean = float(np.mean(values)) + return [float(np.mean(by_pos[pos])) if by_pos[pos] else mean for pos in (1, 2, 3)] + + +def _beam_axial_positions(force_blocks, element_ids: tuple[int, ...]) -> list[float]: + """Beam axial force (NXX) at Section-5 positions, compression positive. + + Sign-flipped to Genie's compression-positive convention. + """ + return _beam_component_positions(force_blocks, element_ids, 0, sign=-1.0) + + +def _beam_moment_positions(force_blocks, element_ids: tuple[int, ...]) -> list[float]: + """Beam bending moment contribution at Section-5 positions. + + Sesam's line-force block reports ``MXY`` with the opposite sign to Genie's + moment convention for these stiffened-plate checks. + """ + return _beam_component_positions(force_blocks, element_ids, 4, sign=-1.0) + + +def _section_area_and_centroid(section: CapSection) -> tuple[float, float]: + """Stiffener area and centroid distance from the plate reference surface.""" + h = float(section.height) + tw = float(section.web_thickness) + bf = float(section.flange_width) + tf = float(section.flange_thickness) + if h <= 0.0 or tw <= 0.0: + return 0.0, 0.0 + + if bf <= 0.0 or tf <= 0.0: + area = h * tw + return area, h / 2.0 + + web_h = max(h - tf, 0.0) + web_area = web_h * tw + flange_area = bf * tf + area = web_area + flange_area + if area <= 0.0: + return 0.0, 0.0 + centroid = (web_area * (web_h / 2.0) + flange_area * (web_h + tf / 2.0)) / area + return area, centroid + + +@dataclass +class _StiffGeom: + """Step-invariant geometry/topology for one stiffener. + + Everything here depends only on the mesh + capacity-model definition (not on + any result case), so it is built once per stiffener and reused across every + case in :func:`resolve_cases`. Only the field *values* vary per case. + """ + + element_ids: tuple[int, ...] + axis: np.ndarray + origin: np.ndarray + edge_trib: list[int] + field_trib: list[int] + cs_by_element: dict[int, tuple[float, float]] + area_by_element: dict[int, float] + t: float + s_spacing: float + section_area: float + section_centroid: float + z_na: float + span: float + continuous: bool + + +def _build_stiff_geom( + mesh: Mesh, + aux: extract.AuxRecords, + model: CapacityModel, + stiff_name: str, +) -> _StiffGeom: + """Resolve the case-invariant geometry for a stiffener (see :class:`_StiffGeom`).""" + st = model.stiffener(stiff_name) + axis, _ = extract.beam_axis_and_span(mesh, st.element_ids) + + candidate_plates = [e for p in model.plates for e in p.element_ids] + # Tributary = the plates bordering the stiffener edge. ``tributary_plate_ids`` + # matches plates that contain *all* the given beam's nodes, which is the right + # test for a single FE edge but never holds for a multi-element stiffener + # chain (no 4-node plate spans every node of a 6-element run). Union the + # per-element edge plates so a finely-meshed/multi-bay stiffener still gets + # its bordering plate field — otherwise its plate stresses resolve to zero. + edge_trib = sorted( + {pe for be in st.element_ids for pe in extract.tributary_plate_ids(mesh, (be,), candidate_plates)} + ) + field_trib = _adjacent_plate_field_ids(model, edge_trib) + + origin = _beam_origin(mesh, st.element_ids) + # Tributary cross-section for the plate axial force (eq (5.1)): use the panel's + # representative plate field — the SAME plate the capacity check uses + # (``model.plates[0]`` -> Genie ``s``/``t``). Picking an arbitrary bordering + # plate via ``next()`` could grab a multi-spacing (e.g. 1300 mm) or — before + # the thickness-gated merge — a different-thickness plate, so ``s_spacing·t`` + # diverged from what the check applied (``AxialLoadsPlate`` then disagreed with + # ``sigma_x·t·s`` by the same factor). Panels are single-thickness after the + # merge, so ``plates[0]`` carries the correct thickness and stiffener spacing. + plate = model.plates[0] if model.plates else None + t = plate.thickness if plate else 0.0 + s_spacing = plate.width if plate else 0.0 + section_area, section_centroid = _section_area_and_centroid(st.section) + plate_area = t * s_spacing + z_na = section_area * section_centroid / (section_area + plate_area) if section_area + plate_area else 0.0 + + # Per-element rotation (cos/sin) into the stiffener frame and area, for every + # tributary plate element — the dominant per-case cost when recomputed. + cs_by_element: dict[int, tuple[float, float]] = {} + area_by_element: dict[int, float] = {} + for pe in set(field_trib) | set(edge_trib): + transform = aux.element_transform_by_element.get(pe) + cs_by_element[pe] = _rotation_cossin(mesh, pe, axis, transform) + area_by_element[pe] = _element_area(mesh, pe) + + return _StiffGeom( + element_ids=st.element_ids, + axis=axis, + origin=origin, + edge_trib=edge_trib, + field_trib=field_trib, + cs_by_element=cs_by_element, + area_by_element=area_by_element, + t=t, + s_spacing=s_spacing, + section_area=section_area, + section_centroid=section_centroid, + z_na=z_na, + span=float(st.span or 0.0), + continuous=st.continuous, + ) + + +def _resolve_stiffener( + mesh: Mesh, + aux: extract.AuxRecords, + model: CapacityModel, + stiff_name: str, + result_case: int, + stress_blocks, + force_blocks, + geom: _StiffGeom, +) -> ResolvedCase: + axis = geom.axis + edge_trib = geom.edge_trib + field_trib = geom.field_trib + cs = geom.cs_by_element + areas = geom.area_by_element + + # Field transverse/shear membrane in the stiffener frame: each adjacent + # plate field is averaged over its membrane result points, then + # area-weighted and negated to Genie's compression-positive convention. If + # an adjacent plate field is split into several shells, use the full field + # rather than only the shell sharing the stiffener nodes; this matches + # Genie's Section-5 field integration on irregular triangular/quadrilateral + # edge fields. ``edge_trib ⊆ field_trib``, so resolve each element's points + # once and slice both views from it. + point_records_by_element = { + pe: _element_membrane_point_records(mesh, aux, stress_blocks, pe, axis, cs.get(pe)) for pe in field_trib + } + points_by_element = {pe: _point_tuples(records) for pe, records in point_records_by_element.items()} + field_points_by_element = points_by_element + field_points = [point for points in field_points_by_element.values() for point in points] + field_records = [record for records in point_records_by_element.values() for record in records] + field_weighted = _area_weighted_element_mean(mesh, field_points_by_element, areas) + overall = -field_weighted if field_weighted is not None else np.zeros(3) + + origin = geom.origin + # Longitudinal axial/moment resultants are edge quantities: sample the plate + # elements that share the stiffener line, then use result points closest to + # that line for the calibrated Section-5 axial reconstruction. + edge_points_by_element = {pe: points_by_element.get(pe, []) for pe in edge_trib} + edge_records_by_element = {pe: point_records_by_element.get(pe, []) for pe in edge_trib} + long_points = [point for points in edge_points_by_element.values() for point in points] + long_records = [record for records in edge_records_by_element.values() for record in records] + long_weighted = _area_weighted_element_mean(mesh, edge_points_by_element, areas) + long_overall = -long_weighted if long_weighted is not None else overall + edge_records = _edge_point_records(long_records, origin, axis) + edge_points = _point_tuples(edge_records) + edge_rotated = [tensor for _, tensor in edge_points] + edge_mid = -float(np.array(edge_rotated).mean(axis=0)[0]) if edge_rotated else float(long_overall[0]) + all_long_pos = [-x for x in _station_values(long_points, origin, axis, 0, -float(long_overall[0]))] + edge_long_pos = [-x for x in _station_values(edge_points, origin, axis, 0, -edge_mid)] + long_pos = [(a + e) / 2.0 for a, e in zip(all_long_pos, edge_long_pos)] + long_mid = float(long_pos[1]) + trans_pos = [-x for x in _station_values(field_points, origin, axis, 1, -float(overall[1]))] + tau_pos = [abs(x) for x in _station_values(field_points, origin, axis, 2, -float(overall[2]))] + v_mid = overall + + # Plate tributary area = thickness x spacing of the bordering plate(s). + t = geom.t + s_spacing = geom.s_spacing + p_sd = _area_weighted_pressure(mesh, aux, result_case, field_trib, areas) + q_dir = p_sd * s_spacing + n_plate_positions = [float(x * t * s_spacing) for x in long_pos] + n_beam_positions, n_beam_provenance = _beam_component_position_data( + mesh, + force_blocks, + geom.element_ids, + 0, + sign=-1.0, + unit="N", + component_label="beam axial force NXX", + ) + n_axial_positions = [float(n_plate + n_beam) for n_plate, n_beam in zip(n_plate_positions, n_beam_positions)] + n_axial = n_axial_positions[1] + n_plate = long_mid * t * s_spacing + n_beam = n_beam_positions[1] + n_sd = ( + 0.2 * max(n_axial_positions[0], 0.0) + + 0.6 * max(n_axial_positions[1], 0.0) + + 0.2 * max(n_axial_positions[2], 0.0) + ) + axial_area = t * s_spacing + geom.section_area + sigma_x_sd = n_sd / axial_area if axial_area else 0.0 + + # section_area, section_centroid = geom.section_area, geom.section_centroid + section_centroid = geom.section_centroid + z_na = geom.z_na + m_plate = [-float(n * z_na) for n in n_plate_positions] + m_beam_force = [float(n * (section_centroid - z_na)) for n in n_beam_positions] + m_beam, m_beam_provenance = _beam_component_position_data( + mesh, + force_blocks, + geom.element_ids, + 4, + sign=-1.0, + unit="Nm", + component_label="beam moment MXY", + ) + moments = [float(a + b + c) for a, b, c in zip(m_plate, m_beam_force, m_beam)] + span = geom.span + q_fe = (0.5 * (moments[0] + moments[2]) - moments[1]) * 8.0 / (span * span) if span else 0.0 + + variables = { + "SigmaYSd": float(v_mid[1]), + "SigmaY1Sd": float(max(trans_pos)), + "SigmaY2Sd": float(min(trans_pos)), + "TauSd": float(tau_pos[1]), + "PSd": float(p_sd), + "Qdir": float(q_dir), + "QFE": float(q_fe), + "Nsd": float(n_sd), + "Naxial": float(n_axial), + "AxialLoadsBeam": float(n_beam), + "AxialLoadsPlate": float(n_plate), + "SigmaXSd": float(sigma_x_sd), + "SigmaXSd1": float(sigma_x_sd), + "SigmaXSd2": float(sigma_x_sd), + } + vectors = { + "AverageLongitudinalMembraneStresses": [float(x) for x in long_pos], + "AverageTransverseMembraneStresses": [float(x) for x in trans_pos], + "AverageShearStresses": [float(x) for x in tau_pos], + "AxialLoads": n_axial_positions, + "AxialLoadsPlate": n_plate_positions, + "AxialLoadsBeam": n_beam_positions, + "MomentsAboutNeutralAxis": moments, + "MomentsAboutNeutralAxisPlate": m_plate, + "MomentsAboutNeutralAxisBeamForce": m_beam_force, + "MomentsAboutNeutralAxisBeamMoment": m_beam, + } + provenance: dict[str, Any] = {} + for pos in (1, 2, 3): + i = pos - 1 + provenance[f"sigma_x_{pos}"] = _panel_sigma_x_provenance( + all_records=long_records, + edge_records=edge_records, + origin=origin, + axis=axis, + position=pos, + ) + provenance[f"sigma_y{pos}"] = _station_stress_provenance( + records=field_records, + origin=origin, + axis=axis, + position=pos, + component=1, + label=f"Transverse stress position {pos}", + calculation=( + "Shell membrane stresses are area-resolved over the adjacent plate field, " + "rotated into the stiffener frame and converted to compression-positive " + "transverse stress. End positions use samples at the end station; the " + "mid position uses the area-weighted element mean." + ), + formula="sigma_y,i = - rotated transverse membrane stress at Section 5 position i", + sign=-1.0, + ) + provenance[f"tau_{pos}"] = _station_stress_provenance( + records=field_records, + origin=origin, + axis=axis, + position=pos, + component=2, + label=f"Shear stress position {pos}", + calculation=( + "Shell membrane shear is resolved over the adjacent plate field, rotated " + "into the stiffener frame and reported as an absolute design shear stress. " + "End positions use samples at the end station; the mid position uses the " + "area-weighted element mean." + ), + formula="tau_i = abs(rotated membrane shear at Section 5 position i)", + absolute=True, + ) + provenance[f"M_{pos}"] = { + "label": f"Moment position {pos}", + "position": pos, + "calculation": ( + "Moment about the neutral axis is assembled from the plate membrane axial " + "force, beam axial-force eccentricity and Sesam beam moment rows." + ), + "formula": "M_i = -N_plate,i*z_NA + N_beam,i*(z_section-z_NA) + M_beam,i", + "terms": [ + _term("plate membrane contribution", m_plate[i], "Nm"), + _term("beam axial eccentricity contribution", m_beam_force[i], "Nm"), + _term("beam moment contribution", m_beam[i], "Nm"), + _term("z_NA", z_na, "m"), + _term("z_section", section_centroid, "m"), + ], + "source_sets": [ + _source_set_payload( + "plate membrane stress sources used by N_plate", + _station_records(long_records, origin, axis, pos), + 0, + sign=-1.0, + ), + *n_beam_provenance[pos]["source_sets"], + *m_beam_provenance[pos]["source_sets"], + ], + } + return ResolvedCase( + result_case=0, + stiffener=stiff_name, + panel_group=model.name, + capacity_model_id=model.id or model.name, + continuous=geom.continuous, + variables=variables, + vectors=vectors, + provenance=provenance, + ) + + +def _recovered_stress_blocks(mesh, aux, res, case, material_by_element, *, log: bool): + """Synthesize STRESS blocks from nodal displacements for a stress-less SIN. + + Returns an empty list when there are no displacements / no plate materials to + recover from (the caller then resolves to zero, as before). + """ + from ada.config import logger + from ada.fem.capacity.stress_recovery import ( + build_recovered_stress, + displacements_by_node, + ) + + disp_blocks = [r for r in res.results if r.name == "RVNODDIS"] + if not disp_blocks or not material_by_element: + return [] + if log: + logger.info( + "SIN has no element stresses (SESTRA ISEL4=-1); recovering membrane " + "stresses for %d plate elements from nodal displacements", + len(material_by_element), + ) + disp = displacements_by_node(disp_blocks[0]) + block, coords_by_element = build_recovered_stress( + mesh, disp, material_by_element, case, aux.element_transform_by_element + ) + # The resolver reads result-point coords from aux; the recovered corner + # points are geometry (case-invariant), so merging once is enough. + for element_id, cmap in coords_by_element.items(): + aux.result_point_coords_by_element.setdefault(element_id, cmap) + return [block] if block.values.size else [] + + +# FE field blocks that participate in load-combination superposition: the plate +# membrane stresses, the beam/line force resultants, and (for stress-less SINs) +# the nodal displacements the recovery fallback reads. Resolution is linear in +# these fields up to the per-station min/max, so a combination must superpose the +# *fields* (Σ fᵢ·blockᵢ) and resolve once — not linearly combine resolved scalars. +_SUPERPOSE_BLOCKS = ("STRESS", "FORCES", "RVNODDIS") + + +def _superpose_into(accum: dict, results, factor: float) -> None: + """Add ``factor`` × each superposable block in ``results`` into ``accum``. + + ``accum`` maps ``(name, index) -> [template_block, values_ndarray, n_id_cols]``. + The basic steps of one SIN share a single mesh / RDPOINTS layout, so each + block's rows align 1:1 across steps by their leading id columns (``elem, + point`` for element fields; ``node`` for nodal) — the fast path just adds the + scaled value columns. A key-based merge covers any row-order drift. + """ + by_name: dict[str, list] = {} + for blk in results: + if blk.name in _SUPERPOSE_BLOCKS: + by_name.setdefault(blk.name, []).append(blk) + for name, blocks in by_name.items(): + for idx, blk in enumerate(blocks): + key = (name, idx) + n_id = len(blk.COLS) + slot = accum.get(key) + if slot is None: + arr = np.array(blk.values, dtype=float) + if arr.size: + arr[:, n_id:] *= factor + accum[key] = [blk, arr, n_id] + continue + _, arr, n_id = slot + src = np.asarray(blk.values, dtype=float) + if src.shape == arr.shape and np.array_equal(src[:, :n_id], arr[:, :n_id]): + if src.size: + arr[:, n_id:] += factor * src[:, n_id:] + else: + _key_superpose(arr, src, n_id, factor) + + +def _key_superpose(dst: np.ndarray, src: np.ndarray, n_id: int, factor: float) -> None: + """Row-order-independent fallback for :func:`_superpose_into`. + + Adds ``factor`` × ``src`` value columns onto ``dst`` rows matched by their + integer id columns. ``dst`` is seeded from the first contributing step, so any + ``src`` key absent from it (impossible for a shared mesh) is simply dropped. + """ + if not dst.size or not src.size: + return + index = {tuple(int(round(v)) for v in row[:n_id]): i for i, row in enumerate(dst)} + for row in src: + i = index.get(tuple(int(round(v)) for v in row[:n_id])) + if i is not None: + dst[i, n_id:] += factor * row[n_id:] + + +def _accum_blocks(accum: dict) -> list: + """Materialise superposed blocks as fresh field-data objects (id columns from + the template, value columns from the accumulator).""" + out = [] + for (_name, _idx), (template, arr, _n_id) in sorted(accum.items(), key=lambda kv: kv[0]): + out.append(dataclasses.replace(template, values=arr)) + return out + + +def _resolve_step( + mesh: Mesh, + aux: extract.AuxRecords, + models: list[CapacityModel], + case: int, + results, + material_by_element: dict[int, tuple[float, float]], + geom_cache: dict[tuple, _StiffGeom], + *, + log: bool, +) -> list[ResolvedCase]: + """Resolve every (model, stiffener) for one case from its field blocks. + + ``results`` is a step's ``FEAResult.results`` list (a basic case) or the + superposed block list of a combination — both expose ``STRESS`` / ``FORCES`` + (and ``RVNODDIS`` for the stress-less recovery fallback) by ``name``. + + ``geom_cache`` holds the step-invariant per-stiffener geometry, built on the + first case and reused across every subsequent one. + """ + stress_blocks = [r for r in results if r.name == "STRESS"] + force_blocks = [r for r in results if r.name == "FORCES"] + if not stress_blocks: + shim = SimpleNamespace(results=list(results)) + stress_blocks = _recovered_stress_blocks(mesh, aux, shim, case, material_by_element, log=log) + out: list[ResolvedCase] = [] + for model in models: + for st in model.stiffeners: + key = (model.id or model.name, st.name) + geom = geom_cache.get(key) + if geom is None: + geom = _build_stiff_geom(mesh, aux, model, st.name) + geom_cache[key] = geom + rc = _resolve_stiffener(mesh, aux, model, st.name, case, stress_blocks, force_blocks, geom) + out.append( + ResolvedCase( + result_case=case, + stiffener=rc.stiffener, + panel_group=rc.panel_group, + capacity_model_id=rc.capacity_model_id, + continuous=rc.continuous, + variables=rc.variables, + vectors=rc.vectors, + provenance=rc.provenance, + ) + ) + return out + + +def resolve_cases( + sin_path: str | pathlib.Path, + models: list[CapacityModel], + result_cases: list[int] | None = None, + *, + on_progress: Callable[[int, int], None] | None = None, +) -> list[ResolvedCase]: + """Resolve design variables for every (result case, stiffener). + + ``result_cases`` may list basic result cases (stored RV* steps) and/or + load-case *combinations* (defined in the SIN's RDRESCMB records but, for + SESTRA "smart" combinations, not themselves stored). A combination is + resolved by superposing its basic cases' FE fields (Σ fᵢ·blockᵢ) before the + Section-6 resolve, which is exact because resolution is linear in the field. + + ``on_progress(completed, total)`` is called once per resolved case so callers + can drive a progress bar. + """ + from ada.fem.formats.sesam.results.read_sin import ( + iter_sin_step_results, + read_sin_metadata, + ) + + meta = read_sin_metadata(sin_path) + available = set(meta.steps) + combinations = meta.combinations + if result_cases is None: + result_cases = meta.steps + + # Split requests into directly-stored basic cases and combinations to + # superpose. Basic cases are resolved as they stream past (memory-bounded); + # only the few combination accumulators are held across the read. + direct: list[int] = [] + combo_plan: dict[int, dict[int, float]] = {} + for c in result_cases: + ci = int(c) + if ci in combinations: + comps = combinations[ci] + if not comps: + raise ValueError(f"result combination {ci} ({meta.result_names.get(ci, '?')!r}) lists no basic cases") + combo_plan[ci] = comps + elif ci in available: + direct.append(ci) + else: + raise ValueError( + f"requested result case {ci} is not in the SIN. " + f"Available basic cases: {sorted(available)}; " + f"available combinations: {sorted(combinations)}" + ) + + needed_combo_steps = {s for comps in combo_plan.values() for s in comps} + missing = sorted(needed_combo_steps - available) + if missing: + raise ValueError( + f"result combination(s) reference basic case(s) {missing} that are not " + f"stored in the SIN. Available basic cases: {sorted(available)}" + ) + direct_set = set(direct) + needed_steps = sorted(direct_set | needed_combo_steps) + + aux = extract.AuxRecords.from_sin(sin_path) + # Plate element -> (E, poisson), used to recover membrane stresses from nodal + # displacements when the SIN carries no element stresses (SESTRA ISEL4=-1). + material_by_element: dict[int, tuple[float, float]] = { + e: (p.material.E, p.material.poisson) for model in models for p in model.plates for e in p.element_ids + } + + out: list[ResolvedCase] = [] + total = len(direct) + len(combo_plan) + done = 0 + accums: dict[int, dict] = {case: {} for case in combo_plan} + # Per-stiffener geometry is step-invariant; build it on the first case and + # reuse across all of them (the rotation bases / tributary topology dominate + # the per-case resolve when recomputed every time). + geom_cache: dict[tuple, _StiffGeom] = {} + # Line forces are only ever read for stiffener beam elements, so narrow the + # per-step RVFORCES decode to them — skips unpacking the whole model's + # forces on every step (the dominant SIN-read cost on a large model). + forces_elements = {int(e) for model in models for st in model.stiffeners for e in st.element_ids} + # Read the SIN once and reuse the (step-invariant) mesh across cases — on a + # multi-hundred-MB SIN re-opening + rebuilding the mesh per case dominates. + mesh = None + for step, res in iter_sin_step_results(sin_path, needed_steps, forces_elements=forces_elements): + if mesh is None: + mesh = res.mesh + # Accumulate this step into every combination that references it (before + # the direct resolve below caches per-block element indices on res). + for case, comps in combo_plan.items(): + factor = comps.get(step) + if factor: + _superpose_into(accums[case], res.results, float(factor)) + if step in direct_set: + out.extend( + _resolve_step(mesh, aux, models, step, res.results, material_by_element, geom_cache, log=done == 0) + ) + done += 1 + if on_progress is not None: + on_progress(done, total) + + for case, comps in combo_plan.items(): + combined = _accum_blocks(accums[case]) + out.extend(_resolve_step(mesh, aux, models, case, combined, material_by_element, geom_cache, log=done == 0)) + done += 1 + if on_progress is not None: + on_progress(done, total) + return out + + +# --------------------------------------------------------------------------- # +# Calibration against Genie CriteriaResults +# --------------------------------------------------------------------------- # +@dataclass +class VarResidual: + case: int + stiffener: str + variable: str + ours: float + genie: float + + @property + def abs_delta(self) -> float: + return abs(self.ours - self.genie) + + @property + def rel_delta(self) -> float: + return self.abs_delta / max(abs(self.genie), 1e-9) + + +def calibration_report( + resolved: list[ResolvedCase], + genie_cases: list, + variables: tuple[str, ...] = ("SigmaYSd", "TauSd"), +) -> list[VarResidual]: + """Compare our resolved scalars against Genie's, per (case, stiffener). + + ``genie_cases`` items expose ``result_case``, ``stiffener`` and a + ``variables`` mapping (the shape ``codecheck.dnv_rp_c201.reference.genie`` emits). + """ + index = {(c.result_case, c.stiffener): c for c in resolved} + residuals: list[VarResidual] = [] + for g in genie_cases: + ours = index.get((int(g.result_case), g.stiffener)) + if ours is None: + continue + for v in variables: + residuals.append( + VarResidual( + int(g.result_case), g.stiffener, v, ours.variables.get(v, 0.0), float(g.variables.get(v, 0.0)) + ) + ) + return residuals diff --git a/src/ada/fem/formats/sesam/read/cards.py b/src/ada/fem/formats/sesam/read/cards.py index 51ce48621..5b8ef8efe 100644 --- a/src/ada/fem/formats/sesam/read/cards.py +++ b/src/ada/fem/formats/sesam/read/cards.py @@ -66,6 +66,17 @@ def str_to_proper_types(split_str: str) -> list[float | str]: BaseTypes.TUBULAR, (("dy", "r"), ("t", "wt")), ), + "GLSEC": ( + BaseTypes.ANGULAR, + ( + ("hz", "h"), + ("ty", "t_w"), + ("by", "w_top"), + ("tz", "t_ftop"), + ("by", "w_btn"), + ("tz", "t_fbtn"), + ), + ), } MAT_MAP = { "MISOSEL": ( @@ -146,6 +157,7 @@ def str_to_proper_types(split_str: str) -> list[float | str]: GIORH = DataCard("GIORH", ("geono", "hz", "ty", "bt", "tt", "bb", "tb", "sfy", "sfz", "NLOBYT|", "NLOBYB|", "NLOBZ|")) GBOX = DataCard("GBOX", ("geono", "hz", "ty", "tb", "tt", "by", "sfy", "sfz")) GPIPE = DataCard("GPIPE", ("geono", "di", "dy", "t", "sfy", "sfz")) +GLSEC = DataCard("GLSEC", ("geono", "hz", "ty", "by", "tz", "sfy", "sfz", "NLOBY|", "NLOBZ|")) # Generic beam properties — area + moments of inertia only, no # profile geometry. The streaming bake uses these to synthesise a # tubular approximation for elements that reference a sec_id with @@ -263,6 +275,10 @@ def str_to_proper_types(split_str: str) -> list[float | str]: "RVNODREA", "nfield", "ires", "inod", "irrea|", "irboc|", "itrans|", "F1|", "F2|", "F3|", "F4|", "F5|", "F6|" ) +# Result-case combination definitions. ``bulk`` is ``nres`` triplets of +# ``(component IRES, real factor, imag factor)`` — the combined result IRES +# (``ires``) is a *factored superposition* of the listed basic result cases. +RDRESCMB = DataCard("RDRESCMB", ["nfield", "ires", "complx", "nres", "bulk"]) re_rdrescmb = get_ff_regex("RDRESCMB", "nfield", "ires", "complx", "nres", "bulk") TDRESREF = DataCard("TDRESREF", ("nfield", "ires", "codnam", "codtxt", "name")) diff --git a/src/ada/fem/formats/sesam/read/read_sections.py b/src/ada/fem/formats/sesam/read/read_sections.py index f49cff1b6..a032f2354 100644 --- a/src/ada/fem/formats/sesam/read/read_sections.py +++ b/src/ada/fem/formats/sesam/read/read_sections.py @@ -36,6 +36,7 @@ def get_sections(bulk_str, fem: FEM, mass_elem, spring_elem) -> FemSections: (get_isection(m, sect_names, fem) for m in cards.GIORH.to_ff_re().finditer(bulk_str)), (get_box_section(m, sect_names, fem) for m in cards.GBOX.to_ff_re().finditer(bulk_str)), (get_tubular_section(m, sect_names, fem) for m in cards.re_gpipe.finditer(bulk_str)), + (get_angular_section(m, sect_names, fem) for m in cards.GLSEC.to_ff_re().finditer(bulk_str)), (get_flatbar(m, sect_names, fem) for m in cards.re_gbarm.finditer(bulk_str)), ) @@ -90,6 +91,23 @@ def get_box_section(match, sect_names, fem) -> Section: ) +def get_angular_section(match, sect_names, fem) -> Section: + d = match.groupdict() + sec_id = str_to_int(d["geono"]) + return Section( + name=sect_names[sec_id], + sec_id=sec_id, + sec_type=Section.TYPES.ANGULAR, + h=roundoff(d["hz"]), + w_top=roundoff(d["by"]), + w_btn=roundoff(d["by"]), + t_w=roundoff(d["ty"]), + t_ftop=roundoff(d["tz"]), + t_fbtn=roundoff(d["tz"]), + parent=fem.parent, + ) + + def get_flatbar(match, sect_names, fem) -> Section: d = match.groupdict() sec_id = str_to_int(d["geono"]) diff --git a/src/ada/fem/formats/sesam/results/read_sif.py b/src/ada/fem/formats/sesam/results/read_sif.py index 5142cc0a7..365a7a9f5 100644 --- a/src/ada/fem/formats/sesam/results/read_sif.py +++ b/src/ada/fem/formats/sesam/results/read_sif.py @@ -103,7 +103,7 @@ # coverage was being silently dropped. cards.GBEAMG, ] -SECTION_CARDS = [cards.GIORH, cards.GBOX, cards.GPIPE] +SECTION_CARDS = [cards.GIORH, cards.GBOX, cards.GPIPE, cards.GLSEC] RESULT_CARDS = [ cards.RVNODDIS, cards.RVSTRESS, @@ -330,13 +330,16 @@ def get_sets(self): if member_map is None: return None set_map = self.get_tdsetnam_map() - istype_i, isorig_i = cards.GSETMEMB.get_indices_from_names(["ISTYPE", "ISORIG"]) sets = dict() - for set_id, props in member_map.items(): - eltype = props[istype_i] - set_type = "nset" if eltype == 1 else "elset" + for set_id, members_by_type in member_map.items(): set_name = set_map[set_id][-1] - members = props[isorig_i:] + # A set may carry both node (ISTYPE 1) and element (ISTYPE 2) records; + # prefer the element membership (what element scoping needs) and fall + # back to nodes for a pure node set. + if members_by_type["elset"]: + set_type, members = "elset", members_by_type["elset"] + else: + set_type, members = "nset", members_by_type["nset"] sets[set_name] = FemSet(set_name, members, set_type=set_type) return sets @@ -609,7 +612,20 @@ def get_gsetmemb(self): res = self._other.get(cards.GSETMEMB.name) if res is None: return None - return {int(x[1]): [int(i) for i in x] for x in res} + # A Sesam set with many members is written as several GSETMEMB records + # that share the same set id (ISREF) but continue the member list, and a + # single set may mix node records (ISTYPE 1) with element records + # (ISTYPE 2). Concatenate the member chunks per set and per type — keying + # by ISREF alone (last record wins) silently dropped most members. ISORIG + # is a header field, not a member; the members are the fields after it. + isref_i, istype_i, isorig_i = cards.GSETMEMB.get_indices_from_names(["ISREF", "ISTYPE", "ISORIG"]) + merged: dict[int, dict[str, list[int]]] = {} + for x in res: + rec = [int(i) for i in x] + entry = merged.setdefault(rec[isref_i], {"nset": [], "elset": []}) + key = "nset" if rec[istype_i] == 1 else "elset" + entry[key].extend(rec[isorig_i + 1 :]) + return merged def get_rdresref(self): res = self.get_result(cards.RDRESREF.name)[0][1] @@ -847,20 +863,23 @@ def get_field_line_data(self): nsp_i, eltyp_i = cards.RDPOINTS.get_indices_from_names(["nsp", "ieltyp"]) rdpoints_map = self.sif.get_rdpoints_map() - # No element result-point geometry → no per-element force - # visualisation we can construct. Some eigen decks ship - # RDPOINTS as an empty type-block in every super-element; - # the nodal field path still works and is the primary bake - # output. - if not rdpoints_map: - return [] + # RDPOINTS supplies the element type and result-point count, but it is + # only needed by the *shell* (stress) path. Some SINs (e.g. SESTRA + # "smart load combination" / force-only runs) ship no RDPOINTS at all + # yet still carry beam/line forces in RVFORCES. For line elements both + # pieces are recoverable without it — the element type from the mesh and + # the result-point count from the record length — so don't bail here. + rdforces_map = self.sif.get_rdforces_map() + elem_type_by_id = self._element_source_type_map() if not rdpoints_map else {} def keyfunc(x): - iielno = x[ielno_i] - rdpoints_res = rdpoints_map[iielno] - _nsp = int(rdpoints_res[nsp_i]) - _elem_type = int(rdpoints_res[eltyp_i]) - return x[ires_i], _nsp, _elem_type, x[irforc_i] + iielno = int(x[ielno_i]) + rdpoints_res = rdpoints_map.get(iielno) + if rdpoints_res is not None: + return x[ires_i], int(rdpoints_res[nsp_i]), int(rdpoints_res[eltyp_i]), x[irforc_i] + ncomp = len(rdforces_map.get(int(x[irforc_i]), ())) or 1 + nsp = (len(x) - (irforc_i + 1)) // ncomp + return x[ires_i], nsp, elem_type_by_id.get(iielno, -1), x[irforc_i] field_results = [] @@ -875,6 +894,20 @@ def keyfunc(x): return field_results + def _element_source_type_map(self) -> dict[int, int]: + """Map element id → Sesam source element type, from the converted mesh. + + Fallback for the element type when RDPOINTS is absent (line-force path). + """ + out: dict[int, int] = {} + if self.mesh is None: + return out + for block in self.mesh.elements: + source_type = int(block.elem_info.source_type) + for eid in block.identifiers: + out[int(eid)] = source_type + return out + def _get_line_field_data(self, rv_forces, ires, irforc, elem_type, nsp) -> ElementFieldData: from ada.fem.results.common import ElementFieldData @@ -908,9 +941,13 @@ def keyfunc(x): field_results = [] - for (ires, nsp, elem_type, irstrs), rv_stresses in groupby( - sorted(sif.get_result(cards.RVSTRESS.name)[0][1][1:], key=keyfunc), key=keyfunc - ): + # Skip stress records whose element has no RDPOINTS entry — without the + # result-point definition there is no integration-point field to build. + # Real models occasionally carry such RVSTRESS rows; dropping them is + # safer than failing the whole read. + rv_rows = [x for x in sif.get_result(cards.RVSTRESS.name)[0][1][1:] if x[iielno_i] in rdpoints_map] + + for (ires, nsp, elem_type, irstrs), rv_stresses in groupby(sorted(rv_rows, key=keyfunc), key=keyfunc): if elem_type not in (25, 24): continue diff --git a/src/ada/fem/formats/sesam/results/read_sin.py b/src/ada/fem/formats/sesam/results/read_sin.py index 38cfae501..0a258eb0c 100644 --- a/src/ada/fem/formats/sesam/results/read_sin.py +++ b/src/ada/fem/formats/sesam/results/read_sin.py @@ -39,7 +39,7 @@ cards.TDRESREF, cards.GBEAMG, ) -_SECTION_CARDS = (cards.GIORH, cards.GBOX, cards.GPIPE) +_SECTION_CARDS = (cards.GIORH, cards.GBOX, cards.GPIPE, cards.GLSEC) _RESULT_CARDS = ( cards.RVNODDIS, cards.RVSTRESS, @@ -59,7 +59,7 @@ _TEXT_CARDS = {"TDSECT", "TDSETNAM", "TDMATER", "TDRESREF"} -def _records_for(sin: SinFile, card, *, step: int | None = None) -> list[list]: +def _records_for(sin: SinFile, card, *, step: int | None = None, elements: set[int] | None = None) -> list[list]: """Pull every record of one type into the SifReader-compatible list shape. @@ -93,8 +93,12 @@ def _records_for(sin: SinFile, card, *, step: int | None = None) -> list[list]: # Only RV* records carry IRES in their first data word — apply the # step filter just there. rec_filter = step if (step is not None and type_name in _RV_TYPE_NAMES) else None + # ``elements`` narrows RVFORCES to the records whose IELNO (second data + # word) the caller needs — the capacity resolve only reads line forces for + # stiffener beam elements, a tiny subset of the model. + elem_filter = elements if type_name == cards.RVFORCES.name else None out_num: list[list] = [] - for rec in sin.iter_records(type_name, where_first_word=rec_filter): + for rec in sin.iter_records(type_name, where_first_word=rec_filter, where_second_word=elem_filter): # NFIELD is len(rec) + 1 (the count includes itself); +1 for # the implicit prefix word that SIN stores but iter_records # strips. @@ -249,7 +253,8 @@ def _read_result_card(self, card, step): sh[0, : len(super_header)] = super_header rows = np.vstack((sh, arr)) if arr.shape[0] else sh else: - rows = _records_for(self.sin, card, step=step) + elements = getattr(self, "_forces_elements", None) if card.name == cards.RVFORCES.name else None + rows = _records_for(self.sin, card, step=step, elements=elements) rows = [super_header, *rows] # The card's record bytes are now copied into ``rows`` — drop the # mmap pages so the next (often equally large) RV* table doesn't @@ -272,12 +277,27 @@ class SinMetadata: ``"RVSTRESS"``, ``"RVFORCES"``) — the GLB/picker layer maps them to display names. Values are sorted unique IRES (step / result- reference id) values seen in that type's records. + + ``combinations`` maps a combination result IRES → ``{basic IRES: + factor}`` (read from RDRESCMB); ``result_names`` maps any result + IRES → its label (from TDRESREF). A combination IRES may *or may + not* also appear in ``field_steps`` — SESTRA "smart load + combinations" usually store only the basic cases, so combination + results must be reconstructed by superposing the basic fields. """ types: list[str] node_count: int element_count: int field_steps: dict[str, list[int]] + combinations: dict[int, dict[int, float]] = None + result_names: dict[int, str] = None + + def __post_init__(self) -> None: + if self.combinations is None: + self.combinations = {} + if self.result_names is None: + self.result_names = {} @property def steps(self) -> list[int]: @@ -291,6 +311,62 @@ def steps(self) -> list[int]: def fields(self) -> list[str]: return list(self.field_steps.keys()) + @property + def combination_ids(self) -> list[int]: + """All defined combination result IRES, sorted.""" + return sorted(self.combinations) + + @property + def selectable_cases(self) -> list[int]: + """Every result case a check can request: basic RV* steps plus any + defined combinations (which superpose those basic steps).""" + return sorted(set(self.steps) | set(self.combinations)) + + +def read_result_combinations(sin: SinFile) -> dict[int, dict[int, float]]: + """Read result-case combination definitions from a SIN. + + Returns ``{combination IRES: {basic IRES: factor}}``. Each RDRESCMB + record is ``[ires, complx, nres, *triplets]`` where ``triplets`` is + ``nres`` groups of ``(component IRES, real factor, imag factor)``. The + imaginary factor is ignored (real-valued static combinations); zero + factors are dropped (the basic case contributes nothing and need not be + read). Component IRES reference basic result cases whose first RV* data + word (``ires``) equals them, so they double as the streaming step id. + """ + if "RDRESCMB" not in sin.type_blocks: + return {} + out: dict[int, dict[int, float]] = {} + for rec in sin.iter_records("RDRESCMB"): + if len(rec) < 3: + continue + ires = int(round(rec[0])) + nres = int(round(rec[2])) + triplets = rec[3:] + comps: dict[int, float] = {} + for i in range(nres): + base = 3 * i + if base + 1 >= len(triplets): + break + basic = int(round(triplets[base])) + factor = float(triplets[base + 1]) + if factor != 0.0: + comps[basic] = comps.get(basic, 0.0) + factor + out[ires] = comps + return out + + +def read_result_names(sin: SinFile) -> dict[int, str]: + """Map result IRES → label from the SIN's TDRESREF text records.""" + if "TDRESREF" not in sin.type_blocks: + return {} + out: dict[int, str] = {} + for prefix, text in sin.iter_text_records("TDRESREF"): + if not prefix or not text: + continue + out[int(round(prefix[0]))] = text + return out + _RV_TYPE_NAMES = ("RVNODDIS", "RVSTRESS", "RVFORCES") @@ -336,6 +412,8 @@ def read_sin_metadata(sin_file: str | pathlib.Path) -> SinMetadata: node_count=node_count, element_count=element_count, field_steps=field_steps, + combinations=read_result_combinations(sin), + result_names=read_result_names(sin), ) finally: sin.close() @@ -370,6 +448,27 @@ def read_sin_file(sin_file: str | pathlib.Path, *, step: int | None = None) -> " return s2m.convert(name_path) +def iter_sin_step_results(sin_file: str | pathlib.Path, steps, *, forces_elements: set[int] | None = None): + """Yield ``(step, FEAResult)`` reading the SIN once and reusing the mesh. + + On large multi-step SINs this is dramatically faster than calling + :func:`read_sin_file` per step: the file is opened and its type blocks + decoded **once**, the step-invariant static blocks and the mesh are read + **once**, and only each step's RV* tables are re-read. Each yielded + ``FEAResult`` shares the same cached :class:`~ada.fem.results.common.Mesh` + instance. No LIS/MLG enrichment (not needed for value extraction). + + ``forces_elements``: when given, the per-step RVFORCES decode is narrowed to + these element ids (IELNO). The capacity resolve only reads line forces for + stiffener beam elements, so this skips decoding the whole model's forces + every step. Leave ``None`` (the bake / full-materialise paths) to read all. + """ + sin = open_sin(sin_file) + with SinStreamReader(sin, forces_elements=forces_elements) as reader: + for step in steps: + yield int(step), reader._load_step(int(step)) + + class SinStreamReader: """Memory-bounded ``FEAStreamReader`` for Sesam SIN. @@ -391,7 +490,7 @@ class SinStreamReader: Accepts a :class:`SinFile` or any ``ByteSource`` (wrapped into one). """ - def __init__(self, source) -> None: + def __init__(self, source, *, forces_elements: set[int] | None = None) -> None: from ada.fem.formats.sesam.results.sin_reader import SinFile self.sin = source if isinstance(source, SinFile) else SinFile(source=source) @@ -399,6 +498,8 @@ def __init__(self, source) -> None: self._rep = None # FEAResultStreamAdapter over the first step (geometry/specs/beams) self._mesh = None # step-invariant Mesh, built once and reused across steps self._reader = None # one SinReader; static blocks read once, RV* re-read per step + # Optional RVFORCES element-id narrowing for the capacity resolve path. + self._forces_elements = set(forces_elements) if forces_elements is not None else None # ── lifecycle ───────────────────────────────────────────────────── def close(self) -> None: @@ -441,6 +542,7 @@ def _load_step(self, step: int, cards: "set[str] | None" = None): # RDPOINTS blocks once, then only re-read this step's RV* tables. if self._reader is None: self._reader = SinReader(sin=self.sin) + self._reader._forces_elements = self._forces_elements self._reader._load_static() reader = self._reader reader.load_step(int(step), cards=cards) @@ -555,4 +657,12 @@ def try_groups(self): return None -__all__ = ["SinMetadata", "SinReader", "SinStreamReader", "read_sin_file", "read_sin_metadata"] +__all__ = [ + "SinMetadata", + "SinReader", + "SinStreamReader", + "read_result_combinations", + "read_result_names", + "read_sin_file", + "read_sin_metadata", +] diff --git a/src/ada/fem/formats/sesam/results/sin_reader.py b/src/ada/fem/formats/sesam/results/sin_reader.py index 6d272051b..ca4021218 100644 --- a/src/ada/fem/formats/sesam/results/sin_reader.py +++ b/src/ada/fem/formats/sesam/results/sin_reader.py @@ -43,9 +43,6 @@ # path caused 15 GiB heap allocations on a 5 GB SIN and froze the # host machine. _MAX_RECORDS_PER_BLOCK = 50_000_000 -# Hard cap on individual dim values for the same reason. A single dim -# > 10^8 is almost certainly a junk u32 read. -_MAX_DIM_VALUE = 100_000_000 # Upper bound on a single record's byte size. Real SIF records are # NFIELD words wide; even outliers like GELMNT1 with 20 node-ids cap # out at < 200 bytes. 4 KiB gives generous padding for variable-NFIELD @@ -445,20 +442,19 @@ def _decode_type_block(source: ByteSource, preamble_off: int, next_preamble: int caps.append(_read_u32_slot(source, payload + (4 + 2 * d) * SLOT_STRIDE)) dims.append(_read_u32_slot(source, payload + (4 + 2 * d + 1) * SLOT_STRIDE)) - # Reject obvious-garbage dims before they balloon the pointer table. - # A junk u32 read can put 2^31 in a dim slot; allocating a list of - # 2 billion Python ints is what froze the host machine. - if any(d > _MAX_DIM_VALUE for d in dims): - raise ValueError(f"dim value > {_MAX_DIM_VALUE} in block {name!r}: {dims} — likely junk header") - total_records = 1 for d in dims: total_records *= d - if total_records > _MAX_RECORDS_PER_BLOCK: - raise ValueError( - f"block {name!r} dims {dims} → {total_records} records " - f"exceeds {_MAX_RECORDS_PER_BLOCK} cap — likely junk header" - ) + + # A dim slot can hold a junk / uninitialised value (e.g. a GSETMEMB ``cap`` + # field of ~1e8 in some exports). Do NOT drop the block over that — the + # pointer table is authoritative: ``_read_pointer_table`` clamps the read to + # the file size and truncates at the first invalid pointer, so it finds the + # real record count regardless of the dim. Only cap the *requested* slot + # count so a 2-billion dim can't balloon the request before that clamp. + read_cap = total_records + 1 + if read_cap <= 0 or read_cap > _MAX_RECORDS_PER_BLOCK: + read_cap = _MAX_RECORDS_PER_BLOCK # Stream the pointer table (each slot is 8 bytes, the value half in # the +4 word), validating + truncating at the real cutoff as it @@ -476,7 +472,7 @@ def _decode_type_block(source: ByteSource, preamble_off: int, next_preamble: int # records live at slots[1..N]. dims=(N,) therefore needs N+1 slots, # otherwise the last record (id N) falls off the end — multi-super- # element files would lose one node/element per SE. - pointer_table = _read_pointer_table(source, pointer_table_offset, total_records + 1) + pointer_table = _read_pointer_table(source, pointer_table_offset, read_cap) total_records = int(pointer_table.size) records_start = pointer_table_offset + total_records * SLOT_STRIDE @@ -964,7 +960,13 @@ def gather_records(self, name: str, *, where_first_word: int | None = None): out[:, 1:] = src.gather_f32(word_idx).reshape(nz.size, n_data) return out - def iter_records(self, name: str, *, where_first_word: int | None = None) -> Iterator[tuple[float, ...]]: + def iter_records( + self, + name: str, + *, + where_first_word: int | None = None, + where_second_word: set[int] | None = None, + ) -> Iterator[tuple[float, ...]]: """Yield one tuple of float32 values per populated record (the SIF "data" fields). @@ -1010,6 +1012,12 @@ def iter_records(self, name: str, *, where_first_word: int | None = None) -> Ite first = int(src.f32(data_byte)) if first != where_first_word: continue + if where_second_word is not None: + # Same cheap pre-filter on the second data word (e.g. IELNO + # for RVFORCES) — skip records for elements the caller does + # not need before paying for the full per-record unpack. + if n_data < 2 or int(src.f32(data_byte + 4)) not in where_second_word: + continue yield struct.unpack(f"<{n_data}f", src.read(data_byte, n_data * 4)) def iter_text_records(self, name: str) -> Iterator[tuple[tuple[float, ...], str]]: diff --git a/src/ada/fem/formats/sesam/write/write_bm_profiles.py b/src/ada/fem/formats/sesam/write/write_bm_profiles.py index 95da1926e..5a4c60ea8 100644 --- a/src/ada/fem/formats/sesam/write/write_bm_profiles.py +++ b/src/ada/fem/formats/sesam/write/write_bm_profiles.py @@ -30,11 +30,13 @@ def z(v): def angular(sec: Section, sec_id) -> str: p = sec.properties + width = sec.w_top if sec.w_top is not None else sec.w_btn + thickness = sec.t_ftop if sec.t_ftop is not None else sec.t_fbtn return write_ff( "GLSEC", [ - (sec_id, sec.h, sec.t_w, sec.w_btn), - (sec.t_fbtn, p.Sfy, p.Sfz, 1), + (sec_id, sec.h, sec.t_w, width), + (thickness, p.Sfy, p.Sfz, 1), ], ) diff --git a/src/ada/fem/results/artefacts.py b/src/ada/fem/results/artefacts.py index fbbd68e10..0d0112783 100644 --- a/src/ada/fem/results/artefacts.py +++ b/src/ada/fem/results/artefacts.py @@ -1750,6 +1750,7 @@ def build_manifest( mesh_glb_filename: str, field_metas: list[FieldArtefactMeta], *, + source_sha256: str | None = None, elem_field_metas: list[ElementFieldArtefactMeta] | None = None, mesh_edges_filename: str | None = None, n_edges: int = 0, @@ -1768,6 +1769,7 @@ def build_manifest( lineage: dict | None = None, fem_concepts: dict | None = None, groups: list[dict] | None = None, + capacity: dict | None = None, legacy_glb_url_template: str | None = None, ) -> dict: """Compose the manifest dict from the bake outputs. @@ -1994,6 +1996,8 @@ def build_manifest( "mesh": mesh_meta, "fields": fields_payload, } + if source_sha256: + manifest["source_sha256"] = str(source_sha256) if history is not None and (history.regions or history.variables or history.series): manifest["history"] = build_history_payload(history) if lineage is not None and (lineage.get("assembly_guid") or lineage.get("groups")): @@ -2008,6 +2012,8 @@ def build_manifest( # no ADA_EXT, so the frontend feeds these into useSceneInfoStore directly). if groups: manifest["groups"] = groups + if capacity: + manifest["capacity"] = capacity if legacy_glb_url_template is not None: manifest["legacy_glb"] = {"url_template": legacy_glb_url_template} return manifest @@ -2291,6 +2297,7 @@ def bake_fea_artefacts_from_source( out_dir: os.PathLike, *, src_key: str = "", + source_sha256: str | None = None, legacy_glb_url_template: str | None = None, ) -> "BakeResult": """End-to-end bake from a source file path. Picks the right @@ -2306,6 +2313,7 @@ def bake_fea_artefacts_from_source( reader, out_dir, src=src, + source_sha256=source_sha256, legacy_glb_url_template=legacy_glb_url_template, ) @@ -2323,9 +2331,11 @@ def bake_artefacts( out_dir: os.PathLike, *, src: str = "", + source_sha256: str | None = None, legacy_glb_url_template: str | None = None, nodal_only: bool = True, include_element_fields: bool = True, + capacity: dict | None = None, on_artefact: Callable[[pathlib.Path], None] | None = None, ) -> BakeResult: """Drive the streaming bake end-to-end. @@ -2497,6 +2507,7 @@ def emit(path: pathlib.Path) -> None: manifest = build_manifest( src=src, + source_sha256=source_sha256, mesh_geom=geom, mesh_glb_filename=mesh_glb_path.name, field_metas=field_metas, @@ -2518,6 +2529,7 @@ def emit(path: pathlib.Path) -> None: lineage=lineage, fem_concepts=fem_concepts, groups=groups, + capacity=capacity, legacy_glb_url_template=legacy_glb_url_template, ) manifest_path = out_dir / "fea.manifest.json" diff --git a/src/ada/visit/rendering/resources/index.zip b/src/ada/visit/rendering/resources/index.zip index 03821ec74..686ab4734 100644 Binary files a/src/ada/visit/rendering/resources/index.zip and b/src/ada/visit/rendering/resources/index.zip differ diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index a96371296..718519267 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ada-py-viewer", - "version": "0.25.0", + "version": "0.28.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ada-py-viewer", - "version": "0.25.0", + "version": "0.28.0", "dependencies": { "meshoptimizer": "^1.1.1" }, @@ -360,6 +360,40 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", @@ -1536,27 +1570,6 @@ "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.10.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.10.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "dev": true, diff --git a/src/frontend/package.json b/src/frontend/package.json index 03384a2e2..004a86369 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -14,7 +14,7 @@ "build:serve": "vite build --config vite.config.serve.ts --emptyOutDir", "build:embed": "vite build --config vite.config.embed.ts", "dev": "vite --force", - "test": "node --import tsx --test src/__tests__/services/wasmSupport.test.ts src/__tests__/services/feaManifestPoll.test.ts src/__tests__/services/feaFieldBlob.test.ts src/__tests__/services/feaElemFieldBlob.test.ts src/__tests__/services/feaMeshEdges.test.ts src/__tests__/services/feaMeshElements.test.ts src/__tests__/services/feaBeamSolidsWarp.test.ts src/__tests__/utils/edgeShaderHelper.test.ts src/__tests__/utils/rangeIndex.test.ts src/__tests__/utils/customBatchedMeshHighlight.test.ts" + "test": "node --import tsx --test src/__tests__/services/wasmSupport.test.ts src/__tests__/services/feaManifestPoll.test.ts src/__tests__/services/feaFieldBlob.test.ts src/__tests__/services/feaElemFieldBlob.test.ts src/__tests__/services/feaMeshEdges.test.ts src/__tests__/services/feaMeshElements.test.ts src/__tests__/services/feaBeamSolidsWarp.test.ts src/__tests__/services/capacityResults.test.ts src/__tests__/services/codecheckCase.test.ts src/__tests__/state/capacityResultsStore.test.ts src/__tests__/components/capacityResultsPanel.test.ts src/__tests__/utils/edgeShaderHelper.test.ts src/__tests__/utils/rangeIndex.test.ts src/__tests__/utils/customBatchedMeshHighlight.test.ts" }, "devDependencies": { "@tailwindcss/postcss": "^4.3.0", diff --git a/src/frontend/src/__tests__/components/capacityResultsPanel.test.ts b/src/frontend/src/__tests__/components/capacityResultsPanel.test.ts new file mode 100644 index 000000000..4ffafee3a --- /dev/null +++ b/src/frontend/src/__tests__/components/capacityResultsPanel.test.ts @@ -0,0 +1,125 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import React from "react"; +import { renderToStaticMarkup } from "react-dom/server"; + +import CapacityResultsPanel from "../../components/capacity/CapacityResultsPanel"; +import type { + CapacityCaseResult, + CapacityRun, +} from "../../state/capacityResultsStore"; + +const ROW: CapacityCaseResult = { + id: "row-1", + case_id: "case-1", + case_label: "Case 1", + capacity_model_id: "model-1", + panel_group: "panelGroup(A)", + stiffener: "Stiffener_A", + governing_usage: 0.92, + governing_check: "Plate buckling", + governing_clause: "6.4", + passed: true, + checks: [ + { + id: "plate", + label: "Plate buckling", + clause: "6.4", + equations: ["(6.10)"], + usage: 0.92, + passed: true, + demand: 10, + resistance: 20, + unit: "kN", + intermediates: { + lambda_p: 0.456, + method: "SCM2", + }, + assumptions: ["Continuous stiffener"], + }, + { + id: "advisory", + label: "Advisory review", + clause: "6.4.3", + equations: ["(6.17)"], + usage: 0.2, + passed: true, + advisory: true, + warnings: ["Mesh density should be reviewed."], + }, + { + id: "failed-advisory", + label: "Failed advisory review", + clause: "8.1", + equations: ["(8.2)"], + usage: 1.2, + passed: false, + advisory: true, + }, + ], +}; + +const RUN: CapacityRun = { + id: "run-1", + result_cases: [{ id: "case-1" }], + capacity_models: [], + check_catalog: [], + case_results: [ROW], + visual_fields: [], +}; + +describe("CapacityResultsPanel", () => { + it("renders a single collapsible checks section with formula references and details", () => { + const html = renderToStaticMarkup( + React.createElement(CapacityResultsPanel, { + run: RUN, + row: ROW, + onClose: () => undefined, + }), + ); + + assert.match(html, /Governing UF/); + assert.match(html, /0\.920/); + assert.match(html, /OK/); + assert.equal(html.match(/>Checks { + const errorRow: CapacityCaseResult = { + ...ROW, + governing_usage: null, + governing_check: "error", + governing_clause: null, + passed: false, + error: "FormulaDomainError: [6.21] invalid C_ys radicand", + checks: [], + }; + const html = renderToStaticMarkup( + React.createElement(CapacityResultsPanel, { + run: RUN, + row: errorRow, + onClose: () => undefined, + }), + ); + + assert.match(html, /ERROR/); + assert.match(html, /Evaluation error/); + assert.match(html, /FormulaDomainError: \[6\.21\] invalid C_ys radicand/); + assert.match(html, /The check could not be evaluated/); + }); +}); diff --git a/src/frontend/src/__tests__/services/capacityResults.test.ts b/src/frontend/src/__tests__/services/capacityResults.test.ts new file mode 100644 index 000000000..437fd938e --- /dev/null +++ b/src/frontend/src/__tests__/services/capacityResults.test.ts @@ -0,0 +1,71 @@ +import {describe, it} from "node:test"; +import assert from "node:assert/strict"; + +import { + CAPACITY_RESULTS_FORMAT, + CAPACITY_RESULTS_VERSION, + validateCapacityResults, +} from "../../services/capacityResults"; +import type {CapacityResults} from "../../state/capacityResultsStore"; +import type {FeaManifest} from "../../services/viewerApi"; + +function payload(overrides: Partial = {}): CapacityResults { + return { + format: CAPACITY_RESULTS_FORMAT, + version: CAPACITY_RESULTS_VERSION, + source: { + sin_name: "model.SIN", + sin_sha256: "abc123", + }, + runs: [ + { + id: "run-001", + result_cases: [{id: "10"}], + capacity_models: [ + { + id: "model-1", + panel_group: "panel", + type: "stiffened_panel", + element_ids: {all: [101]}, + }, + ], + case_results: [], + visual_fields: [], + }, + ], + ...overrides, + }; +} + +describe("validateCapacityResults", () => { + it("accepts the current capacity sidecar version", () => { + assert.doesNotThrow(() => validateCapacityResults(payload())); + }); + + it("rejects unsupported versions", () => { + assert.throws( + () => validateCapacityResults(payload({version: 1})), + /unsupported capacity results version 1/, + ); + }); + + it("rejects sidecars without runs", () => { + assert.throws( + () => validateCapacityResults(payload({runs: []})), + /capacity results contain no runs/, + ); + }); + + it("rejects stale sidecars when the manifest and sidecar hashes disagree", () => { + const manifest = {source_sha256: "def456"} as FeaManifest; + assert.throws( + () => validateCapacityResults(payload(), {manifest}), + /capacity results are stale/, + ); + }); + + it("does not require hashes on legacy manifests or sidecars", () => { + const manifest = {} as FeaManifest; + assert.doesNotThrow(() => validateCapacityResults(payload({source: {sin_name: "model.SIN"}}), {manifest})); + }); +}); diff --git a/src/frontend/src/__tests__/services/codecheckCase.test.ts b/src/frontend/src/__tests__/services/codecheckCase.test.ts new file mode 100644 index 000000000..8598fd9aa --- /dev/null +++ b/src/frontend/src/__tests__/services/codecheckCase.test.ts @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { buildCodecheckCasePayload } from "../../services/codecheckCase"; + +describe("codecheck case export", () => { + it("emits the standard-aware case envelope without changing values", () => { + const values = { fy: 355, sigma_x: 120 }; + const payload = buildCodecheckCasePayload({ + name: "panel case", + check_id: "fe_stiffened", + capacity_model_id: "panel-1", + case_id: "case-10", + values, + }); + + assert.equal(payload.schema, "codecheck/case@1"); + assert.equal(payload.standard_id, "dnv-rp-c201"); + assert.equal(payload.check_id, "fe_stiffened"); + assert.equal(payload.source, "adapy-capacity-viewer"); + assert.deepEqual(payload.values, values); + }); +}); diff --git a/src/frontend/src/__tests__/state/capacityResultsStore.test.ts b/src/frontend/src/__tests__/state/capacityResultsStore.test.ts new file mode 100644 index 000000000..0d578002e --- /dev/null +++ b/src/frontend/src/__tests__/state/capacityResultsStore.test.ts @@ -0,0 +1,143 @@ +import { beforeEach, describe, it } from "node:test"; +import assert from "node:assert/strict"; + +import { + useCapacityResultsStore, + type CapacityResults, +} from "../../state/capacityResultsStore"; +import type { CapacityManifest } from "../../services/viewerApi"; + +const MANIFEST: CapacityManifest = { + version: 1, + results_url: "capacity.results.json", + default_run_id: "run-b", +}; + +const RESULTS: CapacityResults = { + format: "dnv-rp-c201-capacity-results", + version: 1, + runs: [ + { + id: "run-a", + result_cases: [{ id: "1" }], + capacity_models: [], + case_results: [], + visual_fields: [], + }, + { + id: "run-b", + result_cases: [{ id: "7" }, { id: "8" }], + capacity_models: [], + case_results: [ + { + id: "row-1", + case_id: "7", + capacity_model_id: "model-1", + panel_group: "panel", + governing_usage: 0.5, + passed: true, + checks: [ + { + id: "plate", + label: "Plate buckling", + usage: 0.5, + passed: true, + intermediates: { + lambda_p: 0.42, + method: "SCM2", + }, + }, + ], + }, + ], + visual_fields: [], + }, + ], +}; + +describe("capacityResultsStore", () => { + beforeEach(() => { + useCapacityResultsStore.getState().clear(); + }); + + it("uses manifest.default_run_id and the first case as active state", () => { + useCapacityResultsStore + .getState() + .setCapacityData( + MANIFEST, + { sourceName: "model.SIN", resultsUrl: "capacity.results.json" }, + RESULTS, + ); + + const state = useCapacityResultsStore.getState(); + assert.equal(state.activeRunId, "run-b"); + assert.equal(state.activeCaseId, "7"); + assert.equal(state.activeMetricId, "capacity.uf.governing"); + assert.equal(state.selectedResultId, null); + assert.equal(state.error, null); + assert.equal(state.loading, false); + }); + + it("clears selected model when run or case changes", () => { + useCapacityResultsStore + .getState() + .setCapacityData( + MANIFEST, + { sourceName: "model.SIN", resultsUrl: "capacity.results.json" }, + RESULTS, + ); + useCapacityResultsStore + .getState() + .setSelectedCapacityResult("panel-1", "row-1"); + + useCapacityResultsStore.getState().setActiveCaseId("8"); + assert.equal(useCapacityResultsStore.getState().selectedModelId, null); + assert.equal(useCapacityResultsStore.getState().selectedResultId, null); + + useCapacityResultsStore + .getState() + .setSelectedCapacityResult("panel-2", "row-2"); + useCapacityResultsStore.getState().setActiveRunId("run-a"); + assert.equal(useCapacityResultsStore.getState().selectedModelId, null); + assert.equal(useCapacityResultsStore.getState().selectedResultId, null); + }); + + it("clear resets optional filters and loaded data", () => { + useCapacityResultsStore + .getState() + .setCapacityData( + MANIFEST, + { sourceName: "model.SIN", resultsUrl: "capacity.results.json" }, + RESULTS, + ); + useCapacityResultsStore.getState().setFailedOnly(true); + useCapacityResultsStore.getState().setError("bad sidecar"); + + useCapacityResultsStore.getState().clear(); + + const state = useCapacityResultsStore.getState(); + assert.equal(state.results, null); + assert.equal(state.source, null); + assert.equal(state.selectedResultId, null); + assert.equal(state.failedOnly, false); + assert.equal(state.error, null); + }); + + it("preserves per-check intermediate values from the sidecar", () => { + useCapacityResultsStore + .getState() + .setCapacityData( + MANIFEST, + { sourceName: "model.SIN", resultsUrl: "capacity.results.json" }, + RESULTS, + ); + + const check = + useCapacityResultsStore.getState().results!.runs[1].case_results[0] + .checks[0]; + assert.deepEqual(check.intermediates, { + lambda_p: 0.42, + method: "SCM2", + }); + }); +}); diff --git a/src/frontend/src/app.css b/src/frontend/src/app.css index ff67b4272..8ea830391 100644 --- a/src/frontend/src/app.css +++ b/src/frontend/src/app.css @@ -81,4 +81,20 @@ body { -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); outline: 1px solid rgb(17, 17, 17); -} \ No newline at end of file +} + +/* Genie "UfTot" discrete UF bands (no fade) over a 0..1.2 bar: thresholds at + 0.2/0.4/0.6/0.8/1.0 -> 16.67/33.33/50/66.67/83.33%. Matches + .local/reference/genie_uf_color_scheme/genie_uf_color_scheme.png and the + 3D overlay's capacityUfColor. */ +.capacity-uf-gradient { + background: linear-gradient( + 90deg, + #00beff 0%, #00beff 16.6667%, + #00ffff 16.6667%, #00ffff 33.3333%, + #007f00 33.3333%, #007f00 50%, + #ffff00 50%, #ffff00 66.6667%, + #ffa400 66.6667%, #ffa400 83.3333%, + #ff0000 83.3333%, #ff0000 100% + ); +} diff --git a/src/frontend/src/components/capacity/CapacityControls.tsx b/src/frontend/src/components/capacity/CapacityControls.tsx new file mode 100644 index 000000000..88695547d --- /dev/null +++ b/src/frontend/src/components/capacity/CapacityControls.tsx @@ -0,0 +1,2018 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { createPortal } from "react-dom"; + +import CapacityResultsPanel from "@/components/capacity/CapacityResultsPanel"; +import { buildCodecheckCasePayload } from "@/services/codecheckCase"; +import { + CAPACITY_FLOATING_PANEL_RIGHT_PX, + CAPACITY_INPUT_RIGHT_WITH_RESULTS_PX, + caseLabelForRow, + caseResultKey, + formatUf, + formulaReference, + modeButton, + shortName, + ufClass, + type CapacityCaseResultLike, + type CapacityRunLike, + type CapacityVisualFieldLike, +} from "@/components/capacity/capacityFormat"; +import { + useCapacityResultsStore, + WORST_CASE_ID, +} from "@/state/capacityResultsStore"; +import { useObjectInfoStore } from "@/state/objectInfoStore"; +import { + applyCapacityDefinitionView, + applyCapacityGirderLineUf, + applyCapacityIsolation, + applyCapacityIndividualField, + applyCapacitySelectionHighlight, + applyCapacityStations, + clearCapacityDefinitionView, + clearCapacityIsolation, + clearCapacityStations, + clearCapacityVisualField, + loadCapacityCaseDetail, + loadCapacityProvenance, + loadCapacityWorstSummary, + setFeaWireframeVisible, +} from "@/utils/scene/handlers/load_fea_streaming"; + +// Per-position marker colours (positions 1/2/3). Keep in sync with +// load_fea_streaming CAPACITY_STATION_COLORS. +const STATION_COLORS = ["#38bdf8", "#fbbf24", "#fb7185"]; + +const CapacityControls: React.FC = () => { + const { + results, + activeRunId, + activeCaseId, + showDefinitions, + showResults, + isolateDefinitions, + showRestWireframe, + activeMetricId, + selectedModelId, + selectedResultId, + failedOnly, + loading, + error, + caseDetail, + caseDetailLoading, + worstCaseIds, + worstSummary, + worstSummaryLoading, + toggleWorstCase, + setWorstCaseIds, + setActiveRunId, + setActiveCaseId, + setShowDefinitions, + setShowResults, + setIsolateDefinitions, + setShowRestWireframe, + setActiveMetricId, + setSelectedModelId, + setSelectedCapacityResult, + setFailedOnly, + } = useCapacityResultsStore(); + const pickedName = useObjectInfoStore((s) => s.name); + const pickedFaceIndex = useObjectInfoStore((s) => s.faceIndex); + const pickedFileName = useObjectInfoStore((s) => s.fileName); + const pickedClick = useObjectInfoStore((s) => s.clickCoordinate); + const lastHandledPickKeyRef = useRef(null); + const lastHandledClickRef = useRef(null); + const [showInputs, setShowInputs] = useState(false); + const [showResultsPanel, setShowResultsPanel] = useState(false); + const [showStations, setShowStations] = useState(false); + + const run = useMemo(() => { + if (!results?.runs?.length) return null; + return results.runs.find((r) => r.id === activeRunId) ?? results.runs[0]; + }, [results, activeRunId]); + + // v6: the active case's verbose rows are lazy-loaded into caseDetail; legacy + // (<=v5) sidecars inline them on the run, so fall back to that. + const activeRows = useMemo(() => { + if (!run || !activeCaseId) return []; + return ( + caseDetail[activeCaseId] ?? + run.case_results.filter((row) => row.case_id === activeCaseId) + ); + }, [run, activeCaseId, caseDetail]); + + // Fetch the active case's detail on demand (v6 per-case files). + useEffect(() => { + if (!run?.case_detail || !activeCaseId) return; + if (activeCaseId === WORST_CASE_ID) return; // not a real case file + if (caseDetail[activeCaseId] || caseDetailLoading[activeCaseId]) return; + void loadCapacityCaseDetail(activeCaseId); + }, [run, activeCaseId, caseDetail, caseDetailLoading]); + + const isWorst = activeCaseId === WORST_CASE_ID; + + // Load the compact worst summary when the worst view is first opened. + useEffect(() => { + if (!isWorst || !run?.worst_summary_url) return; + if (worstSummary || worstSummaryLoading) return; + void loadCapacityWorstSummary(); + }, [isWorst, run, worstSummary, worstSummaryLoading]); + + // Worst over the selected case subset: per (model, stiffener), the max UF and + // the case it came from. Shaped like a normal row so the table reuses it. + const worstRows = useMemo(() => { + if (!run || !isWorst || !worstSummary) return []; + const selected = worstCaseIds; + const best = new Map(); + for (const caseId of selected) { + const bucket = worstSummary.cases[caseId]; + if (!bucket) continue; + for (const lr of bucket.rows) { + const key = `${lr.m}::${lr.s ?? lr.pg}`; + const prev = best.get(key); + const errorWins = !!lr.e && !prev?.error; + const sameStatusAndHigherUf = + !!lr.e === !!prev?.error && + (lr.u ?? -Infinity) > (prev?.governing_usage ?? -Infinity); + if (!prev || errorWins || sameStatusAndHigherUf) { + best.set(key, { + id: key, + case_id: caseId, + capacity_model_id: lr.m, + panel_group: lr.pg, + stiffener: lr.s ?? undefined, + governing_usage: lr.u, + passed: lr.p, + governing_check: lr.e ? "error" : lr.c ?? null, + governing_clause: lr.cl ?? null, + error: lr.e ?? null, + checks: [], + worstCaseLabel: bucket.label ?? caseId, + }); + } + } + } + let arr = [...best.values()]; + if (failedOnly) arr = arr.filter((r) => !r.passed); + return arr.sort((a, b) => capacityRowScore(b) - capacityRowScore(a)); + }, [run, isWorst, worstSummary, worstCaseIds, failedOnly]); + + const rows = useMemo(() => { + if (isWorst) return worstRows; + if (!activeCaseId) return []; + return activeRows + .filter((row) => !failedOnly || !row.passed) + .slice() + .sort((a, b) => capacityRowScore(b) - capacityRowScore(a)); + }, [isWorst, worstRows, activeRows, activeCaseId, failedOnly]); + + const selectedRow = useMemo(() => { + if (!run || !activeCaseId) return null; + if (selectedResultId) { + const resultMatch = activeRows.find( + (row) => caseResultKey(row) === selectedResultId, + ); + if (resultMatch) return resultMatch; + } + if (!selectedModelId) return null; + return ( + activeRows + .filter((row) => row.capacity_model_id === selectedModelId) + .sort((a, b) => capacityRowScore(b) - capacityRowScore(a))[0] ?? null + ); + }, [run, activeRows, selectedModelId, selectedResultId, activeCaseId]); + + const meshWarningCount = useMemo(() => { + if (!run) return 0; + let bad = 0; + for (const model of run.capacity_models) { + const stiffeners = (model.stiffeners ?? []) as Array< + Record + >; + const violates = stiffeners.some( + (s) => (s.discretization as { ok?: boolean } | undefined)?.ok === false, + ); + if (violates) bad += 1; + } + return bad; + }, [run]); + + useEffect(() => { + if (!run) return; + if (showDefinitions) { + applyCapacityDefinitionView(); + } else { + clearCapacityDefinitionView(); + } + if (showResults && activeCaseId) { + // Always colour per stiffener strip ("individual UF"): each stiffener's + // own line + tributary plate carries that stiffener's UF for the active + // metric. The worst view colours by the worst-over-selected-cases UF; a + // specific case uses that case's rows. Girder models colour only the + // dedicated thick dashed girder line, never the supporting plate faces. + const rows = isWorst ? worstRows : activeRows; + applyCapacityIndividualField( + buildIndividualUfValues(rows, run, activeMetricId), + ); + applyCapacityGirderLineUf(buildGirderUfMap(rows, run, activeMetricId)); + } else { + clearCapacityVisualField(); + applyCapacityGirderLineUf(null); + } + applyCapacitySelectionHighlight(); + }, [ + run, + activeRows, + worstRows, + activeCaseId, + activeMetricId, + showDefinitions, + showResults, + selectedModelId, + selectedRow, + // Re-run so the colour overlay rebuilds (collapsing / restoring the + // non-capacity faces) when "Only definitions" toggles. + isolateDefinitions, + // Recolour the worst view when the selected case subset changes. + isWorst, + worstCaseIds, + ]); + + useEffect(() => { + if (!run) return; + if (isolateDefinitions) { + applyCapacityIsolation(); + } else { + clearCapacityIsolation(); + } + }, [run, isolateDefinitions]); + + // The whole-model wireframe is shown normally, but "Only definitions" hides it + // (so only the capacity models remain) unless the user opts back in via + // "Show rest as wireframe". + useEffect(() => { + setFeaWireframeVisible(!isolateDefinitions || showRestWireframe); + }, [run, isolateDefinitions, showRestWireframe]); + + useEffect(() => { + if (showInputs && showStations && selectedRow) { + const model = run?.capacity_models.find( + (m) => m.id === selectedRow.capacity_model_id, + ); + const stiffeners = (model?.stiffeners ?? []) as Array< + Record + >; + const stiff = stiffeners.find((s) => s.name === selectedRow.stiffener); + applyCapacityStations( + (stiff?.stations as number[][] | undefined) ?? null, + ); + } else { + clearCapacityStations(); + } + }, [run, selectedRow, showInputs, showStations]); + + useEffect(() => { + if (!selectedRow) { + setShowResultsPanel(false); + } + }, [selectedRow]); + + useEffect(() => { + const pickKey = pickedName + ? `${pickedFileName ?? ""}:${pickedName}:${pickedFaceIndex ?? ""}` + : null; + if (!pickKey) { + lastHandledPickKeyRef.current = null; + lastHandledClickRef.current = null; + return; + } + // Each 3D click stores a fresh clickCoordinate object, so a repeat click on + // the same face re-enters here (to drill into the individual strip below); + // effect re-runs from unrelated deps (same click object) stay deduped. + if ( + lastHandledPickKeyRef.current === pickKey && + lastHandledClickRef.current === pickedClick + ) + return; + lastHandledPickKeyRef.current = pickKey; + lastHandledClickRef.current = pickedClick; + if (!run || !pickedName || !showDefinitions) return; + const elementId = elementIdFromName(pickedName); + if (elementId == null) return; + const { + activeCaseId: currentCaseId, + activeMetricId: currentMetricId, + selectedModelId: currentSelectedModelId, + selectedResultId: currentSelectedResultId, + } = useCapacityResultsStore.getState(); + const match = pickCapacityModelForElement( + run, + elementId, + currentCaseId, + currentMetricId, + currentSelectedModelId, + ); + if (!match) return; + if (match.id !== currentSelectedModelId) { + setSelectedModelId(match.id); + return; + } + // Click inside the already-selected model: select the individual + // stiffener/strip under the cursor so the Input/Results/Points panels + // (and the amber strip highlight) follow that specific capacity model. + const rowKey = pickResultRowForElement( + run, + match, + elementId, + currentCaseId, + currentMetricId, + ); + if (rowKey && rowKey !== currentSelectedResultId) { + setSelectedCapacityResult(match.id, rowKey); + } + }, [ + run, + pickedName, + pickedFaceIndex, + pickedFileName, + pickedClick, + showDefinitions, + setSelectedModelId, + setSelectedCapacityResult, + ]); + + if (!results && !loading && !error) return null; + + return ( +
+
+
Capacity
+ {loading &&
Loading
} + {error && ( +
{error}
+ )} +
+ {/* Notices — errors and warnings sit together at the top, presented the + same way (full-width collapsible banners), error (red) above + warning (amber). */} + {run?.errors && run.errors.length > 0 && ( +
+ + {`⛔ ${run.errors.length} capacity check${ + run.errors.length === 1 ? "" : "s" + } failed with an error — affected rows are treated as failures`} + +
    + {run.errors.map((err, i) => ( +
  • + + {err.panel_group} + {err.stiffener ? ` / ${err.stiffener}` : ""} + + {` (case ${err.case_id})`} +
    {err.message}
    +
  • + ))} +
+
+ )} + {meshWarningCount > 0 && ( +
+ + {`⚠ ${meshWarningCount} model(s) under-meshed for SCM2 [6.4.3]`} + +
+ Fewer than 4 elements along the stiffener (first-order). Resolved + stresses may be unreliable. +
+
+ )} + {run && ( +
+
+ + +
+ +
+ + + + + +
+ +
+ + +
+ + {isWorst && ( +
+
+ + Cases in worst ({worstCaseIds.length}/{run.result_cases.length}) + +
+ + +
+
+
+ {run.result_cases.map((c) => ( + + ))} +
+ {worstSummaryLoading && ( +
+ Loading worst summary… +
+ )} +
+ )} + + {showResults && } + +
+ + + + + + + {isWorst && } + + + + {rows.map((row) => { + const rowKey = caseResultKey(row); + const selected = selectedRow + ? caseResultKey(selectedRow) === rowKey + : false; + const worstCaseLabel = ( + row as { worstCaseLabel?: string } + ).worstCaseLabel; + return ( + { + if (isWorst) { + // Drill into the case that produced this worst UF. + setActiveCaseId(row.case_id); + setSelectedModelId(row.capacity_model_id); + } else { + setSelectedCapacityResult( + row.capacity_model_id, + rowKey, + ); + } + }} + > + + + + {isWorst && ( + + )} + + ); + })} + +
ModelUFCheckCase
+ {shortName(row.stiffener ?? row.panel_group)} + + {formatUf(row.governing_usage)} + + {row.error ? "error" : row.governing_check ?? ""} + + {worstCaseLabel ?? row.case_id} +
+
+ + {selectedRow && ( +
+
+
+ {shortName(selectedRow.stiffener ?? selectedRow.panel_group)} +
+
+ + +
+
+
+
UF
+
+ {formatUf(selectedRow.governing_usage)} +
+
Clause
+
{selectedRow.governing_clause ?? ""}
+
Status
+
+ {selectedRow.error + ? "ERROR" + : selectedRow.passed + ? "OK" + : "FAIL"} +
+
+ {selectedRow.error && ( +
+ {selectedRow.error} +
+ )} +
+ {selectedRow.checks.slice(0, 4).map((check) => ( +
+ + {check.label} + + + {formatUf(check.usage)} + + + {formulaReference(check)} + +
+ ))} +
+
+ )} + + {showInputs && + selectedRow && + createPortal( + setShowStations((v) => !v)} + onClose={() => { + setShowInputs(false); + setShowStations(false); + }} + />, + document.body, + )} + + {showResultsPanel && + selectedRow && + createPortal( + setShowResultsPanel(false)} + />, + document.body, + )} +
+ )} +
+ ); +}; + +const FloatingInputPanel: React.FC<{ + run: CapacityRunLike; + row: CapacityCaseResultLike; + rightOffsetPx: number; + showStations: boolean; + onToggleStations: () => void; + onClose: () => void; +}> = ({ run, row, rightOffsetPx, showStations, onToggleStations, onClose }) => ( +
+
+
+
Input
+
+ + + +
+
+ +
+
+ +
+
+); + +/** Two-line "Case / Model" subtitle shared by the Input and Results sidecars. */ +const PanelSubtitle: React.FC<{ + run: CapacityRunLike; + row: CapacityCaseResultLike; +}> = ({ run, row }) => { + const caseLabel = caseLabelForRow(run, row); + const modelName = shortName(row.stiffener ?? row.panel_group); + return ( +
+ Case + + {caseLabel} + + Model + + {modelName} + +
+ ); +}; + +const CapacityLegend: React.FC = () => ( +
+ {/* Genie discrete UF bands (0.2/0.4/0.6/0.8/1.0) over a 0..1.2 bar. */} +
+
+ 0 + 0.2 + 0.4 + 0.6 + 0.8 + 1.0 +
+
+); + +interface InputField { + symbol?: string; + label: string; + value: number | string | null; + unit?: string; + pos?: number; // 1/2/3 → colour-coded to the 3D station marker + ref?: string; // DNV-RP-C201 equation/clause tag, e.g. "(6.17)" + provenance?: InputProvenance; + provenanceKey?: string; + provenanceUrl?: string; + provenanceRowKey?: string; +} + +interface InputGroup { + title: string; + fields: InputField[]; +} + +interface InputProvenance { + label?: string; + calculation?: string; + formula?: string; + terms?: ProvenanceTerm[]; + source_sets?: ProvenanceSourceSet[]; + sources?: ProvenanceSource[]; +} + +interface ProvenanceTerm { + label?: string; + value?: number | string | null; + unit?: string; +} + +interface ProvenanceSourceSet { + label?: string; + source_count?: number; + element_ids?: number[]; + sources?: ProvenanceSource[]; + truncated_source_count?: number; +} + +interface ProvenanceSource { + element_id?: number; + node_ids?: number[]; + result_points?: number[]; + force_position?: number; + along_m?: number; + value?: number | string | null; + raw_value?: number | string | null; + unit?: string; + calculation?: string; +} + +const CapacityInputDetails: React.FC<{ + run: CapacityRunLike; + row: CapacityCaseResultLike; +}> = ({ run, row }) => { + const groups = useMemo(() => buildInputGroups(run, row), [run, row]); + return ( +
+ {groups.map((g) => ( +
+
+ {g.title} +
+
+ {g.fields.map((f, i) => ( + + ))} +
+
+ ))} +
+ ); +}; + +const InputFieldRow: React.FC<{ field: InputField }> = ({ field: f }) => { + const [open, setOpen] = useState(false); + const [loadedProvenance, setLoadedProvenance] = useState< + InputProvenance | undefined + >(undefined); + const [loadingProvenance, setLoadingProvenance] = useState(false); + const [provenanceError, setProvenanceError] = useState(null); + const provenance = f.provenance ?? loadedProvenance; + const canFetchProvenance = !!f.provenanceUrl && !!f.provenanceKey; + const hasProvenance = !!provenance || canFetchProvenance; + + useEffect(() => { + setLoadedProvenance(undefined); + setLoadingProvenance(false); + setProvenanceError(null); + setOpen(false); + }, [f.provenanceUrl, f.provenanceKey, f.provenanceRowKey]); + + const toggleProvenance = async () => { + if (open) { + setOpen(false); + return; + } + setOpen(true); + if (provenance || !f.provenanceUrl || !f.provenanceKey) return; + setLoadingProvenance(true); + setProvenanceError(null); + try { + const payload = await loadCapacityProvenance( + f.provenanceUrl, + f.provenanceRowKey, + ); + const item = provenanceFor(payload, f.provenanceKey); + if (item) { + setLoadedProvenance(item); + } else { + setProvenanceError("No provenance recorded for this value."); + } + } catch (err) { + setProvenanceError("Could not load provenance."); + // eslint-disable-next-line no-console + console.warn(`[capacity] failed to load provenance ${f.provenanceUrl}:`, err); + } finally { + setLoadingProvenance(false); + } + }; + + return ( + <> + + {f.pos != null && ( + + {"\u25cf "} + + )} + {f.symbol ?? ""} + + + {f.label} + {f.ref && {f.ref}} + + + {hasProvenance ? ( + + ) : ( + fmtInputField(f) + )} + + {open && hasProvenance && ( +
+ {loadingProvenance ? ( +
Loading provenance...
+ ) : provenance ? ( + + ) : ( +
{provenanceError ?? "No provenance recorded for this value."}
+ )} +
+ )} + + ); +}; + +const InputProvenanceDetails: React.FC<{ provenance: InputProvenance }> = ({ + provenance, +}) => { + const sets = provenance.source_sets ?? []; + const directSources = provenance.sources ?? []; + return ( +
+ {provenance.calculation &&
{provenance.calculation}
} + {provenance.formula && ( +
{provenance.formula}
+ )} + {provenance.terms?.length ? ( +
+ {provenance.terms.map((term, i) => ( + + {term.label ?? ""} + + {fmtProvenanceValue(term.value, term.unit)} + + + ))} +
+ ) : null} + {sets.map((set, i) => ( + + ))} + {directSources.length ? ( + + ) : null} +
+ ); +}; + +const ProvenanceSourceSetView: React.FC<{ sourceSet: ProvenanceSourceSet }> = ({ + sourceSet, +}) => { + const elements = sourceSet.element_ids ?? []; + const sources = sourceSet.sources ?? []; + return ( +
+
+ {sourceSet.label ?? "sources"} + {sourceSet.source_count != null && ( + ({sourceSet.source_count}) + )} +
+ {elements.length ? ( +
+ Elements + + {compactNumberList(elements)} + +
+ ) : null} + {sources.map((source, i) => ( +
+ {formatProvenanceSource(source)} +
+ ))} + {sourceSet.truncated_source_count ? ( +
+ {sourceSet.truncated_source_count} more source rows +
+ ) : null} +
+ ); +}; + +/** Per-element UF values for the per-stiffener colour view: each stiffener's own + * line + tributary plate strip carries that stiffener's UF (max where strips + * overlap), so within a panel you see each stiffener's UF rather than the panel + * maximum. A specific check metric uses that check's usage from the row; rows + * without per-check usages (the worst-summary rows) fall back to the governing + * UF. */ +function capacityRowScore(row: CapacityCaseResultLike): number { + // A missing engineering result is more severe than any finite utilization. + return row.error ? Number.MAX_VALUE : (row.governing_usage ?? -1); +} + +function buildIndividualUfValues( + rows: CapacityCaseResultLike[], + run: CapacityRunLike, + activeMetricId: string, +): Array<{ element_id: number; value: number | null }> { + const checkId = metricCheckId(activeMetricId); + const byElement = new Map(); + const modelById = new Map(run.capacity_models.map((m) => [m.id, m])); + const stiffMaps = new Map>>(); + for (const r of rows) { + const model = modelById.get(r.capacity_model_id); + if (!model) continue; + // Girder results belong on the dedicated thick dashed line overlay. Do not + // paint the supporting plate or girder FE faces with the girder UF. + if ((model as { type?: string }).type === "girder") continue; + let byName = stiffMaps.get(model.id); + if (!byName) { + const stiffeners = (model.stiffeners ?? []) as Array< + Record + >; + byName = new Map(stiffeners.map((s) => [String(s.name), s])); + stiffMaps.set(model.id, byName); + } + const stiff = byName.get(String(r.stiffener)); + if (!stiff) continue; + const uf = r.error ? 1.01 : rowMetricUf(r, checkId); + if (uf == null) continue; // this check does not apply to the row + const ids = [ + ...((stiff.element_ids as number[] | undefined) ?? []), + ...((stiff.tributary_plate_ids as number[] | undefined) ?? []), + ]; + for (const id of ids) { + const prev = byElement.get(id); + if (prev == null || uf > prev) byElement.set(id, uf); + } + } + return [...byElement].map(([element_id, value]) => ({ element_id, value })); +} + +/** Specific check id for a metric, or null for the governing-UF metric. */ +function metricCheckId(activeMetricId: string): string | null { + return activeMetricId.startsWith("capacity.uf.") && + activeMetricId !== "capacity.uf.governing" + ? activeMetricId.slice("capacity.uf.".length) + : null; +} + +/** The row's UF for the active metric (governing, or a specific check). */ +function rowMetricUf( + r: CapacityCaseResultLike, + checkId: string | null, +): number | null { + if (checkId && r.checks?.length) { + return r.checks.find((c) => c.id === checkId)?.usage ?? null; + } + return r.governing_usage ?? 0; +} + +/** Per-girder-model UF for colouring the girder lines in the 3D view. */ +function buildGirderUfMap( + rows: CapacityCaseResultLike[], + run: CapacityRunLike, + activeMetricId: string, +): Map { + const checkId = metricCheckId(activeMetricId); + const girderIds = new Set( + run.capacity_models + .filter((m) => (m as { type?: string }).type === "girder") + .map((m) => m.id), + ); + const out = new Map(); + if (girderIds.size === 0) return out; + for (const r of rows) { + if (!girderIds.has(r.capacity_model_id)) continue; + const uf = r.error ? 1.01 : rowMetricUf(r, checkId); + if (uf == null) continue; + const prev = out.get(r.capacity_model_id); + if (prev == null || uf > prev) out.set(r.capacity_model_id, uf); + } + return out; +} + +function asNum(v: unknown): number | null { + const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN; + return Number.isFinite(n) ? n : null; +} + +function scaled(v: unknown, factor: number): number | null { + const n = asNum(v); + return n == null ? null : n * factor; +} + +function provenanceFor( + provenance: Record, + key: string, +): InputProvenance | undefined { + const value = provenance[key]; + return value && typeof value === "object" ? (value as InputProvenance) : undefined; +} + +/** Round a value to the precision the input sidecar shows (see fmtInputField): + * 1 decimal for |v|>=100, 2 decimals for |v|>=1, else 3 significant figures. + * Used so the exported numbers match what the user reads in the sidebar + * (e.g. 180.0 rather than 180.00000715255737). */ +function displayRound(v: number): number { + if (!Number.isFinite(v)) return v; + const a = Math.abs(v); + if (a >= 100) return Number(v.toFixed(1)); + if (a >= 1 || a === 0) return Number(v.toFixed(2)); + return Number(v.toPrecision(3)); +} + +function fmtInputField(f: InputField): string { + if (typeof f.value === "string") return f.value; + if (f.value == null || !Number.isFinite(f.value)) return "-"; + const v = f.value; + const a = Math.abs(v); + const s = + a >= 1e6 + ? v.toExponential(3) + : a >= 100 + ? v.toFixed(1) + : a >= 1 || a === 0 + ? v.toFixed(2) + : v.toPrecision(3); + return f.unit && f.unit !== "-" ? `${s} ${f.unit}` : s; +} + +function fmtProvenanceValue(value: unknown, unit?: string): string { + if (typeof value === "string") return unit && unit !== "-" ? `${value} ${unit}` : value; + const n = asNum(value); + if (n == null) return "-"; + const a = Math.abs(n); + const s = + a >= 1e6 || (a > 0 && a < 1e-3) + ? n.toExponential(3) + : a >= 100 + ? n.toFixed(2) + : a >= 1 || a === 0 + ? n.toFixed(4) + : n.toPrecision(4); + return unit && unit !== "-" ? `${s} ${unit}` : s; +} + +function compactNumberList(values: number[], limit = 18): string { + const shown = values.slice(0, limit).join(", "); + return values.length > limit ? `${shown}, +${values.length - limit}` : shown; +} + +function formatProvenanceSource(source: ProvenanceSource): string { + const parts: string[] = []; + if (source.element_id != null) parts.push(`el ${source.element_id}`); + if (source.node_ids?.length) parts.push(`nodes ${compactNumberList(source.node_ids, 8)}`); + if (source.result_points?.length) { + parts.push(`rp ${compactNumberList(source.result_points, 8)}`); + } + if (source.force_position != null) parts.push(`pos ${source.force_position}`); + if (source.along_m != null) parts.push(`x=${fmtProvenanceValue(source.along_m, "m")}`); + if (source.value != null) parts.push(`value ${fmtProvenanceValue(source.value, source.unit)}`); + if (source.raw_value != null) parts.push(`raw ${fmtProvenanceValue(source.raw_value, source.unit)}`); + return parts.join(" | "); +} + +/** Map a capacity-model section to the UI's stiffener_type choice. */ +function stiffenerTypeForExport(section: Record): string { + const bf = asNum(section.flange_width); + if (bf == null || bf <= 0) return "flatbar"; + // HP/bulb profiles are one-sided — the engine maps them to the angle type. + if (/^hp|bulb/i.test(String(section.name ?? ""))) return "angle"; + return "tee"; +} + +/** Geometry/material a stiffener result was checked with, in mm / MPa. + * ``profileName`` / ``eccentricityMm`` are display-only (not part of what the + * engine consumes). */ +interface ResolvedGeometry { + span: number | null; + s: number | null; + t: number | null; + hw: number | null; + tw: number | null; + bf: number | null; + tf: number | null; + stiffenerType: string; + profileName: string; + eccentricityMm: number | null; + fy: number | null; + E: number | null; + nu: number | null; + gammaM: number | null; + continuous: boolean; + optimizeZStar: boolean; +} + +/** Resolve the geometry/material for a result row, preferring the v8 + * ``check_inputs`` (the exact values the engine consumed) over the display + * ``capacity_model`` dict. The two can differ — notably the stiffener span, + * which drives the transverse plate resistance sigma_y,R (eq. 4.6) — so using + * ``check_inputs`` is what makes the Input panel and the Export reproduce the + * engine result. Falls back to the display dict for pre-v8 bundles. */ +function resolveGeometry( + run: CapacityRunLike, + row: CapacityCaseResultLike, +): ResolvedGeometry { + const model = run.capacity_models.find((m) => m.id === row.capacity_model_id); + const plate = (model?.plates?.[0] ?? {}) as Record; + const stiffeners = (model?.stiffeners ?? []) as Array>; + const stiff = + stiffeners.find((s) => s.name === row.stiffener) ?? stiffeners[0] ?? {}; + const section = (stiff.section ?? {}) as Record; + const mat = (stiff.material ?? plate.material ?? {}) as Record; + const profileName = String(section.name ?? "—"); + const eccentricityMm = scaled(stiff.eccentricity, 1e3); + const ci = row.check_inputs; + if (ci) { + return { + span: asNum(ci.span_mm), + s: asNum(ci.plate?.s_mm), + t: asNum(ci.plate?.t_mm), + hw: asNum(ci.stiffener?.hw_mm), + tw: asNum(ci.stiffener?.tw_mm), + bf: asNum(ci.stiffener?.bf_mm), + tf: asNum(ci.stiffener?.tf_mm), + stiffenerType: ci.stiffener?.type ?? stiffenerTypeForExport(section), + profileName, + eccentricityMm, + fy: asNum(ci.material?.fy_mpa), + E: asNum(ci.material?.E_mpa), + nu: asNum(mat.poisson), + gammaM: asNum(ci.material?.gamma_m), + continuous: ci.continuous !== false, + optimizeZStar: ci.optimize_z_star === true, + }; + } + return { + span: scaled(stiff.span ?? plate.length, 1e3), + s: scaled(plate.width, 1e3), + t: scaled(plate.thickness, 1e3), + hw: scaled(section.height, 1e3), + tw: scaled(section.web_thickness, 1e3), + bf: scaled(section.flange_width, 1e3), + tf: scaled(section.flange_thickness, 1e3), + stiffenerType: stiffenerTypeForExport(section), + profileName, + eccentricityMm, + fy: scaled(mat.fy, 1e-6), + E: scaled(mat.E, 1e-6), + nu: asNum(mat.poisson), + gammaM: asNum(mat.gamma_m), + continuous: stiff.continuous !== false, + optimizeZStar: false, + }; +} + +/** Build the codecheck.ui ``fe_stiffened`` value map from a result row. + * Units match that check's fields (mm / MPa / kNm). Geometry/material come from + * the as-checked ``check_inputs`` (so an imported case reproduces the engine); + * stresses come from the same resolved vectors / loads the Input panel shows. */ +function buildUiCaseValues( + run: CapacityRunLike, + row: CapacityCaseResultLike, +): Record { + const g = resolveGeometry(run, row); + const loads = (row.loads ?? {}) as Record; + const vec = (row.resolved_vectors ?? {}) as Record; + const sigmaX = (vec.AverageLongitudinalMembraneStresses ?? []) as unknown[]; + // Round to the sidebar's display precision so the exported numbers match + // what the user reads (180.0, not 180.00000715255737). + const num = (v: unknown, factor: number, fallback = 0): number => { + const n = scaled(v, factor); + return displayRound(n == null ? fallback : n); + }; + const dr = (v: number | null, fallback: number): number => + displayRound(v == null ? fallback : v); + // Uniform sigma_x is stored as a single station; fan it out to all three. + const sx1 = num(sigmaX[0], 1e-6); + const sx2 = sigmaX[1] != null ? num(sigmaX[1], 1e-6) : sx1; + const sx3 = sigmaX[2] != null ? num(sigmaX[2], 1e-6) : sx1; + return { + model: "general", + continuous: g.continuous, + z_star: 0, + optimize_z_star: g.optimizeZStar, + fy: dr(g.fy, 355), + E: dr(g.E, 210000), + gamma_M: dr(g.gammaM, 1.15), + stiffener_type: g.stiffenerType, + hw: dr(g.hw, 0), + tw: dr(g.tw, 0), + bf: dr(g.bf, 0), + tf: dr(g.tf, 0), + s: dr(g.s, 0), + t: dr(g.t, 0), + span: dr(g.span, 0), + sigma_x_1: sx1, + sigma_x_2: sx2, + sigma_x_3: sx3, + tau_1: num(loads.tau_1, 1e-6), + tau_2: num(loads.tau_2, 1e-6), + tau_3: num(loads.tau_3, 1e-6), + sigma_y1: num(loads.sigma_y1, 1e-6), + sigma_y2: num(loads.sigma_y2, 1e-6), + sigma_y3: num(loads.sigma_y3, 1e-6), + M_1: num(loads.M_1, 1e-3), + M_2: num(loads.M_2, 1e-3), + M_3: num(loads.M_3, 1e-3), + lateral_pressure: num(loads.p_Sd, 1e-6), + }; +} + +/** Girder run ``check_inputs`` payload (see + * girder_capacity_check._build_girder_check_inputs — mm / MPa / mm² / mm⁴). */ +interface GirderCheckInputs { + girder?: { + hw_mm?: number; + tw_mm?: number; + bf_mm?: number; + tf_mm?: number; + section?: string; + }; + bay?: { + LG_mm?: number; + l_mm?: number; + l1_mm?: number; + l2_mm?: number; + s_mm?: number; + continuous?: boolean; + }; + plate?: { s_mm?: number; t_mm?: number }; + stiffener?: { + As_mm2?: number; + Is_mm4?: number; + section?: string; + continuous_through_girder?: boolean; + // Representative stiffener profile (sidecar v11+; bulbs already idealized + // as angles) — needed by the UI's Stipla DNV-G export. + type?: string; + hw_mm?: number; + tw_mm?: number; + bf_mm?: number; + tf_mm?: number; + }; + material?: { fy_mpa?: number; E_mpa?: number; gamma_m?: number }; + method?: string; + welded?: boolean; +} + +function girderCheckInputs(row: CapacityCaseResultLike): GirderCheckInputs { + return (row.check_inputs ?? {}) as unknown as GirderCheckInputs; +} + +/** Build the codecheck.ui ``fe_girder`` value map from a girder result + * row. Geometry comes from the as-checked ``check_inputs``; the loads invert + * ``GirderLoads.from_membrane_stress`` (eq. 5.2): the UI takes girder-direction + * membrane stresses, so sigma_y,i = N_Gi / (A_G + l·t) with A_G = hw·tw + bf·tf + * — the same section the UI rebuilds, so the imported case reproduces the + * engine loads. */ +function buildUiGirderValues( + row: CapacityCaseResultLike, +): Record { + const ci = girderCheckInputs(row); + const loads = (row.loads ?? {}) as Record; + const vec = (row.resolved_vectors ?? {}) as Record; + const sigmaXVec = (vec.AverageStiffenerDirectionMembraneStresses ?? []) as unknown[]; + const dr = (v: number | null, fallback = 0): number => + displayRound(v == null ? fallback : v); + const num = (v: unknown, factor: number): number => + displayRound(scaled(v, factor) ?? 0); + // Optional stiffener-profile dimension: blank when the sidecar lacks it. + const stDim = (v: unknown): number | string => { + const n = asNum(v); + return n == null ? "" : displayRound(n); + }; + const hw = asNum(ci.girder?.hw_mm) ?? 0; + const tw = asNum(ci.girder?.tw_mm) ?? 0; + const bf = asNum(ci.girder?.bf_mm) ?? 0; + const tf = asNum(ci.girder?.tf_mm) ?? 0; + const lSpan = asNum(ci.bay?.l_mm) ?? 0; + const l1Span = asNum(ci.bay?.l1_mm) ?? lSpan; + const l2Span = asNum(ci.bay?.l2_mm) ?? lSpan; + const t = asNum(ci.plate?.t_mm) ?? 0; + const areaMm2 = hw * tw + bf * tf + lSpan * t; + // N [N] / A [mm²] = sigma [MPa] exactly. + const sigmaY = (nG: unknown): number => { + const n = asNum(nG); + return displayRound(areaMm2 > 0 && n != null ? n / areaMm2 : 0); + }; + const sigmaX = num(loads.sigma_x_Sd, 1e-6); + const sigmaXAt = (index: number): number => + sigmaXVec[index] != null ? num(sigmaXVec[index], 1e-6) : sigmaX; + return { + method: ci.method ?? "GCM3", + effective_width_method: "auto", + z_star: 0, + k_moment_reduction: 1, + welded: ci.welded !== false, + fy: dr(asNum(ci.material?.fy_mpa), 355), + E: dr(asNum(ci.material?.E_mpa), 210000), + gamma_M: dr(asNum(ci.material?.gamma_m), 1.15), + hw: dr(hw), + tw: dr(tw), + bf: dr(bf), + tf: dr(tf), + flange_type: "symmetric", + s: dr(asNum(ci.plate?.s_mm)), + t: dr(t), + As: dr(asNum(ci.stiffener?.As_mm2)), + Is: dr(asNum(ci.stiffener?.Is_mm4)), + // Stiffener profile (bulbs already idealized as angles by the sidecar): + // the UI's Stipla DNV-G export needs the profile, not just A_s/I_s. Blank + // on older sidecars — the explicit A_s/I_s above still govern the check. + stiffener_type: ci.stiffener?.type ?? "tee", + st_hw: stDim(ci.stiffener?.hw_mm), + st_tw: stDim(ci.stiffener?.tw_mm), + st_bf: stDim(ci.stiffener?.bf_mm), + st_tf: stDim(ci.stiffener?.tf_mm), + continuous_through_girder: ci.stiffener?.continuous_through_girder !== false, + LG: dr(asNum(ci.bay?.LG_mm)), + l_span: dr(lSpan), + l1_span: dr(l1Span), + l2_span: dr(l2Span), + // The sidecar run leaves the optional lengths at the engine defaults + // (L_Gk = L_G, L_GT = L_G, no panel length): export explicit blanks so the + // UI form does not substitute its own suggested defaults on import. + LGk: "", + LGT: "", + Lp: "", + continuous: ci.bay?.continuous !== false, + sigma_y_1: sigmaY(loads.N_G1), + sigma_y_2: sigmaY(loads.N_G2), + sigma_y_3: sigmaY(loads.N_G3), + M_1: num(loads.M_G1, 1e-3), + M_2: num(loads.M_G2, 1e-3), + M_3: num(loads.M_G3, 1e-3), + tau_1: num(loads.tau_1, 1e-6), + tau_2: num(loads.tau_2, 1e-6), + tau_3: num(loads.tau_3, 1e-6), + sigma_x: sigmaX, + sigma_x_1: sigmaXAt(0), + sigma_x_2: sigmaXAt(1), + sigma_x_3: sigmaXAt(2), + shear_force: num(loads.V_Sd, 1e-3), + lateral_pressure: num(loads.p_Sd, 1e-6), + p_dir: num(loads.p_dir, 1e-6), + }; +} + +function slugForFile(value: string): string { + return ( + (value || "case").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") + ); +} + +/** Download the current input as a codecheck.ui case file. The schema + * (``codecheck/case@1`` + ``standard_id``/``check_id``/``values``) is what that + * app's "Import JSON…" expects. Girder rows export the Section-7 ``fe_girder`` + * check; panel rows the ``fe_stiffened`` check. */ +function downloadUiCase(run: CapacityRunLike, row: CapacityCaseResultLike): void { + const isGirder = + run.capacity_models.find((m) => m.id === row.capacity_model_id)?.type === + "girder"; + const name = `${shortName(row.stiffener ?? row.panel_group)} ${caseLabelForRow( + run, + row, + )}`.trim(); + const payload = buildCodecheckCasePayload({ + name, + check_id: isGirder ? "fe_girder" : "fe_stiffened", + capacity_model_id: row.capacity_model_id, + case_id: row.case_id, + values: isGirder + ? buildUiGirderValues(row) + : buildUiCaseValues(run, row), + }); + const blob = new Blob([JSON.stringify(payload, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + try { + const a = document.createElement("a"); + a.href = url; + a.download = `${slugForFile(name)}.case.json`; + a.style.display = "none"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } finally { + URL.revokeObjectURL(url); + } +} + +function buildInputGroups( + run: CapacityRunLike, + row: CapacityCaseResultLike, +): InputGroup[] { + const model = run.capacity_models.find((m) => m.id === row.capacity_model_id); + if (model?.type === "girder") return buildGirderInputGroups(row); + const plate = (model?.plates?.[0] ?? {}) as Record; + const stiffeners = (model?.stiffeners ?? []) as Array< + Record + >; + const stiff = + stiffeners.find((s) => s.name === row.stiffener) ?? stiffeners[0] ?? {}; + const disc = (stiff.discretization ?? {}) as Record; + const loads = (row.loads ?? {}) as Record; + const rv = (row.resolved_variables ?? {}) as Record; + const vec = (row.resolved_vectors ?? {}) as Record; + const provenance = (row.resolved_provenance ?? {}) as Record; + const sigmaX = (vec.AverageLongitudinalMembraneStresses ?? []) as unknown[]; + // Show the geometry/material the check actually used (v8 check_inputs), so the + // sidebar matches the result and the Export. + const g = resolveGeometry(run, row); + // v8: DNV-RP-C201-correct Section-6 design values from the engine's own eqs + // (6.16/6.17/6.2). Fall back to the Genie resolved_variables for pre-v8 + // bundles. (Genie's SigmaYSd is the mid-point value, not eq 6.17 — see the + // standard — so prefer the engine figures when present.) + const rd = (row.resolved_design ?? {}) as Record; + const sigmaYSd = asNum(rd.sigma_y_Sd_mpa) ?? scaled(rv.SigmaYSd, 1e-6); + const tauSd = asNum(rd.tau_Sd_mpa) ?? scaled(rv.TauSd, 1e-6); + const nSd = asNum(rd.N_Sd_kn) ?? scaled(rv.Nsd, 1e-3); + const f = ( + symbol: string, + label: string, + value: number | string | null, + unit?: string, + pos?: number, + ref?: string, + provenanceKey?: string, + ): InputField => ({ + symbol, + label, + value, + unit, + pos, + ref, + provenanceKey, + provenance: provenanceKey ? provenanceFor(provenance, provenanceKey) : undefined, + provenanceUrl: provenanceKey ? row.provenance_url : undefined, + provenanceRowKey: provenanceKey ? row.provenance_key : undefined, + }); + return [ + { + title: "Geometry", + fields: [ + f("t", "Plate thickness", g.t, "mm"), + f("s", "Stiffener spacing", g.s, "mm"), + f("l", "Span", g.span, "mm"), + f("z_w", "Eccentricity", g.eccentricityMm, "mm"), + ], + }, + { + title: "Stiffener section", + fields: [ + f("", "Profile", g.profileName), + f("h_w", "Web height", g.hw, "mm"), + f("t_w", "Web thickness", g.tw, "mm"), + f("b_f", "Flange width", g.bf, "mm"), + f("t_f", "Flange thickness", g.tf, "mm"), + ], + }, + { + title: "Material", + fields: [ + f("f_y", "Yield strength", g.fy, "MPa"), + f("E", "Young's modulus", g.E, "MPa"), + f("ν", "Poisson", g.nu, "-"), + f("γ_M", "Material factor", g.gammaM, "-"), + ], + }, + { + title: "Axial membrane stress", + fields: [ + f("σ_x1", "Position 1", scaled(sigmaX[0], 1e-6), "MPa", 1, "[5.3.2]", "sigma_x_1"), + f("σ_x2", "Position 2", scaled(sigmaX[1], 1e-6), "MPa", 2, undefined, "sigma_x_2"), + f("σ_x3", "Position 3", scaled(sigmaX[2], 1e-6), "MPa", 3, undefined, "sigma_x_3"), + ], + }, + { + title: "Transverse stress", + fields: [ + f("σ_y1", "Position 1", scaled(loads.sigma_y1, 1e-6), "MPa", 1, "[5.3.4]", "sigma_y1"), + f("σ_y2", "Position 2", scaled(loads.sigma_y2, 1e-6), "MPa", 2, undefined, "sigma_y2"), + f("σ_y3", "Position 3", scaled(loads.sigma_y3, 1e-6), "MPa", 3, undefined, "sigma_y3"), + ], + }, + { + title: "Shear", + fields: [ + f("τ_1", "Position 1", scaled(loads.tau_1, 1e-6), "MPa", 1, "[5.3.5]", "tau_1"), + f("τ_2", "Position 2", scaled(loads.tau_2, 1e-6), "MPa", 2, undefined, "tau_2"), + f("τ_3", "Position 3", scaled(loads.tau_3, 1e-6), "MPa", 3, undefined, "tau_3"), + ], + }, + { + title: "Moment", + fields: [ + f("M_1", "Position 1", scaled(loads.M_1, 1e-3), "kN·m", 1, "[5.3.3]", "M_1"), + f("M_2", "Position 2", scaled(loads.M_2, 1e-3), "kN·m", 2, undefined, "M_2"), + f("M_3", "Position 3", scaled(loads.M_3, 1e-3), "kN·m", 3, undefined, "M_3"), + ], + }, + { + title: "Lateral load", + fields: [ + f("p_Sd", "Lateral pressure", scaled(loads.p_Sd, 1e-3), "kPa", undefined, undefined, "p_Sd"), + ], + }, + { + title: "Resolved design (Section 6)", + fields: [ + f("σ_ySd", "Transverse design stress", sigmaYSd, "MPa", undefined, "(6.17)"), + f("τ_Sd", "Shear design stress", tauSd, "MPa", undefined, "(6.16)"), + f("N_Sd", "Axial design force", nSd, "kN", undefined, "(6.2)"), + ], + }, + { + title: "Options", + fields: [ + f("", "Continuous", g.continuous ? "yes" : "no"), + f("", "Optimize z*", g.optimizeZStar ? "yes" : "no", undefined, undefined, "[6.10.2]"), + f("", "Tension field", (loads.tension_field as string) ?? "none"), + ], + }, + { + title: "Modelling [6.4.3]", + fields: [ + f("", "Elements along stiffener", asNum(disc.elements_along)), + f( + "", + "Element order", + disc.element_order === 2 ? "2nd order" : "1st order", + ), + f("", "Min. required", asNum(disc.min_required)), + f( + "", + "Status", + disc.ok === false ? "⚠ under-meshed" : "OK", + undefined, + undefined, + "[6.4.3]", + ), + ], + }, + ]; +} + +/** Section-7 girder Input panel: the girder bay/section, the supported + * stiffeners and the [7.8] design loads — the girder counterpart of the panel + * groups above, built from the girder ``check_inputs`` + ``loads``. */ +function buildGirderInputGroups(row: CapacityCaseResultLike): InputGroup[] { + const ci = girderCheckInputs(row); + const loads = (row.loads ?? {}) as Record; + const vec = (row.resolved_vectors ?? {}) as Record; + const provenance = (row.resolved_provenance ?? {}) as Record; + const sigmaXVec = (vec.AverageStiffenerDirectionMembraneStresses ?? []) as unknown[]; + const hw = asNum(ci.girder?.hw_mm) ?? 0; + const tw = asNum(ci.girder?.tw_mm) ?? 0; + const bf = asNum(ci.girder?.bf_mm) ?? 0; + const tf = asNum(ci.girder?.tf_mm) ?? 0; + const lSpan = asNum(ci.bay?.l_mm) ?? 0; + const l1Span = asNum(ci.bay?.l1_mm) ?? lSpan; + const l2Span = asNum(ci.bay?.l2_mm) ?? lSpan; + const t = asNum(ci.plate?.t_mm) ?? 0; + const areaMm2 = hw * tw + bf * tf + lSpan * t; + const sigmaY = (nG: unknown): number | null => { + const n = asNum(nG); + return areaMm2 > 0 && n != null ? displayRound(n / areaMm2) : null; + }; + const sigmaX = scaled(loads.sigma_x_Sd, 1e-6); + const sigmaXAt = (index: number): number | null => + sigmaXVec[index] != null ? scaled(sigmaXVec[index], 1e-6) : sigmaX; + const f = ( + symbol: string, + label: string, + value: number | string | null, + unit?: string, + pos?: number, + ref?: string, + provenanceKey?: string, + ): InputField => ({ + symbol, + label, + value, + unit, + pos, + ref, + provenanceKey, + provenance: provenanceKey ? provenanceFor(provenance, provenanceKey) : undefined, + provenanceUrl: provenanceKey ? row.provenance_url : undefined, + provenanceRowKey: provenanceKey ? row.provenance_key : undefined, + }); + return [ + { + title: "Girder bay", + fields: [ + f("L_G", "Girder span", asNum(ci.bay?.LG_mm), "mm"), + f("L_1", "Adjacent stiffener span", l1Span, "mm", undefined, "[5.3]"), + f("L_2", "Adjacent stiffener span", l2Span, "mm", undefined, "[5.3]"), + f("l", "Effective span", lSpan, "mm", undefined, "[7.4]"), + f("s", "Stiffener spacing", asNum(ci.plate?.s_mm), "mm"), + f("t", "Plate thickness", asNum(ci.plate?.t_mm), "mm"), + ], + }, + { + title: "Girder section", + fields: [ + f("", "Profile", ci.girder?.section || "—"), + f("h_wG", "Web height", asNum(ci.girder?.hw_mm), "mm"), + f("t_wG", "Web thickness", asNum(ci.girder?.tw_mm), "mm"), + f("b_fG", "Flange width", asNum(ci.girder?.bf_mm), "mm"), + f("t_fG", "Flange thickness", asNum(ci.girder?.tf_mm), "mm"), + ], + }, + { + title: "Supported stiffeners", + fields: [ + f("", "Profile", ci.stiffener?.section || "—"), + f("h_w", "Web height", asNum(ci.stiffener?.hw_mm), "mm"), + f("t_w", "Web thickness", asNum(ci.stiffener?.tw_mm), "mm"), + f("b_f", "Flange width", asNum(ci.stiffener?.bf_mm), "mm"), + f("t_f", "Flange thickness", asNum(ci.stiffener?.tf_mm), "mm"), + f("A_s", "Stiffener area", asNum(ci.stiffener?.As_mm2), "mm²"), + f("I_s", "Moment of inertia", asNum(ci.stiffener?.Is_mm4), "mm⁴"), + f( + "", + "Continuous through girder", + ci.stiffener?.continuous_through_girder === false ? "no" : "yes", + ), + ], + }, + { + title: "Material", + fields: [ + f("f_y", "Yield strength", asNum(ci.material?.fy_mpa), "MPa"), + f("E", "Young's modulus", asNum(ci.material?.E_mpa), "MPa"), + f("γ_M", "Material factor", asNum(ci.material?.gamma_m), "-"), + ], + }, + { + title: "Stiffener-direction stress", + fields: [ + f("σ_x1", "Position 1", sigmaXAt(0), "MPa", 1, "[7.8.5]", "sigma_x_1"), + f("σ_x2", "Position 2", sigmaXAt(1), "MPa", 2, undefined, "sigma_x_2"), + f("σ_x3", "Position 3", sigmaXAt(2), "MPa", 3, undefined, "sigma_x_3"), + ], + }, + { + title: "Girder membrane stress", + fields: [ + f("σ_y1", "Position 1", sigmaY(loads.N_G1), "MPa", 1, "(5.2)", "sigma_y_1"), + f("σ_y2", "Position 2", sigmaY(loads.N_G2), "MPa", 2, undefined, "sigma_y_2"), + f("σ_y3", "Position 3", sigmaY(loads.N_G3), "MPa", 3, undefined, "sigma_y_3"), + ], + }, + { + title: "Girder axial force (compression +)", + fields: [ + f("N_G1", "Position 1", scaled(loads.N_G1, 1e-3), "kN", 1, "[7.8.2]", "N_G1"), + f("N_G2", "Position 2", scaled(loads.N_G2, 1e-3), "kN", 2, undefined, "N_G2"), + f("N_G3", "Position 3", scaled(loads.N_G3, 1e-3), "kN", 3, undefined, "N_G3"), + ], + }, + { + title: "Girder moment (tension in plate flange +)", + fields: [ + f("M_G1", "Position 1", scaled(loads.M_G1, 1e-3), "kN·m", 1, "(7.42)", "M_G1"), + f("M_G2", "Position 2", scaled(loads.M_G2, 1e-3), "kN·m", 2, "(7.43)", "M_G2"), + f("M_G3", "Position 3", scaled(loads.M_G3, 1e-3), "kN·m", 3, "(7.44)", "M_G3"), + ], + }, + { + title: "Shear stress", + fields: [ + f("τ_1", "Position 1", scaled(loads.tau_1, 1e-6), "MPa", 1, "(7.45)", "tau_1"), + f("τ_2", "Position 2", scaled(loads.tau_2, 1e-6), "MPa", 2, undefined, "tau_2"), + f("τ_3", "Position 3", scaled(loads.tau_3, 1e-6), "MPa", 3, undefined, "tau_3"), + ], + }, + { + title: "Other loads", + fields: [ + f("p_Sd", "Lateral pressure", scaled(loads.p_Sd, 1e-3), "kPa", undefined, undefined, "p_Sd"), + f("V_Sd", "Web shear force", scaled(loads.V_Sd, 1e-3), "kN", undefined, "(7.68)", "V_Sd"), + ], + }, + { + title: "Method", + fields: [ + f("", "Girder capacity model", ci.method ?? "GCM3", undefined, undefined, "Table 7-1"), + f("", "Continuous girder", ci.bay?.continuous === false ? "no" : "yes"), + f("", "Welded", ci.welded === false ? "no" : "yes"), + ], + }, + ]; +} + +function metricLabel( + run: CapacityRunLike, + field: CapacityVisualFieldLike, +): string { + const ref = metricReference(run, field); + return ref ? `${field.label} ${ref}` : field.label; +} + +function metricReference( + run: CapacityRunLike, + field: CapacityVisualFieldLike, +): string { + const equations = field.equations?.filter(Boolean); + if (equations?.length) return equations.join(", "); + if (field.clause) return `(${field.clause})`; + const checkId = field.check_id ?? field.id.replace(/^capacity\.uf\./, ""); + const check = run.check_catalog?.find((entry) => entry.id === checkId); + if (check?.equations?.length) return check.equations.join(", "); + if (check?.clause) return `(${check.clause})`; + return ""; +} + +function pickCapacityModelForElement( + run: CapacityRunLike, + elementId: number, + activeCaseId: string | null, + activeMetricId: string, + selectedModelId: string | null, +) { + const candidates = run.capacity_models.filter((model) => + (model.element_ids.all ?? []).includes(elementId), + ); + if (candidates.length === 0) return null; + const stillSelected = candidates.find( + (model) => model.id === selectedModelId, + ); + if (stillSelected) return stillSelected; + + const scoreByModel = activeMetricScores(run, activeCaseId, activeMetricId); + return ( + candidates.slice().sort((a, b) => { + const scoreDiff = + (scoreByModel.get(b.id) ?? -Infinity) - + (scoreByModel.get(a.id) ?? -Infinity); + if (scoreDiff !== 0) return scoreDiff; + return ( + (a.element_ids.all?.length ?? Number.MAX_SAFE_INTEGER) - + (b.element_ids.all?.length ?? Number.MAX_SAFE_INTEGER) + ); + })[0] ?? null + ); +} + +/** Result-row key for the individual stiffener/strip of ``model`` that owns the + * picked element, so a click inside an already-selected panel selects that + * specific capacity model (not just the panel's worst). Overlapping strips + * resolve to the highest UF for the active metric. The worst view keeps + * model-level selection (its rows aggregate over cases). */ +function pickResultRowForElement( + run: CapacityRunLike, + model: CapacityRunLike["capacity_models"][number], + elementId: number, + activeCaseId: string | null, + activeMetricId: string, +): string | null { + if (!activeCaseId || activeCaseId === WORST_CASE_ID) return null; + const stiffeners = (model.stiffeners ?? []) as Array>; + const owners = new Set( + stiffeners + .filter((s) => { + const own = (s.element_ids as number[] | undefined) ?? []; + const trib = (s.tributary_plate_ids as number[] | undefined) ?? []; + return own.includes(elementId) || trib.includes(elementId); + }) + .map((s) => String(s.name)), + ); + if (owners.size === 0) return null; + const store = useCapacityResultsStore.getState(); + const rows = ( + store.caseDetail[activeCaseId] ?? + run.case_results.filter((r) => r.case_id === activeCaseId) + ).filter( + (r) => r.capacity_model_id === model.id && owners.has(String(r.stiffener)), + ); + if (rows.length === 0) return null; + const checkId = metricCheckId(activeMetricId); + const best = rows + .slice() + .sort( + (a, b) => (rowMetricUf(b, checkId) ?? -1) - (rowMetricUf(a, checkId) ?? -1), + )[0]; + return caseResultKey(best); +} + +function activeMetricScores( + run: CapacityRunLike, + activeCaseId: string | null, + activeMetricId: string, +): Map { + const out = new Map(); + const caseId = + activeCaseId ?? run.result_cases[0]?.id ?? run.field_case_steps?.[0]; + if (!caseId) return out; + + // Legacy (<=v5) inline json field path. + const field = run.visual_fields.find((f) => f.id === activeMetricId); + const fieldCase = field?.cases?.find((c) => c.case_id === caseId); + if (fieldCase) { + for (const value of fieldCase.values) { + if ( + !value.capacity_model_id || + value.value == null || + !isFinite(value.value) + ) + continue; + const previous = out.get(value.capacity_model_id); + if (previous == null || value.value > previous) + out.set(value.capacity_model_id, value.value); + } + return out; + } + + // v6: per-model score from the active case's loaded detail rows. This is a + // tie-breaker for picking (which model an element belongs to), so missing + // (not-yet-loaded) detail just falls back to element-count ordering. + const store = useCapacityResultsStore.getState(); + const rows = + store.caseDetail[caseId] ?? + run.case_results.filter((r) => r.case_id === caseId); + const checkId = + activeMetricId.startsWith("capacity.uf.") && + activeMetricId !== "capacity.uf.governing" + ? activeMetricId.slice("capacity.uf.".length) + : null; + for (const row of rows) { + const score = checkId + ? row.checks?.find((c) => c.id === checkId)?.usage ?? null + : row.governing_usage; + if (score == null || !isFinite(score)) continue; + const previous = out.get(row.capacity_model_id); + if (previous == null || score > previous) { + out.set(row.capacity_model_id, score); + } + } + return out; +} + +function elementIdFromName(name: string): number | null { + const match = /^E(\d+)$/.exec(name.trim()); + if (!match) return null; + const parsed = Number.parseInt(match[1], 10); + return Number.isFinite(parsed) ? parsed : null; +} + +export default CapacityControls; diff --git a/src/frontend/src/components/capacity/CapacityResultsPanel.tsx b/src/frontend/src/components/capacity/CapacityResultsPanel.tsx new file mode 100644 index 000000000..55d2e7a61 --- /dev/null +++ b/src/frontend/src/components/capacity/CapacityResultsPanel.tsx @@ -0,0 +1,365 @@ +import React, { useMemo } from "react"; + +import { + CAPACITY_FLOATING_PANEL_RIGHT_PX, + CAPACITY_RESULTS_PANEL_WIDTH_PX, + caseLabelForRow, + formulaReference, + formatUf, + shortName, + ufTextClass, + type CapacityCaseResultLike, + type CapacityRunLike, +} from "@/components/capacity/capacityFormat"; +import type { CapacityCheckResult } from "@/state/capacityResultsStore"; + +interface CapacityResultsPanelProps { + run: CapacityRunLike; + row: CapacityCaseResultLike; + onClose: () => void; +} + +export const CapacityResultsPanel: React.FC = ({ + run, + row, + onClose, +}) => { + const checks = row.checks ?? []; + const governingCheckId = useMemo(() => pickGoverningCheckId(row), [row]); + const overviewStatus = row.error ? "ERROR" : row.passed ? "OK" : "FAIL"; + const overviewStatusClass = row.passed + ? "border-emerald-500/50 bg-emerald-900/50 text-emerald-200" + : "border-red-500/50 bg-red-900/50 text-red-200"; + const gaugePct = ufGaugePercent(row.governing_usage); + + return ( +
+
+
+
Results
+ +
+
+ Case + + {caseLabelForRow(run, row)} + + Model + + {shortName(row.stiffener ?? row.panel_group)} + +
+
+ +
+
+
+
+
+ Governing UF +
+
+ {formatUf(row.governing_usage)} +
+
+ + {overviewStatus} + +
+
+
+
+ + 0.0 + + 1.0 + + 1.2+ +
+
+
+ Check + + {row.governing_check ?? "-"} + + Clause + {row.governing_clause ?? "-"} +
+ {row.error && ( +
+
+ Evaluation error +
+
{row.error}
+
+ )} +
+ +
+
+

Checks

+ + {checks.length} total + +
+ {checks.length === 0 && ( +
+ {row.error + ? "The check could not be evaluated; see the error above." + : "No checks available."} +
+ )} + {checks.map((check) => { + const mergedCheck = mergeCatalogReference(run, check); + const status = checkStatus(check); + const reference = formulaReference(mergedCheck); + const isGoverning = check.id === governingCheckId; + return ( +
+ +
+ + {check.label} + + + {formatUf(check.usage)} + + + + {reference} + +
+
+ +
+ ); + })} +
+ + {!!row.notes?.length && ( +
+
Notes
+
    + {row.notes.map((note, index) => ( +
  • {note}
  • + ))} +
+
+ )} +
+
+ ); +}; + +const CheckDetail: React.FC<{ + check: CapacityCheckResult; + reference: string; +}> = ({ check, reference }) => { + const intermediateEntries = Object.entries(check.intermediates ?? {}).filter( + ([, value]) => value != null, + ); + return ( +
+
+
+ Formula reference +
+
{reference}
+
+
+ + + +
+ {intermediateEntries.length > 0 && ( +
+
+ Intermediate values +
+
+ {intermediateEntries.map(([key, value]) => ( + +
+ {formatIntermediateName(key)} +
+
+ {formatIntermediateValue(value)} +
+
+ ))} +
+
+ )} + {!!check.warnings?.length && ( +
+ {check.warnings.map((warning, index) => ( +
{warning}
+ ))} +
+ )} + {!!check.assumptions?.length && ( +
+
+ Assumptions +
+
    + {check.assumptions.map((assumption, index) => ( +
  • {assumption}
  • + ))} +
+
+ )} + {check.advisory && ( +
+ Advisory check. Review the referenced DNV-RP-C201 clause before + accepting the result. +
+ )} +
+ ); +}; + +const Metric: React.FC<{ + label: string; + value: string; + valueClass?: string; +}> = ({ label, value, valueClass }) => ( +
+
{label}
+
{value}
+
+); + +const StatusPill: React.FC<{ + status: "OK" | "FAIL" | "ADVISORY"; + passed?: boolean; +}> = ({ status, passed = status !== "FAIL" }) => { + const klass = + status === "FAIL" + ? "border-red-500/50 bg-red-900/50 text-red-200" + : status === "ADVISORY" + ? passed + ? "border-emerald-500/50 bg-emerald-900/50 text-emerald-200" + : "border-red-500/50 bg-red-900/50 text-red-200" + : "border-emerald-500/50 bg-emerald-900/50 text-emerald-200"; + return ( + + {status} + + ); +}; + +function mergeCatalogReference( + run: CapacityRunLike, + check: CapacityCheckResult, +): CapacityCheckResult { + const catalog = run.check_catalog?.find((entry) => entry.id === check.id); + if (!catalog) return check; + return { + ...check, + clause: check.clause ?? catalog.clause, + equations: check.equations?.length ? check.equations : catalog.equations, + }; +} + +function pickGoverningCheckId(row: CapacityCaseResultLike): string | null { + const byName = row.checks.find( + (check) => + check.id === row.governing_check || check.label === row.governing_check, + ); + if (byName) return byName.id; + return ( + row.checks + .slice() + .sort((a, b) => (b.usage ?? -Infinity) - (a.usage ?? -Infinity))[0]?.id ?? + null + ); +} + +function checkStatus(check: CapacityCheckResult): "OK" | "FAIL" | "ADVISORY" { + if (check.advisory) return "ADVISORY"; + if (!check.passed) return "FAIL"; + return "OK"; +} + +function ufGaugePercent(value: number | null | undefined): number { + if (value == null || !isFinite(value)) return 0; + return Math.max(0, Math.min(100, (value / 1.2) * 100)); +} + +function formatEngineeringValue( + value: number | null | undefined, + unit?: string, +): string { + if (value == null || !isFinite(value)) return "-"; + const suffix = unit ? ` ${unit}` : ""; + return `${formatNumber(value)}${suffix}`; +} + +function formatIntermediateValue(value: number | string | null): string { + if (value == null) return "-"; + if (typeof value === "number") return formatNumber(value); + return value; +} + +function formatNumber(value: number): string { + const abs = Math.abs(value); + if (abs >= 1000 || (abs > 0 && abs < 0.001)) return value.toExponential(3); + if (abs >= 100) return value.toFixed(1); + if (abs >= 1 || abs === 0) return value.toFixed(3); + return value.toPrecision(3); +} + +function formatIntermediateName(key: string): string { + return key + .replace(/_/g, " ") + .replace(/\b\w/g, (letter) => letter.toUpperCase()); +} + +export default CapacityResultsPanel; diff --git a/src/frontend/src/components/capacity/capacityFormat.ts b/src/frontend/src/components/capacity/capacityFormat.ts new file mode 100644 index 000000000..7ceab0650 --- /dev/null +++ b/src/frontend/src/components/capacity/capacityFormat.ts @@ -0,0 +1,86 @@ +import type { + CapacityCaseResult, + CapacityCheckResult, + CapacityRun, + CapacityVisualField, +} from "@/state/capacityResultsStore"; + +export const CAPACITY_FLOATING_PANEL_RIGHT_PX = 16; +export const CAPACITY_FLOATING_PANEL_GAP_PX = 16; +export const CAPACITY_RESULTS_PANEL_WIDTH_PX = 384; +export const CAPACITY_INPUT_RIGHT_WITH_RESULTS_PX = + CAPACITY_FLOATING_PANEL_RIGHT_PX + + CAPACITY_FLOATING_PANEL_GAP_PX + + CAPACITY_RESULTS_PANEL_WIDTH_PX; + +export type CapacityRunLike = CapacityRun; +export type CapacityCaseResultLike = CapacityCaseResult; +export type CapacityVisualFieldLike = CapacityVisualField; + +export function modeButton(active: boolean): string { + return ( + "px-2 py-1 rounded-sm border " + + (active + ? "bg-blue-600 border-blue-500 text-white" + : "bg-gray-800 border-gray-600 text-gray-300 hover:bg-gray-700") + ); +} + +export function formatUf(value: number | null | undefined): string { + if (value == null || !isFinite(value)) return "-"; + return value.toFixed(3); +} + +export function ufClass(value: number | null | undefined): string { + return "px-2 py-1 font-mono " + ufTextClass(value); +} + +// Genie "UfTot" discrete bands (thresholds 0.2/0.4/0.6/0.8/1.0), mapped to +// readable text colours that track the genie_uf_color_scheme palette +// (light-blue / cyan / green / yellow / orange / red). No fading between bands. +export function ufTextClass(value: number | null | undefined): string { + if (value == null || !isFinite(value)) return "text-gray-400"; + if (value >= 1.0) return "text-red-500"; + if (value >= 0.8) return "text-orange-400"; + if (value >= 0.6) return "text-yellow-300"; + if (value >= 0.4) return "text-green-400"; + if (value >= 0.2) return "text-cyan-300"; + return "text-sky-300"; +} + +export function shortName(name: string): string { + return name + .replace(/^panelGroup\(/, "") + .replace(/\)$/, "") + .replace(/^Stiffener_/, ""); +} + +export function caseResultKey(row: CapacityCaseResultLike): string { + return ( + row.id ?? + `${row.case_id}::${row.capacity_model_id}::${row.stiffener ?? row.panel_group}` + ); +} + +/** Human-readable case label for a result row. Worst-view rows carry the case + * they came from in ``worstCaseLabel``; otherwise resolve via the run's + * ``result_cases`` (falling back to the row's own label / id). */ +export function caseLabelForRow( + run: CapacityRunLike, + row: CapacityCaseResultLike, +): string { + const worstLabel = (row as { worstCaseLabel?: string }).worstCaseLabel; + if (worstLabel) return worstLabel; + const match = run.result_cases.find((rc) => rc.id === row.case_id); + return match?.label ?? row.case_label ?? `Case ${row.case_id}`; +} + +export function formulaReference( + check: Pick, +): string { + const clause = check.clause ? `DNV-RP-C201 ${check.clause}` : "DNV-RP-C201"; + const equations = check.equations?.length + ? ` ${check.equations.join(", ")}` + : ""; + return `${clause}${equations}`; +} diff --git a/src/frontend/src/components/simulation/SimulationControls.tsx b/src/frontend/src/components/simulation/SimulationControls.tsx index 50e88e342..2d7c951db 100644 --- a/src/frontend/src/components/simulation/SimulationControls.tsx +++ b/src/frontend/src/components/simulation/SimulationControls.tsx @@ -29,6 +29,7 @@ import PlayPauseIcon from "../icons/PlayPauseIcon"; import StopIcon from "../icons/StopIcon"; import SimulationDataInfoPanel from "./SimulationDataInfoPanel"; import FEMDataPanelIcon from "../icons/FEMDataPanelIcon"; +import CapacityControls from "../capacity/CapacityControls"; const SimulationControls = () => { const sessionActive = useFeaAnimationStore((s) => s.sessionActive); @@ -58,6 +59,7 @@ const SimulationControls = () => {
)} + {sessionActive && } ); }; diff --git a/src/frontend/src/hooks/useUrlParamLoad.ts b/src/frontend/src/hooks/useUrlParamLoad.ts index 650d71da7..d6585f742 100644 --- a/src/frontend/src/hooks/useUrlParamLoad.ts +++ b/src/frontend/src/hooks/useUrlParamLoad.ts @@ -2,6 +2,7 @@ import {useEffect, useRef} from "react"; import {scopeUrlPart, useScopeStore} from "@/state/scopeStore"; import {sceneRef} from "@/state/refs"; import {runtime} from "@/runtime/config"; +import {isStreamingFEAResult} from "@/utils/scene/fileKinds"; // Consume ``?scope=...&file=...`` query params on viewer mount. // @@ -10,7 +11,7 @@ import {runtime} from "@/runtime/config"; // this hook waits for both the scope list (populated by AuthGate's // /api/me call) and the three.js scene (mounted by ThreeCanvas) to // be ready, then switches scope if requested and dispatches the -// regular `overlay_file_in_scene` load. +// regular scene-load path. // // Params consumed: // ?file — source key under the active scope (e.g. ``cantilever.step``) @@ -79,14 +80,22 @@ export function useUrlParamLoad(): void { await new Promise((r) => setTimeout(r, SCENE_WAIT_INTERVAL_MS)); } try { - const {overlay_file_in_scene} = await import( - "@/utils/scene/handlers/overlay_file_in_scene" - ); + if (!derivedParam && isStreamingFEAResult(fileParam)) { + const {load_fea_with_defaults} = await import( + "@/utils/scene/handlers/load_fea_streaming" + ); + await load_fea_with_defaults(fileParam); + return; + } + // ``derivedParam`` (e.g. ``_derived/foo.step.glb``) is // the explicit derived-blob hand-off from /convert's // "View in 3D" button. Pass it through so the loader // can fetch the blob directly without a second // ``/convert`` round-trip to re-resolve the path. + const {overlay_file_in_scene} = await import( + "@/utils/scene/handlers/overlay_file_in_scene" + ); await overlay_file_in_scene(fileParam, derivedParam ?? undefined); } catch (err) { // eslint-disable-next-line no-console diff --git a/src/frontend/src/index.html b/src/frontend/src/index.html index 2e7097401..b03808973 100644 --- a/src/frontend/src/index.html +++ b/src/frontend/src/index.html @@ -3,6 +3,7 @@ + ADA-PY Viewer diff --git a/src/frontend/src/services/capacityResults.ts b/src/frontend/src/services/capacityResults.ts new file mode 100644 index 000000000..bb7d893d5 --- /dev/null +++ b/src/frontend/src/services/capacityResults.ts @@ -0,0 +1,72 @@ +import type {CapacityResults} from "@/state/capacityResultsStore"; +import type {FeaManifest} from "@/services/viewerApi"; +import {CAPACITY_RESULTS_VERSION} from "@/services/capacityResultsVersion"; + +export {CAPACITY_RESULTS_VERSION} from "@/services/capacityResultsVersion"; + +export const CAPACITY_RESULTS_FORMAT = "dnv-rp-c201-capacity-results"; +// v2: stations. v3: [6.4.3] discretization. v4/v5: per-stiffener tributary plates. +// v6: scalable bundle — the spine keeps only models/catalog/result cases/field +// metadata; per-element UF values move to AFEL colour blobs (Range-fetched one +// (field, case) step at a time) and per-(case, model, stiffener) rows move to +// per-case detail files (capacity.detail/.json, fetched on demand). +// v7: add the worst-over-cases summary (capacity.summary.json) + trim detail +// resolved_vectors. +// v8: per-case detail rows carry check_inputs — the exact geometry/material the +// check consumed (mm/MPa), used by the input panel + Export so an exported case +// reproduces the engine (esp. the stiffener span feeding sigma_y,R, eq. (4.6)). +// v9: beam-column interaction UFs floored at 0 (no negative usage factors). +// v10: capacity-model topology fix — thickness-gated panel merge, stiffener axial +// force integrated over the panel's own tributary plate, models keyed by +// (capacity_model_id, stiffener). Corrects over-reported axial forces/UFs. +// v11: the bundle gains a second run — the DNV-RP-C201 Section-7 girder check +// (scope "girder", capacity models of type "girder", g-prefixed case ids so the +// per-case detail cache never collides with the stiffened-panel run). +// v12: errored checks are first-class failed rows in per-case detail and compact +// worst summaries; ERROR takes precedence over every numeric UF. +// v13: provenance is indexed by row in one lazy file per case; detail rows carry +// provenance_url + provenance_key instead of pointing at one file per row. +// Kept in lockstep with viewer_export.py. +export interface CapacityValidationContext { + manifest?: Pick | null; +} + +export function validateCapacityResults( + results: CapacityResults, + context: CapacityValidationContext = {}, +): void { + if (!results || typeof results !== "object") { + throw new Error("capacity results must be an object"); + } + if (results.format !== CAPACITY_RESULTS_FORMAT) { + throw new Error(`unsupported capacity results format ${String(results.format)}`); + } + if (results.version !== CAPACITY_RESULTS_VERSION) { + throw new Error(`unsupported capacity results version ${String(results.version)}`); + } + if (!Array.isArray(results.runs) || results.runs.length === 0) { + throw new Error("capacity results contain no runs"); + } + + const manifestHash = normalizeSha256(context.manifest?.source_sha256); + const sidecarHash = normalizeSha256(readSidecarSourceHash(results)); + if (manifestHash && sidecarHash && manifestHash !== sidecarHash) { + throw new Error( + "capacity results are stale for this source: " + + `manifest source_sha256=${manifestHash}, sidecar source.sin_sha256=${sidecarHash}`, + ); + } +} + +function readSidecarSourceHash(results: CapacityResults): string | null { + const source = results.source; + if (!source || typeof source !== "object") return null; + const value = source.sin_sha256; + return typeof value === "string" ? value : null; +} + +function normalizeSha256(value: string | null | undefined): string | null { + const clean = value?.trim().toLowerCase() ?? ""; + if (!clean) return null; + return clean; +} diff --git a/src/frontend/src/services/capacityResultsVersion.ts b/src/frontend/src/services/capacityResultsVersion.ts new file mode 100644 index 000000000..d26d61dec --- /dev/null +++ b/src/frontend/src/services/capacityResultsVersion.ts @@ -0,0 +1,2 @@ +/** Capacity sidecar schema accepted by this frontend build. */ +export const CAPACITY_RESULTS_VERSION = 13; diff --git a/src/frontend/src/services/codecheckCase.ts b/src/frontend/src/services/codecheckCase.ts new file mode 100644 index 000000000..35a5cd9b4 --- /dev/null +++ b/src/frontend/src/services/codecheckCase.ts @@ -0,0 +1,21 @@ +export interface CodecheckCaseInput { + name: string; + check_id: "fe_stiffened" | "fe_girder"; + capacity_model_id: string; + case_id: string; + values: Record; +} + +export function buildCodecheckCasePayload(input: CodecheckCaseInput) { + return { + schema: "codecheck/case@1" as const, + standard_id: "dnv-rp-c201" as const, + name: input.name, + check_id: input.check_id, + source: "adapy-capacity-viewer" as const, + capacity_model_id: input.capacity_model_id, + case_id: input.case_id, + values: input.values, + saved_at: new Date().toISOString(), + }; +} diff --git a/src/frontend/src/services/viewerApi.ts b/src/frontend/src/services/viewerApi.ts index c3b97aeef..303a2a379 100644 --- a/src/frontend/src/services/viewerApi.ts +++ b/src/frontend/src/services/viewerApi.ts @@ -309,6 +309,9 @@ export interface FeaManifestField { export interface FeaManifest { version: number; src: string; + /** Optional SHA-256 of the original FEA source. Used to reject stale + * capacity-result sidecars when both sides provide the hash. */ + source_sha256?: string | null; mesh: { url: string; n_points: number; @@ -382,9 +385,34 @@ export interface FeaManifest { * so the frontend feeds these into useSceneInfoStore for the Scene > FEM groups picker. * Members are tagged EL{id} / P{id} to resolve against the AFEM element ranges. */ groups?: {name: string; members: string[]; fe_object_type?: "node" | "element"}[]; + /** Optional DNV/code-check capacity result sidecar. The sidecar is loaded + * lazily by the FEA streaming path after geometry is visible. */ + capacity?: CapacityManifest; legacy_glb?: {url_template: string}; } +export interface CapacityManifestField { + name: string; + label: string; + support: "element" | "node" | string; + range?: [number, number]; + blob_url?: string; +} + +export interface CapacityManifestOverlay { + name: string; + url: string; +} + +export interface CapacityManifest { + version: number; + results_url: string; + default_run_id?: string; + field_strategy?: string; + fields?: CapacityManifestField[]; + overlays?: CapacityManifestOverlay[]; +} + export interface FeaManifestLineage { /** ``ada.Assembly.guid`` of the source. Matches the * ``assembly_guid`` written into a CAD GLB's ``ADA_EXT_data`` diff --git a/src/frontend/src/state/capacityResultsStore.ts b/src/frontend/src/state/capacityResultsStore.ts new file mode 100644 index 000000000..edc6221fd --- /dev/null +++ b/src/frontend/src/state/capacityResultsStore.ts @@ -0,0 +1,461 @@ +import { create } from "zustand"; + +import type { CapacityManifest } from "@/services/viewerApi"; + +export interface CapacitySource { + sourceName: string; + resultsUrl: string; +} + +export interface CapacityCheckResult { + id: string; + label: string; + clause?: string; + equations?: string[]; + usage: number | null; + passed: boolean; + advisory?: boolean; + status?: "OK" | "FAIL" | "ADVISORY" | string; + demand?: number | null; + resistance?: number | null; + unit?: string; + intermediates?: Record; + warnings?: string[]; + assumptions?: string[]; +} + +export interface CapacityCheckCatalogEntry { + id: string; + label?: string; + clause?: string; + equations?: string[]; + advisory?: boolean; +} + +export interface CapacityCaseResult { + id?: string; + case_id: string; + case_label?: string; + capacity_model_id: string; + panel_group: string; + stiffener?: string; + governing_usage: number | null; + passed: boolean; + governing_check?: string | null; + governing_clause?: string | null; + /** Exception text when the check could not produce an engineering result. */ + error?: string | null; + checks: CapacityCheckResult[]; + loads?: Record; + resolved_variables?: Record; + resolved_vectors?: Record; + resolved_provenance?: Record; + provenance_url?: string; + /** v13 key selecting this row inside an aggregated per-case provenance file. */ + provenance_key?: string; + /** v8: the exact geometry/material/options the check consumed (mm/MPa). The + * input panel and Export prefer this over the display capacity_model dict so + * an exported case reproduces the engine — notably the stiffener span that + * drives the transverse plate resistance sigma_y,R (eq. 4.6). */ + check_inputs?: CapacityCheckInputs; + /** v8: the Section-6 design values the engine derives from the three-position + * loads via DNV-RP-C201 eqs (6.16/6.17/6.2). Shown in "Resolved design" so + * the figures are standard-correct (not the Genie reference convention). */ + resolved_design?: CapacityResolvedDesign; + notes?: string[]; +} + +/** v8 resolved_design payload (see viewer_export._resolved_design_payload). */ +export interface CapacityResolvedDesign { + sigma_y_Sd_mpa?: number | null; + tau_Sd_mpa?: number | null; + N_Sd_kn?: number | null; +} + +/** v8 check_inputs payload (see capacity_check._build_check_inputs). */ +export interface CapacityCheckInputs { + span_mm?: number; + plate?: { s_mm?: number; t_mm?: number }; + stiffener?: { + hw_mm?: number; + tw_mm?: number; + bf_mm?: number; + tf_mm?: number; + type?: string; + }; + material?: { fy_mpa?: number; E_mpa?: number; gamma_m?: number }; + continuous?: boolean; + welded?: boolean; + model_type?: string; + optimize_z_star?: boolean; +} + +export interface CapacityModel { + id: string; + panel_group: string; + stiffener?: string; + type: string; + element_ids: { + plates?: number[]; + stiffeners?: number[]; + all?: number[]; + }; + plates?: Array>; + stiffeners?: Array>; + geometry?: Record; +} + +export interface CapacityVisualFieldValue { + element_id: number; + value: number | null; + capacity_model_id?: string; +} + +export interface CapacityVisualFieldCase { + case_id: string; + values: CapacityVisualFieldValue[]; +} + +export interface CapacityVisualField { + id: string; + label: string; + check_id?: string | null; + clause?: string | null; + equations?: string[]; + support: string; + kind: string; + storage: "json" | "afel" | string; + range?: [number, number]; + /** Legacy ``storage: "json"`` only — inline per-(case, element) values. v6 + * ``storage: "afel"`` fields omit this and carry the blob pointer below. */ + cases?: CapacityVisualFieldCase[]; + /** v6 AFEL colour blob (``storage: "afel"``). The element axis and case-step + * axis are shared across all fields and live on the run + * (``element_axis`` / ``field_case_steps``). One blob holds every case + * (step); the viewer Range-fetches a single (field, case) step. */ + blob_url?: string; + header_bytes?: number; + stride_bytes?: number; + dtype?: string; + byte_order?: "little" | "big" | string; +} + +export interface CapacityCaseDetailPointer { + strategy: string; + /** Filename template with a ``{case}`` placeholder, manifest-relative. */ + url_template: string; +} + +/** Synthetic "case" id for the worst-over-selected-cases view. */ +export const WORST_CASE_ID = "__worst__"; + +/** UI selection stashed per run so switching Run (stiffened panel <-> girder) + * and back restores what was open and selected. */ +interface RunUiMemory { + activeCaseId: string | null; + activeMetricId: string; + selectedModelId: string | null; + selectedResultId: string | null; + worstCaseIds: string[]; + worstSummary: CapacityWorstSummary | null; +} + +/** One compact row in the worst-over-cases summary (no heavy check detail). */ +export interface CapacityWorstRow { + /** Unique per (case, model, stiffener). */ + k: string; + /** capacity_model_id. */ + m: string; + /** stiffener name. */ + s?: string | null; + /** panel_group. */ + pg: string; + /** governing usage factor. */ + u: number | null; + /** passed. */ + p: boolean; + /** governing check id. */ + c?: string | null; + /** governing clause. */ + cl?: string | null; + /** Exception text; when present this row outranks every numeric UF. */ + e?: string | null; +} + +export interface CapacityWorstSummary { + format: string; + version: number; + cases: Record; +} + +/** One check that raised. It is also represented as a failed case-result row; + * this compact spine entry drives the banner and red 3D definition edges. */ +export interface CapacityRunError { + id?: string; + case_id: string; + capacity_model_id: string; + panel_group: string; + stiffener?: string; + /** Exception text, usually prefixed with the offending clause, e.g. + * "[6.21] Square root of negative value ...". */ + message: string; +} + +export interface CapacityRun { + id: string; + label?: string; + standard?: string; + scope?: string; + group?: string | null; + result_cases: Array<{ id: string; label?: string }>; + capacity_models: CapacityModel[]; + check_catalog?: CapacityCheckCatalogEntry[]; + /** v6: empty in the spine; per-case rows are lazy-loaded into the store's + * ``caseDetail`` map. Legacy sidecars (<=v5) inline the full array here. */ + case_results: CapacityCaseResult[]; + /** v7 (additive): checks that raised and were skipped. Absent/empty on a clean + * run and on older sidecars. */ + errors?: CapacityRunError[]; + visual_fields: CapacityVisualField[]; + /** v6: shared element axis for the AFEL colour blobs (payload-row order). */ + element_axis?: number[]; + /** v6: shared case-step axis for the AFEL colour blobs (step order). */ + field_case_steps?: string[]; + /** v6: pointer to the per-case detail files. */ + case_detail?: CapacityCaseDetailPointer; + /** v6: manifest-relative URL of the compact worst-over-cases summary. */ + worst_summary_url?: string; +} + +export interface CapacityResults { + format: string; + version: number; + source?: Record; + runs: CapacityRun[]; +} + +export interface CapacityResultsState { + manifest: CapacityManifest | null; + source: CapacitySource | null; + results: CapacityResults | null; + activeRunId: string | null; + activeCaseId: string | null; + activeMode: "definition" | "results"; + showDefinitions: boolean; + showResults: boolean; + isolateDefinitions: boolean; + /** With "Only definitions" (isolateDefinitions) on, optionally still draw the + * rest of the model as a wireframe for context. Default on — the capacity + * models stand out while the rest stays visible as a faint wireframe. */ + showRestWireframe: boolean; + activeMetricId: string; + selectedModelId: string | null; + selectedResultId: string | null; + failedOnly: boolean; + loading: boolean; + error: string | null; + /** v6 lazy per-case detail: ``case_id -> rows``, fetched on demand when a + * case becomes active. Legacy (<=v5) sidecars never populate this; controls + * fall back to the inline ``run.case_results``. */ + caseDetail: Record; + caseDetailLoading: Record; + /** Worst-over-cases: which result cases are included (default all). */ + worstCaseIds: string[]; + worstSummary: CapacityWorstSummary | null; + worstSummaryLoading: boolean; + /** Selection state remembered per run id (restored on run switch). */ + runUiMemory: Record; + setLoading: (loading: boolean) => void; + setError: (error: string | null) => void; + setCapacityData: ( + manifest: CapacityManifest, + source: CapacitySource, + results: CapacityResults, + ) => void; + setCaseDetail: (caseId: string, rows: CapacityCaseResult[]) => void; + setCaseDetailLoading: (caseId: string, loading: boolean) => void; + setWorstCaseIds: (caseIds: string[]) => void; + toggleWorstCase: (caseId: string) => void; + setWorstSummary: (summary: CapacityWorstSummary | null) => void; + setWorstSummaryLoading: (loading: boolean) => void; + clear: () => void; + setActiveRunId: (runId: string | null) => void; + setActiveCaseId: (caseId: string | null) => void; + setActiveMode: (mode: "definition" | "results") => void; + setShowDefinitions: (showDefinitions: boolean) => void; + setShowResults: (showResults: boolean) => void; + setIsolateDefinitions: (isolateDefinitions: boolean) => void; + setShowRestWireframe: (showRestWireframe: boolean) => void; + setActiveMetricId: (metricId: string) => void; + setSelectedModelId: (modelId: string | null) => void; + setSelectedCapacityResult: ( + modelId: string | null, + resultId: string | null, + ) => void; + setFailedOnly: (failedOnly: boolean) => void; +} + +const DEFAULT_METRIC = "capacity.uf.governing"; + +export const useCapacityResultsStore = create((set) => ({ + manifest: null, + source: null, + results: null, + activeRunId: null, + activeCaseId: null, + activeMode: "results", + showDefinitions: true, + showResults: true, + isolateDefinitions: true, + showRestWireframe: true, + activeMetricId: DEFAULT_METRIC, + selectedModelId: null, + selectedResultId: null, + failedOnly: false, + loading: false, + error: null, + caseDetail: {}, + caseDetailLoading: {}, + worstCaseIds: [], + worstSummary: null, + worstSummaryLoading: false, + runUiMemory: {}, + setLoading: (loading) => set({ loading }), + setError: (error) => set({ error, loading: false }), + setCapacityData: (manifest, source, results) => { + const run = pickRun(results, manifest.default_run_id); + const caseId = + run?.result_cases?.[0]?.id ?? run?.case_results?.[0]?.case_id ?? null; + set({ + manifest, + source, + results, + activeRunId: run?.id ?? null, + activeCaseId: caseId, + showDefinitions: true, + showResults: true, + isolateDefinitions: true, + showRestWireframe: true, + activeMetricId: DEFAULT_METRIC, + selectedModelId: null, + selectedResultId: null, + caseDetail: {}, + caseDetailLoading: {}, + worstCaseIds: run?.result_cases?.map((c) => c.id) ?? [], + worstSummary: null, + worstSummaryLoading: false, + runUiMemory: {}, + loading: false, + error: null, + }); + }, + setCaseDetail: (caseId, rows) => + set((state) => ({ + caseDetail: { ...state.caseDetail, [caseId]: rows }, + caseDetailLoading: { ...state.caseDetailLoading, [caseId]: false }, + })), + setCaseDetailLoading: (caseId, loading) => + set((state) => ({ + caseDetailLoading: { ...state.caseDetailLoading, [caseId]: loading }, + })), + setWorstCaseIds: (worstCaseIds) => set({ worstCaseIds }), + toggleWorstCase: (caseId) => + set((state) => { + const has = state.worstCaseIds.includes(caseId); + return { + worstCaseIds: has + ? state.worstCaseIds.filter((id) => id !== caseId) + : [...state.worstCaseIds, caseId], + }; + }), + setWorstSummary: (worstSummary) => set({ worstSummary, worstSummaryLoading: false }), + setWorstSummaryLoading: (worstSummaryLoading) => set({ worstSummaryLoading }), + clear: () => + set({ + manifest: null, + source: null, + results: null, + activeRunId: null, + activeCaseId: null, + activeMode: "results", + showDefinitions: true, + showResults: true, + isolateDefinitions: true, + showRestWireframe: true, + activeMetricId: DEFAULT_METRIC, + selectedModelId: null, + selectedResultId: null, + failedOnly: false, + caseDetail: {}, + caseDetailLoading: {}, + worstCaseIds: [], + worstSummary: null, + worstSummaryLoading: false, + runUiMemory: {}, + loading: false, + error: null, + }), + // Runs carry disjoint case ids (the girder run g-prefixes them) and their own + // worst summary, so switching runs swaps the active case, selection, metric + // and worst-case subset. The outgoing run's state is stashed in runUiMemory + // and restored when the user switches back (panel <-> girder round trips keep + // what was open and selected); a first visit starts from the run's defaults. + setActiveRunId: (activeRunId) => + set((state) => { + const run = + state.results?.runs.find((r) => r.id === activeRunId) ?? + state.results?.runs[0] ?? + null; + const runUiMemory = { ...state.runUiMemory }; + if (state.activeRunId) { + runUiMemory[state.activeRunId] = { + activeCaseId: state.activeCaseId, + activeMetricId: state.activeMetricId, + selectedModelId: state.selectedModelId, + selectedResultId: state.selectedResultId, + worstCaseIds: state.worstCaseIds, + worstSummary: state.worstSummary, + }; + } + const saved = activeRunId ? runUiMemory[activeRunId] : undefined; + return { + activeRunId, + runUiMemory, + selectedModelId: saved?.selectedModelId ?? null, + selectedResultId: saved?.selectedResultId ?? null, + activeCaseId: saved?.activeCaseId ?? run?.result_cases?.[0]?.id ?? null, + // Metric ids are run-specific (panel vs girder checks); fall back to + // the governing UF, which every run carries. + activeMetricId: saved?.activeMetricId ?? DEFAULT_METRIC, + worstCaseIds: + saved?.worstCaseIds ?? run?.result_cases?.map((c) => c.id) ?? [], + worstSummary: saved?.worstSummary ?? null, + worstSummaryLoading: false, + }; + }), + setActiveCaseId: (activeCaseId) => + set({ activeCaseId, selectedModelId: null, selectedResultId: null }), + setActiveMode: (activeMode) => set({ activeMode }), + setShowDefinitions: (showDefinitions) => set({ showDefinitions }), + setShowResults: (showResults) => set({ showResults }), + setIsolateDefinitions: (isolateDefinitions) => set({ isolateDefinitions }), + setShowRestWireframe: (showRestWireframe) => set({ showRestWireframe }), + setActiveMetricId: (activeMetricId) => set({ activeMetricId }), + setSelectedModelId: (selectedModelId) => + set({ selectedModelId, selectedResultId: null }), + setSelectedCapacityResult: (selectedModelId, selectedResultId) => + set({ selectedModelId, selectedResultId }), + setFailedOnly: (failedOnly) => set({ failedOnly }), +})); + +function pickRun( + results: CapacityResults, + preferredId?: string, +): CapacityRun | null { + if (!results.runs?.length) return null; + if (preferredId) { + const preferred = results.runs.find((run) => run.id === preferredId); + if (preferred) return preferred; + } + return results.runs[0] ?? null; +} diff --git a/src/frontend/src/utils/scene/handlers/load_fea_streaming.ts b/src/frontend/src/utils/scene/handlers/load_fea_streaming.ts index b2a13317c..dc75cfa0f 100644 --- a/src/frontend/src/utils/scene/handlers/load_fea_streaming.ts +++ b/src/frontend/src/utils/scene/handlers/load_fea_streaming.ts @@ -1,4 +1,7 @@ import * as THREE from "three"; +import {LineMaterial} from "three/examples/jsm/lines/LineMaterial"; +import {LineSegments2} from "three/examples/jsm/lines/LineSegments2"; +import {LineSegmentsGeometry} from "three/examples/jsm/lines/LineSegmentsGeometry"; import {SceneOperations} from "@/flatbuffers/scene/scene-operations"; import {runtime} from "@/runtime/config"; @@ -7,19 +10,30 @@ import {GLTFLoader} from "three/examples/jsm/loaders/GLTFLoader"; import {cacheAndBuildTree} from "@/state/model_worker/cacheModelUtils"; import {fetchElemFieldStep} from "@/services/feaElemFieldBlob"; import {fetchFieldStep, makeViewerApiFetcher} from "@/services/feaFieldBlob"; +import {validateCapacityResults} from "@/services/capacityResults"; import type {FeaFetcher, FeaRangeFetcher} from "@/services/fea/feaFetcher"; import {fetchBeamSolidsWarp, ParsedBeamSolidsWarp} from "@/services/feaBeamSolidsWarp"; import {fetchMeshEdges} from "@/services/feaMeshEdges"; import {fetchMeshElements, MeshElementEntry} from "@/services/feaMeshElements"; import {convert_to_custom_batch_mesh} from "@/utils/scene/convert_to_custom_batch_mesh"; -import {FeaManifest, FeaManifestField, viewerApi} from "@/services/viewerApi"; +import {CustomBatchedMesh} from "@/utils/mesh_select/CustomBatchedMesh"; +import { + CapacityManifest, + FeaManifest, + FeaManifestField, + FeaManifestFieldPerType, + ScopeUrl, + viewerApi, +} from "@/services/viewerApi"; import {sceneRef} from "@/state/refs"; import {scopeUrlPart, useScopeStore} from "@/state/scopeStore"; import {useModelState} from "@/state/modelState"; import {useAnimationStore} from "@/state/animationStore"; import {useFeaAnimationStore} from "@/state/feaAnimationStore"; +import {useCapacityResultsStore, WORST_CASE_ID} from "@/state/capacityResultsStore"; +import type {CapacityResults, CapacityWorstSummary} from "@/state/capacityResultsStore"; import {useConversionStore} from "@/state/conversionStore"; -import {usePerfStore} from "@/state/perfStore"; +import {usePerfStore, requestRender} from "@/state/perfStore"; import {applyFieldToMesh} from "../fea/applyField"; import {applyElemFieldToMesh} from "../fea/applyElemField"; import {resetFeaAnimationPhase} from "../fea/feaAnimationDriver"; @@ -62,8 +76,49 @@ interface ActiveFeaStreaming { * attributes are linked to the beam-solid mesh after the first * apply seeds the morph attribute. */ beamSolidEdges?: THREE.LineSegments; + /** Whole-model element-edge wireframe (AFEG). Retained so the capacity + * "Only definitions" view can hide it (and the "Show rest as wireframe" + * toggle can bring it back) without disturbing the face meshes. */ + feaEdges?: THREE.LineSegments; + /** Capacity-model boundary overlay built from AFEM element draw ranges. */ + capacityBoundaryOverlay?: THREE.LineSegments; + /** Thick dashed polyline marking the girder itself (girder-run models), + * drawn through each girder model's station points. */ + capacityGirderLineOverlay?: LineSegments2; + /** Dashed near-black polylines marking the stiffeners of the panel + * capacity models (station-point lines). */ + capacityStiffenerLineOverlay?: THREE.LineSegments; + /** Red boundary overlay for the selected capacity model. */ + capacitySelectedBoundaryOverlay?: THREE.LineSegments; + /** Amber boundary overlay for the individually selected stiffener strip + * within the selected capacity model. */ + capacitySelectedStripOverlay?: THREE.LineSegments; + /** Non-pickable non-indexed overlay used for hard-edged capacity colors. */ + capacityColorOverlay?: THREE.Mesh; + /** Capacity color overlay for optional beam-solid geometry. */ + beamSolidCapacityColorOverlay?: THREE.Mesh; + /** Pre-isolation hidden-range snapshot per mesh, so "show only + * definitions" can restore exactly what was visible when it was + * switched on. Present only while isolation is active. */ + capacityIsolationSaved?: {main?: Set; beam?: Set}; + /** Numbered marker sprites at the 3 Section-5 stations of the selected + * stiffener (positions 1/2/3 of the resolved design stresses). */ + capacityStationsGroup?: THREE.Group; + /** Fetchers retained so the capacity overlay can Range-fetch AFEL colour + * blobs and lazy-load per-case detail after the initial load, using the + * same manifest-relative resolver as the FEA artefacts (capacity files are + * co-located in the artefact dir). */ + capacityFetch?: { + fetcher: FeaFetcher; + rangeFetcher: FeaRangeFetcher; + cacheKey: string; + }; } +// Per-position marker colours (positions 1/2/3). Keep in sync with the capacity +// side panel (CapacityControls STATION_COLORS) so the dots match the markers. +const CAPACITY_STATION_COLORS = ["#38bdf8", "#fbbf24", "#fb7185"]; + let active: ActiveFeaStreaming | null = null; /** Drop the cached state on next call (e.g. when the user replaces @@ -94,7 +149,9 @@ export function setBeamSolidsVisible(visible: boolean): void { export function clearActiveFeaStreaming(): void { active = null; + clearCapacityStepCache(); useFeaAnimationStore.getState().reset(); + useCapacityResultsStore.getState().clear(); resetFeaAnimationPhase(); // Drop any "go to node" marker + active-row state. The marker // mesh would otherwise survive into the next loaded model and @@ -471,6 +528,1187 @@ function snapshotBasePositions(geometry: THREE.BufferGeometry): Float32Array { return new Float32Array(attr.array as Float32Array); } +async function loadCapacityResultsIfPresent( + fetcher: FeaFetcher, + scope: ScopeUrl, + sourceName: string, + manifest: FeaManifest, +): Promise { + const capacity = await resolveCapacityManifest(fetcher, scope, sourceName, manifest); + const store = useCapacityResultsStore.getState(); + if (!capacity?.results_url) { + store.clear(); + return; + } + if ( + store.source?.sourceName === sourceName + && store.source.resultsUrl === capacity.results_url + && store.results + ) { + return; + } + + store.setLoading(true); + try { + const buf = await fetchCapacityBuffer(fetcher, scope, capacity.results_url); + const text = new TextDecoder("utf-8").decode(buf); + const results = JSON.parse(text) as CapacityResults; + validateCapacityResults(results, {manifest}); + useCapacityResultsStore.getState().setCapacityData( + capacity, + {sourceName, resultsUrl: capacity.results_url}, + results, + ); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + useCapacityResultsStore.getState().setError(message); + // eslint-disable-next-line no-console + console.warn("[fea-streaming] failed to load capacity results:", err); + } +} + +async function resolveCapacityManifest( + fetcher: FeaFetcher, + scope: ScopeUrl, + sourceName: string, + manifest: FeaManifest, +): Promise { + if (manifest.capacity?.results_url) return manifest.capacity; + + const sourcePath = sourceName.replace(/^\/+/, ""); + const slash = sourcePath.lastIndexOf("/"); + const dir = slash >= 0 ? sourcePath.slice(0, slash + 1) : ""; + const filename = slash >= 0 ? sourcePath.slice(slash + 1) : sourcePath; + const stem = filename.replace(/\.[^.]+$/, ""); + + for (const candidate of ["capacity.results.json"]) { + try { + await fetcher(candidate); + return {version: 1, results_url: candidate, field_strategy: "json-auto"}; + } catch { + // Missing auto-discovery candidates are expected. + } + } + + for (const candidate of [ + `${dir}${stem}.c201.json`, + `${dir}${stem}.capacity.json`, + `${dir}capacity.results.json`, + ]) { + try { + await viewerApi.getBlob(scope, candidate); + return {version: 1, results_url: `scope://${candidate}`, field_strategy: "json-auto"}; + } catch { + // Missing auto-discovery candidates are expected. + } + } + return null; +} + +function fetchCapacityBuffer( + fetcher: FeaFetcher, + scope: ScopeUrl, + resultsUrl: string, +): Promise { + if (resultsUrl.startsWith("scope://")) { + return viewerApi.getBlob(scope, resultsUrl.slice("scope://".length)); + } + return fetcher(resultsUrl); +} + +export async function applyCapacityVisualField( + metricId?: string, + caseId?: string, +): Promise { + if (!active?.mesh) return false; + const store = useCapacityResultsStore.getState(); + const results = store.results; + if (!results) return false; + const run = results.runs.find((r) => r.id === store.activeRunId) ?? results.runs[0]; + if (!run) return false; + const fieldId = metricId ?? store.activeMetricId; + const activeCaseId = + caseId + ?? store.activeCaseId + ?? run.result_cases?.[0]?.id + ?? run.field_case_steps?.[0] + ?? run.case_results?.[0]?.case_id; + if (!activeCaseId) return false; + const field = run.visual_fields.find((f) => f.id === fieldId); + if (!field) return false; + + let values: Array<{element_id: number; value: number | null}> | null = null; + if (activeCaseId === WORST_CASE_ID) { + // Worst over the user-selected case subset (max UF per element). + if (field.storage === "afel") { + values = await fetchAfelWorstValues(run, field, selectedWorstCaseIds(run)); + } + } else if (field.storage === "afel") { + values = await fetchAfelCapacityValues(run, field, activeCaseId); + } else if (field.storage === "json") { + // Legacy (<=v5) inline values. + values = field.cases?.find((c) => c.case_id === activeCaseId)?.values ?? null; + } + if (!values) return false; + // The store may have moved on while we awaited the Range fetch (user clicked + // a different case/metric). Both call sites paint the *active* selection, so + // drop the paint if it no longer matches what is selected now. + if (!active?.mesh) return false; + const now = useCapacityResultsStore.getState(); + if (now.activeCaseId !== activeCaseId || now.activeMetricId !== fieldId) { + return false; + } + + paintCapacityEntries(active.mesh, values); + if (active.beamSolidMesh) { + paintCapacityEntries(active.beamSolidMesh, values); + } + applyCapacitySelectionHighlight(); + requestRender(); + return true; +} + +// Cache decoded AFEL (field, case) steps so re-selecting a case repaints +// instantly. Keyed by source + blob url + step. Cleared on scene swap via the +// underlying ELEM_*_CACHE in feaElemFieldBlob (whole-blob) plus this map. +const CAPACITY_STEP_CACHE = new Map>(); + +function clearCapacityStepCache(): void { + CAPACITY_STEP_CACHE.clear(); +} + +/** Range-fetch one (field, case) step of an AFEL capacity colour blob and map it + * to ``{element_id, value}`` paint entries via the run's shared element axis. + * Returns ``null`` when the fetch context, axis, or step is unavailable. */ +async function fetchAfelCapacityValues( + run: CapacityResults["runs"][number], + field: CapacityResults["runs"][number]["visual_fields"][number], + caseId: string, +): Promise | null> { + const ctx = active?.capacityFetch; + const labels = run.element_axis; + const steps = run.field_case_steps ?? run.result_cases.map((c) => c.id); + if (!ctx || !labels?.length || !field.blob_url) return null; + const stepIndex = steps.indexOf(caseId); + if (stepIndex < 0) return null; + + const cacheKey = `${active?.sourceName ?? ""}::${field.blob_url}::${stepIndex}`; + const cached = CAPACITY_STEP_CACHE.get(cacheKey); + if (cached) return cached; + + // Reuse the AFEL single-step Range fetch the simulation element-field path + // uses. n_ips = n_components = 1, so a step is one float per element. + const bucket: FeaManifestFieldPerType = { + elem_type: "capacity", + n_elements: labels.length, + n_ips: 1, + ip_layout: [], + element_labels: labels, + blob: { + url: field.blob_url, + header_bytes: field.header_bytes ?? 1024, + stride_bytes: field.stride_bytes ?? labels.length * 4, + dtype: field.dtype ?? "float32", + byte_order: (field.byte_order as "little" | "big") ?? "little", + }, + scalar_range: {}, + }; + let step: Float32Array; + try { + step = await fetchElemFieldStep(ctx.rangeFetcher, ctx.fetcher, bucket, stepIndex, ctx.cacheKey); + } catch (err) { + // eslint-disable-next-line no-console + console.warn(`[capacity] failed to fetch AFEL step for ${field.blob_url}:`, err); + return null; + } + const out: Array<{element_id: number; value: number | null}> = []; + for (let i = 0; i < labels.length; i++) { + const v = step[i]; + if (Number.isFinite(v)) out.push({element_id: labels[i], value: v}); + } + CAPACITY_STEP_CACHE.set(cacheKey, out); + return out; +} + +/** The worst-view case subset, intersected with the run's actual cases. An + * empty selection means "no cases" (the user unchecked them all). */ +function selectedWorstCaseIds(run: CapacityResults["runs"][number]): string[] { + const all = new Set(run.field_case_steps ?? run.result_cases.map((c) => c.id)); + const selected = useCapacityResultsStore.getState().worstCaseIds ?? []; + return selected.filter((id) => all.has(id)); +} + +/** Per-element worst (max) UF over a set of cases for one AFEL field. Fetches + * each case's step (Range, ~34 KB, cached) and reduces element-wise. */ +async function fetchAfelWorstValues( + run: CapacityResults["runs"][number], + field: CapacityResults["runs"][number]["visual_fields"][number], + caseIds: string[], +): Promise | null> { + if (caseIds.length === 0) return []; + const labels = run.element_axis; + if (!labels?.length) return null; + const rowOf = new Map(); + for (let i = 0; i < labels.length; i++) rowOf.set(labels[i], i); + + const best = new Float32Array(labels.length).fill(Number.NEGATIVE_INFINITY); + for (const caseId of caseIds) { + const perCase = await fetchAfelCapacityValues(run, field, caseId); + if (!perCase) continue; + for (const entry of perCase) { + if (entry.value == null) continue; + const row = rowOf.get(entry.element_id); + if (row !== undefined && entry.value > best[row]) best[row] = entry.value; + } + } + const out: Array<{element_id: number; value: number | null}> = []; + for (let i = 0; i < labels.length; i++) { + if (Number.isFinite(best[i])) out.push({element_id: labels[i], value: best[i]}); + } + return out; +} + +/** Lazy-load the compact worst-over-cases summary into the capacity store. */ +export async function loadCapacityWorstSummary(): Promise { + const ctx = active?.capacityFetch; + if (!ctx) return; + const store = useCapacityResultsStore.getState(); + const results = store.results; + if (!results) return; + const run = results.runs.find((r) => r.id === store.activeRunId) ?? results.runs[0]; + const url = run?.worst_summary_url; + if (!url) return; + if (store.worstSummary || store.worstSummaryLoading) return; + + store.setWorstSummaryLoading(true); + try { + const buf = await ctx.fetcher(url); + const summary = JSON.parse(new TextDecoder("utf-8").decode(buf)) as CapacityWorstSummary; + useCapacityResultsStore.getState().setWorstSummary(summary); + } catch (err) { + // eslint-disable-next-line no-console + console.warn("[capacity] failed to load worst summary:", err); + useCapacityResultsStore.getState().setWorstSummary(null); + } +} + +/** Lazy-load one result case's verbose detail rows into the capacity store. + * Idempotent: a no-op when the case is already loaded or in flight. Resolves + * the per-case file via the run's ``case_detail.url_template``. */ +export async function loadCapacityCaseDetail(caseId: string): Promise { + const ctx = active?.capacityFetch; + if (!ctx) return; + const store = useCapacityResultsStore.getState(); + const results = store.results; + if (!results) return; + const run = results.runs.find((r) => r.id === store.activeRunId) ?? results.runs[0]; + const template = run?.case_detail?.url_template; + if (!run || !template) return; + if (store.caseDetail[caseId] || store.caseDetailLoading[caseId]) return; + + store.setCaseDetailLoading(caseId, true); + try { + const url = template.replace("{case}", encodeURIComponent(caseId)); + const buf = await ctx.fetcher(url); + const payload = JSON.parse(new TextDecoder("utf-8").decode(buf)) as { + case_results?: CapacityResults["runs"][number]["case_results"]; + }; + useCapacityResultsStore.getState().setCaseDetail(caseId, payload.case_results ?? []); + } catch (err) { + // eslint-disable-next-line no-console + console.warn(`[capacity] failed to load case detail for ${caseId}:`, err); + useCapacityResultsStore.getState().setCaseDetail(caseId, []); + } +} + +/** Colour elements by explicit per-element UF values (rest neutral) — used for + * the "individual UF" view, where each stiffener's own line + tributary plate + * strip is coloured by that stiffener's UF, revealing the within-panel + * variation instead of the panel-governing maximum. */ +interface CapacityProvenancePayload { + resolved_provenance?: Record; + provenance?: Record; + rows?: Record>; +} + +const capacityProvenanceCache = new Map>(); + +/** Lazy-load one row's input provenance. Newer sidecars keep this out of the + * per-case detail file so selecting a case stays light; old sidecars may still + * carry ``resolved_provenance`` inline and do not call this helper. */ +export async function loadCapacityProvenance( + url: string, + rowKey?: string, +): Promise> { + const ctx = active?.capacityFetch; + if (!ctx || !url) return {}; + const cacheKey = `${ctx.cacheKey}:${url}`; + let pending = capacityProvenanceCache.get(cacheKey); + if (!pending) { + pending = (async () => { + const buf = await ctx.fetcher(url); + return JSON.parse( + new TextDecoder("utf-8").decode(buf), + ) as CapacityProvenancePayload; + })(); + capacityProvenanceCache.set(cacheKey, pending); + } + const payload = await pending; + if (rowKey && payload.rows) return payload.rows[rowKey] ?? {}; + return payload.resolved_provenance ?? payload.provenance ?? {}; +} + +export function applyCapacityIndividualField( + values: Array<{element_id: number; value: number | null}>, +): boolean { + if (!active?.mesh) return false; + paintCapacityEntries(active.mesh, values); + if (active.beamSolidMesh) { + paintCapacityEntries(active.beamSolidMesh, values); + } + applyCapacitySelectionHighlight(); + return true; +} + +export function applyCapacityDefinitionView(): boolean { + if (!active?.mesh) return false; + const store = useCapacityResultsStore.getState(); + const results = store.results; + if (!results) return false; + const run = results.runs.find((r) => r.id === store.activeRunId) ?? results.runs[0]; + if (!run) return false; + + // Girder-run models clutter the scene when every (adjacent) rectangle is + // outlined; draw the boundary only for the selected girder — its + // associated tributary plates — while the girder lines mark every model. + const boundaryModels = run.capacity_models.filter( + (model) => + (model as {type?: string}).type !== "girder" || + model.id === store.selectedModelId, + ); + rebuildCapacityBoundaryOverlay( + boundaryModels.map((model) => ({ + id: model.id, + panel_group: model.panel_group, + element_ids: model.element_ids, + })), + capacityFailedModelIds(run, store), + ); + rebuildCapacityGirderLineOverlay(run.capacity_models, lastGirderUf); + rebuildCapacityStiffenerLineOverlay(run.capacity_models); + applyCapacitySelectionHighlight(); + return true; +} + +/** Distinct colour for the girder line itself in the definitions view. */ +const CAPACITY_GIRDER_LINE_COLOR = 0xf59e0b; // amber +/** Dashed stiffener-line colour in the definitions view. */ +const CAPACITY_STIFFENER_LINE_COLOR = 0x111827; // near-black + +/** Last per-girder UF values applied (kept so the definition rebuild keeps + * the result colours instead of flashing back to amber). */ +let lastGirderUf: Map | null = null; + +/** Colour the girder lines by per-model UF (girder run, results on). Pass + * ``null`` to return to the neutral amber definition colour. */ +export function applyCapacityGirderLineUf(values: Map | null): void { + lastGirderUf = values; + const store = useCapacityResultsStore.getState(); + const run = + store.results?.runs.find((r) => r.id === store.activeRunId) ?? + store.results?.runs[0]; + if (!run) return; + rebuildCapacityGirderLineOverlay(run.capacity_models, lastGirderUf); +} + +/** Girder-run models mark the girder member itself with a thick dashed line + * model's station points (start/mid/end of the bay — the girder line). Amber + * in the definitions view; coloured by the girder's UF when result values are + * supplied. Depth testing keeps hidden girders behind foreground geometry. */ +function rebuildCapacityGirderLineOverlay( + models: Array<{id: string; type?: string; stiffeners?: Array>}>, + ufByModelId?: Map | null, +): void { + disposeCapacityGirderLineOverlay(); + if (!active?.mesh) return; + const positions: number[] = []; + const colors: number[] = []; + const amber = new THREE.Color(CAPACITY_GIRDER_LINE_COLOR); + const band = new Float32Array(3); + for (const model of models) { + if (model.type !== "girder") continue; + const uf = ufByModelId?.get(model.id); + let r = amber.r; + let g = amber.g; + let b = amber.b; + if (uf != null && isFinite(uf)) { + capacityUfColor(uf, band); + [r, g, b] = band; + } + for (const stiff of model.stiffeners ?? []) { + const stations = stiff.stations as number[][] | undefined; + if (!stations || stations.length < 2) continue; + for (let i = 0; i + 1 < stations.length; i++) { + positions.push(...stations[i].slice(0, 3), ...stations[i + 1].slice(0, 3)); + colors.push(r, g, b, r, g, b); + } + } + } + if (positions.length === 0) return; + const geom = new LineSegmentsGeometry(); + geom.setPositions(positions); + geom.setColors(colors); + const mat = new LineMaterial({ + color: 0xffffff, + vertexColors: true, + linewidth: 3.5, + dashed: true, + dashScale: 1, + dashSize: 0.16, + gapSize: 0.08, + depthTest: true, + depthWrite: false, + transparent: true, + opacity: 1.0, + polygonOffset: true, + polygonOffsetFactor: -2, + polygonOffsetUnits: -2, + }); + const overlay = new LineSegments2(geom, mat); + overlay.computeLineDistances(); + overlay.name = "capacity-girder-lines"; + overlay.layers.mask = active.mesh.layers.mask; + overlay.renderOrder = 7; // above the boundary overlay + active.mesh.add(overlay); + active.capacityGirderLineOverlay = overlay; +} + +function disposeCapacityGirderLineOverlay(): void { + if (!active?.capacityGirderLineOverlay) return; + active.capacityGirderLineOverlay.removeFromParent(); + active.capacityGirderLineOverlay.geometry.dispose(); + const material = active.capacityGirderLineOverlay.material; + if (Array.isArray(material)) material.forEach((m) => m.dispose()); + else material.dispose(); + active.capacityGirderLineOverlay = undefined; +} + +/** Dashed near-black polylines indicating the stiffeners of the capacity + * models: each panel stiffener's station line, and — for girder models — the + * supported-stiffener lines carried in ``stiffener_stations`` (the girder's + * own ``stiffeners[0]`` is the amber girder line, not a stiffener). */ +function rebuildCapacityStiffenerLineOverlay( + models: Array<{ + type?: string; + stiffeners?: Array>; + stiffener_stations?: number[][][]; + }>, +): void { + disposeCapacityStiffenerLineOverlay(); + if (!active?.mesh) return; + const positions: number[] = []; + const pushPolyline = (stations: number[][] | undefined): void => { + if (!stations || stations.length < 2) return; + for (let i = 0; i + 1 < stations.length; i++) { + positions.push(...stations[i].slice(0, 3), ...stations[i + 1].slice(0, 3)); + } + }; + for (const model of models) { + if (model.type === "girder") { + for (const line of model.stiffener_stations ?? []) pushPolyline(line); + continue; + } + for (const stiff of model.stiffeners ?? []) { + pushPolyline(stiff.stations as number[][] | undefined); + } + } + if (positions.length === 0) return; + const geom = new THREE.BufferGeometry(); + geom.setAttribute("position", new THREE.BufferAttribute(new Float32Array(positions), 3)); + const mat = new THREE.LineDashedMaterial({ + color: CAPACITY_STIFFENER_LINE_COLOR, + dashSize: 0.12, + gapSize: 0.08, + depthTest: true, + depthWrite: false, + transparent: true, + opacity: 0.9, + }); + const overlay = new THREE.LineSegments(geom, mat); + overlay.computeLineDistances(); // required for dashed rendering + overlay.name = "capacity-stiffener-lines"; + overlay.layers.mask = active.mesh.layers.mask; + overlay.renderOrder = 4; + active.mesh.add(overlay); + active.capacityStiffenerLineOverlay = overlay; +} + +function disposeCapacityStiffenerLineOverlay(): void { + if (!active?.capacityStiffenerLineOverlay) return; + active.capacityStiffenerLineOverlay.removeFromParent(); + active.capacityStiffenerLineOverlay.geometry.dispose(); + const material = active.capacityStiffenerLineOverlay.material; + if (Array.isArray(material)) material.forEach((m) => m.dispose()); + else material.dispose(); + active.capacityStiffenerLineOverlay = undefined; +} + +/** Capacity models whose check raised (and was skipped) in the current case + * context — a specific case, or the union of the selected cases in the worst + * view. Their definition edges are painted red so the failure is visible in + * the 3D scene, matching the red error banner in the Capacity sidecar. */ +function capacityFailedModelIds( + run: {errors?: Array<{capacity_model_id: string; case_id: string}>}, + store: {activeCaseId: string | null; worstCaseIds: string[]}, +): Set { + const out = new Set(); + const errors = run.errors ?? []; + if (errors.length === 0) return out; + let cases: Set | null; + if (store.activeCaseId === WORST_CASE_ID) { + cases = new Set(store.worstCaseIds); + } else if (store.activeCaseId) { + cases = new Set([store.activeCaseId]); + } else { + cases = null; // no case context → flag any error + } + for (const err of errors) { + if (cases === null || cases.has(err.case_id)) out.add(err.capacity_model_id); + } + return out; +} + +export function clearCapacityDefinitionView(): void { + disposeCapacityBoundaryOverlay(); + disposeCapacityGirderLineOverlay(); + disposeCapacityStiffenerLineOverlay(); + clearCapacitySelectionHighlight(); +} + +export function clearCapacityVisualField(): void { + if (!active) return; + disposeCapacityColorOverlay("main"); + disposeCapacityColorOverlay("beam"); +} + +/** "Show only definitions" — hide every FEA draw range that is not part of a + * capacity model, leaving just the capacity panels (their faces, plus the + * boundary/colour overlays when those are on). The pre-isolation hidden set is + * snapshotted so {@link clearCapacityIsolation} restores exactly what the user + * had visible. Idempotent: re-applies cleanly when the run/models change. */ +export function applyCapacityIsolation(): boolean { + if (!active?.mesh) return false; + const store = useCapacityResultsStore.getState(); + const results = store.results; + if (!results) return false; + const run = results.runs.find((r) => r.id === store.activeRunId) ?? results.runs[0]; + if (!run) return false; + + const keep = new Set(); + for (const model of run.capacity_models) { + for (const elementId of model.element_ids.all ?? []) keep.add(`E${elementId}`); + } + + active.capacityIsolationSaved = active.capacityIsolationSaved ?? {}; + isolateMeshToRanges(active.mesh, keep, "main"); + if (active.beamSolidMesh) isolateMeshToRanges(active.beamSolidMesh, keep, "beam"); + requestRender(); + return true; +} + +function isolateMeshToRanges(mesh: THREE.Mesh, keep: Set, slot: "main" | "beam"): void { + if (!(mesh instanceof CustomBatchedMesh) || !active?.capacityIsolationSaved) return; + const saved = active.capacityIsolationSaved; + // Snapshot the user's pre-isolation hidden ranges once, so toggling off + // restores them rather than blindly un-hiding everything. + if (saved[slot] === undefined) saved[slot] = new Set(mesh.getHiddenRanges()); + const baseline = saved[slot]!; + + // Reset to the snapshot baseline, then hide everything outside `keep`. + mesh.unhideAllDrawRanges(); + const toHide: string[] = []; + for (const id of mesh.drawRanges.keys()) { + if (!keep.has(id) || baseline.has(id)) toHide.push(id); + } + if (toHide.length) mesh.hideBatchDrawRange(toHide); +} + +/** Show numbered markers (1/2/3) at the stiffener's Section-5 stations. + * ``points`` are [start, mid, end] coordinates in the mesh (SIN) frame; the + * sprites are added as children of the FEA mesh so they share its transform. */ +export function applyCapacityStations(points: number[][] | null | undefined): boolean { + clearCapacityStations(); + if (!active?.mesh || !points || points.length === 0) return false; + const group = new THREE.Group(); + group.name = "capacity-stations"; + points.slice(0, 3).forEach((p, i) => { + if (!p || p.length < 3) return; + const sprite = makeStationSprite(String(i + 1), CAPACITY_STATION_COLORS[i] ?? "#38bdf8"); + sprite.position.set(p[0], p[1], p[2]); + sprite.raycast = () => undefined; + group.add(sprite); + }); + if (group.children.length === 0) return false; + active.mesh.add(group); + active.capacityStationsGroup = group; + requestRender(); + return true; +} + +export function clearCapacityStations(): void { + if (!active?.capacityStationsGroup) return; + const group = active.capacityStationsGroup; + group.removeFromParent(); + group.traverse((obj) => { + if (obj instanceof THREE.Sprite) { + obj.material.map?.dispose(); + obj.material.dispose(); + } + }); + active.capacityStationsGroup = undefined; + requestRender(); +} + +function makeStationSprite(label: string, color: string): THREE.Sprite { + const size = 64; + const canvas = document.createElement("canvas"); + canvas.width = canvas.height = size; + const ctx = canvas.getContext("2d") as CanvasRenderingContext2D; + ctx.beginPath(); + ctx.arc(size / 2, size / 2, size / 2 - 5, 0, Math.PI * 2); + ctx.fillStyle = color; + ctx.fill(); + ctx.lineWidth = 5; + ctx.strokeStyle = "#0f172a"; + ctx.stroke(); + ctx.fillStyle = "#0f172a"; + ctx.font = "bold 40px sans-serif"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(label, size / 2, size / 2 + 2); + + const texture = new THREE.CanvasTexture(canvas); + texture.minFilter = THREE.LinearFilter; + const material = new THREE.SpriteMaterial({ + map: texture, + sizeAttenuation: false, + depthTest: false, + depthWrite: false, + transparent: true, + }); + const sprite = new THREE.Sprite(material); + // sizeAttenuation:false → constant screen size; ~5% of viewport height. + sprite.scale.set(0.05, 0.05, 1); + sprite.renderOrder = 12; + return sprite; +} + +/** Undo {@link applyCapacityIsolation}: restore the snapshotted hidden set. */ +export function clearCapacityIsolation(): void { + if (!active?.capacityIsolationSaved) return; + const saved = active.capacityIsolationSaved; + restoreMeshHidden(active.mesh, saved.main); + if (active.beamSolidMesh) restoreMeshHidden(active.beamSolidMesh, saved.beam); + active.capacityIsolationSaved = undefined; + requestRender(); +} + +function restoreMeshHidden(mesh: THREE.Mesh | undefined, savedHidden: Set | undefined): void { + if (!(mesh instanceof CustomBatchedMesh)) return; + mesh.unhideAllDrawRanges(); + if (savedHidden && savedHidden.size) mesh.hideBatchDrawRange(savedHidden); +} + +export function applyCapacitySelectionHighlight(): void { + if (!active?.mesh) return; + const store = useCapacityResultsStore.getState(); + const results = store.results; + if (!results) return; + const run = results.runs.find((r) => r.id === store.activeRunId) ?? results.runs[0]; + if (!run) return; + if (!store.showDefinitions) { + updateCapacitySelectionOverlay(run, null); + return; + } + updateCapacitySelectionOverlay(run, store.selectedModelId); +} + +function clearCapacitySelectionHighlight(): void { + disposeCapacitySelectedBoundaryOverlay(); + applySelectionOverlay(active?.mesh, []); + applySelectionOverlay(active?.beamSolidMesh, []); +} + +function paintCapacityEntries( + mesh: THREE.Mesh, + values: Array<{element_id: number; value: number | null; capacity_model_id?: string}>, +): boolean { + const drawRanges = (mesh as unknown as { + drawRanges?: Map; + }).drawRanges; + if (!drawRanges) return false; + + const overlay = capacityColorOverlayFor(mesh); + if (!overlay) return false; + const colorAttr = overlay.geometry.getAttribute("color") as THREE.BufferAttribute | undefined; + if (!colorAttr) return false; + const colors = colorAttr.array as Float32Array; + seedNeutralColors(colors); + const tmp = new Float32Array(3); + + for (const entry of values) { + if (entry.value == null || !isFinite(entry.value)) continue; + const dr = drawRanges.get(`E${entry.element_id}`); + if (!dr) continue; + capacityUfColor(entry.value, tmp); + const [vStart, vCount] = dr; + for (let i = vStart; i < vStart + vCount; i++) { + const off = i * 4; + colors[off + 0] = tmp[0]; + colors[off + 1] = tmp[1]; + colors[off + 2] = tmp[2]; + colors[off + 3] = 1.0; + } + } + + colorAttr.needsUpdate = true; + return true; +} + +/** Show or hide the whole-model element-edge wireframe. Used by the capacity + * "Only definitions" view: with isolation on (and "show rest as wireframe" + * off) the wireframe of the non-capacity model would otherwise still draw + * over an otherwise-isolated scene. */ +export function setFeaWireframeVisible(visible: boolean): void { + if (active?.feaEdges) { + active.feaEdges.visible = visible; + requestRender(); + } +} + +/** Element ids that belong to a capacity model in the active run — the set the + * "Only definitions" view keeps visible. */ +function capacityKeepElementIds(): Set | null { + const store = useCapacityResultsStore.getState(); + if (!store.isolateDefinitions) return null; + const results = store.results; + const run = results?.runs.find((r) => r.id === store.activeRunId) ?? results?.runs[0]; + if (!run) return null; + const keep = new Set(); + for (const model of run.capacity_models) { + for (const id of model.element_ids.all ?? []) keep.add(id); + } + return keep; +} + +function capacityColorOverlayFor(mesh: THREE.Mesh): THREE.Mesh | null { + if (!active) return null; + const isBeam = active.beamSolidMesh === mesh; + const keep = capacityKeepElementIds(); + const wantIsolated = keep !== null; + const existing = isBeam ? active.beamSolidCapacityColorOverlay : active.capacityColorOverlay; + // Rebuild when the isolation state changed: an isolated overlay collapses + // non-capacity faces to zero area, a non-isolated one greys them. Comparing + // here (rather than disposing from the isolation toggle) keeps it correct + // regardless of React effect ordering. + if (existing && existing.userData.capacityIsolated === wantIsolated) return existing; + if (existing) disposeCapacityColorOverlay(isBeam ? "beam" : "main"); + + const overlay = buildCapacityColorOverlay(mesh, keep); + if (!overlay) return null; + overlay.userData.capacityIsolated = wantIsolated; + mesh.add(overlay); + if (isBeam) active.beamSolidCapacityColorOverlay = overlay; + else active.capacityColorOverlay = overlay; + return overlay; +} + +function buildCapacityColorOverlay( + mesh: THREE.Mesh, + keepElementIds: Set | null, +): THREE.Mesh | null { + const src = mesh.geometry; + const indexAttr = src.getIndex(); + const posAttr = src.getAttribute("position") as THREE.BufferAttribute | undefined; + if (!indexAttr || !posAttr || posAttr.itemSize !== 3) return null; + + const indexArr = indexAttr.array as Uint16Array | Uint32Array; + const positions = new Float32Array(indexArr.length * 3); + // RGBA vertex colours: alpha 0 (transparent) until a result value paints + // the element, so un-valued elements keep the shaded base mesh look. + const colors = new Float32Array(indexArr.length * 4); + seedNeutralColors(colors); + for (let i = 0; i < indexArr.length; i++) { + const srcIdx = indexArr[i]; + const out = i * 3; + positions[out + 0] = posAttr.getX(srcIdx); + positions[out + 1] = posAttr.getY(srcIdx); + positions[out + 2] = posAttr.getZ(srcIdx); + } + + const geom = new THREE.BufferGeometry(); + geom.setAttribute("position", new THREE.BufferAttribute(positions, 3)); + geom.setAttribute("color", new THREE.BufferAttribute(colors, 4)); + duplicateMorphPositions(src, geom, indexArr); + + // "Only definitions": collapse every triangle that is not part of a capacity + // model to zero area (all three verts coincident, base + morph) so the rest + // of the model contributes no grey faces while the capacity panels stay lit. + if (keepElementIds) { + collapseNonCapacityTriangles(mesh, geom, indexArr, keepElementIds); + } + + // Opaque, double-sided, unlit: the UF colour must read identically on both + // faces of a shell. With <1 opacity the lit base mesh bleeds through and the + // two sides look subtly different; a too-small polygon offset also lets the + // coplanar base z-fight through on large models (large coordinates → coarse + // depth precision), which shows as colour appearing on only one side. Keep + // depthWrite off so the white capacity-boundary lines still layer on top. + const mat = new THREE.MeshBasicMaterial({ + vertexColors: true, + side: THREE.DoubleSide, + transparent: true, + opacity: 1.0, + depthTest: true, + depthWrite: false, + polygonOffset: true, + polygonOffsetFactor: -4, + polygonOffsetUnits: -16, + }); + const overlay = new THREE.Mesh(geom, mat); + overlay.name = "capacity-color-overlay"; + overlay.renderOrder = 2; + overlay.layers.mask = mesh.layers.mask; + overlay.raycast = () => undefined; + overlay.morphTargetInfluences = mesh.morphTargetInfluences; + overlay.morphTargetDictionary = mesh.morphTargetDictionary; + return overlay; +} + +function duplicateMorphPositions( + source: THREE.BufferGeometry, + target: THREE.BufferGeometry, + indexArr: Uint16Array | Uint32Array, +): void { + const morphPositions = source.morphAttributes.position as THREE.BufferAttribute[] | undefined; + if (!morphPositions?.length) return; + target.morphAttributes.position = morphPositions.map((attr) => { + const out = new Float32Array(indexArr.length * 3); + for (let i = 0; i < indexArr.length; i++) { + const srcIdx = indexArr[i]; + const dst = i * 3; + out[dst + 0] = attr.getX(srcIdx); + out[dst + 1] = attr.getY(srcIdx); + out[dst + 2] = attr.getZ(srcIdx); + } + return new THREE.BufferAttribute(out, 3); + }); + target.morphTargetsRelative = source.morphTargetsRelative === true; +} + +/** Collapse every overlay triangle whose element is not in ``keepElementIds`` + * to zero area, in both the base and morph position buffers, so the "Only + * definitions" view draws no grey faces for the rest of the model while the + * capacity panels keep their UF colours and deform in lockstep. */ +function collapseNonCapacityTriangles( + mesh: THREE.Mesh, + geom: THREE.BufferGeometry, + indexArr: Uint16Array | Uint32Array, + keepElementIds: Set, +): void { + const drawRanges = (mesh as unknown as { + drawRanges?: Map; + }).drawRanges; + if (!drawRanges) return; + + // Mark overlay index positions that belong to a kept (capacity) element. + const keepIdx = new Uint8Array(indexArr.length); + for (const [key, [vStart, vCount]] of drawRanges) { + const m = /^E(\d+)$/.exec(key); + if (!m || !keepElementIds.has(Number(m[1]))) continue; + for (let i = vStart; i < vStart + vCount && i < keepIdx.length; i++) keepIdx[i] = 1; + } + + const posAttr = geom.getAttribute("position") as THREE.BufferAttribute; + const morphs = (geom.morphAttributes.position ?? []) as THREE.BufferAttribute[]; + const collapse = (attr: THREE.BufferAttribute, k: number): void => { + const x = attr.getX(k); + const y = attr.getY(k); + const z = attr.getZ(k); + attr.setXYZ(k + 1, x, y, z); + attr.setXYZ(k + 2, x, y, z); + }; + // Draw ranges are triangle-aligned, so the first vertex of each triangle + // tells us whether the whole triangle is kept. + for (let k = 0; k + 2 < indexArr.length; k += 3) { + if (keepIdx[k]) continue; + collapse(posAttr, k); + for (const morph of morphs) collapse(morph, k); + } + posAttr.needsUpdate = true; + for (const morph of morphs) morph.needsUpdate = true; +} + +function updateCapacitySelectionOverlay( + run: { + capacity_models: Array<{ + id: string; + panel_group: string; + element_ids: {all?: number[]}; + stiffeners?: Array>; + }>; + }, + selectedModelId: string | null, +): void { + disposeCapacitySelectedBoundaryOverlay(); + const rangeIds = selectedModelId + ? run.capacity_models + .find((model) => model.id === selectedModelId) + ?.element_ids.all + ?.map((id) => `E${id}`) ?? [] + : []; + + // Keep capacity selection visually edge-based. The default mesh face + // selection color is too close to result colors and can obscure UF plots. + applySelectionOverlay(active?.mesh, []); + applySelectionOverlay(active?.beamSolidMesh, []); + + if (!selectedModelId || rangeIds.length === 0) return; + const selectedModel = run.capacity_models.find((model) => model.id === selectedModelId); + if (!selectedModel) return; + const overlay = buildCapacityBoundaryOverlay( + [selectedModel], + 0xff1f3d, + "capacity-selected-model-boundary", + false, + ); + if (overlay && active?.mesh) { + active.mesh.add(overlay); + active.capacitySelectedBoundaryOverlay = overlay; + linkLineMorphToMesh(active.mesh); + } + + // When an individual stiffener row is selected within a multi-stiffener + // panel, outline that stiffener's own strip (line + tributary plate) in + // amber so the specific capacity model under inspection stands out. + const selectedResultId = useCapacityResultsStore.getState().selectedResultId; + const stiffeners = selectedModel.stiffeners ?? []; + const stiffName = selectedResultId?.split("::").pop() ?? null; + if (!stiffName || stiffeners.length < 2) return; + const stiff = stiffeners.find((s) => String(s.name) === stiffName); + if (!stiff) return; + const stripIds = [ + ...(((stiff.element_ids as number[] | undefined) ?? [])), + ...(((stiff.tributary_plate_ids as number[] | undefined) ?? [])), + ]; + if (stripIds.length === 0) return; + const strip = buildCapacityBoundaryOverlay( + [ + { + id: `${selectedModel.id}::${stiffName}`, + panel_group: selectedModel.panel_group, + element_ids: {all: stripIds}, + }, + ], + 0xffc53d, + "capacity-selected-strip-boundary", + false, + ); + if (strip && active?.mesh) { + active.mesh.add(strip); + active.capacitySelectedStripOverlay = strip; + linkLineMorphToMesh(active.mesh); + } +} + +function applySelectionOverlay(mesh: THREE.Mesh | undefined, rangeIds: string[]): void { + const maybeSelectable = mesh as unknown as { + updateSelectionGroups?: (rangeIds: string[]) => void; + } | undefined; + maybeSelectable?.updateSelectionGroups?.(rangeIds); +} + +// Failed-model definition edges (a check raised for this model/case). +const CAPACITY_FAILED_EDGE_COLOR = 0xef4444; + +function rebuildCapacityBoundaryOverlay( + models: Array<{id: string; panel_group: string; element_ids: {all?: number[]}}>, + failedModelIds?: Set, +): void { + disposeCapacityBoundaryOverlay(); + const overlay = buildCapacityBoundaryOverlay( + models, + 0xf8fafc, + "capacity-model-boundaries", + true, + failedModelIds, + ); + if (overlay && active?.mesh) { + active.mesh.add(overlay); + active.capacityBoundaryOverlay = overlay; + linkLineMorphToMesh(active.mesh); + } +} + +function buildCapacityBoundaryOverlay( + models: Array<{id: string; panel_group: string; element_ids: {all?: number[]}}>, + color: number, + name: string, + depthTest: boolean, + failedModelIds?: Set, +): THREE.LineSegments | null { + if (!active?.mesh) return null; + const mesh = active.mesh; + const geometry = mesh.geometry; + const indexAttr = geometry.getIndex(); + const drawRanges = (mesh as unknown as { + drawRanges?: Map; + }).drawRanges; + if (!indexAttr || !drawRanges) return null; + const indexArr = indexAttr.array as Uint16Array | Uint32Array; + const edgeIndices: number[] = []; + // Vertices that belong to a failed model's boundary — painted red. + const failedVerts = new Set(); + + for (const model of models) { + const isFailed = failedModelIds?.has(model.id) ?? false; + const edgeCounts = new Map(); + for (const elementId of model.element_ids.all ?? []) { + const dr = drawRanges.get(`E${elementId}`); + if (!dr) continue; + const [vStart, vCount] = dr; + for (let i = vStart; i + 2 < vStart + vCount; i += 3) { + addBoundaryEdge(edgeCounts, indexArr[i], indexArr[i + 1]); + addBoundaryEdge(edgeCounts, indexArr[i + 1], indexArr[i + 2]); + addBoundaryEdge(edgeCounts, indexArr[i + 2], indexArr[i]); + } + } + for (const [, [a, b, count]] of edgeCounts) { + if (count !== 1) continue; + edgeIndices.push(a, b); + if (isFailed) { + failedVerts.add(a); + failedVerts.add(b); + } + } + } + if (edgeIndices.length === 0) return null; + + const lineGeom = new THREE.BufferGeometry(); + lineGeom.setAttribute("position", geometry.attributes.position); + lineGeom.setIndex(new THREE.BufferAttribute(new Uint32Array(edgeIndices), 1)); + const matOpts: THREE.LineBasicMaterialParameters = { + color, + depthTest, + depthWrite: false, + transparent: true, + opacity: depthTest ? 0.92 : 1.0, + }; + if (failedVerts.size > 0) { + // Per-vertex colours so failed models read red while the rest stay the + // normal boundary colour, all within one morph-linked LineSegments. + const posCount = (geometry.attributes.position as THREE.BufferAttribute).count; + const colors = new Float32Array(posCount * 3); + const base = new THREE.Color(color); + for (let i = 0; i < posCount; i++) { + colors[i * 3 + 0] = base.r; + colors[i * 3 + 1] = base.g; + colors[i * 3 + 2] = base.b; + } + const red = new THREE.Color(CAPACITY_FAILED_EDGE_COLOR); + for (const v of failedVerts) { + colors[v * 3 + 0] = red.r; + colors[v * 3 + 1] = red.g; + colors[v * 3 + 2] = red.b; + } + lineGeom.setAttribute("color", new THREE.BufferAttribute(colors, 3)); + matOpts.vertexColors = true; + matOpts.color = 0xffffff; // let the per-vertex colour show through + } + const lineMat = new THREE.LineBasicMaterial(matOpts); + const overlay = new THREE.LineSegments(lineGeom, lineMat); + overlay.name = name; + overlay.layers.mask = mesh.layers.mask; + overlay.renderOrder = depthTest ? 3 : 6; + return overlay; +} + +function disposeCapacityBoundaryOverlay(): void { + if (!active?.capacityBoundaryOverlay) return; + active.capacityBoundaryOverlay.removeFromParent(); + active.capacityBoundaryOverlay.geometry.dispose(); + const material = active.capacityBoundaryOverlay.material; + if (Array.isArray(material)) material.forEach((m) => m.dispose()); + else material.dispose(); + active.capacityBoundaryOverlay = undefined; +} + +function disposeCapacitySelectedBoundaryOverlay(): void { + for (const key of ["capacitySelectedBoundaryOverlay", "capacitySelectedStripOverlay"] as const) { + const overlay = active?.[key]; + if (!overlay) continue; + overlay.removeFromParent(); + overlay.geometry.dispose(); + const material = overlay.material; + if (Array.isArray(material)) material.forEach((m) => m.dispose()); + else material.dispose(); + active![key] = undefined; + } +} + +function disposeCapacityColorOverlay(which: "main" | "beam"): void { + if (!active) return; + const overlay = which === "main" ? active.capacityColorOverlay : active.beamSolidCapacityColorOverlay; + if (!overlay) return; + overlay.removeFromParent(); + overlay.geometry.dispose(); + const material = overlay.material; + if (Array.isArray(material)) material.forEach((m) => m.dispose()); + else material.dispose(); + if (which === "main") active.capacityColorOverlay = undefined; + else active.beamSolidCapacityColorOverlay = undefined; +} + +function addBoundaryEdge( + edgeCounts: Map, + a: number, + b: number, +): void { + const lo = Math.min(a, b); + const hi = Math.max(a, b); + const key = `${lo}:${hi}`; + const prev = edgeCounts.get(key); + if (prev) prev[2] += 1; + else edgeCounts.set(key, [lo, hi, 1]); +} + +/** Seed the RGBA capacity colour buffer fully transparent: elements without a + * result value contribute nothing, so the normally-shaded base mesh shows + * through instead of a flat unlit grey. */ +function seedNeutralColors(colors: Float32Array): void { + colors.fill(0); +} + +// Genie "UfTot" discrete colour bands (no gradient between them) — sampled from +// .local/reference/genie_uf_color_scheme/genie_uf_color_scheme.png. Thresholds at +// 0.2 / 0.4 / 0.6 / 0.8 / 1.0. RGB in 0..1. +const CAPACITY_UF_BANDS: ReadonlyArray = [ + [1.0, [1.0, 0.0, 0.0]], // >= 1.0 #FF0000 red + [0.8, [1.0, 0.6431, 0.0]], // >= 0.8 #FFA400 orange + [0.6, [1.0, 1.0, 0.0]], // >= 0.6 #FFFF00 yellow + [0.4, [0.0, 0.498, 0.0]], // >= 0.4 #007F00 green + [0.2, [0.0, 1.0, 1.0]], // >= 0.2 #00FFFF cyan + [0.0, [0.0, 0.7451, 1.0]], // < 0.2 #00BEFF light blue +]; + +function capacityUfColor(value: number, out: Float32Array): void { + const band = CAPACITY_UF_BANDS.find(([threshold]) => value >= threshold); + const [r, g, b] = (band ?? CAPACITY_UF_BANDS[CAPACITY_UF_BANDS.length - 1])[1]; + out[0] = r; + out[1] = g; + out[2] = b; +} + /** Load the mesh GLB, fetch the chosen field's blob, and apply the * (component, step) selection. Subsequent calls for the same source * + field skip the network and just swap the step. */ @@ -705,7 +1943,13 @@ export async function load_fea_streaming(args: { if (!mesh) throw new Error("loaded GLB has no mesh"); const basePositions = snapshotBasePositions(mesh.geometry); - active = {sourceName, manifest, mesh, basePositions}; + active = { + sourceName, + manifest, + mesh, + basePositions, + capacityFetch: {fetcher, rangeFetcher, cacheKey}, + }; // Material flags (vertexColors + morphTargets) are flipped on // inside applyFieldToMesh so they cover both the array-typed // material that prepareLoadedModel installs on @@ -850,6 +2094,7 @@ export async function load_fea_streaming(args: { // first applyFieldToMesh call below seeds the // morph attribute — see linkLineMorphToMesh. mesh.add(segments); + if (active) active.feaEdges = segments; } } catch (err) { // Wireframe overlay is decorative — log and continue @@ -863,6 +2108,12 @@ export async function load_fea_streaming(args: { stage("loading field data", 0.55); throwIfAborted(); + // Load capacity results after any model replacement. replace_model() + // clears the active FEA session, including the capacity store; loading + // the sidecar before that would make the Capacity panel disappear even + // though the sidecar request succeeded. + await loadCapacityResultsIfPresent(fetcher, scope, sourceName, manifest); + // Resolve the warp source. The picked field drives colour // regardless; warp depends on category: // * displacement → warp by self (legacy behaviour). diff --git a/src/frontend/version-plugin.ts b/src/frontend/version-plugin.ts index b05a85eb4..55365a72b 100644 --- a/src/frontend/version-plugin.ts +++ b/src/frontend/version-plugin.ts @@ -1,6 +1,8 @@ import {execSync} from "child_process"; import type {Plugin} from "vite"; +import {CAPACITY_RESULTS_VERSION} from "./src/services/capacityResultsVersion"; + // Replace the in index.html with the build id + frontend git // sha, for EVERY vite config (default / serve / embed). The cloud viewer builds via // vite.config.serve.ts, which doesn't run embed-script.cjs — so without this the hosted viewer @@ -32,10 +34,15 @@ export function versionInjectPlugin(): Plugin { sha = env ? env.slice(0, 8) : ""; } const ts = Date.now(); - return html.replace( - //g, - ``, - ); + return html + .replace( + //g, + ``, + ) + .replace( + //g, + ``, + ); }, }; } diff --git a/src/jupyter_widget/package-lock.json b/src/jupyter_widget/package-lock.json new file mode 100644 index 000000000..1b2adc25f --- /dev/null +++ b/src/jupyter_widget/package-lock.json @@ -0,0 +1,6925 @@ +{ + "name": "adapy_viewer_widget", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "adapy_viewer_widget", + "version": "0.1.0", + "license": "BSD-3-Clause", + "dependencies": { + "@jupyterlab/application": "^4.0.0" + }, + "devDependencies": { + "@jupyterlab/builder": "^4.0.0", + "@types/json-schema": "^7.0.11", + "@types/react": "^18.0.26", + "@types/react-addons-linked-state-mixin": "^0.14.22", + "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/parser": "^6.1.0", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-prettier": "^5.0.0", + "npm-run-all2": "^7.0.1", + "prettier": "^3.0.0", + "rimraf": "^5.0.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^15.10.1", + "stylelint-config-recommended": "^13.0.0", + "stylelint-config-standard": "^34.0.0", + "stylelint-csstree-validator": "^3.0.0", + "stylelint-prettier": "^4.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", + "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^2.4.1" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz", + "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": "^14 || ^16 || >=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz", + "integrity": "sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz", + "integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==", + "hasInstallScript": true, + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jupyter/react-components": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@jupyter/react-components/-/react-components-0.16.7.tgz", + "integrity": "sha512-BKIPkJ9V011uhtdq1xBOu2M3up59CqsRbDS4aq8XhnHR4pwqfRV6k6irE5YBOETCoIwWZZ5RZO+cJcZ3DcsT5A==", + "license": "BSD-3-Clause", + "dependencies": { + "@jupyter/web-components": "^0.16.7", + "react": ">=17.0.0 <19.0.0" + } + }, + "node_modules/@jupyter/web-components": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@jupyter/web-components/-/web-components-0.16.7.tgz", + "integrity": "sha512-1a8awgvvP9J9pCV5vBRuQxdBk29764qiMJsJYEndrWH3cB/FlaO+sZIBm4OTf56Eqdgl8R3/ZSLM1+3mgXOkPg==", + "license": "BSD-3-Clause", + "dependencies": { + "@microsoft/fast-colors": "^5.3.1", + "@microsoft/fast-element": "^1.12.0", + "@microsoft/fast-foundation": "^2.49.4", + "@microsoft/fast-web-utilities": "^5.4.1" + } + }, + "node_modules/@jupyter/ydoc": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@jupyter/ydoc/-/ydoc-3.5.0.tgz", + "integrity": "sha512-kY/p4vP4tB606AxoEUZDBDKjKLVTF2q6AJE0bMHA5l2lL0SxrjufjkXCK/EEI/vUCOpHRakysNS8644tVAvhDg==", + "license": "BSD-3-Clause", + "dependencies": { + "@jupyterlab/nbformat": "^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0", + "@lumino/coreutils": "^1.11.0 || ^2.0.0", + "@lumino/disposable": "^1.10.0 || ^2.0.0", + "@lumino/signaling": "^1.10.0 || ^2.0.0", + "y-protocols": "^1.0.5", + "yjs": "^13.5.40" + } + }, + "node_modules/@jupyterlab/application": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@jupyterlab/application/-/application-4.5.9.tgz", + "integrity": "sha512-aTcjIkDfrOrTejzoDSitlYLuJ+tSR9KXee82hWNQRBU622IZD5+nQLo0Jw7Wxfsa+s1YoZXDQQZB9UOP4f8M6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@fortawesome/fontawesome-free": "^5.12.0", + "@jupyterlab/apputils": "^4.6.9", + "@jupyterlab/coreutils": "^6.5.9", + "@jupyterlab/docregistry": "^4.5.9", + "@jupyterlab/rendermime": "^4.5.9", + "@jupyterlab/rendermime-interfaces": "^3.13.9", + "@jupyterlab/services": "^7.5.9", + "@jupyterlab/statedb": "^4.5.9", + "@jupyterlab/translation": "^4.5.9", + "@jupyterlab/ui-components": "^4.5.9", + "@lumino/algorithm": "^2.0.4", + "@lumino/application": "^2.4.8", + "@lumino/commands": "^2.3.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/messaging": "^2.0.4", + "@lumino/polling": "^2.1.5", + "@lumino/properties": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/widgets": "^2.7.5" + } + }, + "node_modules/@jupyterlab/apputils": { + "version": "4.6.9", + "resolved": "https://registry.npmjs.org/@jupyterlab/apputils/-/apputils-4.6.9.tgz", + "integrity": "sha512-bMPLc0mVMRWhNZ9XzuOb7bVpDcBckKEhQJ4XqBqzN53DOVbqSxjM9Z3GrrxvuKJEfqRQxCebJVIqwlpZ8T2Uxw==", + "license": "BSD-3-Clause", + "dependencies": { + "@jupyterlab/coreutils": "^6.5.9", + "@jupyterlab/observables": "^5.5.9", + "@jupyterlab/rendermime-interfaces": "^3.13.9", + "@jupyterlab/services": "^7.5.9", + "@jupyterlab/settingregistry": "^4.5.9", + "@jupyterlab/statedb": "^4.5.9", + "@jupyterlab/statusbar": "^4.5.9", + "@jupyterlab/translation": "^4.5.9", + "@jupyterlab/ui-components": "^4.5.9", + "@lumino/algorithm": "^2.0.4", + "@lumino/commands": "^2.3.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/domutils": "^2.0.4", + "@lumino/messaging": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/virtualdom": "^2.0.4", + "@lumino/widgets": "^2.7.5", + "@types/react": "^18.0.26", + "react": "^18.2.0", + "sanitize-html": "~2.12.1" + } + }, + "node_modules/@jupyterlab/builder": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@jupyterlab/builder/-/builder-4.5.9.tgz", + "integrity": "sha512-M2mi+TtcxSE/fiKe5RfEqrzMEms6S++W163Qc+07sj2g78t6G84Bya9dgliUmBmLN4qqYhG0NXURdCmMmWRIAg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/algorithm": "^2.0.4", + "@lumino/application": "^2.4.8", + "@lumino/commands": "^2.3.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/domutils": "^2.0.4", + "@lumino/dragdrop": "^2.1.8", + "@lumino/messaging": "^2.0.4", + "@lumino/properties": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/virtualdom": "^2.0.4", + "@lumino/widgets": "^2.7.5", + "ajv": "^8.12.0", + "commander": "^9.4.1", + "css-loader": "^6.7.1", + "duplicate-package-checker-webpack-plugin": "^3.0.0", + "fs-extra": "^10.1.0", + "glob": "~7.1.6", + "license-webpack-plugin": "^4.0.2", + "mini-css-extract-plugin": "^2.7.0", + "mini-svg-data-uri": "^1.4.4", + "path-browserify": "^1.0.0", + "process": "^0.11.10", + "source-map-loader": "~1.0.2", + "style-loader": "~3.3.1", + "supports-color": "^7.2.0", + "terser-webpack-plugin": "^5.3.7", + "webpack": "^5.76.1", + "webpack-cli": "^5.0.1", + "webpack-merge": "^5.8.0", + "webpack-sources": "^3.4.1", + "worker-loader": "^3.0.2" + }, + "bin": { + "build-labextension": "lib/build-labextension.js" + } + }, + "node_modules/@jupyterlab/builder/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jupyterlab/builder/node_modules/schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@jupyterlab/builder/node_modules/schema-utils/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@jupyterlab/builder/node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/@jupyterlab/builder/node_modules/source-map-loader": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.0.2.tgz", + "integrity": "sha512-oX8d6ndRjN+tVyjj6PlXSyFPhDdVAPsZA30nD3/II8g4uOv8fCz0DMn5sy8KtVbDfKQxOpGwGJnK3xIW3tauDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-urls": "^2.0.0", + "iconv-lite": "^0.6.2", + "loader-utils": "^2.0.0", + "schema-utils": "^2.7.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/@jupyterlab/codeeditor": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@jupyterlab/codeeditor/-/codeeditor-4.5.9.tgz", + "integrity": "sha512-0rFO319u6YKHgEPhkb8Yr84ZxJS6wixjWuM0yn/NesnzInJ8YOfX9uC1raCeD+HcsR6TjmEP7rsbE+FamVWDIA==", + "license": "BSD-3-Clause", + "dependencies": { + "@codemirror/state": "^6.5.4", + "@jupyter/ydoc": "^3.1.0", + "@jupyterlab/apputils": "^4.6.9", + "@jupyterlab/coreutils": "^6.5.9", + "@jupyterlab/nbformat": "^4.5.9", + "@jupyterlab/observables": "^5.5.9", + "@jupyterlab/statusbar": "^4.5.9", + "@jupyterlab/translation": "^4.5.9", + "@jupyterlab/ui-components": "^4.5.9", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/dragdrop": "^2.1.8", + "@lumino/messaging": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/widgets": "^2.7.5", + "react": "^18.2.0" + } + }, + "node_modules/@jupyterlab/coreutils": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-6.5.9.tgz", + "integrity": "sha512-5sAzdFlxO3T78sFMMkRWCBA4VZoCjtSeNNk3F9DvEh/dz9N9OVw4YuW8+lFiFkVp7m/sR6Mho+QGVc8hpG4KsQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/signaling": "^2.1.5", + "minimist": "~1.2.0", + "path-browserify": "^1.0.0", + "url-parse": "~1.5.4" + } + }, + "node_modules/@jupyterlab/docregistry": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@jupyterlab/docregistry/-/docregistry-4.5.9.tgz", + "integrity": "sha512-9bPGgyPejZ010SrhLo6MwYLaQwllUb+15bGvWkiRD2Oom24J06bUzhc8QmtS2hc5IIdA4T0mTHbeOq6AD7tg6A==", + "license": "BSD-3-Clause", + "dependencies": { + "@jupyter/ydoc": "^3.1.0", + "@jupyterlab/apputils": "^4.6.9", + "@jupyterlab/codeeditor": "^4.5.9", + "@jupyterlab/coreutils": "^6.5.9", + "@jupyterlab/observables": "^5.5.9", + "@jupyterlab/rendermime": "^4.5.9", + "@jupyterlab/rendermime-interfaces": "^3.13.9", + "@jupyterlab/services": "^7.5.9", + "@jupyterlab/translation": "^4.5.9", + "@jupyterlab/ui-components": "^4.5.9", + "@lumino/algorithm": "^2.0.4", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/messaging": "^2.0.4", + "@lumino/properties": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/widgets": "^2.7.5", + "react": "^18.2.0" + } + }, + "node_modules/@jupyterlab/nbformat": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@jupyterlab/nbformat/-/nbformat-4.5.9.tgz", + "integrity": "sha512-P9ip05CqbMh7UjTaX8I8icLK6VxCj/8kc2VKcOnxlNfzQBpi3pQyaD/u5mQj9FJf3tnMXaDIiIed4u26tO9XhA==", + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/coreutils": "^2.2.2" + } + }, + "node_modules/@jupyterlab/observables": { + "version": "5.5.9", + "resolved": "https://registry.npmjs.org/@jupyterlab/observables/-/observables-5.5.9.tgz", + "integrity": "sha512-5VPxmjzOhwF3F1sqJa7TljftVtxP/+/CE/cPi0Z7tRcBUy8HXr3O7kLP2u+NzNl6kCiVdFxkG8ruNOCHMZYkIQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/algorithm": "^2.0.4", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/messaging": "^2.0.4", + "@lumino/signaling": "^2.1.5" + } + }, + "node_modules/@jupyterlab/rendermime": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@jupyterlab/rendermime/-/rendermime-4.5.9.tgz", + "integrity": "sha512-/XAyfKtnI7uKMAml7HfaIJ+zmreKd8oVKKfaVuNyxpAsT4/qQ+AYBjOhkKlDTwMjFLaN/hB7Hk2bsPzj/UajzA==", + "license": "BSD-3-Clause", + "dependencies": { + "@jupyterlab/apputils": "^4.6.9", + "@jupyterlab/coreutils": "^6.5.9", + "@jupyterlab/nbformat": "^4.5.9", + "@jupyterlab/observables": "^5.5.9", + "@jupyterlab/rendermime-interfaces": "^3.13.9", + "@jupyterlab/services": "^7.5.9", + "@jupyterlab/translation": "^4.5.9", + "@lumino/coreutils": "^2.2.2", + "@lumino/messaging": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/widgets": "^2.7.5", + "lodash.escape": "^4.0.1" + } + }, + "node_modules/@jupyterlab/rendermime-interfaces": { + "version": "3.13.9", + "resolved": "https://registry.npmjs.org/@jupyterlab/rendermime-interfaces/-/rendermime-interfaces-3.13.9.tgz", + "integrity": "sha512-XO7maJMTArs3oblMlDq3NRIOfshQjhFy9PM/HqUZPFNvh+dMXxkBdMonVF2chJrDo+SGbEq5D2Y0EiFMgZHjTg==", + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/coreutils": "^1.11.0 || ^2.2.2", + "@lumino/widgets": "^1.37.2 || ^2.7.5" + } + }, + "node_modules/@jupyterlab/services": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/@jupyterlab/services/-/services-7.5.9.tgz", + "integrity": "sha512-Ctmt1xxY7EQXrP0ml7Mm8vlM8XXpUr7R+LKCK4TNjLccZTetP/swDC4zLlvY4N+ej2XpoJG9E1TFgDJLz9Euhw==", + "license": "BSD-3-Clause", + "dependencies": { + "@jupyter/ydoc": "^3.1.0", + "@jupyterlab/coreutils": "^6.5.9", + "@jupyterlab/nbformat": "^4.5.9", + "@jupyterlab/settingregistry": "^4.5.9", + "@jupyterlab/statedb": "^4.5.9", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/polling": "^2.1.5", + "@lumino/properties": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "ws": "^8.11.0" + } + }, + "node_modules/@jupyterlab/settingregistry": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@jupyterlab/settingregistry/-/settingregistry-4.5.9.tgz", + "integrity": "sha512-qV9v9y7ZA2F3OVh4fdztqOdx0VhNoGdUnipC1QEPNWyIzIkd8VFRFyu7BHAgxE9H85GoeIeFDdcqgY8niVH44g==", + "license": "BSD-3-Clause", + "dependencies": { + "@jupyterlab/nbformat": "^4.5.9", + "@jupyterlab/statedb": "^4.5.9", + "@lumino/commands": "^2.3.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/signaling": "^2.1.5", + "@rjsf/utils": "^5.13.4", + "ajv": "^8.12.0", + "json5": "^2.2.3" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/@jupyterlab/statedb": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@jupyterlab/statedb/-/statedb-4.5.9.tgz", + "integrity": "sha512-zvgX0RarmueX/gWTMcHPxVd6DCwehydNCpRYXMS9rUsn8yWIqtOp1Bx9P0hE8Qubc/5PehvF3NTZeI4OemGZrg==", + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/commands": "^2.3.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/properties": "^2.0.4", + "@lumino/signaling": "^2.1.5" + } + }, + "node_modules/@jupyterlab/statusbar": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@jupyterlab/statusbar/-/statusbar-4.5.9.tgz", + "integrity": "sha512-nb1Aq33BwCHhccPtlc3w0ls5MSvLthZZk6BCzG2uNhRXTEHOF9LQZEE0b+BU380Nu36TOAOKUReYoFYYOxxSaQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@jupyterlab/ui-components": "^4.5.9", + "@lumino/algorithm": "^2.0.4", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/messaging": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/widgets": "^2.7.5", + "react": "^18.2.0" + } + }, + "node_modules/@jupyterlab/translation": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@jupyterlab/translation/-/translation-4.5.9.tgz", + "integrity": "sha512-Pvj9lIo+unVDfr/TumR1b0P6/yvl3Ow5pDxjbFAJE0n/cy41BcLN9h0UnSge37ioaRnDBe3z0hOzCrADh8D/Wg==", + "license": "BSD-3-Clause", + "dependencies": { + "@jupyterlab/coreutils": "^6.5.9", + "@jupyterlab/rendermime-interfaces": "^3.13.9", + "@jupyterlab/services": "^7.5.9", + "@jupyterlab/statedb": "^4.5.9", + "@lumino/coreutils": "^2.2.2" + } + }, + "node_modules/@jupyterlab/ui-components": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@jupyterlab/ui-components/-/ui-components-4.5.9.tgz", + "integrity": "sha512-fKU7QGvOzrjO+xMzGuNAklLWOdsdMUZg3+GYm5inkg6Qmqq9r/DYLwuc2dKAHUN3swZmm0MjhT3Z1NyZygARlg==", + "license": "BSD-3-Clause", + "dependencies": { + "@jupyter/react-components": "^0.16.6", + "@jupyter/web-components": "^0.16.6", + "@jupyterlab/coreutils": "^6.5.9", + "@jupyterlab/observables": "^5.5.9", + "@jupyterlab/rendermime-interfaces": "^3.13.9", + "@jupyterlab/translation": "^4.5.9", + "@lumino/algorithm": "^2.0.4", + "@lumino/commands": "^2.3.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/messaging": "^2.0.4", + "@lumino/polling": "^2.1.5", + "@lumino/properties": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/virtualdom": "^2.0.4", + "@lumino/widgets": "^2.7.5", + "@rjsf/core": "^5.13.4", + "@rjsf/utils": "^5.13.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typestyle": "^2.0.4" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/@lumino/algorithm": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lumino/algorithm/-/algorithm-2.0.4.tgz", + "integrity": "sha512-gddBhESPqu25KWLeAK9Kz8tS9Ph7P45i0CNG7Ia4XMhK9PHLtTsBdJTC9jP+MqhbzC8zDT/4ekvYRV9ojRPj7Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@lumino/application": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@lumino/application/-/application-2.4.9.tgz", + "integrity": "sha512-7TvwwgcB3+cyTAgfHA7vbLkRvwJMEcWRZAN5NirBu5Jh7itVCaNIWMtsr4wUn4lPqyANHpfzcd3hKx56JKNrVg==", + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/commands": "^2.3.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/widgets": "^2.8.0" + } + }, + "node_modules/@lumino/collections": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lumino/collections/-/collections-2.0.4.tgz", + "integrity": "sha512-D/Py9L5HET6+XUYGxFqDEEth4B65X2c7B/GQVRR8q5Fl7EArVL6e98ZXw8BMkuPcTNa0zlENpCKXzlcoJZxXgQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/algorithm": "^2.0.4" + } + }, + "node_modules/@lumino/commands": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@lumino/commands/-/commands-2.3.3.tgz", + "integrity": "sha512-7Ci0QdFzt4NKFMhULr19sJPpOLHJw/oYlq6Pb0/Kq1s05+cIoLimr5wiyjkbAlNoGO/8A8SEBGHy3uctZz6G3A==", + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/algorithm": "^2.0.4", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/domutils": "^2.0.4", + "@lumino/keyboard": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/virtualdom": "^2.0.4" + } + }, + "node_modules/@lumino/coreutils": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@lumino/coreutils/-/coreutils-2.2.2.tgz", + "integrity": "sha512-zaKJaK7rawPATn2BGHkbMrR6oK3s9PxNe9KreLwWF2dB4ZBHDiEmNLRyHRorfJ7XqVOEXAsAAj0jFn+qJPC/4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/algorithm": "^2.0.4" + } + }, + "node_modules/@lumino/disposable": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@lumino/disposable/-/disposable-2.1.5.tgz", + "integrity": "sha512-hO9AkJK0oEGzxopuxI8LaZqwzSNwXJTGCdr5K4gh6al+zxpN7rOCh6Aq3zDxkIHJU4zybxv8r02ardx9XJsG3A==", + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/signaling": "^2.1.5" + } + }, + "node_modules/@lumino/domutils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lumino/domutils/-/domutils-2.0.4.tgz", + "integrity": "sha512-naYGUQn3e0CLtz/tjKOZP8SOBg0SW7EguhkxLpNUXlVUvx7rVsfr0VI22FVL+jgI0FbxXpEkxpSMxtK73jxJAg==", + "license": "BSD-3-Clause" + }, + "node_modules/@lumino/dragdrop": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@lumino/dragdrop/-/dragdrop-2.1.8.tgz", + "integrity": "sha512-5sBYkTka598+XsgjY2tWOC+WYCh9NEgx8RhLvQ3x+V182YhcpEXw38RWGQZyNpQ4m4vtQWKv42A26q+ae6sMwg==", + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5" + } + }, + "node_modules/@lumino/keyboard": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lumino/keyboard/-/keyboard-2.0.4.tgz", + "integrity": "sha512-kIVkdSz8F5wtZr8hZp0CMX+E0eMCOnFH6XCT7j2UBQ80ERJHFy0eX+IbNo3dtRQ7+CcDhBV4hQquFNFa+/04QQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@lumino/messaging": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lumino/messaging/-/messaging-2.0.4.tgz", + "integrity": "sha512-NbZnchAPOciSe9Qn/g6EzG0LRaw7bygFIXbCD440ZhzvugdBeAerwYhrA795jkXPNrrl3olp5AlO0cBB/XZNtg==", + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/algorithm": "^2.0.4", + "@lumino/collections": "^2.0.4" + } + }, + "node_modules/@lumino/polling": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@lumino/polling/-/polling-2.1.5.tgz", + "integrity": "sha512-YhQRWTNRVSi5R5uatwh1jkxASY5JKyAGWmtnfQOZWLDUFmsIjOTsS8NaYg1BgneZjWM3fbA18dCDDT7PPs5X1g==", + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/signaling": "^2.1.5" + } + }, + "node_modules/@lumino/properties": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lumino/properties/-/properties-2.0.4.tgz", + "integrity": "sha512-XsL2qLZk+1FbfuTrkyjciI8PMDw3YcaBkqVQ+iv7OOJf9bUlrmTpCMY0Hu5d3hV2W3TWlRsdbvRRLEBJSKv0iA==", + "license": "BSD-3-Clause" + }, + "node_modules/@lumino/signaling": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@lumino/signaling/-/signaling-2.1.5.tgz", + "integrity": "sha512-Wkx6WR45ynmKBlW0GBEoh4xk9+QluKr1JHuMftqcStBHSQBCnN54UKRRDbySXHGRhhx6p4neu7sGomgQSlQK8w==", + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/algorithm": "^2.0.4", + "@lumino/coreutils": "^2.2.2" + } + }, + "node_modules/@lumino/virtualdom": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lumino/virtualdom/-/virtualdom-2.0.4.tgz", + "integrity": "sha512-7MFthA9KUsqZTGm/D98FZt1QupjIGyd3XyB4SIugn6DQAqhjBiyykCZydnRq3qmuMHybQel33dNIbHpzyNyQwA==", + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/algorithm": "^2.0.4" + } + }, + "node_modules/@lumino/widgets": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@lumino/widgets/-/widgets-2.8.0.tgz", + "integrity": "sha512-mgWcbxxolqC58mpN4HcPKQvZ1AzQFStonsffaIsR4Kor2vRhc7+RFc0hd5VnhgJowvoFmmREQhZW8ETR1ydCVw==", + "license": "BSD-3-Clause", + "dependencies": { + "@lumino/algorithm": "^2.0.4", + "@lumino/commands": "^2.3.3", + "@lumino/coreutils": "^2.2.2", + "@lumino/disposable": "^2.1.5", + "@lumino/domutils": "^2.0.4", + "@lumino/dragdrop": "^2.1.8", + "@lumino/keyboard": "^2.0.4", + "@lumino/messaging": "^2.0.4", + "@lumino/properties": "^2.0.4", + "@lumino/signaling": "^2.1.5", + "@lumino/virtualdom": "^2.0.4" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, + "node_modules/@microsoft/fast-colors": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@microsoft/fast-colors/-/fast-colors-5.3.1.tgz", + "integrity": "sha512-72RZXVfCbwQzvo5sXXkuLXLT7rMeYaSf5r/6ewQiv/trBtqpWRm4DEH2EilHw/iWTBKOXs1qZNQndgUMa5n4LA==", + "license": "MIT" + }, + "node_modules/@microsoft/fast-element": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@microsoft/fast-element/-/fast-element-1.14.0.tgz", + "integrity": "sha512-zXvuSOzvsu8zDTy9eby8ix8VqLop2rwKRgp++ZN2kTCsoB3+QJVoaGD2T/Cyso2ViZQFXNpiNCVKfnmxBvmWkQ==", + "license": "MIT" + }, + "node_modules/@microsoft/fast-foundation": { + "version": "2.50.0", + "resolved": "https://registry.npmjs.org/@microsoft/fast-foundation/-/fast-foundation-2.50.0.tgz", + "integrity": "sha512-8mFYG88Xea1jZf2TI9Lm/jzZ6RWR8x29r24mGuLojNYqIR2Bl8+hnswoV6laApKdCbGMPKnsAL/O68Q0sRxeVg==", + "license": "MIT", + "dependencies": { + "@microsoft/fast-element": "^1.14.0", + "@microsoft/fast-web-utilities": "^5.4.1", + "tabbable": "^5.2.0", + "tslib": "^1.13.0" + } + }, + "node_modules/@microsoft/fast-web-utilities": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/fast-web-utilities/-/fast-web-utilities-5.4.1.tgz", + "integrity": "sha512-ReWYncndjV3c8D8iq9tp7NcFNc1vbVHvcBFPME2nNFKNbS1XCesYZGlIlf3ot5EmuOXPlrzUHOWzQ2vFpIkqDg==", + "license": "MIT", + "dependencies": { + "exenv-es6": "^1.1.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@rjsf/core": { + "version": "5.24.13", + "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.24.13.tgz", + "integrity": "sha512-ONTr14s7LFIjx2VRFLuOpagL76sM/HPy6/OhdBfq6UukINmTIs6+aFN0GgcR0aXQHFDXQ7f/fel0o/SO05Htdg==", + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "markdown-to-jsx": "^7.4.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@rjsf/utils": "^5.24.x", + "react": "^16.14.0 || >=17" + } + }, + "node_modules/@rjsf/utils": { + "version": "5.24.13", + "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.24.13.tgz", + "integrity": "sha512-rNF8tDxIwTtXzz5O/U23QU73nlhgQNYJ+Sv5BAwQOIyhIE2Z3S5tUiSVMwZHt0julkv/Ryfwi+qsD4FiE5rOuw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "json-schema-merge-allof": "^0.8.1", + "jsonpointer": "^5.0.1", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.14.0 || >=17" + } + }, + "node_modules/@types/create-react-class": { + "version": "15.6.9", + "resolved": "https://registry.npmjs.org/@types/create-react-class/-/create-react-class-15.6.9.tgz", + "integrity": "sha512-BfHsUCFeDRRzPMAyXPOcZm1xmet+a4/jcMmYi/6qJD+UdE6CHKg8x+cVhzunrlT4xJX5pevW+uazpuMKi2d2Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-addons-linked-state-mixin": { + "version": "0.14.27", + "resolved": "https://registry.npmjs.org/@types/react-addons-linked-state-mixin/-/react-addons-linked-state-mixin-0.14.27.tgz", + "integrity": "sha512-yVxzQcKDiq32uziGQ/ka586qSFxz2ePYZ3dTCp4JHJKk/E6M0LP0R28ft3oFAnTGJRFBAB3if3pkP8w57Y04IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/create-react-class": "*", + "@types/react": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz", + "integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.3.0", + "map-obj": "^4.1.0", + "quick-lru": "^5.1.1", + "type-fest": "^1.2.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/compute-gcd": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/compute-gcd/-/compute-gcd-1.2.1.tgz", + "integrity": "sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg==", + "dependencies": { + "validate.io-array": "^1.0.3", + "validate.io-function": "^1.0.2", + "validate.io-integer-array": "^1.0.0" + } + }, + "node_modules/compute-lcm": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/compute-lcm/-/compute-lcm-1.1.2.tgz", + "integrity": "sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ==", + "dependencies": { + "compute-gcd": "^1.2.1", + "validate.io-array": "^1.0.3", + "validate.io-function": "^1.0.2", + "validate.io-integer-array": "^1.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-functions-list": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.3.3.tgz", + "integrity": "sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz", + "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/duplicate-package-checker-webpack-plugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/duplicate-package-checker-webpack-plugin/-/duplicate-package-checker-webpack-plugin-3.0.0.tgz", + "integrity": "sha512-aO50/qPC7X2ChjRFniRiscxBLT/K01bALqfcDaf8Ih5OqQ1N4iT/Abx9Ofu3/ms446vHTm46FACIuJUmgUQcDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^2.3.0", + "find-root": "^1.0.0", + "lodash": "^4.17.4", + "semver": "^5.4.1" + } + }, + "node_modules/duplicate-package-checker-webpack-plugin/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.375", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz", + "integrity": "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.0.tgz", + "integrity": "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz", + "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz", + "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.13" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exenv-es6": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exenv-es6/-/exenv-es6-1.1.1.tgz", + "integrity": "sha512-vlVu3N8d6yEMpMsEm+7sUBAI81aqYYuEvfK0jNqmdb/OPXzzH7QWDDnVjMvDSY47JdHEqx/dfC/q8WkfoTmpGQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true, + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/free-style": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/free-style/-/free-style-3.1.0.tgz", + "integrity": "sha512-vJujYSIyT30iDoaoeigNAxX4yB1RUrh+N2ZMhIElMr3BvCuGXOw7XNJMEEJkDUeamK2Rnb/IKFGKRKlTWIGRWA==", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", + "dev": true, + "license": "MIT" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/json-schema-compare": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz", + "integrity": "sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.4" + } + }, + "node_modules/json-schema-merge-allof": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/json-schema-merge-allof/-/json-schema-merge-allof-0.8.1.tgz", + "integrity": "sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==", + "license": "MIT", + "dependencies": { + "compute-lcm": "^1.1.2", + "json-schema-compare": "^0.2.2", + "lodash": "^4.17.20" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/known-css-properties": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.29.0.tgz", + "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lib0": { + "version": "0.2.117", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", + "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", + "license": "MIT", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash.escape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", + "integrity": "sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-to-jsx": { + "version": "7.7.17", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.17.tgz", + "integrity": "sha512-7mG/1feQ0TX5I7YyMZVDgCC/y2I3CiEhIRQIhyov9nGBP5eoVrOXXHuL5ZP8GRfxVZKRiXWJgwXkb9It+nQZfQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "react": ">= 0.14.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/mathml-tag-names": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", + "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/meow": { + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/meow/-/meow-10.1.5.tgz", + "integrity": "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.2", + "camelcase-keys": "^7.0.0", + "decamelize": "^5.0.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.2", + "read-pkg-up": "^8.0.0", + "redent": "^4.0.0", + "trim-newlines": "^4.0.2", + "type-fest": "^1.2.2", + "yargs-parser": "^20.2.9" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz", + "integrity": "sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", + "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-run-all2": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-7.0.2.tgz", + "integrity": "sha512-7tXR+r9hzRNOPNTvXegM+QzCuMjzUIIq66VDunL6j60O4RrExx32XUhlrS7UK4VcdGw5/Wxzb3kfNcFix9JKDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "cross-spawn": "^7.0.6", + "memorystream": "^0.3.1", + "minimatch": "^9.0.0", + "pidtree": "^0.6.0", + "read-package-json-fast": "^4.0.0", + "shell-quote": "^1.7.3", + "which": "^5.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "npm-run-all2": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0", + "npm": ">= 9" + } + }, + "node_modules/npm-run-all2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm-run-all2/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm-run-all2/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", + "license": "MIT" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.1.tgz", + "integrity": "sha512-e0F9AOF1JMrCfBsyJOwU9lNvQ0WtXTq0j/4jk0BQ5JSI9VAybPXmDpPRw/2FQ3e5d3ZFN1mLh7jW99m/jjaptw==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-resolve-nested-selector": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", + "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/read-package-json-fast": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz", + "integrity": "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/read-pkg": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-6.0.0.tgz", + "integrity": "sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^3.0.2", + "parse-json": "^5.2.0", + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-8.0.0.tgz", + "integrity": "sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^5.0.0", + "read-pkg": "^6.0.0", + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/redent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", + "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^5.0.0", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-html": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.12.1.tgz", + "integrity": "sha512-Plh+JAn0UVDpBRP/xEjsk+xDCoOvMBwQUf/K+/cBAVuTbtX8bj2VB7S1sL1dssVpykqp0/KPSesHrqXtokVBpA==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sanitize-html/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.1.3.tgz", + "integrity": "sha512-6YHeF+XzDOrT/ycFJNI53cgEsp/tHTMl37hi7uVyqFAlTXW109JazaQCkbc+jjoL2637qkH1amLi+JzrIpt5lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.2", + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0", + "source-map": "^0.6.1", + "whatwg-mimetype": "^2.3.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/source-map-loader/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/source-map-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/source-map-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-indent": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", + "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/style-search": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", + "integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==", + "dev": true, + "license": "ISC" + }, + "node_modules/stylelint": { + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.11.0.tgz", + "integrity": "sha512-78O4c6IswZ9TzpcIiQJIN49K3qNoXTM8zEJzhaTE/xRTCZswaovSEVIa/uwbOltZrk16X4jAxjaOhzz/hTm1Kw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@csstools/css-parser-algorithms": "^2.3.1", + "@csstools/css-tokenizer": "^2.2.0", + "@csstools/media-query-list-parser": "^2.1.4", + "@csstools/selector-specificity": "^3.0.0", + "balanced-match": "^2.0.0", + "colord": "^2.9.3", + "cosmiconfig": "^8.2.0", + "css-functions-list": "^3.2.1", + "css-tree": "^2.3.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.1", + "fastest-levenshtein": "^1.0.16", + "file-entry-cache": "^7.0.0", + "global-modules": "^2.0.0", + "globby": "^11.1.0", + "globjoin": "^0.1.4", + "html-tags": "^3.3.1", + "ignore": "^5.2.4", + "import-lazy": "^4.0.0", + "imurmurhash": "^0.1.4", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.29.0", + "mathml-tag-names": "^2.1.3", + "meow": "^10.1.5", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.28", + "postcss-resolve-nested-selector": "^0.1.1", + "postcss-safe-parser": "^6.0.0", + "postcss-selector-parser": "^6.0.13", + "postcss-value-parser": "^4.2.0", + "resolve-from": "^5.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "style-search": "^0.1.0", + "supports-hyperlinks": "^3.0.0", + "svg-tags": "^1.0.0", + "table": "^6.8.1", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "stylelint": "bin/stylelint.mjs" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + } + }, + "node_modules/stylelint-config-recommended": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-13.0.0.tgz", + "integrity": "sha512-EH+yRj6h3GAe/fRiyaoO2F9l9Tgg50AOFhaszyfov9v6ayXJ1IkSHwTxd7lB48FmOeSGDPLjatjO11fJpmarkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "stylelint": "^15.10.0" + } + }, + "node_modules/stylelint-config-standard": { + "version": "34.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-34.0.0.tgz", + "integrity": "sha512-u0VSZnVyW9VSryBG2LSO+OQTjN7zF9XJaAJRX/4EwkmU0R2jYwmBSN10acqZisDitS0CLiEiGjX7+Hrq8TAhfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "stylelint-config-recommended": "^13.0.0" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "stylelint": "^15.10.0" + } + }, + "node_modules/stylelint-csstree-validator": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/stylelint-csstree-validator/-/stylelint-csstree-validator-3.1.0.tgz", + "integrity": "sha512-mrollDZ8h3EhI0nrjpzMzrDQ+Zmcwf0dU1+tolx6uY9dmBIykR1Hc07ktmLddlXwP8h+BSIpn1LIrDLsySTVOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.2.1" + }, + "engines": { + "node": "^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + }, + "peerDependencies": { + "stylelint": ">=7.0.0 <16.0.0" + } + }, + "node_modules/stylelint-csstree-validator/node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/stylelint-csstree-validator/node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/stylelint-prettier": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/stylelint-prettier/-/stylelint-prettier-4.1.0.tgz", + "integrity": "sha512-dd653q/d1IfvsSQshz1uAMe+XDm6hfM/7XiFH0htYY8Lse/s5ERTg7SURQehZPwVvm/rs7AsFhda9EQ2E9TS0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "prettier": ">=3.0.0", + "stylelint": ">=15.8.0" + } + }, + "node_modules/stylelint/node_modules/@csstools/selector-specificity": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.1.1.tgz", + "integrity": "sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.0.13" + } + }, + "node_modules/stylelint/node_modules/balanced-match": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", + "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stylelint/node_modules/file-entry-cache": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-7.0.2.tgz", + "integrity": "sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/stylelint/node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/stylelint/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tabbable": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz", + "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==", + "license": "MIT" + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/trim-newlines": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz", + "integrity": "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/typestyle": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/typestyle/-/typestyle-2.4.0.tgz", + "integrity": "sha512-/d1BL6Qi+YlMLEydnUEB8KL/CAjAN8cyt3/UyGnOyBrWf7bLGcR/6yhmsaUstO2IcYwZfagjE7AIzuI2vUW9mg==", + "license": "MIT", + "dependencies": { + "csstype": "3.0.10", + "free-style": "3.1.0" + } + }, + "node_modules/typestyle/node_modules/csstype": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz", + "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate.io-array": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz", + "integrity": "sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==", + "license": "MIT" + }, + "node_modules/validate.io-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-function/-/validate.io-function-1.0.2.tgz", + "integrity": "sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ==" + }, + "node_modules/validate.io-integer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/validate.io-integer/-/validate.io-integer-1.0.5.tgz", + "integrity": "sha512-22izsYSLojN/P6bppBqhgUDjCkr5RY2jd+N2a3DCAUey8ydvrZ/OkGvFPR7qfOpwR2LC5p4Ngzxz36g5Vgr/hQ==", + "dependencies": { + "validate.io-number": "^1.0.3" + } + }, + "node_modules/validate.io-integer-array": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-integer-array/-/validate.io-integer-array-1.0.0.tgz", + "integrity": "sha512-mTrMk/1ytQHtCY0oNO3dztafHYyGU88KL+jRxWuzfOmQb+4qqnWmI+gykvGp8usKZOM0H7keJHEbRaFiYA0VrA==", + "dependencies": { + "validate.io-array": "^1.0.3", + "validate.io-integer": "^1.0.4" + } + }, + "node_modules/validate.io-number": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz", + "integrity": "sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg==" + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=10.4" + } + }, + "node_modules/webpack": { + "version": "5.107.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz", + "integrity": "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.22.0", + "es-module-lexer": "^2.1.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.2", + "mime-db": "^1.54.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.5.0", + "watchpack": "^2.5.1", + "webpack-sources": "^3.5.0" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", + "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/worker-loader": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz", + "integrity": "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/worker-loader/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/worker-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/worker-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/worker-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y-protocols": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.7.tgz", + "integrity": "sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yjs": { + "version": "13.6.31", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.31.tgz", + "integrity": "sha512-Eq+5BRfbeGyqGVrTJL3bEcr8gKkxPuyuoHmAwpk52fDb8kOVMrfVSTRPd6yiGgX5Fskb96qCRjzjbRjrL4YEnw==", + "license": "MIT", + "peer": true, + "dependencies": { + "lib0": "^0.2.99" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/tests/core/fem/capacity/__init__.py b/tests/core/fem/capacity/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/core/fem/capacity/test_capacity_manager.py b/tests/core/fem/capacity/test_capacity_manager.py new file mode 100644 index 000000000..804d7a5b7 --- /dev/null +++ b/tests/core/fem/capacity/test_capacity_manager.py @@ -0,0 +1,694 @@ +"""Tests for the capacity manager (DNV-RP-C201 capacity-model reconstruction). + +These validate against the matched Mini-topside reference dataset that lives in +the sibling ``codecheck`` repo (licensed; not vendored here), so every test +skips when the reference is absent. Set ``CODECHECK_REFERENCE_DIR`` to the +codecheck ``.local/reference`` directory. + +Validated gates: +* geometry (thickness / length / width) of every plate field matches Genie's + ``model.json``; +* resolved transverse-membrane and shear design stresses match Genie's + ``*__CriteriaResults.json`` for (almost) every (result case, stiffener). +""" + +from __future__ import annotations + +import glob +import json +import os +import pathlib +import re + +import pytest + +_DEFAULT_REFERENCE_DIR = pathlib.Path(__file__).resolve().parents[5] / "codecheck" / ".local" / "reference" +REFERENCE_DIR = pathlib.Path(os.environ.get("CODECHECK_REFERENCE_DIR", _DEFAULT_REFERENCE_DIR)) +REF = REFERENCE_DIR / "example_mini_topside_codecheck" / "temp" / "Assembly" +SIN = REF / "Analysis_pm" / "20260617_122105_R1.SIN" +MODEL_JSON = REF / "Cc2.run1" / "model.json" + +pytestmark = pytest.mark.skipif( + not (SIN.exists() and MODEL_JSON.exists()), + reason=f"Mini-topside capacity reference not found under {REF}", +) + +# A second, richer reference: the (partially completed) codecheck2 Genie run +# covers the full double bottom — girders in two directions — where Genie merges +# the per-cell concept fields into full-width stiffened panels. It exercises the +# geometric merge that the small codecheck1 reference does not. +REF2 = REFERENCE_DIR / "example_mini_topside_codecheck2" / "temp" / "Assembly" +SIN2 = REF2 / "Analysis_pm" / "20260620_150626_R1.SIN" +MODEL_JSON2 = REF2 / "Cc2.run1" / "model.json" +ref2 = pytest.mark.skipif( + not (SIN2.exists() and MODEL_JSON2.exists()), + reason=f"Mini-topside capacity reference (codecheck2) not found under {REF2}", +) + +_STIFF_RE = re.compile(r"Stiffener \(Name/Id\): \(([^/]+?) / \d+\)") + + +@pytest.fixture(scope="module") +def manager(): + from ada.fem.capacity import CapacityManager, ModelJsonSource + + return CapacityManager.from_sin(SIN, ModelJsonSource(MODEL_JSON)) + + +@pytest.fixture(scope="module") +def genie_variables(): + out = {} + for f in glob.glob(str(REF / "Cc2.run1" / "SesamCore_RUN1_panelGroup_*__CriteriaResults.json")): + for rc in json.loads(pathlib.Path(f).read_text()): + case = int(rc["ResultCase"]) + for rule in rc.get("Results", []): + for cr in rule.get("CriteriaResults", []): + m = _STIFF_RE.search(cr.get("EntityReference", "")) + if m: + out[(case, m.group(1).strip())] = (cr.get("Variables", {}), cr.get("VariableVectors", {})) + return out + + +def test_identifies_all_panel_groups(manager): + models = manager.capacity_models() + genie = json.loads(MODEL_JSON.read_text()) + assert len(models) == len(genie["BucklingModels"]) == 10 + assert {m.name for m in models} == {bm["Name"] for bm in genie["BucklingModels"]} + + +def test_plate_geometry_matches_model_json(manager): + models = manager.capacity_models() + genie = {bm["Name"]: bm for bm in json.loads(MODEL_JSON.read_text())["BucklingModels"]} + checked = 0 + for m in models: + gplates = {p["Name"]: p["Geometry"] for p in genie[m.name]["Plates"]} + for p in m.plates: + g = gplates[p.name] + assert p.thickness == pytest.approx(g["Thickness"], abs=1e-4) + assert p.length == pytest.approx(g["Length"], rel=1e-3, abs=1e-4) + assert p.width == pytest.approx(g["Width"], rel=1e-3, abs=1e-4) + checked += 1 + assert checked > 30 + + +def test_stiffener_span_matches_plate_length(manager): + models = manager.capacity_models() + genie = {bm["Name"]: bm for bm in json.loads(MODEL_JSON.read_text())["BucklingModels"]} + for m in models: + ref_span = genie[m.name]["Plates"][0]["Geometry"]["Length"] + for s in m.stiffeners: + assert s.span == pytest.approx(ref_span, rel=1e-3, abs=1e-4) + + +def test_dominant_flange_picks_real_flange_of_genie_t_girder(): + """Genie models a T-girder as an unsymmetrical I with a dummy flange + (width == tw, token thickness); the real flange can sit in either slot. + Reference: TG850x300x16x20 arrived as top 16 x 0.1 mm / bottom 300 x 20 mm + and was checked with the dummy (UF 8.13 from a near-zero flange).""" + from ada.fem.capacity.model import dominant_flange + + # real flange in the bottom slot (the TG850x300x16x20 case) + assert dominant_flange(0.016, (0.016, 0.0001), (0.3, 0.02)) == (0.3, 0.02) + # real flange in the top slot + assert dominant_flange(0.016, (0.3, 0.02), (0.016, 0.0001)) == (0.3, 0.02) + # true unsymmetrical I: the larger flange governs + assert dominant_flange(0.012, (0.2, 0.02), (0.15, 0.015)) == (0.2, 0.02) + # no real flange at all (flat bar idealization) + assert dominant_flange(0.016, (0.016, 0.0001), (None, None)) == (0.0, 0.0) + # plain T stored top-only (legacy single-flange path still works) + assert dominant_flange(0.012, (0.12, 0.016)) == (0.12, 0.016) + + +def test_glsec_sections_parse_as_angular_from_sin(): + from ada.fem.formats.sesam.results.read_sin import read_sin_file + from ada.sections.categories import BaseTypes + + sec = read_sin_file(SIN).mesh.sections[3] + assert sec.type == BaseTypes.ANGULAR + assert sec.h == pytest.approx(0.22) + assert sec.t_w == pytest.approx(0.01) + assert sec.w_top == pytest.approx(0.041) + assert sec.t_ftop == pytest.approx(0.02505) + + +def test_sin_source_builds_mini_grid_x100_capacity_models_like_genie(): + from ada.fem.capacity import CapacityManager, SinSource + + native = { + model.name: model + for model in CapacityManager.from_sin(SIN, SinSource(group="Mini_grid_x100")).capacity_models() + } + genie = json.loads(MODEL_JSON.read_text()) + genie_models = {model["Name"]: model for model in genie["BucklingModels"]} + + assert set(native) == set(genie_models) + for name, model in native.items(): + ref = genie_models[name] + native_stiffeners = sorted((s.name, tuple(int(e) for e in s.element_ids)) for s in model.stiffeners) + genie_stiffeners = sorted( + (stiffener["Name"], tuple(int(e) for e in stiffener["FiniteElements"])) for stiffener in ref["Stiffeners"] + ) + native_plates = sorted(tuple(int(e) for e in plate.element_ids) for plate in model.plates) + genie_plates = sorted(tuple(int(e) for e in plate["FiniteElements"]) for plate in ref["Plates"]) + assert native_stiffeners == genie_stiffeners + assert native_plates == genie_plates + + +def test_sin_source_scopes_area_set_to_capacity_grid_like_genie(): + from ada.fem.capacity import CapacityManager, SinSource + + native = CapacityManager.from_sin(SIN, SinSource(group="Mini_area_dbl_btm")).capacity_models() + + assert {model.name for model in native} == { + "panelGroup(Mini_dbl_btm_f0_i1_j1)", + "panelGroup(Mini_dbl_btm_f0_i2_j1)", + "panelGroup(Mini_dbl_btm_f0_i3_j1)", + "panelGroup(Mini_dbl_btm_f0_i4_j1)", + } + assert sum(len(model.stiffeners) for model in native) == 12 + + +def test_mini_group_scopes_the_whole_model(): + """The top-level ``Mini`` set spans the whole model, so scoping to it must + match the unscoped run. + + Regression for the SIN set reader: a large/mixed set is written as several + ``GSETMEMB`` records sharing one set id (and may mix node + element records). + Keying by set id alone kept only the last record, so ``Mini`` resolved to a + fraction of its elements and the check covered only part of the structure. + """ + from ada.fem.capacity import CapacityManager, SinSource + from ada.fem.formats.sesam.results.read_sin import read_sin_file + + mesh = read_sin_file(SIN, step=1).mesh + mini_members = SinSource._set_members(mesh, "Mini") + n_elements = sum(len(block.identifiers) for block in mesh.elements) + assert len(mini_members) == n_elements # every element, not just the last record's chunk + + whole = {m.name for m in CapacityManager.from_sin(SIN, SinSource()).capacity_models()} + scoped = {m.name for m in CapacityManager.from_sin(SIN, SinSource(group="Mini")).capacity_models()} + assert scoped == whole + assert len(whole) > 50 # the full Mini model, not a partial scope + + +def test_sin_source_full_mini_models_are_unique_rectangular_panels(): + import numpy as np + + from ada.fem.capacity import CapacityManager, SinSource + + manager = CapacityManager.from_sin(SIN, SinSource()) + models = manager.capacity_models() + + owners: dict[int, str] = {} + for model in models: + for plate in model.plates: + for element_id in plate.element_ids: + assert ( + element_id not in owners + ), f"shell element {element_id} belongs to both {owners[element_id]} and {model.name}" + owners[element_id] = model.name + + # The geometric merge fuses the per-cell concept fields into maximal + # rectangular stiffened panels (Genie-style), so there are far fewer, larger + # panels than the ~153 atomic cells — without dropping any stiffener and + # while every panel stays a unique, rectangular plate field. + unmerged = CapacityManager.from_sin(SIN, SinSource(merge_panels=False)).capacity_models() + assert len(models) < len(unmerged) + assert 50 <= len(models) < 150 + assert sum(len(model.stiffeners) for model in models) == sum(len(model.stiffeners) for model in unmerged) + assert sum(len(model.stiffeners) for model in models) >= 650 + assert owners + for model in models: + plate_ids = [element_id for plate in model.plates for element_id in plate.element_ids] + beam_ids = [element_id for stiffener in model.stiffeners for element_id in stiffener.element_ids] + assert _plate_field_area_ratio(manager.mesh, plate_ids, beam_ids, np) >= 0.95 + + +def test_sin_source_filters_primary_girders_by_secondary_concept_profile(): + from ada.fem.capacity import SinSource + from ada.fem.capacity.extract import AuxRecords, geono_of + from ada.fem.formats.sesam.results.read_sin import read_sin_file + + mesh = read_sin_file(SIN).mesh + aux = AuxRecords.from_sin(SIN) + + raw = SinSource(group="Mini_grid_x100", classify_secondary=False).groups(mesh, aux) + classified = SinSource(group="Mini_grid_x100").groups(mesh, aux) + + raw_stiffeners = [s for model in raw for s in model.stiffeners] + kept_stiffeners = [s for model in classified for s in model.stiffeners] + raw_geonos = {geono_of(mesh, s.element_ids[0]) for s in raw_stiffeners} + kept_geonos = {geono_of(mesh, s.element_ids[0]) for s in kept_stiffeners} + + assert 5 in raw_geonos # TG600 primary girders are candidates before classification. + assert kept_geonos == {3} # HP220 secondary stiffener profile. + assert len(kept_stiffeners) == 54 + assert any("_gbm" in s.name for s in kept_stiffeners) + + +def test_genie_mirror_roundtrips(manager, tmp_path): + out = tmp_path / "mirror.json" + manager.to_genie_json(out) + data = json.loads(out.read_text()) + assert len(data["BucklingModels"]) == 10 + + +# Panel groups whose plate fields are a single shell element each — the +# resolution is exact here. Multi-element fields (the ``west_main`` groups) need +# the tributary/along-span refinement still under calibration (see +# stress_resolve module docstring), so they're reported but not asserted exact. +_SINGLE_ELEMENT_GROUPS = ("dbl_btm", "west_small") + + +def _is_single_element_group(name: str) -> bool: + return any(tag in name for tag in _SINGLE_ELEMENT_GROUPS) + + +def test_transverse_and_shear_match_genie(manager, genie_variables): + """Resolved transverse-membrane and shear stresses match Genie's. + + Asserts on the *median* relative error over non-trivial values: the membrane + resolution is fundamentally exact, while a tail of (case, stiffener) pairs + where Genie reports the same stiffener under several connections with + different tributaries is not yet disambiguated (a per-connection refinement, + see stress_resolve module docstring). The median is robust to that tail. + """ + import statistics + + resolved = manager.resolve_cases() + matched = 0 + trans_rel: list[float] = [] + shear_rel: list[float] = [] + for rc in resolved: + g = genie_variables.get((rc.result_case, rc.stiffener)) + if g is None: + continue + matched += 1 + gv, gvec = g + gtrans = gvec.get("AverageTransverseMembraneStresses") + if gtrans and abs(gtrans[1]) > 1.0: # skip ~0 references + trans_rel.append(abs(rc.variables["SigmaYSd"] - gtrans[1]) / abs(gtrans[1])) + gtau = gv.get("TauSd", 0.0) + if abs(gtau) > 1.0: + shear_rel.append(abs(rc.variables["TauSd"] - gtau) / abs(gtau)) + + assert matched == 540 + assert len(trans_rel) > 400 and len(shear_rel) > 400 + # Median relative error is at the float-noise floor → the resolution matches. + assert statistics.median(trans_rel) < 1e-4 + assert statistics.median(shear_rel) < 1e-4 + + +def test_rdpoints_sampling_matches_genie_longitudinal_and_transverse_ends(manager, genie_variables): + """RDPOINTS-coordinate sampling closes the single-connection stress residuals. + + The remaining tail is concentrated in the midpoint field average for a few + irregular plate fields, so assert the calibrated median plus per-vector + within-2% fractions. + """ + import statistics + + resolved = manager.resolve_cases() + trans_start_rel: list[float] = [] + trans_end_rel: list[float] = [] + long_rel: list[float] = [] + axial_rel: list[float] = [] + for rc in resolved: + g = genie_variables.get((rc.result_case, rc.stiffener)) + if g is None: + continue + _gv, gvec = g + ours_t = rc.vectors.get("AverageTransverseMembraneStresses", []) + genie_t = gvec.get("AverageTransverseMembraneStresses", []) + if len(ours_t) == 3 and len(genie_t) == 3: + if abs(genie_t[0]) > 1.0: + trans_start_rel.append(abs(ours_t[0] - genie_t[0]) / abs(genie_t[0])) + if abs(genie_t[2]) > 1.0: + trans_end_rel.append(abs(ours_t[2] - genie_t[2]) / abs(genie_t[2])) + + ours_l = rc.vectors.get("AverageLongitudinalMembraneStresses", []) + genie_l = gvec.get("AverageLongitudinalMembraneStresses", []) + if len(ours_l) == 3 and len(genie_l) == 3 and abs(genie_l[1]) > 1.0: + long_rel.append(abs(ours_l[1] - genie_l[1]) / abs(genie_l[1])) + + ours_a = rc.vectors.get("AxialLoads", []) + genie_a = gvec.get("AxialLoads", []) + if len(ours_a) == 3 and len(genie_a) == 3 and abs(genie_a[1]) > 1.0: + axial_rel.append(abs(ours_a[1] - genie_a[1]) / abs(genie_a[1])) + + thresholds = ( + (trans_start_rel, 0.98), + (trans_end_rel, 0.94), + (long_rel, 0.90), + (axial_rel, 0.90), + ) + for residuals, within2 in thresholds: + assert len(residuals) > 400 + assert statistics.median(residuals) < 1e-3 + assert sum(r <= 0.02 for r in residuals) / len(residuals) > within2 + + +def test_irregular_plate_field_sampling_matches_genie_tail(manager, genie_variables): + """Triangular/split adjacent fields use RDPOINTS transforms and full fields.""" + resolved = {(rc.result_case, rc.stiffener): rc for rc in manager.resolve_cases([4, 10])} + + transformed = resolved[(4, "Stiffener_Mini_west_main_f0_i1_j1_sbm1")] + _gv, gvec = genie_variables[(4, transformed.stiffener)] + assert transformed.vectors["AverageTransverseMembraneStresses"][0] == pytest.approx( + gvec["AverageTransverseMembraneStresses"][0], rel=5e-4 + ) + assert transformed.vectors["AverageShearStresses"][2] == pytest.approx(gvec["AverageShearStresses"][2], rel=5e-4) + + split_field = resolved[(10, "Stiffener_Mini_west_main_f0_i2_j2_sbm5")] + _gv, gvec = genie_variables[(10, split_field.stiffener)] + assert split_field.vectors["AverageTransverseMembraneStresses"][2] == pytest.approx( + gvec["AverageTransverseMembraneStresses"][2], rel=5e-4 + ) + assert split_field.vectors["AverageShearStresses"][2] == pytest.approx(gvec["AverageShearStresses"][2], rel=5e-4) + + +def test_axial_loads_preserve_section_5_positions(manager): + """AxialLoads is a Section-5 three-position resultant, not a repeated scalar.""" + resolved = manager.resolve_cases() + non_uniform = [ + rc.vectors["AxialLoads"] + for rc in resolved + if "AxialLoads" in rc.vectors and max(rc.vectors["AxialLoads"]) - min(rc.vectors["AxialLoads"]) > 1e-6 + ] + + assert non_uniform + + +def test_plate_axial_force_matches_membrane_stress_times_tributary(manager): + """eq (5.1) plate part: ``AxialLoadsPlate == sigma_x * t * s`` with the SAME + tributary ``(t, s)`` the capacity check consumes (``model.plates[0]``). + + A mismatch means the stress resolver integrated the plate axial force over a + different plate than the check applies it to — the wrong-tributary / + heterogeneous-panel bug, where a stiffener's force was integrated over (e.g.) + a 40 mm / double-spacing plate while the check used the panel's 10 mm / single + spacing, inflating N_Sd and the beam-column UF several-fold. + """ + models = {(m.id or m.name): m for m in manager.capacity_models()} + checked = 0 + for rc in manager.resolve_cases(): + n_plate = rc.vectors.get("AxialLoadsPlate") + sigma = rc.vectors.get("AverageLongitudinalMembraneStresses") + model = models.get(rc.capacity_model_id) + if not n_plate or not sigma or model is None or not model.plates: + continue + ts = model.plates[0].thickness * model.plates[0].width + for n, s in zip(n_plate, sigma): + assert n == pytest.approx(s * ts, rel=1e-6, abs=1.0) + checked += 1 + assert checked > 0 + + +def test_axial_stress_scalars_are_emitted_from_resolved_compression_force(manager): + """RP scalar inputs carry the resolved axial stress, not an absent/zero value.""" + from ada.fem.capacity.stress_resolve import _section_area_and_centroid + + models = {(m.id or m.name): m for m in manager.capacity_models()} + checked = 0 + for rc in manager.resolve_cases(): + model = models.get(rc.capacity_model_id) + if model is None or not model.plates: + continue + section_area, _centroid = _section_area_and_centroid(model.stiffener(rc.stiffener).section) + area = model.plates[0].thickness * model.plates[0].width + section_area + expected = rc.variables["Nsd"] / area if area else 0.0 + assert rc.variables["SigmaXSd"] == pytest.approx(expected, rel=1e-12, abs=1e-9) + assert rc.variables["SigmaXSd1"] == pytest.approx(expected, rel=1e-12, abs=1e-9) + assert rc.variables["SigmaXSd2"] == pytest.approx(expected, rel=1e-12, abs=1e-9) + checked += 1 + assert checked > 0 + + +def test_section_5_moment_components_are_resolved(manager, genie_variables): + """Beam moment/force vectors are emitted for q_FE in the Section-6 check.""" + import statistics + + resolved = manager.resolve_cases() + beam_moment_rel: list[float] = [] + plate_moment_rel: list[float] = [] + q_fe_nonzero = 0 + for rc in resolved: + g = genie_variables.get((rc.result_case, rc.stiffener)) + if g is None: + continue + gv, gvec = g + ours_bm = rc.vectors.get("MomentsAboutNeutralAxisBeamMoment", []) + genie_bm = gvec.get("MomentsAboutNeutralAxisBeamMoment", []) + ours_pm = rc.vectors.get("MomentsAboutNeutralAxisPlate", []) + genie_pm = gvec.get("MomentsAboutNeutralAxisPlate", []) + if len(ours_bm) == 3 and len(genie_bm) == 3: + for ours, ref in zip(ours_bm, genie_bm): + if abs(ref) > 1.0: + beam_moment_rel.append(abs(ours - ref) / abs(ref)) + if len(ours_pm) == 3 and len(genie_pm) == 3: + for ours, ref in zip(ours_pm, genie_pm): + if abs(ref) > 1.0: + plate_moment_rel.append(abs(ours - ref) / abs(ref)) + if abs(gv.get("QFE", 0.0)) > 1e-9 and "MomentsAboutNeutralAxis" in rc.vectors: + q_fe_nonzero += 1 + + assert q_fe_nonzero > 0 + assert statistics.median(beam_moment_rel) < 1e-3 + assert statistics.median(plate_moment_rel) < 1e-3 + + +def test_shell_pressure_resolves_to_qdir_when_plate_field_is_loaded(manager, monkeypatch): + from ada.fem.capacity import stress_resolve + + aux = manager.aux + models = manager.capacity_models() + aux.pressure_by_case_element = { + 10: {int(element_id): 1200.0 for model in models for plate in model.plates for element_id in plate.element_ids} + } + monkeypatch.setattr(stress_resolve.extract.AuxRecords, "from_sin", staticmethod(lambda _path: aux)) + + resolved = manager.resolve_cases([10]) + + by_group = {model.name: model for model in models} + assert resolved + for rc in resolved: + width = by_group[rc.panel_group].plates[0].width + assert rc.variables["PSd"] == pytest.approx(1200.0) + assert rc.variables["Qdir"] == pytest.approx(1200.0 * width, rel=1e-5) + + +def test_sin_pressure_records_do_not_load_unrelated_capacity_grid(): + from ada.fem.capacity import CapacityManager, SinSource + + manager = CapacityManager.from_sin(SIN, SinSource(group="Mini_grid_x100")) + assert manager.aux.pressure_by_case_element + + resolved = manager.resolve_cases([2, 10]) + + assert resolved + assert all(rc.variables["PSd"] == pytest.approx(0.0) for rc in resolved) + assert all(rc.variables["Qdir"] == pytest.approx(0.0) for rc in resolved) + + +def _plate_field_area_ratio(mesh, plate_ids, beam_ids, np): + from ada.fem.capacity.extract import beam_axis_and_span, element_node_coords + + axis, _span = beam_axis_and_span(mesh, (beam_ids[0],)) + first = element_node_coords(mesh, plate_ids[0]) + normal = np.cross(first[1] - first[0], first[2] - first[0]) + normal = normal / (np.linalg.norm(normal) or 1.0) + perp = np.cross(normal, axis) + perp = perp / (np.linalg.norm(perp) or 1.0) + points = np.vstack([element_node_coords(mesh, element_id) for element_id in plate_ids]) + origin = points.mean(axis=0) + + area = 0.0 + xy_blocks = [] + for element_id in plate_ids: + coords = element_node_coords(mesh, element_id) - origin + xy = np.column_stack((coords @ axis, coords @ perp)) + xy_blocks.append(xy) + x = xy[:, 0] + y = xy[:, 1] + area += 0.5 * abs(float(np.dot(x, np.roll(y, -1)) - np.dot(y, np.roll(x, -1)))) + xy_all = np.vstack(xy_blocks) + bbox_area = float(np.ptp(xy_all[:, 0]) * np.ptp(xy_all[:, 1])) + return area / bbox_area if bbox_area else 0.0 + + +# --------------------------------------------------------------------------- # +# codecheck2 — double bottom with girders in two directions (geometric merge) +# --------------------------------------------------------------------------- # +def _genie_plate_sets(model_json: pathlib.Path) -> set[frozenset]: + bms = json.loads(model_json.read_text())["BucklingModels"] + return {frozenset(int(e) for p in bm["Plates"] for e in p["FiniteElements"]) for bm in bms} + + +def _stiffened_plate_sets(models) -> set[frozenset]: + return {frozenset(e for p in m.plates for e in p.element_ids) for m in models if m.stiffeners} + + +@ref2 +def test_merge_reproduces_genie_double_bottom_panels(): + """The geometric merge rebuilds Genie's merged double-bottom panels exactly. + + Genie splits the girders-in-two-directions double bottom into a grid of cells + and merges them back into full-width stiffened panels. The lateral merge + reproduces a solid majority of those panels element-for-element (the residual + is Genie's irregular opening/triangular cells, intentionally left split). + """ + from ada.fem.capacity import CapacityManager, SinSource + + native = CapacityManager.from_sin(SIN2, SinSource()).capacity_models() + ours = _stiffened_plate_sets(native) + genie = _genie_plate_sets(MODEL_JSON2) + + assert len(genie & ours) >= 45 # of 72 Genie panels; was ~30 before merging + + # A specific full-width merged bay of the double bottom (six i-cells merged + # across the longitudinal girders into one panel) is reproduced exactly. + bms = json.loads(MODEL_JSON2.read_text())["BucklingModels"] + target = next(bm for bm in bms if bm["Name"].startswith("panelGroup(Mini_dbl_btm_f13_i1_j3")) + assert len(target["Plates"]) == 24 # 24 plate fields … + target_set = frozenset(int(e) for p in target["Plates"] for e in p["FiniteElements"]) + assert len(target_set) >= 24 # … spanning ≥ 24 shell elements, full width across the bay + assert target_set in ours + + +@ref2 +def test_merge_collapses_overrun_double_bottom_cells(): + from ada.fem.capacity import CapacityManager, SinSource + + merged = CapacityManager.from_sin(SIN2, SinSource()).capacity_models() + unmerged = CapacityManager.from_sin(SIN2, SinSource(merge_panels=False)).capacity_models() + + assert len(merged) < len(unmerged) + # No stiffener is lost in the merge — only the grouping changes. + assert sum(len(m.stiffeners) for m in merged) == sum(len(m.stiffeners) for m in unmerged) + # Every merged panel is still a single rectangular plate field with unique ownership. + owners: dict[int, str] = {} + for m in merged: + for p in m.plates: + for e in p.element_ids: + assert e not in owners + owners[e] = m.name + + +@ref2 +def test_plate_fields_match_genie_subdivision_on_y_grid(): + """Plate fields are split per stiffener bay, matching Genie field-for-field. + + The ``Mini_grid_y100`` cut (a vertical plane) exposed that the old element-id + run heuristic mis-counted fields (``nP == nS`` instead of ``nS + 1``). The + geometric per-bay split reproduces Genie's field decomposition exactly for + every regular panel, on this orientation as well as the x grid. + """ + from ada.fem.capacity import CapacityManager, SinSource + + ours = {m.name: m for m in CapacityManager.from_sin(SIN2, SinSource(group="Mini_grid_y100")).capacity_models()} + genie = {m["Name"]: m for m in json.loads(MODEL_JSON2.read_text())["BucklingModels"]} + + checked = 0 + for name, model in ours.items(): + if name not in genie: + continue # Genie's opening-split cells (trailing-integer names) are not reproduced + our_fields = sorted(tuple(sorted(int(e) for e in p.element_ids)) for p in model.plates) + genie_fields = sorted(tuple(sorted(int(e) for e in p["FiniteElements"])) for p in genie[name]["Plates"]) + assert our_fields == genie_fields, f"{name}: plate field decomposition differs from Genie" + assert len(model.plates) == len(model.stiffeners) + 1 # one field per inter-stiffener strip + 2 edges + checked += 1 + assert checked >= 6 + + +@ref2 +def test_unstiffened_panels_are_disjoint_rectangular_fields(): + from ada.fem.capacity import CapacityManager, SinSource + + models = CapacityManager.from_sin(SIN2, SinSource(include_unstiffened=True)).capacity_models() + unstiffened = [m for m in models if not m.stiffeners] + stiffened_plates = {e for m in models if m.stiffeners for p in m.plates for e in p.element_ids} + + assert unstiffened # the double bottom has plate fields carrying no secondary stiffener + assert all(m.name.startswith("unstiffenedPanel(") for m in unstiffened) + + seen: set[int] = set() + for m in unstiffened: + for p in m.plates: + for e in p.element_ids: + assert e not in stiffened_plates # unstiffened fields never overlap a stiffened panel + assert e not in seen # and are mutually disjoint + seen.add(e) + + +# --------------------------------------------------------------------------- # +# Load-case combinations (RDRESCMB) → field superposition +# --------------------------------------------------------------------------- # +def test_metadata_exposes_named_load_combinations(): + """The Mini SIN's two RDRESCMB combinations surface with names + factors.""" + from ada.fem.formats.sesam.results.read_sin import read_sin_metadata + + meta = read_sin_metadata(SIN) + assert meta.combination_ids == [9, 10] + assert meta.result_names[9] == "lcc1" + assert meta.result_names[10] == "lcc2" + # lcc1 = 1.2·c1 + 1.1·c2 + 1.0·c5 + 2.0·c6 + 1.3·c8 (zero-factor cases dropped). + lcc1 = meta.combinations[9] + assert set(lcc1) == {1, 2, 5, 6, 8} + assert lcc1[1] == pytest.approx(1.2, rel=1e-6) + assert lcc1[6] == pytest.approx(2.0, rel=1e-6) + # Combinations are selectable cases even though only basic steps are RV*-stored. + assert set(meta.selectable_cases) >= {9, 10} + + +def test_combination_superposition_reproduces_stored_combination(manager): + """Resolving a combination by superposing its basic cases reproduces the + field SESTRA happened to also store for it (Mini stores lcc1/lcc2 as RV* + steps 9/10), to float32 precision for every resolved design variable. + + This is the load-combination path the Ruben "case 14" blocker needs: there + the combination is *not* stored, so superposition is the only route — Mini + just happens to also carry the stored field, giving an exact oracle. + """ + from ada.fem.capacity import extract + from ada.fem.capacity.stress_resolve import _resolve_step + from ada.fem.formats.sesam.results.read_sin import iter_sin_step_results + + models = manager.capacity_models() + superposed = {(c.result_case, c.stiffener): c for c in manager.resolve_cases([9, 10])} + + aux = extract.AuxRecords.from_sin(SIN) + material = {e: (p.material.E, p.material.poisson) for m in models for p in m.plates for e in p.element_ids} + stored: dict[tuple[int, str], object] = {} + mesh = None + geom_cache: dict = {} + for step, res in iter_sin_step_results(SIN, [9, 10]): + mesh = mesh or res.mesh + for rc in _resolve_step(mesh, aux, models, step, res.results, material, geom_cache, log=False): + stored[(rc.result_case, rc.stiffener)] = rc + + keys = set(superposed) & set(stored) + assert len(keys) > 50 # both paths resolve every (case, stiffener) pair + + # Per-variable scale (max |stored| over all pairs) normalises the tolerance so + # near-zero quantities (e.g. QFE on a flat moment field) aren't judged on noise. + scale: dict[str, float] = {} + for k in keys: + for var, val in stored[k].variables.items(): + scale[var] = max(scale.get(var, 0.0), abs(val)) + worst = 0.0 + for k in keys: + a, b = superposed[k], stored[k] + for var, av in a.variables.items(): + worst = max(worst, abs(av - b.variables.get(var, 0.0)) / max(scale[var], 1e-9)) + assert worst < 1e-4 # float32 superposition noise floor + + +def test_resolve_rejects_unknown_case_but_accepts_combinations(manager): + from ada.fem.formats.sesam.results.read_sin import read_sin_metadata + + meta = read_sin_metadata(SIN) + with pytest.raises(ValueError, match="not in the SIN"): + manager.resolve_cases([999]) + # A combination id is accepted (resolved by superposition), not rejected. + resolved = manager.resolve_cases([meta.combination_ids[0]]) + assert resolved and {rc.result_case for rc in resolved} == {meta.combination_ids[0]} diff --git a/tests/core/fem/capacity/test_capacity_sources.py b/tests/core/fem/capacity/test_capacity_sources.py new file mode 100644 index 000000000..0b0a7e7eb --- /dev/null +++ b/tests/core/fem/capacity/test_capacity_sources.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import numpy as np + + +def test_concept_named_stiffener_runs_join_connected_colinear_segments(monkeypatch): + from ada.fem.capacity import extract, sources + + class Index: + elem_nodes = { + 1: (1, 2), + 2: (2, 3), + 3: (10, 11), + 4: (3, 4), + } + + coords = { + 1: np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]), + 2: np.array([[1.0, 0.0, 0.0], [2.0, 0.0, 0.0]]), + 3: np.array([[0.0, 1.0, 0.0], [1.0, 1.0, 0.0]]), + 4: np.array([[2.0, 0.0, 0.0], [2.0, 1.0, 0.0]]), + } + + monkeypatch.setattr(extract, "_ensure_index", lambda _mesh: Index()) + monkeypatch.setattr(extract, "element_node_coords", lambda _mesh, element_id: coords[element_id]) + monkeypatch.setattr(extract, "geono_of", lambda _mesh, _element_id: 7) + + runs = sources._stiffener_runs(None, [1, 2, 3, 4], {1: "S1", 2: "S1", 3: "S1", 4: "S1"}) + + assert runs == [(1, 2), (3,), (4,)] + + +def test_unnamed_stiffener_runs_keep_one_element_fallback(): + from ada.fem.capacity import sources + + assert sources._stiffener_runs(None, [3, 1, 2], {}) == [(3,), (1,), (2,)] + + +def test_named_run_splits_at_transverse_girder(monkeypatch): + """A concept name spans the whole stiffener; the run is cut at supports. + + Four colinear beams (1-4) form one named stiffener; a perpendicular girder + (10, not a secondary stiffener) crosses at the interior node between beams 2 + and 3. The DNV span is the bay between supports, so the run splits there. + """ + from ada.fem.capacity import extract, sources + + class Index: + elem_nodes = {1: (1, 2), 2: (2, 3), 3: (3, 4), 4: (4, 5), 10: (3, 20)} + node_elems = {1: [1], 2: [1, 2], 3: [2, 3, 10], 4: [3, 4], 5: [4], 20: [10]} + + coords = { + 1: np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]), + 2: np.array([[1.0, 0.0, 0.0], [2.0, 0.0, 0.0]]), + 3: np.array([[2.0, 0.0, 0.0], [3.0, 0.0, 0.0]]), + 4: np.array([[3.0, 0.0, 0.0], [4.0, 0.0, 0.0]]), + 10: np.array([[2.0, 0.0, 0.0], [2.0, 1.0, 0.0]]), # perpendicular girder + } + + monkeypatch.setattr(extract, "_ensure_index", lambda _mesh: Index()) + monkeypatch.setattr(extract, "element_node_coords", lambda _mesh, element_id: coords[element_id]) + + runs = sources._split_runs_at_supports( + None, + [(1, 2, 3, 4)], + secondary_ids={1, 2, 3, 4}, + all_line_ids={1, 2, 3, 4, 10}, + all_shell_ids=set(), + ) + + assert runs == [(1, 2), (3, 4)] + + +def test_named_run_without_supports_stays_whole(monkeypatch): + from ada.fem.capacity import extract, sources + + class Index: + elem_nodes = {1: (1, 2), 2: (2, 3), 3: (3, 4)} + node_elems = {1: [1], 2: [1, 2], 3: [2, 3], 4: [3]} + + coords = { + 1: np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]), + 2: np.array([[1.0, 0.0, 0.0], [2.0, 0.0, 0.0]]), + 3: np.array([[2.0, 0.0, 0.0], [3.0, 0.0, 0.0]]), + } + + monkeypatch.setattr(extract, "_ensure_index", lambda _mesh: Index()) + monkeypatch.setattr(extract, "element_node_coords", lambda _mesh, element_id: coords[element_id]) + + runs = sources._split_runs_at_supports( + None, [(1, 2, 3)], secondary_ids={1, 2, 3}, all_line_ids={1, 2, 3}, all_shell_ids=set() + ) + + assert runs == [(1, 2, 3)] + + +def test_flatbar_name_fallback_supplies_capacity_dimensions(): + from ada.fem.capacity.stiffened_plate import _flatbar_from_name + + assert _flatbar_from_name("Fbar150x10") == (0.15, 0.01) + assert _flatbar_from_name("FB325_5x12_5") == (0.3255, 0.0125) + assert _flatbar_from_name("HP180x8") is None diff --git a/tests/core/fem/capacity/test_stress_recovery.py b/tests/core/fem/capacity/test_stress_recovery.py new file mode 100644 index 000000000..84c5f02a2 --- /dev/null +++ b/tests/core/fem/capacity/test_stress_recovery.py @@ -0,0 +1,78 @@ +"""Membrane stress recovery from nodal displacements (plane-stress, closed form).""" + +from __future__ import annotations + +import numpy as np + +from ada.fem.capacity import extract, stress_recovery + +E = 210e9 +NU = 0.3 + + +def _patch_element(monkeypatch, node_ids, coords): + monkeypatch.setattr(extract, "element_node_ids", lambda mesh, eid: node_ids) + monkeypatch.setattr(extract, "element_node_coords", lambda mesh, eid: np.asarray(coords, float)) + + +def test_quad_uniaxial_strain_matches_plane_stress(monkeypatch): + # Unit square in the global xy-plane; displacement u_x = a*x → eps_xx = a. + coords = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)] + _patch_element(monkeypatch, [1, 2, 3, 4], coords) + a = 1e-3 + disp = {nid: np.array([a * coords[i][0], 0.0, 0.0]) for i, nid in enumerate([1, 2, 3, 4])} + + pts = stress_recovery.recover_membrane_corner_points(None, 99, disp, E, NU) + assert len(pts) == 4 + + f = E / (1 - NU * NU) + expected = np.array([f * a, NU * f * a, 0.0]) + for _xyz, tensor in pts: # uniform strain → identical at every corner + np.testing.assert_allclose(tensor, expected, rtol=1e-9, atol=1.0) + + +def test_quad_pure_shear(monkeypatch): + coords = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)] + _patch_element(monkeypatch, [1, 2, 3, 4], coords) + g = 2e-3 # u_x = g*y → gamma_xy = g, eps_xx = eps_yy = 0 + disp = {nid: np.array([g * coords[i][1], 0.0, 0.0]) for i, nid in enumerate([1, 2, 3, 4])} + + pts = stress_recovery.recover_membrane_corner_points(None, 1, disp, E, NU) + G = E / (2 * (1 + NU)) + for _xyz, tensor in pts: + np.testing.assert_allclose(tensor, [0.0, 0.0, G * g], rtol=1e-9, atol=1.0) + + +def test_tri_constant_strain(monkeypatch): + coords = [(0, 0, 0), (1, 0, 0), (0, 1, 0)] + _patch_element(monkeypatch, [1, 2, 3], coords) + a = 1e-3 + disp = {nid: np.array([a * coords[i][0], 0.0, 0.0]) for i, nid in enumerate([1, 2, 3])} + + pts = stress_recovery.recover_membrane_corner_points(None, 7, disp, E, NU) + assert len(pts) == 3 + f = E / (1 - NU * NU) + for _xyz, tensor in pts: + np.testing.assert_allclose(tensor, [f * a, NU * f * a, 0.0], rtol=1e-9, atol=1.0) + + +def test_local_frame_rotation_invariance(monkeypatch): + # Same uniaxial state, but the element lies in a tilted plane and the load is + # applied along its first edge: recovered local-frame stress is unchanged. + coords = [(0, 0, 0), (1, 1, 1), (0, 2, 2), (-1, 1, 1)] + _patch_element(monkeypatch, [1, 2, 3, 4], coords) + ex = np.array(coords[1]) - np.array(coords[0]) + ex = ex / np.linalg.norm(ex) + a = 1e-3 + disp = {nid: a * float(np.dot(np.array(coords[i]), ex)) * ex for i, nid in enumerate([1, 2, 3, 4])} + + pts = stress_recovery.recover_membrane_corner_points(None, 3, disp, E, NU) + f = E / (1 - NU * NU) + expected = np.array([f * a, NU * f * a, 0.0]) + for _xyz, tensor in pts: + np.testing.assert_allclose(tensor, expected, rtol=1e-6, atol=10.0) + + +def test_unsupported_shape_returns_empty(monkeypatch): + _patch_element(monkeypatch, [1, 2], [(0, 0, 0), (1, 0, 0)]) + assert stress_recovery.recover_membrane_corner_points(None, 1, {}, E, NU) == [] diff --git a/tests/core/fem/results/test_fea_artefacts.py b/tests/core/fem/results/test_fea_artefacts.py index 5a5636675..e1ad45c63 100644 --- a/tests/core/fem/results/test_fea_artefacts.py +++ b/tests/core/fem/results/test_fea_artefacts.py @@ -1004,6 +1004,23 @@ def test_build_manifest_omits_history_when_empty(monkeypatch): assert "history" not in manifest +def test_build_manifest_includes_optional_source_hash(): + from ada.fem.results.artefacts import MeshGeometry, build_manifest + + geom = MeshGeometry( + points=np.zeros((1, 3), dtype=np.float64), + cell_blocks=[], + ) + manifest = build_manifest( + src="dummy.SIN", + source_sha256="abc123", + mesh_geom=geom, + mesh_glb_filename="fea.mesh.glb", + field_metas=[], + ) + assert manifest["source_sha256"] == "abc123" + + def test_classify_field_by_name(): """Spot-check the name-based fallback so unfamiliar solvers get a sensible category. The bake-level test asserts the manifest diff --git a/tests/core/materials/test_materials_container.py b/tests/core/materials/test_materials_container.py new file mode 100644 index 000000000..3890598f1 --- /dev/null +++ b/tests/core/materials/test_materials_container.py @@ -0,0 +1,17 @@ +from ada import Material +from ada.api.containers.materials import Materials + + +def test_add_material_with_existing_idless_materials_allocates_numeric_id(): + mats = Materials([Material("A"), Material("B")]) + + assert len(mats.id_map) == 2 + assert mats.get_by_name("A").id == 1 + assert mats.get_by_name("B").id == 2 + + copied_mat = Material("A").copy_to(new_name="A_density_factor") + added_mat = mats.add(copied_mat) + + assert added_mat is copied_mat + assert added_mat.id == 3 + assert mats.get_by_id(3) is added_mat diff --git a/tests/fem/capacity/test_girder_adjacent_spans.py b/tests/fem/capacity/test_girder_adjacent_spans.py new file mode 100644 index 000000000..f87932bb3 --- /dev/null +++ b/tests/fem/capacity/test_girder_adjacent_spans.py @@ -0,0 +1,9 @@ +from ada.fem.capacity.girder import _adjacent_stiffener_spans + + +def test_adjacent_stiffener_spans_from_two_sides() -> None: + assert _adjacent_stiffener_spans({-1: 1.3, 1: 1.625}, 2.925) == (2.6, 3.25) + + +def test_adjacent_stiffener_spans_mirrors_single_side() -> None: + assert _adjacent_stiffener_spans({1: 1.4}, 2.8) == (2.8, 2.8)