diff --git a/deploy/Dockerfile.viewer b/deploy/Dockerfile.viewer index f26724dab..be8ab75c5 100644 --- a/deploy/Dockerfile.viewer +++ b/deploy/Dockerfile.viewer @@ -23,7 +23,7 @@ # MUST stay >=0.9.0 — older bases ship an emscripten 3.1.58 wheel that micropip # refuses against pyodide 0.29.4 ("Wheel was built with Emscripten v3.1.58 but # Pyodide was built with v4.0.9"). -ARG ADACPP_BASE_IMAGE=ghcr.io/krande/adacpp-wasm-base:0.16.1 +ARG ADACPP_BASE_IMAGE=ghcr.io/krande/adacpp-wasm-base:0.18.0 FROM ${ADACPP_BASE_IMAGE} AS adacpp-wheel # Stage 0b: build the pure-python adapy wheel for pyodide. The browser diff --git a/deploy/Dockerfile.worker b/deploy/Dockerfile.worker index f39446a0d..92e5cbc05 100644 --- a/deploy/Dockerfile.worker +++ b/deploy/Dockerfile.worker @@ -8,6 +8,10 @@ # meaningfully shrink things. # # Rebuild marker (bump to re-pull the ADACPP_BRANCH overlay fresh): +# 2026-07-19 — ADACPP_BRANCH switched to feat/step-emit-mapped-instances (AP242 assembly +# instancing + the closed-B-spline-edge / periodic-domain tessellation fixes that zero the +# dropped-face warnings on the NURBS-heavy corpus files). The previous value +# (fix/step-emit-degenerate-loop) is fully contained in the pinned 0.17.1. # 2026-07-15 — overlay retired. Everything the previous markers were previewing (selectable # tessellation tracks, boundary pinning ON by default, the watertight `cdt` track) shipped in # ada-cpp 0.14.0, which pixi.toml now pins. The worker takes it from conda. diff --git a/files/sat_files/degenerate_first_loop.sat b/files/sat_files/degenerate_first_loop.sat new file mode 100644 index 000000000..2b8202711 --- /dev/null +++ b/files/sat_files/degenerate_first_loop.sat @@ -0,0 +1,106 @@ +400 0 1 0 +14 ada-cpp-test 12 minimal-fixture 1 0 +-1 9.9999999999999995e-07 1e-10 +-75 vertex $146 -1 -1 $-1 $-1 $147 # +-147 point $-1 -1 -1 $-1 62.600000000000023 28.313333333333301 0.30000000000002874 # +-375 edge $531 -1 -1 $-1 $75 1 $75 0 $532 $-1 forward @7 unknown T 62.600000000000023 28.313333333333301 0.30000000000002874 62.600000000000023 28.313333333333301 0.30000000000002874 # +-532 coedge $-1 -1 -1 $-1 $532 $532 $-1 $375 forward $731 $-1 # +-731 loop $-1 -1 -1 $-1 $997 $532 $998 T 62.600000000000023 28.313333333333301 0.30000000000002874 62.600000000000023 28.313333333333301 0.30000000000002874 hole $999 F # +-997 loop $-1 -1 -1 $-1 $-1 $1372 $998 T 62.600000000000009 0 -0.25 62.600000000000009 29.600000000000001 1.375 periphery $999 F # +-998 face $1373 -1 -1 $-1 $1374 $731 $2 $-1 $999 forward double out T 62.600000000000009 0 -0.25 62.600000000000023 29.600000000000001 1.375 F # +-999 plane-surface $-1 -1 -1 $-1 62.600000000000023 14.800000000000008 0.56249999999999289 -1 -8.224661348075122e-31 4.5280583383210426e-16 8.5019249555939899e-31 -1 6.123233995736766e-17 forward_v I I I I # +-1372 coedge $-1 -1 -1 $-1 $1871 $1872 $1873 $1874 forward $997 $-1 # +-1871 coedge $-1 -1 -1 $-1 $2547 $1372 $2548 $2549 reversed $997 $-1 # +-1872 coedge $-1 -1 -1 $-1 $1372 $2547 $2550 $2551 forward $997 $-1 # +-1874 edge $2556 -1 -1 $-1 $2557 0 $2558 1.625 $1372 $2559 forward @7 unknown T 62.600000000000009 0 -0.25 62.600000000000009 0 1.375 # +-2547 coedge $-1 -1 -1 $-1 $1872 $1871 $3434 $3461 reversed $997 $-1 # +-2549 edge $3464 -1 -1 $-1 $3446 0 $2558 29.600000000000001 $1871 $3465 forward @7 unknown T 62.600000000000009 0 1.375 62.600000000000009 29.600000000000001 1.375 # +-2551 edge $3467 -1 -1 $-1 $3468 0 $2557 29.600000000000001 $2550 $3469 forward @7 unknown T 62.600000000000009 0 -0.25 62.600000000000009 29.600000000000001 -0.25 # +-2557 vertex $3482 -1 -1 $-1 $3475 $3483 # +-2558 vertex $3484 -1 -1 $-1 $1874 $3485 # +-2559 intcurve-curve $-1 -1 -1 $-1 forward { exactcur full nubs 3 open 2 + 0 3 1.625 3 + 62.600000000000009 0 -0.25 + 62.600000000000009 0 0.2916666666666714 + 62.600000000000009 0 0.8333333333333357 + 62.600000000000009 0 1.375 + 0 + null_surface + null_surface + nullbs + nullbs + -1 + -1 + I I + 0 + 0 + 0 + + -1 + none F F 1 F 0 } I I # +-3446 vertex $4557 -1 -1 $-1 $3461 $4558 # +-3461 edge $4576 -1 -1 $-1 $3468 0 $3446 1 $3434 $4577 forward @7 unknown T 62.600000000000009 29.600000000000001 -0.25 62.600000000000009 29.600000000000001 1.375 # +-3465 intcurve-curve $-1 -1 -1 $-1 forward { exactcur full nubs 3 open 2 + 0 3 29.600000000000001 3 + 62.600000000000009 29.600000000000001 1.375 + 62.600000000000009 19.733333333333338 1.375 + 62.600000000000009 9.8666666666666671 1.375 + 62.600000000000009 0 1.375 + 0 + null_surface + null_surface + nullbs + nullbs + -1 + -1 + I I + 0 + 0 + 0 + + -1 + none F F 1 F 0 } I I # +-3468 vertex $4581 -1 -1 $-1 $4580 $4582 # +-3469 intcurve-curve $-1 -1 -1 $-1 forward { exactcur full nubs 3 open 2 + 0 3 29.600000000000001 3 + 62.600000000000009 29.600000000000001 -0.25 + 62.600000000000009 19.733333333333338 -0.25 + 62.600000000000009 9.8666666666666671 -0.25 + 62.600000000000009 0 -0.25 + 0 + null_surface + null_surface + nullbs + nullbs + -1 + -1 + I I + 0 + 0 + 0 + + -1 + none F F 1 F 0 } I I # +-3483 point $-1 -1 -1 $-1 62.600000000000009 0 -0.25 # +-3485 point $-1 -1 -1 $-1 62.600000000000009 0 1.375 # +-4558 point $-1 -1 -1 $-1 62.600000000000009 29.600000000000001 1.375 # +-4577 intcurve-curve $-1 -1 -1 $-1 forward { exactcur full nubs 1 open 2 + 0 1 1 1 + 62.600000000000009 29.600000000000001 -0.25 + 62.600000000000009 29.600000000000001 1.375 + 0 + null_surface + null_surface + nullbs + nullbs + -1 + -1 + I I + 0 + 0 + 0 + + -1 + none F F 1 F 0 } I I # +-4582 point $-1 -1 -1 $-1 62.600000000000009 29.600000000000001 -0.25 # +End-of-ACIS-data diff --git a/pixi.lock b/pixi.lock index 4f4fce33b..7a897acce 100644 --- a/pixi.lock +++ b/pixi.lock @@ -2757,6 +2757,689 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_38.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.51.36231-h1b9f54f_38.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_38.conda + ifc-validation: + channels: + - url: https://conda.anaconda.org/conda-forge/ + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.14.1-py312h5d8c7f2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.16.1-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aom-3.14.1-pl5321h039972f_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.6-he440d0b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.8-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-he90730b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cgal-cpp-6.2-h8e6ac34_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cli11-2.6.2-h54a6638_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.3.3-py312h0a2e395_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cyrus-sasl-2.1.28-hac629b4_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h24cb091_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/double-conversion-3.4.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/eigen-5.0.1-hc65338a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/eigen-abi-5.0.1.80-hf414acd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/expat-2.8.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ffmpeg-8.1.2-gpl_h54862ce_904.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fmt-12.1.0-hff5e90c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.18.1-h27c8c51_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.63.0-py312h8a5da7c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/freeglut-3.2.2-h215f996_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/freeimage-3.18.0-hd1b7436_25.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.3-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.8.0-py312h447239a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.7-h2b0a6b4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/geos-3.14.1-h480dda7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gl2ps-1.4.2-h36e74d4_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/glew-2.3.0-h71661d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/glslang-16.3.0-h96af755_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gmp-6.3.0-hac33072_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.15-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-14.2.1-ha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf4-4.2.15-h2a13503_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h19486de_110.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ifcopenshell-0.8.5-py312h4f5b897_6.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/imath-3.2.2-hde8ca8f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/intel-gmmlib-22.10.0-hb700be7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/intel-media-driver-26.1.6-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/jasper-4.2.9-h1588d4d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/jsoncpp-1.9.8-h171cf75_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/jxrlib-1.1-hd590300_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.5.0-py312h0a2e395_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbde042b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lame-3.100-h166bdaf_1003.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.19.1-h0c24ade_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.46.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/level-zero-1.29.0-hb700be7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260526.0-cxx17_h7b12aa8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libaec-1.1.5-h088129d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libass-0.17.5-h434b012_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-8_h4a7cf45_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libboost-1.90.0-hd24cca6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libboost-devel-1.90.0-hfcd1e18_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libboost-headers-1.90.0-ha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcap-2.78-hd0affe5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-8_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h7a8fb5f_6.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.21.0-hae6b9f4_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdovi-3.4.0-ha23c83e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.127-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-devel-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libflac-1.5.0-he200343_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h73754d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-devel-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.88.2-h0d30a3d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglu-9.0.3-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-devel-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libharfbuzz-14.2.1-h17a8019_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libharfbuzz-devel-14.2.1-h17a8019_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.13.0-default_he001693_1000.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libhwy-1.4.0-h3a9caae_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.2.0-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.12.0-h174a0a3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-8_h47877c9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm22-22.1.8-hf7376ad_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-devel-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnetcdf-4.10.1-nompi_h3fa17b5_201.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libntlm-1.8-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.5-hd0c01bc_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.33-pthreads_h94d23a6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopengl-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopengl-devel-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-2026.2.1-h1f0fae8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-auto-batch-plugin-2026.2.1-h7e124b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-auto-plugin-2026.2.1-h7e124b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-hetero-plugin-2026.2.1-hd41364c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-cpu-plugin-2026.2.1-h1f0fae8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-gpu-plugin-2026.2.1-h1f0fae8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-npu-plugin-2026.2.1-h1f0fae8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-ir-frontend-2026.2.1-hd41364c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-onnx-frontend-2026.2.1-h607c73d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-paddle-frontend-2026.2.1-h607c73d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-pytorch-frontend-2026.2.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-frontend-2026.2.1-h21c0c73_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-lite-frontend-2026.2.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopus-1.6.1-h280c20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.19-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libplacebo-7.360.1-hc50b9dd_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.58-h421ea60_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpq-18.4-hd5a49e9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-7.35.1-h2840a7c_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpsl-0.22.0-h49b2146_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libraqm-0.10.5-h6406941_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libraw-0.22.2-h074291d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.62.3-h4c96295_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsndfile-1.2.2-hc7d488a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.3-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-257.13-h084b8d7_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libtheora-1.1.1-h4ab18f5_1006.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.2-h9d88235_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libudev1-257.13-h084b8d7_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libunwind-1.8.3-h65a8314_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liburing-2.14-hb700be7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libusb-1.0.29-h73b1eb8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.2-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libva-2.24.1-he1eb515_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h54a6638_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libvpl-2.16.0-h54a6638_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libvpx-1.15.2-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libvulkan-loader-1.4.341.0-h5279c79_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.13.2-hca5e8e5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbfile-1.2.0-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.3-hca6bf5a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.3-h49c6c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzip-1.11.2-h6991a6a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.11.0-py312hc1765e9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mesalib-26.0.3-h8cca3c9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mpfr-4.2.2-he0a73b1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.9-hc50e24c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.2.1-py312h0a2e395_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.7.1-py312h8a5da7c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.5.1-py312h33ff503_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/occt-8.0.0-all_hbdb0871_202.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ocl-icd-2.3.4-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/opencl-headers-2025.06.13-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openexr-3.4.13-hf734b31_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openh264-2.6.0-h65dd3cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openjph-0.30.1-h8d634f6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openldap-2.6.13-hbde042b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hda50119_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.3.0-py312h50c33e8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/proj-9.8.1-he0df7b0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/propcache-0.5.2-py312h8a5da7c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pugixml-1.15-h3f63f65_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-h9a6aba3_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.13-hd63d673_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/qhull-2020.2-h434a139_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.11.1-pl5321h16c4a6b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rapidjson-1.1.0.post20240409-h3f2d84a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rocksdb-10.6.2-h13d676d_0_default.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sdl2-2.32.56-h54a6638_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sdl3-3.4.12-hdeec2a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/shaderc-2026.3-h1b60276_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/shapely-2.1.2-py312h383787d_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/spirv-tools-2026.2-hb700be7_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.53.3-hbc0de68_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/svt-av1-4.2.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tbb-2023.0.0-hab88423_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tbb-devel-2023.0.0-hab88423_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd70dff1_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-17.0.1-py312h4c3975b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/utfcpp-4.09-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/viskores-1.1.1-cpu_hc82bd48_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/vtk-9.6.2-py312h244374b_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/vtk-base-9.6.2-py312h3c33751_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/vtk-io-ffmpeg-9.6.2-py312h47b46e0_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.26.0-hd6090a7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/x264-1!164.3095-h166bdaf_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/x265-3.5-h924138e_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-keysyms-0.4.1-hb711507_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-renderutil-0.3.10-hb711507_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-wm-0.4.2-hb711507_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.48-h280c20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.13-he1eb515_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcomposite-0.4.7-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdamage-1.1.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.7-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.2-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxi-1.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.5-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxscrnsaver-1.2.4-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxshmfence-1.3.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxt-1.3.1-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxxf86vm-1.1.7-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-xorgproto-2025.1-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.24.2-py312h8a5da7c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zfp-1.0.1-h909a3a2_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.3-hceb46e0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.7.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.1.1-pyhc364b38_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.49-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wslink-2.5.7-pyhd8ed1ab_0.conda + osx-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.7.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.1.1-pyhc364b38_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wslink-2.5.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aiohttp-3.14.1-py312h0b1c2f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aom-3.14.1-pl5321h3d58d69_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/blosc-1.21.6-hd145fbb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-1.2.0-hf139dec_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-bin-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/c-ares-1.34.8-ha1e9b39_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cairo-1.18.4-h7656bdc_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cgal-cpp-6.2-h2a11f3b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cli11-2.6.2-h2fb4741_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/contourpy-1.3.3-py312hb0c38da_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cyrus-sasl-2.1.28-h7cc0300_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/dav1d-1.2.1-h0dc2134_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/dbus-1.16.2-h6e7f9a9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/double-conversion-3.4.0-hcc62823_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/eigen-5.0.1-h4ff50a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/eigen-abi-5.0.1.80-h969a130_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/expat-2.8.1-hcc62823_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ffmpeg-8.1.2-gpl_he601db1_104.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/fmt-12.1.0-hda137b5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/fontconfig-2.18.1-h7a4440b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/fonttools-4.63.0-py312heb39f77_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/freeimage-3.18.0-h0f7f9a8_25.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.14.3-h694c41f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/fribidi-1.0.16-h8616949_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/frozenlist-1.8.0-py312h90c63f8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/gdk-pixbuf-2.44.7-hae309b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/geos-3.14.1-he483b9e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/gflags-2.2.2-hac325c4_1005.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/glew-2.3.0-h19ec1ea_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/glslang-16.3.0-he78e680_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-hf036a51_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/graphite2-1.3.15-hcc62823_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/harfbuzz-14.2.1-h694c41f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/hdf4-4.2.15-h8138101_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/hdf5-1.14.6-nompi_h13accda_110.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ifcopenshell-0.8.5-py312h743479f_6.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/imath-3.2.2-h6337977_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/jasper-4.2.9-hbeb4536_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/jsoncpp-1.9.8-hebe015c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/jxrlib-1.1-h10d778d_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/kiwisolver-1.5.0-py312hb1dc2e7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.22.2-h3ddfcb2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lame-4.0-h6c71e33_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lcms2-2.19.1-h5ea7634_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lerc-4.1.0-h35c7297_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20260526.0-cxx17_h1a06613_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libaec-1.1.5-he7c3a48_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libass-0.17.5-hf78704f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.11.0-8_he492b99_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libboost-1.90.0-h5950822_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libboost-devel-1.90.0-hd676150_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libboost-headers-1.90.0-h694c41f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.11.0-8_h9b27e0a_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.21.0-h06afde3_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.8-h19cb2f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.25-h517ebb2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libdovi-3.4.0-h59a3c7f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.1-hcc62823_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype-2.14.3-h694c41f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype6-2.14.3-h58fbd8d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgcc-15.2.0-h08519bb_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran-15.2.0-h7e5c614_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-15.2.0-hd16e46c_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libglib-2.88.2-hf28f236_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libharfbuzz-14.2.1-h97ceea7_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libharfbuzz-devel-14.2.1-h97ceea7_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libhwloc-2.13.0-default_h4e3125e_1000.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libhwy-1.4.0-h7747b51_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libintl-0.25.1-h3184127_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.2.0-ha1e9b39_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libjxl-0.12.0-h473410d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblapack-3.11.0-8_h859234e_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libllvm22-22.1.8-hab754da_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-devel-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libnetcdf-4.10.1-nompi_h565eff6_201.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.68.1-h70048d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libntlm-1.8-h6e16a3a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libogg-1.3.5-he3325bb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.33-openmp_h9e49c7b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-2026.2.1-h96313a9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-batch-plugin-2026.2.1-h4a57bf7_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-plugin-2026.2.1-h4a57bf7_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-hetero-plugin-2026.2.1-h5d0de11_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-intel-cpu-plugin-2026.2.1-h96313a9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-ir-frontend-2026.2.1-h5d0de11_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-onnx-frontend-2026.2.1-h8456301_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-paddle-frontend-2026.2.1-h8456301_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-pytorch-frontend-2026.2.1-h409306b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-frontend-2026.2.1-h10fe5cc_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-lite-frontend-2026.2.1-h409306b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopus-1.6.1-hc6ced15_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libplacebo-7.360.1-h1ed201f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.58-he930e7c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libpq-18.4-h5935a4f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-7.35.1-h087ac9f_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libpsl-0.22.0-ha9fe4b0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libraqm-0.10.5-h1888d0e_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libraw-0.22.2-h3650196_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/librsvg-2.62.3-h7321050_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.3-h8f8c405_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.1-hed3591d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libtheora-1.1.1-hfdf4475_1006.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.2-h95d6d7f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libusb-1.0.29-h2287256_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libvorbis-1.3.7-ha059160_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libvpx-1.15.2-heffb93a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libvulkan-loader-1.4.341.0-ha6bc089_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.6.0-hb807250_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxcb-1.17.0-hf1f96e2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-16-2.15.3-h7a90416_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.15.3-h953d39d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzip-1.11.2-h31df5bb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-22.1.8-h0d3cbff_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lz4-c-1.10.0-h240833e_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/matplotlib-base-3.11.0-py312ha85c1d5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/mesalib-26.0.3-hd99e324_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/mpfr-4.2.2-h31caf2d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/mpg123-1.32.9-h78e78a4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/msgpack-python-1.2.1-py312hb1dc2e7_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/multidict-6.7.1-py312hfc03ebc_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/nlohmann_json-3.12.0-h06076ce_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/numpy-2.5.1-py312h746d82c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/occt-8.0.0-all_hb6057d6_202.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openexr-3.4.13-h3ec6c1b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openh264-2.6.0-hd629203_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openjpeg-2.5.4-h52bb76a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openjph-0.30.1-h2c0e27e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openldap-2.6.13-h2f5043c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.3-hc881268_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pango-1.56.4-hf280016_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pcre2-10.47-h13923f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pillow-12.3.0-py312he84af14_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pixman-0.46.4-h2fb4741_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/proj-9.8.1-he69a98e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/propcache-0.5.2-py312heb39f77_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pthread-stubs-0.4-h00291cd_1002.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pugixml-1.15-h46091d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.12.13-ha9537fe_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/qhull-2020.2-h3c5361c_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/qt6-main-6.11.1-pl5321h64d128d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/rapidjson-1.1.0.post20240409-h92383a6_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/rocksdb-10.6.2-h5fc20da_0_default.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/sdl2-2.32.56-h2fb4741_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/sdl3-3.4.12-hf9078ff_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/shaderc-2026.3-h3d334e4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/shapely-2.1.2-py312hd8edc82_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/snappy-1.2.2-h01f5ddf_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/spirv-tools-2026.2-hc1436ee_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/sqlite-3.53.3-h6775aab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/svt-av1-4.2.0-hcc62823_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tbb-2023.0.0-he3dc2fa_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tbb-devel-2023.0.0-he3dc2fa_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hb794df6_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/unicodedata2-17.0.1-py312h1a1c95f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/utfcpp-4.09-h694c41f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/viskores-1.1.1-cpu_h791b1e3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/vtk-9.6.2-py312h2320ed6_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/vtk-base-9.6.2-py312h972fd0c_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/vtk-io-ffmpeg-9.6.2-py312haeed73e_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/x264-1!164.3095-h775f41a_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/x265-3.5-hbb4e6a2_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/xorg-libxau-1.0.12-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xorg-libxdmcp-1.1.5-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/yarl-1.24.2-py312heb39f77_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zfp-1.0.1-h1b13a81_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-ng-2.3.3-h8bce59a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.7.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.1.1-pyhc364b38_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wslink-2.5.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aiohttp-3.14.1-py312h6b91d65_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aom-3.14.1-pl5321h06fc181_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/blosc-1.21.6-hfd34d9b_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.2.0-h2d644bc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h477c42c_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cgal-cpp-6.2-h29dcab7_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cli11-2.6.2-h5112557_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/contourpy-1.3.3-py312h78d62e6_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/dav1d-1.2.1-hcfcfb64_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/double-conversion-3.4.0-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/eigen-5.0.1-h5112557_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/eigen-abi-5.0.1.100-hc11354d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/expat-2.8.1-hac47afa_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ffmpeg-8.1.2-gpl_he504192_904.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fmt-12.1.0-h7f4e812_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.18.1-hd47e2ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fonttools-4.63.0-py312h05f76fc_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/freeglut-3.2.2-hac47afa_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/freeimage-3.18.0-h81f0cfa_25.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.3-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fribidi-1.0.16-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/frozenlist-1.8.0-py312hfdf67e6_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/gdk-pixbuf-2.44.7-h1f5b9c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/geos-3.14.1-hdade9fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/gflags-2.2.2-he0c23c2_1005.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/gl2ps-1.4.2-h26c196c_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/glew-2.3.0-hdfd1c44_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/glib-2.88.2-h395db07_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/glib-tools-2.88.2-h74ecf4c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/glslang-16.3.0-h294ba9c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/gmp-6.3.0-hfeafd45_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.15-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/harfbuzz-14.2.1-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/hdf4-4.2.15-h5557f11_7.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.6-nompi_hae35d4c_110.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/icu-78.3-h637d24d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ifcopenshell-0.8.5-py312h35b9407_6.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/imath-3.2.2-h1608b31_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/jasper-4.2.9-h8ad263b_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/jsoncpp-1.9.8-h477610d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/jxrlib-1.1-hcfcfb64_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.5.0-py312h78d62e6_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h719d79b_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lame-4.0-h4a41cd3_0.conda + - 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/libboost-1.90.0-h9dfe17d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-devel-1.90.0-h1c1089f_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-headers-1.90.0-h57928b3_1.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/libcurl-8.21.0-h51a1c48_2.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_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.3-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.3-hdbac1cb_1.conda + - 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.2-h7ce1215_0.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/libharfbuzz-14.2.1-h03b5201_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libharfbuzz-devel-14.2.1-h03b5201_1.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/libhwy-1.4.0-h2419aca_0.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/libintl-devel-0.22.5-h5728263_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.2.0-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjxl-0.12.0-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/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/libnetcdf-4.10.1-nompi_he1c4404_201.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/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/libpsl-0.22.0-h25e0afd_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libraqm-0.10.5-h50d6d30_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libraw-0.22.2-h345c428_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/librsvg-2.62.3-h15cfe45_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.3-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libtheora-1.1.1-hc70643c_1006.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.2-h8f73337_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libusb-1.0.29-h1839187_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libvorbis-1.3.7-h5112557_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libvulkan-loader-1.4.341.0-h477610d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.6.0-h4d5522a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxcb-1.17.0-h0e4246c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.3-h3cfd58e_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.3-h8ef44ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzip-1.11.2-h3135430_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-22.1.8-h4fa8253_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/matplotlib-base-3.11.0-py312hcbcab24_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mesalib-26.0.3-h667bac9_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.1.0-hac47afa_233.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/mpg123-1.32.9-h01009b0_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/msgpack-python-1.2.1-py312h78d62e6_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.5.1-py312ha3f287d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/occt-8.0.0-all_h8ecc14b_202.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.1.0-h57928b3_233.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openexr-3.4.13-hd20f895_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openh264-2.6.0-h1eab103_1.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.30.1-hf13a347_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.3-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pango-1.56.4-h13911b6_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.47-hd2b5f0e_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pillow-12.3.0-py312h31f0997_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.4-h5112557_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/proj-9.8.1-hd30e2cd_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/propcache-0.5.2-py312h05f76fc_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pthread-stubs-0.4-h0e40799_1002.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pugixml-1.15-h372dad0_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.13-h0159041_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/qhull-2020.2-hc790b64_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/qt6-main-6.11.1-pl5321hfcac499_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rapidjson-1.1.0.post20240409-he0c23c2_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rocksdb-10.6.2-h6b9cf66_0_default.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/sdl2-2.32.56-h5112557_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/sdl3-3.4.12-h5112557_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/shaderc-2026.3-hbcac29b_0.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/spirv-tools-2026.2-h49e36cd_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/sqlite-3.53.3-hdb435a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/svt-av1-4.2.0-hac47afa_0.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/tbb-devel-2023.0.0-h7c1ad13_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h967ab96_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/unicodedata2-17.0.1-py312he06e257_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/utfcpp-4.09-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_39.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.51.36231-h1b9f54f_39.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_39.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/viskores-1.1.1-cpu_h4b717ef_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.51.36231-h84cd919_39.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vtk-9.6.2-py312h5ad3caf_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vtk-base-9.6.2-py312h3243052_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vtk-io-ffmpeg-9.6.2-py312h858e5c0_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/x264-1!164.3095-h8ffe710_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/x265-3.5-h2d74725_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxau-1.0.12-hba3369d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.5-hba3369d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/yarl-1.24.2-py312h05f76fc_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zfp-1.0.1-h2f0f97f_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-ng-2.3.3-h0261ad2_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda jupyterlite: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -7546,7 +8229,7 @@ environments: packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ada-cpp-0.16.1-py312h29cc896_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ada-cpp-0.18.0-py312h29cc896_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.13.5-py312h5d8c7f2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.16-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/aom-3.9.1-hac33072_0.conda @@ -8175,7 +8858,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/xsdata-26.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ada-cpp-0.16.1-py312h9b4feea_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ada-cpp-0.18.0-py312h9b4feea_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/aiohttp-3.13.5-py312h0b1c2f0_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/aom-3.9.1-hf036a51_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/argon2-cffi-bindings-25.1.0-py312h80b0991_2.conda @@ -8573,7 +9256,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/xsdata-26.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-20_gnu.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ada-cpp-0.16.1-py312h710a262_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ada-cpp-0.18.0-py312h710a262_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/aiohttp-3.13.5-py312h6b91d65_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/aom-3.9.1-he0c23c2_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py312he06e257_2.conda @@ -8785,7 +9468,7 @@ environments: packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ada-cpp-0.16.1-py312h29cc896_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ada-cpp-0.18.0-py312h29cc896_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.13.5-py312h5d8c7f2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.16-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/aom-3.9.1-hac33072_0.conda @@ -9414,7 +10097,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/xsdata-26.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ada-cpp-0.16.1-py312h9b4feea_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ada-cpp-0.18.0-py312h9b4feea_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/aiohttp-3.13.5-py312h0b1c2f0_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/aom-3.9.1-hf036a51_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/argon2-cffi-bindings-25.1.0-py312h80b0991_2.conda @@ -9812,7 +10495,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/xsdata-26.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-20_gnu.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ada-cpp-0.16.1-py312h710a262_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ada-cpp-0.18.0-py312h710a262_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/aiohttp-3.13.5-py312h6b91d65_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/aom-3.9.1-he0c23c2_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py312he06e257_2.conda @@ -10294,7 +10977,7 @@ environments: packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ada-cpp-0.16.1-py312h29cc896_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ada-cpp-0.18.0-py312h29cc896_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/asyncpg-0.31.0-py312h5253ce2_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.5.0-py312h90b7ffd_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py312hdb49522_1.conda @@ -10576,7 +11259,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.48.0-he364bde_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xsdata-26.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ada-cpp-0.16.1-py312h9b4feea_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ada-cpp-0.18.0-py312h9b4feea_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/asyncpg-0.31.0-py312hf7082af_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/backports.zstd-1.5.0-py312h5f4ecc6_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py312h4b46afd_1.conda @@ -10769,7 +11452,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xsdata-26.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-20_gnu.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ada-cpp-0.16.1-py312h710a262_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ada-cpp-0.18.0-py312h710a262_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/asyncpg-0.31.0-py312he5662c2_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.5.0-py312h06d0912_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py312hc6d9e41_1.conda @@ -11838,7 +12521,7 @@ environments: packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ada-cpp-0.16.1-py312h29cc896_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ada-cpp-0.18.0-py312h29cc896_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/asyncpg-0.31.0-py312h5253ce2_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.5.0-py312h90b7ffd_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py312hdb49522_1.conda @@ -12172,7 +12855,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.48.0-he364bde_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xsdata-26.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ada-cpp-0.16.1-py312h9b4feea_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ada-cpp-0.18.0-py312h9b4feea_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/asyncpg-0.31.0-py312hf7082af_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/backports.zstd-1.5.0-py312h5f4ecc6_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py312h4b46afd_1.conda @@ -12396,7 +13079,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xsdata-26.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-20_gnu.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ada-cpp-0.16.1-py312h710a262_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ada-cpp-0.18.0-py312h710a262_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/asyncpg-0.31.0-py312he5662c2_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.5.0-py312h06d0912_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py312hc6d9e41_1.conda @@ -12533,6 +13216,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + strong: + - _openmp_mutex >=4.5 size: 28948 timestamp: 1770939786096 - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-7_kmp_llvm.conda @@ -12545,28 +13231,29 @@ packages: license_family: BSD size: 8244 timestamp: 1764092331208 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ada-cpp-0.16.1-py312h29cc896_0.conda - sha256: 4e89c4483c96663c19827a17c0d6ae93f99c4356aeaa5e97b4a873f8c2b32b83 - md5: 2ffdf0467bb1a29c5a950a97e930f35a +- conda: https://conda.anaconda.org/conda-forge/linux-64/ada-cpp-0.18.0-py312h29cc896_0.conda + sha256: 922c13c51c891488fb8e3466933ad5a3337323d2880805b82e5977f423e45fa0 + md5: ef4f3cfaf12393a4a0dc08b6f155e818 depends: - python - gmsh >=4.15.2,<4.16.0a0 - - libgcc >=14 - __glibc >=2.17,<3.0.a0 + - libgcc >=14 - libstdcxx >=14 - - rocksdb >=10.6.2,<10.7.0a0 - libzlib >=1.3.2,<2.0a0 - - occt >=7.9.3,<7.9.4.0a0 - python_abi 3.12.* *_cp312 + - rocksdb >=10.6.2,<10.7.0a0 + - occt >=7.9.3,<7.9.4.0a0 - ifcopenshell >=0.8.5,<0.8.6.0a0 license: GPL-3.0-or-later + license_family: GPL purls: - pkg:pypi/ada-cpp?source=hash-mapping run_exports: weak: - - ada-cpp >=0.16.1,<0.16.2.0a0 - size: 15282543 - timestamp: 1784298690208 + - ada-cpp >=0.18.0,<0.18.1.0a0 + size: 15495648 + timestamp: 1784533793981 - conda: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.13.5-py312h5d8c7f2_0.conda sha256: 52f4d07b10fe4a1ded570b0708594d2d9075223e1dd94d0c5988eb71f724a5f2 md5: 68edaee7692efb8bbef5e95375090189 @@ -12588,6 +13275,27 @@ packages: - pkg:pypi/aiohttp?source=hash-mapping size: 1034187 timestamp: 1775000054521 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.14.1-py312h5d8c7f2_0.conda + sha256: 7e03efaf3671cfca5d3195dc18aa3ca6bc182e0e34d30a95adb4f64f71d8f10c + md5: 10e4a93c73bfe09c5909cb1d41a1e40e + depends: + - __glibc >=2.17,<3.0.a0 + - aiohappyeyeballs >=2.5.0 + - aiosignal >=1.4.0 + - attrs >=17.3.0 + - frozenlist >=1.1.1 + - libgcc >=14 + - multidict >=4.5,<7.0 + - propcache >=0.2.0 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - typing_extensions >=4.4 + - yarl >=1.17.0,<2.0 + license: MIT AND Apache-2.0 + license_family: Apache + run_exports: {} + size: 1078273 + timestamp: 1780913823270 - conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.16-hb03c661_0.conda sha256: 64484cb7e7cf65d9c7188498d595125a6e0751604dd7fb483e1bb327eec0f738 md5: 18d273b22e96c97c2017813f099faa82 @@ -12599,6 +13307,33 @@ packages: purls: [] size: 591046 timestamp: 1780398678742 +- conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.16.1-hb03c661_0.conda + sha256: cf93ca0f1f107e95a35969a4622684e08fcb8cf37f8cf4a1e9e424828386c921 + md5: 8904e09bda369377b3dd07e2ac828c5d + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: LGPL-2.1-or-later + license_family: LGPL + run_exports: + weak: + - alsa-lib >=1.2.16.1,<1.3.0a0 + size: 592377 + timestamp: 1781521980743 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aom-3.14.1-pl5321h039972f_1.conda + sha256: b1d972a9b949a88babee681437535550b3ca5dbca6a23a40dffeb7900fec19fd + md5: 5a78a69eb3b50f24b379e9d2a93163ae + depends: + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - libgcc >=14 + license: BSD-2-Clause + license_family: BSD + run_exports: + weak: + - aom >=3.14.1,<3.15.0a0 + size: 3103347 + timestamp: 1780752473089 - conda: https://conda.anaconda.org/conda-forge/linux-64/aom-3.9.1-hac33072_0.conda sha256: b08ef033817b5f9f76ce62dfcac7694e7b6b4006420372de22494503decac855 md5: 346722a0be40f6edc53f12640d301338 @@ -12711,6 +13446,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - blosc >=1.21.6,<2.0a0 size: 48427 timestamp: 1733513201413 - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda @@ -12725,6 +13463,11 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 size: 20103 timestamp: 1764017231353 - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda @@ -12738,6 +13481,7 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: {} size: 21021 timestamp: 1764017221344 - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py312hdb49522_1.conda @@ -12766,6 +13510,9 @@ packages: license: bzip2-1.0.6 license_family: BSD purls: [] + run_exports: + weak: + - bzip2 >=1.0.8,<2.0a0 size: 260182 timestamp: 1771350215188 - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda @@ -12776,9 +13523,22 @@ packages: - libgcc >=14 license: MIT license_family: MIT - purls: [] - size: 207882 - timestamp: 1765214722852 + purls: [] + size: 207882 + timestamp: 1765214722852 +- conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.8-hb03c661_0.conda + sha256: dee93a82d045f9aa8277829aa465224afa3c7a6ac18388de66099b2be7f705e7 + md5: 6130ad6705adc993b5d8482b7f66e01f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + run_exports: + weak: + - c-ares >=1.34.8,<2.0a0 + size: 210929 + timestamp: 1784091008551 - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-he90730b_1.conda sha256: 06525fa0c4e4f56e771a3b986d0fdf0f0fc5a3270830ee47e127a5105bde1b9a md5: bb6c4808bfa69d6f7f6b07e5846ced37 @@ -12804,6 +13564,9 @@ packages: - xorg-libxrender >=0.9.12,<0.10.0a0 license: LGPL-2.1-only or MPL-1.1 purls: [] + run_exports: + weak: + - cairo >=1.18.4,<2.0a0 size: 989514 timestamp: 1766415934926 - conda: https://conda.anaconda.org/conda-forge/linux-64/calculix-2.23-pl5321h3af09bb_3.conda @@ -12884,6 +13647,22 @@ packages: purls: [] size: 6073810 timestamp: 1777880025171 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cgal-cpp-6.2-h8e6ac34_0.conda + sha256: 7445fb5492788725304eddd8337389d99ef82e83579a4deb81dfacd8549af429 + md5: 3bebd5189ceda7566a2eaa97822175bd + depends: + - __glibc >=2.17,<3.0.a0 + - eigen + - eigen-abi >=5.0.1.80,<5.0.1.81.0a0 + - libboost >=1.90.0,<1.91.0a0 + - libboost-devel + - libgcc >=14 + - libstdcxx >=14 + - mpfr >=4.2.2,<5.0a0 + license: GPL3/LGPL3 + run_exports: {} + size: 6227868 + timestamp: 1781697042209 - conda: https://conda.anaconda.org/conda-forge/linux-64/cli11-2.6.2-h54a6638_0.conda sha256: 1d635e8963e094d95d35148df4b46e495f93bb0750ad5069f4e0e6bbb47ac3bf md5: 83dae3dfadcfec9b37a9fbff6f7f7378 @@ -12894,6 +13673,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: {} size: 99051 timestamp: 1772207728613 - conda: https://conda.anaconda.org/conda-forge/linux-64/code-aster-18.0.10-py312_nompi_h60fb801_0.conda @@ -12999,6 +13779,7 @@ packages: license_family: BSD purls: - pkg:pypi/contourpy?source=hash-mapping + run_exports: {} size: 320386 timestamp: 1769155979897 - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.14.1-py312h8a5da7c_0.conda @@ -13064,6 +13845,9 @@ packages: license: BSD-3-Clause-Attribution license_family: BSD purls: [] + run_exports: + weak: + - cyrus-sasl >=2.1.28,<3.0a0 size: 210103 timestamp: 1771943128249 - conda: https://conda.anaconda.org/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda @@ -13074,6 +13858,9 @@ packages: license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - dav1d >=1.2.1,<1.2.2.0a0 size: 760229 timestamp: 1685695754230 - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h24cb091_1.conda @@ -13088,6 +13875,9 @@ packages: - libexpat >=2.7.3,<3.0a0 license: AFL-2.1 OR GPL-2.0-or-later purls: [] + run_exports: + weak: + - dbus >=1.16.2,<2.0a0 size: 447649 timestamp: 1764536047944 - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.21-py312h8285ef7_0.conda @@ -13114,6 +13904,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - double-conversion >=3.4.0,<3.5.0a0 size: 71809 timestamp: 1765193127016 - conda: https://conda.anaconda.org/conda-forge/linux-64/eigen-5.0.1-hc65338a_0.conda @@ -13122,6 +13915,7 @@ packages: license: MPL-2.0 license_family: MOZILLA purls: [] + run_exports: {} size: 1311364 timestamp: 1773744756289 - conda: https://conda.anaconda.org/conda-forge/linux-64/eigen-abi-5.0.1.80-hf414acd_0.conda @@ -13132,6 +13926,7 @@ packages: license: MPL-2.0 license_family: MOZILLA purls: [] + run_exports: {} size: 13265 timestamp: 1773744756289 - conda: https://conda.anaconda.org/conda-forge/linux-64/expat-2.8.1-hecca717_0.conda @@ -13146,6 +13941,20 @@ packages: purls: [] size: 148238 timestamp: 1779278694477 +- conda: https://conda.anaconda.org/conda-forge/linux-64/expat-2.8.1-hecca717_1.conda + sha256: bc1e8177a4ebbbfcda98b6f4a1575f3337e69f0d2f1025a84570533ca27b89eb + md5: e211a78613b9d50a096644b97cede8f7 + depends: + - __glibc >=2.17,<3.0.a0 + - libexpat 2.8.1 hecca717_1 + - libgcc >=14 + license: MIT + license_family: MIT + run_exports: + weak: + - libexpat >=2.8.1,<3.0a0 + size: 147797 + timestamp: 1781203608158 - conda: https://conda.anaconda.org/conda-forge/linux-64/fastar-0.11.0-py312h42cbcb6_0.conda sha256: 3aad77c606bd1d9c2e35336c83b262e0064c3538c7d2b1215b80e9904959a302 md5: 71a3f70b63e42bdc994d7d9bdcc53817 @@ -13243,6 +14052,73 @@ packages: purls: [] size: 13023802 timestamp: 1777748381557 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ffmpeg-8.1.2-gpl_h54862ce_904.conda + sha256: 35accb6f0fe430d89ca22e240ea3c7d964c23acfba637d40807dbf9207c96fbd + md5: 5cfeaeb36151f58c6166eb30eaa7d06a + depends: + - __glibc >=2.17,<3.0.a0 + - alsa-lib >=1.2.16.1,<1.3.0a0 + - aom >=3.14.1,<3.15.0a0 + - bzip2 >=1.0.8,<2.0a0 + - dav1d >=1.2.1,<1.2.2.0a0 + - fontconfig >=2.18.1,<3.0a0 + - fonts-conda-ecosystem + - gmp >=6.3.0,<7.0a0 + - lame >=3.100,<3.101.0a0 + - libass >=0.17.5,<0.17.6.0a0 + - libexpat >=2.8.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libgcc >=14 + - libharfbuzz >=14.2.1 + - libiconv >=1.18,<2.0a0 + - libjxl >=0.12.0,<0.13.0a0 + - liblzma >=5.8.3,<6.0a0 + - libopenvino >=2026.2.1,<2026.2.2.0a0 + - libopenvino-auto-batch-plugin >=2026.2.1,<2026.2.2.0a0 + - libopenvino-auto-plugin >=2026.2.1,<2026.2.2.0a0 + - libopenvino-hetero-plugin >=2026.2.1,<2026.2.2.0a0 + - libopenvino-intel-cpu-plugin >=2026.2.1,<2026.2.2.0a0 + - libopenvino-intel-gpu-plugin >=2026.2.1,<2026.2.2.0a0 + - libopenvino-intel-npu-plugin >=2026.2.1,<2026.2.2.0a0 + - libopenvino-ir-frontend >=2026.2.1,<2026.2.2.0a0 + - libopenvino-onnx-frontend >=2026.2.1,<2026.2.2.0a0 + - libopenvino-paddle-frontend >=2026.2.1,<2026.2.2.0a0 + - libopenvino-pytorch-frontend >=2026.2.1,<2026.2.2.0a0 + - libopenvino-tensorflow-frontend >=2026.2.1,<2026.2.2.0a0 + - libopenvino-tensorflow-lite-frontend >=2026.2.1,<2026.2.2.0a0 + - libopus >=1.6.1,<2.0a0 + - libplacebo >=7.360.1,<7.361.0a0 + - librsvg >=2.62.3,<3.0a0 + - libstdcxx >=14 + - libva >=2.24.1,<3.0a0 + - libvorbis >=1.3.7,<1.4.0a0 + - libvpl >=2.16.0,<2.17.0a0 + - libvpx >=1.15.2,<1.16.0a0 + - libvulkan-loader >=1.4.341.0,<2.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - libxcb >=1.17.0,<2.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - libzlib >=1.3.2,<2.0a0 + - openh264 >=2.6.0,<2.6.1.0a0 + - openssl >=3.5.7,<4.0a0 + - pulseaudio-client >=17.0,<17.1.0a0 + - sdl2 >=2.32.56,<3.0a0 + - shaderc >=2026.3,<2026.4.0a0 + - svt-av1 >=4.2.0,<4.2.1.0a0 + - x264 >=1!164.3095,<1!165 + - x265 >=3.5,<3.6.0a0 + - xorg-libx11 >=1.8.13,<2.0a0 + constrains: + - __cuda >=12.8 + license: GPL-2.0-or-later + license_family: GPL + run_exports: + weak: + - ffmpeg >=8.1.2,<9.0a0 + size: 13046477 + timestamp: 1784313887787 - conda: https://conda.anaconda.org/conda-forge/linux-64/fftw-3.3.11-mpi_openmpi_h76e6d66_0.conda sha256: 0949915580511f07e46d5509d8d16e7ae52910c426aa026dfba51cd188f0d93f md5: 1f27b20b2c508b341d2f2fffc038318b @@ -13316,6 +14192,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - fmt >=12.1.0,<12.2.0a0 size: 198107 timestamp: 1767681153946 - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.18.1-h27c8c51_0.conda @@ -13331,6 +14210,10 @@ packages: - libzlib >=1.3.2,<2.0a0 license: MIT purls: [] + run_exports: + weak: + - fontconfig >=2.18.1,<3.0a0 + - fonts-conda-ecosystem size: 281880 timestamp: 1780450077431 - conda: https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.63.0-py312h8a5da7c_0.conda @@ -13348,6 +14231,7 @@ packages: license_family: MIT purls: - pkg:pypi/fonttools?source=hash-mapping + run_exports: {} size: 3007892 timestamp: 1778770568019 - conda: https://conda.anaconda.org/conda-forge/linux-64/freeglut-3.2.2-h215f996_4.conda @@ -13369,6 +14253,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - freeglut >=3.2.2,<4.0a0 size: 146159 timestamp: 1776928018299 - conda: https://conda.anaconda.org/conda-forge/linux-64/freeimage-3.18.0-hd1b7436_25.conda @@ -13390,6 +14277,9 @@ packages: - openjpeg >=2.5.4,<3.0a0 license: GPL-2.0-or-later OR GPL-3.0-or-later OR FreeImage purls: [] + run_exports: + weak: + - freeimage >=3.18.0,<3.19.0a0 size: 469606 timestamp: 1780001489759 - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.3-ha770c72_0.conda @@ -13400,6 +14290,10 @@ packages: - libfreetype6 2.14.3 h73754d4_0 license: GPL-2.0-only OR FTL purls: [] + run_exports: + weak: + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 size: 173839 timestamp: 1774298173462 - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda @@ -13410,6 +14304,9 @@ packages: - libgcc >=14 license: LGPL-2.1-or-later purls: [] + run_exports: + weak: + - fribidi >=1.0.16,<2.0a0 size: 61244 timestamp: 1757438574066 - conda: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.8.0-py312h447239a_0.conda @@ -13425,6 +14322,7 @@ packages: license_family: APACHE purls: - pkg:pypi/frozenlist?source=compressed-mapping + run_exports: {} size: 55016 timestamp: 1779999817627 - conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.6-h2b0a6b4_0.conda @@ -13443,6 +14341,24 @@ packages: purls: [] size: 577414 timestamp: 1774985848058 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.7-h2b0a6b4_0.conda + sha256: 1c22e37f9d7e06e9e0582ee5a55c2ddd19ea75f71f44eb13b56f504ef5c37aa5 + md5: 5d355db3e937086e22cf4cb5fe19787c + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libglib >=2.88.2,<3.0a0 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libpng >=1.6.58,<1.7.0a0 + - libtiff >=4.7.1,<4.8.0a0 + license: LGPL-2.1-or-later + license_family: LGPL + run_exports: + weak: + - gdk-pixbuf >=2.44.7,<3.0a0 + size: 581631 + timestamp: 1782591374199 - conda: https://conda.anaconda.org/conda-forge/linux-64/geos-3.14.1-h480dda7_0.conda sha256: 08896dcd94e14a83f247e91748444e610f344ab42d80cbf2b6082b481c3f8f4b md5: 4d4efd0645cd556fab54617c4ad477ef @@ -13452,6 +14368,9 @@ packages: - libstdcxx >=14 license: LGPL-2.1-only purls: [] + run_exports: + weak: + - geos >=3.14.1,<3.14.2.0a0 size: 1974942 timestamp: 1761593471198 - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda @@ -13464,6 +14383,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - gflags >=2.2.2,<2.3.0a0 size: 119654 timestamp: 1726600001928 - conda: https://conda.anaconda.org/conda-forge/linux-64/gl2ps-1.4.2-h36e74d4_2.conda @@ -13479,6 +14401,9 @@ packages: license: LGPL-2.0-or-later license_family: LGPL purls: [] + run_exports: + weak: + - gl2ps >=1.4.2,<1.4.3.0a0 size: 75835 timestamp: 1773985381918 - conda: https://conda.anaconda.org/conda-forge/linux-64/glew-2.2.0-h3abd4de_0.conda @@ -13496,6 +14421,23 @@ packages: purls: [] size: 544416 timestamp: 1760370322297 +- conda: https://conda.anaconda.org/conda-forge/linux-64/glew-2.3.0-h71661d4_0.conda + sha256: 535d152ee06e3d3015a5ab410dfea9574e1678e226fa166f859a0b9e1153e597 + md5: 7eefecda1c71c380bfc406d16e78bbee + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libgl >=1.7.0,<2.0a0 + - libglu >=9.0.3,<9.1.0a0 + - xorg-libx11 >=1.8.12,<2.0a0 + - xorg-libxext + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - glew >=2.3.0,<2.4.0a0 + size: 492673 + timestamp: 1766373546677 - conda: https://conda.anaconda.org/conda-forge/linux-64/glfw-3.4-hb03c661_1.conda sha256: 8586f786e71a52dc37be6a78c37fd8526b80539082352196f6f7440392ce33d0 md5: bf1df8613072b03a7fb4b77b9fa54048 @@ -13526,6 +14468,21 @@ packages: purls: [] size: 1353512 timestamp: 1769369779923 +- conda: https://conda.anaconda.org/conda-forge/linux-64/glslang-16.3.0-h96af755_0.conda + sha256: 3c9b6a90937a96ad27d160304cdbe5e9961db613aba2b84ff673429f0c61d48e + md5: d175cb2c14104728ada04883786a309d + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - spirv-tools >=2026,<2027.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - glslang >=16,<17.0a0 + size: 1366082 + timestamp: 1777747028121 - conda: https://conda.anaconda.org/conda-forge/linux-64/gmp-6.3.0-hac33072_2.conda sha256: 309cf4f04fec0c31b6771a5809a1909b4b3154a2208f52351e1ada006f4c750c md5: c94a5994ef49749880a8139cf9afcbe1 @@ -13534,6 +14491,9 @@ packages: - libstdcxx-ng >=12 license: GPL-2.0-or-later OR LGPL-3.0-or-later purls: [] + run_exports: + weak: + - gmp >=6.3.0,<7.0a0 size: 460055 timestamp: 1718980856608 - conda: https://conda.anaconda.org/conda-forge/linux-64/gmsh-4.15.0-hbdcdd55_4.conda @@ -13605,6 +14565,9 @@ packages: - libstdcxx >=14 license: LGPL-2.0-or-later purls: [] + run_exports: + weak: + - graphite2 >=1.3.15,<2.0a0 size: 100054 timestamp: 1780454302233 - conda: https://conda.anaconda.org/conda-forge/linux-64/h5py-3.16.0-nompi_py312ha4f8f14_102.conda @@ -13643,6 +14606,18 @@ packages: purls: [] size: 2362258 timestamp: 1780450503234 +- conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-14.2.1-ha770c72_1.conda + sha256: 853beec89ff91cbbf630f1d305b69153eb28a3cb78af95ddc1fb4d30e524c6f4 + md5: 72e956a71241633d8c80aa198fd08784 + depends: + - libharfbuzz-devel 14.2.1 h17a8019_1 + license: MIT + license_family: MIT + run_exports: + weak: + - libharfbuzz >=14.2.1 + size: 11039 + timestamp: 1782800611635 - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf4-4.2.15-h2a13503_7.conda sha256: 0d09b6dc1ce5c4005ae1c6a19dc10767932ef9a5e9c755cfdbb5189ac8fb0684 md5: bd77f8da987968ec3927990495dc22e4 @@ -13654,6 +14629,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - hdf4 >=4.2.15,<4.2.16.0a0 size: 756742 timestamp: 1695661547874 - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-mpi_openmpi_h8367ee7_9.conda @@ -13692,6 +14670,26 @@ packages: purls: [] size: 3719822 timestamp: 1777518369641 +- conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h19486de_110.conda + sha256: ac57ce3abbda2a82d6192bc36fbd7fe96e867d84c999ef81a3da034dfd01a995 + md5: 9c6e11a83468e1d5decae516077a73fb + depends: + - __glibc >=2.17,<3.0.a0 + - libaec >=1.1.5,<2.0a0 + - libcurl >=8.20.0,<9.0a0 + - libgcc >=14 + - libgfortran + - libgfortran5 >=14.3.0 + - libstdcxx >=14 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.6,<4.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - hdf5 >=1.14.6,<1.14.7.0a0 + size: 3721555 + timestamp: 1780581675871 - conda: https://conda.anaconda.org/conda-forge/linux-64/httptools-0.7.1-py314h5bd0f2a_1.conda sha256: 91bfdf1dad0fa57efc2404ca00f5fee8745ad9b56ec1d0df298fd2882ad39806 md5: 067a52c66f453b97771650bbb131e2b5 @@ -13742,8 +14740,43 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - icu >=78.3,<79.0a0 size: 12723451 timestamp: 1773822285671 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ifcopenshell-0.8.5-py312h4f5b897_6.conda + sha256: 6ac2f7fad97f17914da74447c65ab1cd6abc8773ab84713aecdd50d0933df95d + md5: bcae54a611acee41c758d0b4ec814f45 + depends: + - python + - shapely + - typing_extensions + - hdf5 >=1.14.6,<1.15.0a0 + - occt >=8.0.0,<8.1.0a0 + - cgal-cpp >=6.2,<6.3.0a0 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - python_abi 3.12.* *_cp312 + - libzlib >=1.3.2,<2.0a0 + - hdf5 >=1.14.6,<1.14.7.0a0 + - rocksdb >=10.6.2,<10.7.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - zstd >=1.5.7,<1.6.0a0 + - mpfr >=4.2.2,<5.0a0 + - occt >=8.0.0,<8.0.1.0a0 + - libboost >=1.90.0,<1.91.0a0 + - gmp >=6.3.0,<7.0a0 + license: LGPL-3.0-or-later + license_family: LGPL + run_exports: + weak: + - ifcopenshell >=0.8.5,<0.8.6.0a0 + - rocksdb >=10.6.2,<10.7.0a0 + size: 52751366 + timestamp: 1782250481526 - conda: https://conda.anaconda.org/conda-forge/linux-64/ifcopenshell-0.8.5-py312h60c64db_4.conda sha256: 014f7186233ff1e8c45cc0ad7472a61d55dd6aba37252b42605a2dfa5430c90c md5: 5db6773ea1943a743edf0f641570b690 @@ -13783,6 +14816,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - imath >=3.2.2,<3.2.3.0a0 size: 160289 timestamp: 1759983212466 - conda: https://conda.anaconda.org/conda-forge/linux-64/intel-gmmlib-22.10.0-hb700be7_0.conda @@ -13795,6 +14831,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - intel-gmmlib >=22.10.0,<23.0a0 size: 1013714 timestamp: 1774422680665 - conda: https://conda.anaconda.org/conda-forge/linux-64/intel-media-driver-26.1.6-hecca717_0.conda @@ -13809,6 +14848,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - intel-media-driver >=26.1.6,<26.2.0a0 size: 8782375 timestamp: 1776080148587 - conda: https://conda.anaconda.org/conda-forge/linux-64/jasper-4.2.9-h1588d4d_1.conda @@ -13825,6 +14867,9 @@ packages: - libjpeg-turbo >=3.1.2,<4.0a0 license: JasPer-2.0 purls: [] + run_exports: + weak: + - jasper >=4.2.9,<5.0a0 size: 684185 timestamp: 1773677703432 - conda: https://conda.anaconda.org/conda-forge/linux-64/jsoncpp-1.9.6-hf42df4d_1.conda @@ -13838,6 +14883,19 @@ packages: purls: [] size: 169093 timestamp: 1733780223643 +- conda: https://conda.anaconda.org/conda-forge/linux-64/jsoncpp-1.9.8-h171cf75_0.conda + sha256: afb052623a441994b8540fd876cae013e8d532fe0294720d97f0a155a450d331 + md5: 1f68d84c72f584f81f0d72245131e36f + depends: + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - libgcc >=14 + license: LicenseRef-Public-Domain OR MIT + run_exports: + weak: + - jsoncpp >=1.9.8,<1.9.9.0a0 + size: 190220 + timestamp: 1782507129612 - conda: https://conda.anaconda.org/conda-forge/linux-64/jxrlib-1.1-hd590300_3.conda sha256: 2057ca87b313bde5b74b93b0e696f8faab69acd4cb0edebb78469f3f388040c0 md5: 5aeabe88534ea4169d4c49998f293d6c @@ -13846,6 +14904,9 @@ packages: license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - jxrlib >=1.1,<1.2.0a0 size: 239104 timestamp: 1703333860145 - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda @@ -13856,6 +14917,9 @@ packages: - libgcc >=13 license: LGPL-2.1-or-later purls: [] + run_exports: + weak: + - keyutils >=1.6.3,<2.0a0 size: 134088 timestamp: 1754905959823 - conda: https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.5.0-py312h0a2e395_0.conda @@ -13871,6 +14935,7 @@ packages: license_family: BSD purls: - pkg:pypi/kiwisolver?source=hash-mapping + run_exports: {} size: 77120 timestamp: 1773067050308 - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda @@ -13889,6 +14954,24 @@ packages: purls: [] size: 1386730 timestamp: 1769769569681 +- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbde042b_1.conda + sha256: 9b07046870772f28740e3f6149f09ff222843733087a33c5540b169c6289652d + md5: 54157a1c8c0bb70f62dd0b17fba7e7f2 + depends: + - __glibc >=2.17,<3.0.a0 + - keyutils >=1.6.3,<2.0a0 + - libedit >=3.1.20250104,<3.2.0a0 + - libedit >=3.1.20250104,<4.0a0 + - libgcc >=14 + - libstdcxx >=14 + - openssl >=3.5.7,<4.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - krb5 >=1.22.2,<1.23.0a0 + size: 1388990 + timestamp: 1781859420533 - conda: https://conda.anaconda.org/conda-forge/linux-64/lame-3.100-h166bdaf_1003.tar.bz2 sha256: aad2a703b9d7b038c0f745b853c6bb5f122988fe1a7a096e0e606d9cbec4eaab md5: a8832b479f93521a9e7b5b743803be51 @@ -13897,6 +14980,9 @@ packages: license: LGPL-2.0-only license_family: LGPL purls: [] + run_exports: + weak: + - lame >=3.100,<3.101.0a0 size: 508258 timestamp: 1664996250081 - conda: https://conda.anaconda.org/conda-forge/linux-64/lazy-object-proxy-1.12.0-py312h4c3975b_1.conda @@ -13922,6 +15008,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - lcms2 >=2.19.1,<3.0a0 size: 251971 timestamp: 1780211695895 - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda @@ -13937,6 +15026,19 @@ packages: purls: [] size: 728002 timestamp: 1774197446916 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.46.1-default_hbd61a6d_102.conda + sha256: 27d83f1188cd19bcb7754a078b3fa7f4cfb8527f8eb2fde54dd01fc529d1adec + md5: 449500f2c089da11c40f5c21312e3e07 + depends: + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - binutils_impl_linux-64 2.46.1 + license: GPL-3.0-only + license_family: GPL + run_exports: {} + size: 745303 + timestamp: 1784214507189 - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda sha256: f84cb54782f7e9cea95e810ea8fef186e0652d0fa73d3009914fa2c1262594e1 md5: a752488c68f2e7c456bcbd8f16eec275 @@ -13947,6 +15049,9 @@ packages: license: Apache-2.0 license_family: Apache purls: [] + run_exports: + weak: + - lerc >=4.1.0,<5.0a0 size: 261513 timestamp: 1773113328888 - conda: https://conda.anaconda.org/conda-forge/linux-64/level-zero-1.29.0-hb700be7_0.conda @@ -13959,6 +15064,7 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: {} size: 875773 timestamp: 1780142086148 - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda @@ -13976,6 +15082,24 @@ packages: purls: [] size: 1384817 timestamp: 1770863194876 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260526.0-cxx17_h7b12aa8_1.conda + sha256: 32933de2d4fa6e6ffd949052815b49cb65a0649ad70007155c533ab97ea8cefd + md5: c4393db381bffa0a83a8d9e47b238106 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + constrains: + - abseil-cpp =20260526.0 + - libabseil-static =20260526.0=cxx17* + license: Apache-2.0 + license_family: Apache + run_exports: + weak: + - libabseil >=20260526.0,<20260527.0a0 + - libabseil =*=cxx17* + size: 1437712 + timestamp: 1780524559298 - conda: https://conda.anaconda.org/conda-forge/linux-64/libaec-1.1.5-h088129d_0.conda sha256: 822e4ae421a7e9c04e841323526321185f6659222325e1a9aedec811c686e688 md5: 86f7414544ae606282352fa1e116b41f @@ -13986,6 +15110,9 @@ packages: license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - libaec >=1.1.5,<2.0a0 size: 36544 timestamp: 1769221884824 - conda: https://conda.anaconda.org/conda-forge/linux-64/libamd-3.3.3-h456b2da_7100101.conda @@ -14019,6 +15146,26 @@ packages: purls: [] size: 152179 timestamp: 1749328931930 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libass-0.17.5-h434b012_0.conda + sha256: 24d4b59a0267e1c159c3af82df106b42faeefccceba3c489044c93abf113c503 + md5: c1cb4d6e8a6e3f724740dee5346fc8b4 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libzlib >=1.3.2,<2.0a0 + - fribidi >=1.0.16,<2.0a0 + - fontconfig >=2.18.1,<3.0a0 + - fonts-conda-ecosystem + - libiconv >=1.18,<2.0a0 + - harfbuzz >=14.2.1 + license: ISC + run_exports: + weak: + - libass >=0.17.5,<0.17.6.0a0 + size: 154964 + timestamp: 1782298715788 - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-8_h4a7cf45_openblas.conda build_number: 8 sha256: b2da6bfd72a1c9cb143ccf64bf5b28790cb4eb58bd1cb978f6537b2322f7d48b @@ -14035,6 +15182,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libblas >=3.11.0,<4.0a0 size: 18804 timestamp: 1779859100675 - conda: https://conda.anaconda.org/conda-forge/linux-64/libboost-1.88.0-hd24cca6_7.conda @@ -14052,9 +15202,27 @@ packages: constrains: - boost-cpp <0.0a0 license: BSL-1.0 - purls: [] - size: 3072984 - timestamp: 1766347479317 + purls: [] + size: 3072984 + timestamp: 1766347479317 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libboost-1.90.0-hd24cca6_1.conda + sha256: fef9f2977ac341fd0fd7802bccffff0f220e4896f6fef29040428071d0aa863b + md5: 4dfa9b413062a24e09938fb6f91af821 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - icu >=78.1,<79.0a0 + - libgcc >=14 + - liblzma >=5.8.1,<6.0a0 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - boost-cpp <0.0a0 + license: BSL-1.0 + run_exports: {} + size: 3229874 + timestamp: 1766347309472 - conda: https://conda.anaconda.org/conda-forge/linux-64/libboost-devel-1.88.0-hfcd1e18_7.conda sha256: 249e7a58aee14a619d4f6bca3ad955b7a0a84aad6ab201f734bb21ea16e654e6 md5: 97ac87592030b16fa193c877538be3d5 @@ -14067,6 +15235,20 @@ packages: purls: [] size: 40112 timestamp: 1766347628036 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libboost-devel-1.90.0-hfcd1e18_1.conda + sha256: a46970575b8ec39fe103833ed988bc9d56292146b417aeddb333a94e980053b0 + md5: 5e5227b43bdd65d0028a322b2636d02e + depends: + - libboost 1.90.0 hd24cca6_1 + - libboost-headers 1.90.0 ha770c72_1 + constrains: + - boost-cpp <0.0a0 + license: BSL-1.0 + run_exports: + weak: + - libboost >=1.90.0,<1.91.0a0 + size: 38562 + timestamp: 1766347475462 - conda: https://conda.anaconda.org/conda-forge/linux-64/libboost-headers-1.88.0-ha770c72_7.conda sha256: 88194078f2de6b68c40563871ccf638fd48cd1cf1d203ac4e653cee9cedd31a6 md5: d9011bcea61514b510209b882a459a57 @@ -14076,6 +15258,15 @@ packages: purls: [] size: 14584021 timestamp: 1766347497416 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libboost-headers-1.90.0-ha770c72_1.conda + sha256: be43ec008a4fd297b5cdccea6c8e05bedd1d40315bdaefe4cc2f75363dab3f73 + md5: a1da1e4c15eb57bb506b33e283107dc5 + constrains: + - boost-cpp <0.0a0 + license: BSL-1.0 + run_exports: {} + size: 14676487 + timestamp: 1766347330772 - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda sha256: 318f36bd49ca8ad85e6478bd8506c88d82454cc008c1ac1c6bf00a3c42fa610e md5: 72c8fd1af66bd67bf580645b426513ed @@ -14085,6 +15276,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libbrotlicommon >=1.2.0,<1.3.0a0 size: 79965 timestamp: 1764017188531 - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda @@ -14097,6 +15291,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libbrotlidec >=1.2.0,<1.3.0a0 size: 34632 timestamp: 1764017199083 - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda @@ -14109,6 +15306,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libbrotlienc >=1.2.0,<1.3.0a0 size: 298378 timestamp: 1764017210931 - conda: https://conda.anaconda.org/conda-forge/linux-64/libbtf-2.3.2-hf02c80a_7100101.conda @@ -14141,6 +15341,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libcap >=2.78,<2.79.0a0 size: 124335 timestamp: 1775488792584 - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-8_h0358290_openblas.conda @@ -14156,6 +15359,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libcblas >=3.11.0,<4.0a0 size: 18778 timestamp: 1779859107964 - conda: https://conda.anaconda.org/conda-forge/linux-64/libccolamd-3.3.4-hf02c80a_7100101.conda @@ -14211,6 +15417,9 @@ packages: license: Apache-2.0 license_family: Apache purls: [] + run_exports: + weak: + - libcups >=2.3.3,<2.4.0a0 size: 4518030 timestamp: 1770902209173 - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.20.0-hcf29cc6_0.conda @@ -14230,6 +15439,26 @@ packages: purls: [] size: 468706 timestamp: 1777461492876 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.21.0-hae6b9f4_2.conda + sha256: ba8354084b34fce56d5697982fce4a384c148e972e15c0ab891187617fe7ec29 + md5: f9c59d277a16ec8f272b2d5dd2ec3335 + depends: + - __glibc >=2.17,<3.0.a0 + - krb5 >=1.22.2,<1.23.0a0 + - libgcc >=14 + - libnghttp2 >=1.68.1,<2.0a0 + - libpsl >=0.22.0,<0.23.0a0 + - libssh2 >=1.11.1,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.7,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: curl + license_family: MIT + run_exports: + weak: + - libcurl >=8.21.0,<9.0a0 + size: 480327 + timestamp: 1782911787190 - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda sha256: aa8e8c4be9a2e81610ddf574e05b64ee131fab5e0e3693210c9d6d2fba32c680 md5: 6c77a605a7a689d17d4819c0f8ac9a00 @@ -14239,6 +15468,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libdeflate >=1.25,<1.26.0a0 size: 73490 timestamp: 1761979956660 - conda: https://conda.anaconda.org/conda-forge/linux-64/libdovi-3.3.2-ha23c83e_4.conda @@ -14254,6 +15486,20 @@ packages: purls: [] size: 311420 timestamp: 1777838991858 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libdovi-3.4.0-ha23c83e_0.conda + sha256: cea351b57c30d70e288b53ea69a1dcf6b750992f5d7717a7fc364072fa1209e7 + md5: 4377d220f09344452b227d699cacce4f + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + constrains: + - __glibc >=2.17 + license: MIT + run_exports: + weak: + - libdovi >=3.4.0,<4.0a0 + size: 404998 + timestamp: 1784281566921 - conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.127-hb03c661_0.conda sha256: 7d3187c11b7ae66c5595a8afd5a7ce352a490527fdf6614cab129bc7f2c16ba3 md5: d8d16b9b32a3c5df7e5b3350e2cbe058 @@ -14264,6 +15510,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libdrm >=2.4.127,<2.5.0a0 size: 311505 timestamp: 1778975798004 - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda @@ -14277,6 +15526,9 @@ packages: license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - libedit >=3.1.20250104,<3.2.0a0 size: 134676 timestamp: 1738479519902 - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_3.conda @@ -14287,6 +15539,7 @@ packages: - libglvnd 1.7.0 ha4b6fd6_3 license: LicenseRef-libglvnd purls: [] + run_exports: {} size: 46500 timestamp: 1779728188901 - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-devel-1.7.0-ha4b6fd6_3.conda @@ -14299,6 +15552,9 @@ packages: - xorg-libx11 license: LicenseRef-libglvnd purls: [] + run_exports: + weak: + - libegl >=1.7.0,<2.0a0 size: 31718 timestamp: 1779728222280 - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda @@ -14309,6 +15565,9 @@ packages: license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - libev >=4.33,<4.34.0a0 size: 112766 timestamp: 1702146165126 - conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda @@ -14334,6 +15593,19 @@ packages: purls: [] size: 77294 timestamp: 1779278686680 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + sha256: 16feffd9ddbbe5b718515d38ee376c685ba95491cd901244e24671d20b952a77 + md5: b24d3c612f71e7aa74158d92106318b2 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - expat 2.8.1.* + license: MIT + license_family: MIT + run_exports: {} + size: 77856 + timestamp: 1781203599810 - conda: https://conda.anaconda.org/conda-forge/linux-64/libfabric-2.5.1-ha770c72_1.conda sha256: c2b53beed03c735b18116d5227410b15cc9fbef209795212afd179f8c8a5fd70 md5: 0b82ff1ab3db9122f20cafe93f61bc3b @@ -14364,6 +15636,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libffi >=3.5.2,<3.6.0a0 size: 58592 timestamp: 1769456073053 - conda: https://conda.anaconda.org/conda-forge/linux-64/libflac-1.5.0-he200343_1.conda @@ -14378,6 +15653,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libflac >=1.5.0,<1.6.0a0 size: 424563 timestamp: 1764526740626 - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda @@ -14387,6 +15665,7 @@ packages: - libfreetype6 >=2.14.3 license: GPL-2.0-only OR FTL purls: [] + run_exports: {} size: 8049 timestamp: 1774298163029 - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h73754d4_0.conda @@ -14401,6 +15680,7 @@ packages: - freetype >=2.14.3 license: GPL-2.0-only OR FTL purls: [] + run_exports: {} size: 384575 timestamp: 1774298162622 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda @@ -14415,6 +15695,7 @@ packages: license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] + run_exports: {} size: 1041084 timestamp: 1778269013026 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda @@ -14425,6 +15706,9 @@ packages: license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] + run_exports: + strong: + - libgcc size: 27694 timestamp: 1778269016987 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_19.conda @@ -14437,6 +15721,7 @@ packages: license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] + run_exports: {} size: 27655 timestamp: 1778269042954 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_19.conda @@ -14450,6 +15735,7 @@ packages: license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] + run_exports: {} size: 2483673 timestamp: 1778269025089 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_3.conda @@ -14461,6 +15747,7 @@ packages: - libglx 1.7.0 ha4b6fd6_3 license: LicenseRef-libglvnd purls: [] + run_exports: {} size: 133469 timestamp: 1779728207669 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-devel-1.7.0-ha4b6fd6_3.conda @@ -14472,6 +15759,9 @@ packages: - libglx-devel 1.7.0 ha4b6fd6_3 license: LicenseRef-libglvnd purls: [] + run_exports: + weak: + - libgl >=1.7.0,<2.0a0 size: 115664 timestamp: 1779728218325 - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.88.1-h0d30a3d_2.conda @@ -14490,6 +15780,24 @@ packages: purls: [] size: 4754097 timestamp: 1778508800134 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.88.2-h0d30a3d_0.conda + sha256: 4bee10e62796f01e4fa2b5849135b1cc061337fe9cf5eb9bd79e9664922ae0e4 + md5: 889febc66cd9e4190f80ef9718fa239b + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libiconv >=1.18,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - libffi >=3.5.2,<3.6.0a0 + - pcre2 >=10.47,<10.48.0a0 + constrains: + - glib >2.66 + license: LGPL-2.1-or-later + run_exports: + weak: + - libglib >=2.88.2,<3.0a0 + size: 4754220 + timestamp: 1782463895250 - conda: https://conda.anaconda.org/conda-forge/linux-64/libglu-9.0.3-h5888daf_1.conda sha256: a0105eb88f76073bbb30169312e797ed5449ebb4e964a756104d6e54633d17ef md5: 8422fcc9e5e172c91e99aef703b3ce65 @@ -14500,6 +15808,9 @@ packages: - libstdcxx >=13 license: SGI-B-2.0 purls: [] + run_exports: + weak: + - libglu >=9.0.3,<9.1.0a0 size: 325262 timestamp: 1748692137626 - conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_3.conda @@ -14509,6 +15820,7 @@ packages: - __glibc >=2.17,<3.0.a0 license: LicenseRef-libglvnd purls: [] + run_exports: {} size: 133586 timestamp: 1779728183422 - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_3.conda @@ -14520,6 +15832,7 @@ packages: - xorg-libx11 >=1.8.13,<2.0a0 license: LicenseRef-libglvnd purls: [] + run_exports: {} size: 76586 timestamp: 1779728199059 - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-devel-1.7.0-ha4b6fd6_3.conda @@ -14532,6 +15845,9 @@ packages: - xorg-xorgproto license: LicenseRef-libglvnd purls: [] + run_exports: + weak: + - libglx >=1.7.0,<2.0a0 size: 27363 timestamp: 1779728211402 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda @@ -14542,8 +15858,55 @@ packages: license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] + run_exports: + strong: + - _openmp_mutex >=4.5 size: 603817 timestamp: 1778268942614 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libharfbuzz-14.2.1-h17a8019_1.conda + sha256: 821c315f6b5171aa89e735be2ad84e74eb3f898fc7610ee36cfecd95dfa789e8 + md5: fb4669c3990b94ea32fbb81f433e9aa6 + depends: + - __glibc >=2.17,<3.0.a0 + - cairo >=1.18.4,<2.0a0 + - graphite2 >=1.3.15,<2.0a0 + - icu >=78.3,<79.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libgcc >=14 + - libglib >=2.88.2,<3.0a0 + - libpng >=1.6.58,<1.7.0a0 + - libstdcxx >=14 + - libzlib >=1.3.2,<2.0a0 + license: MIT + license_family: MIT + run_exports: {} + size: 1297668 + timestamp: 1782800580119 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libharfbuzz-devel-14.2.1-h17a8019_1.conda + sha256: 6d8e793042affd18ca1784b7794fab14d16f54f984777ce6ab86bacaa465ab0d + md5: 99cf21100441e51272f1cd6fe0632a20 + depends: + - __glibc >=2.17,<3.0.a0 + - cairo >=1.18.4,<2.0a0 + - freetype + - graphite2 >=1.3.15,<2.0a0 + - icu >=78.3,<79.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libgcc >=14 + - libglib >=2.88.2,<3.0a0 + - libharfbuzz 14.2.1 h17a8019_1 + - libpng >=1.6.58,<1.7.0a0 + - libstdcxx >=14 + - libzlib >=1.3.2,<2.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - libharfbuzz >=14.2.1 + size: 1970690 + timestamp: 1782800603897 - conda: https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.12.2-default_hafda6a7_1000.conda sha256: 2cf160794dda62cf93539adf16d26cfd31092829f2a2757dbdd562984c1b110a md5: 0ed3aa3e3e6bc85050d38881673a692f @@ -14569,6 +15932,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libhwloc >=2.13.0,<2.13.1.0a0 size: 2436378 timestamp: 1770953868164 - conda: https://conda.anaconda.org/conda-forge/linux-64/libhwy-1.4.0-h10be129_0.conda @@ -14582,6 +15948,19 @@ packages: purls: [] size: 1435782 timestamp: 1776989559668 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libhwy-1.4.0-h3a9caae_0.conda + sha256: 02ab5e50c3921e88ad4dd0bc8f3fe282d2d7d03a20203d3281954b94641deb3d + md5: 9544a7225c8366ea2c397aad5fb53470 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: Apache-2.0 OR BSD-3-Clause + run_exports: + weak: + - libhwy >=1.4.0,<1.5.0a0 + size: 1431901 + timestamp: 1784325535334 - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda sha256: c467851a7312765447155e071752d7bf9bf44d610a5687e32706f480aad2833f md5: 915f5995e94f60e9a4826e0b0920ee88 @@ -14590,6 +15969,9 @@ packages: - libgcc >=14 license: LGPL-2.1-only purls: [] + run_exports: + weak: + - libiconv >=1.18,<2.0a0 size: 790176 timestamp: 1754908768807 - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.4.1-hb03c661_0.conda @@ -14604,6 +15986,20 @@ packages: purls: [] size: 633831 timestamp: 1775962768273 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.2.0-hb03c661_0.conda + sha256: 716332fd31b3808da7a4235a05212a9e9da05e854864c3def2dea0b5d2bf7610 + md5: 466badda5536d85ddc63ee9404f29735 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + run_exports: + weak: + - libjpeg-turbo >=3.2.0,<4.0a0 + size: 652868 + timestamp: 1783731886811 - conda: https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.11.2-h174a0a3_1.conda sha256: 0c8a78c6a42a6e4c6de3a5e82d692f60400d43f4cc80591745f28b37daad9c70 md5: 850f48943d6b4589800a303f0de6a816 @@ -14619,6 +16015,23 @@ packages: purls: [] size: 1846962 timestamp: 1777065125966 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.12.0-h174a0a3_1.conda + sha256: 1811d6c6558fbfe89326616c207cc7584032b60bc6f4329d2a76b961e2936a15 + md5: 1fff55640e1f12d7606965915be37bbb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - libhwy >=1.4.0,<1.5.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - libjxl >=0.12.0,<0.13.0a0 + size: 1849836 + timestamp: 1783146019356 - conda: https://conda.anaconda.org/conda-forge/linux-64/libklu-2.3.5-h95ff59c_7100101.conda sha256: 6b4d462642c240dc3671af74f7705b23f34eea0f71e0d9dbcf14b4ed008311ff md5: efaa5e7dc6989363585fbb591480b256 @@ -14653,6 +16066,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - liblapack >=3.11.0,<3.12.0a0 size: 18790 timestamp: 1779859115086 - conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm20-20.1.8-hf7376ad_1.conda @@ -14671,6 +16087,24 @@ packages: purls: [] size: 43977914 timestamp: 1757353652788 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm22-22.1.8-hf7376ad_1.conda + sha256: e9b5f301d6b001a9b8ce782157f56b75c92c4fbc9eba95dc6345c1139251d13b + md5: 298bb2483fc7d15396147cf1c1465359 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - libxml2 + - libxml2-16 >=2.14.6 + - libzlib >=1.3.2,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + run_exports: + weak: + - libllvm22 >=22.1.8,<22.2.0a0 + size: 44320272 + timestamp: 1781788728739 - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda sha256: ec30e52a3c1bf7d0425380a189d209a52baa03f22fb66dd3eb587acaa765bd6d md5: b88d90cad08e6bc8ad540cb310a761fb @@ -14681,6 +16115,9 @@ packages: - xz 5.8.3.* license: 0BSD purls: [] + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 size: 113478 timestamp: 1775825492909 - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-devel-5.8.3-hb03c661_0.conda @@ -14692,6 +16129,9 @@ packages: - liblzma 5.8.3 hb03c661_0 license: 0BSD purls: [] + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 size: 491429 timestamp: 1775825511214 - conda: https://conda.anaconda.org/conda-forge/linux-64/libmed-4.2-nompi_hd150add_28.conda @@ -14768,6 +16208,33 @@ packages: purls: [] size: 862900 timestamp: 1776686244733 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnetcdf-4.10.1-nompi_h3fa17b5_201.conda + build_number: 101 + sha256: b5b8e15e635891d3a612562539a991721a0ab8c935c7fc231cdf08c96af4699a + md5: 4612a01496444744808e82fa6da901d4 + depends: + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libcurl >=8.21.0,<9.0a0 + - hdf5 >=1.14.6,<1.14.7.0a0 + - zstd >=1.5.7,<1.6.0a0 + - hdf4 >=4.2.15,<4.2.16.0a0 + - blosc >=1.21.6,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - libaec >=1.1.5,<2.0a0 + - bzip2 >=1.0.8,<2.0a0 + - libzip >=1.11.2,<2.0a0 + - openssl >=3.5.7,<4.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - libnetcdf >=4.10.1,<4.10.2.0a0 + size: 1005827 + timestamp: 1783982062736 - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda sha256: 663444d77a42f2265f54fb8b48c5450bfff4388d9c0f8253dd7855f0d993153f md5: 2a45e7f8af083626f009645a6481f12d @@ -14783,6 +16250,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libnghttp2 >=1.68.1,<2.0a0 size: 663344 timestamp: 1773854035739 - conda: https://conda.anaconda.org/conda-forge/linux-64/libnl-3.11.0-hb9d3cd8_0.conda @@ -14804,6 +16274,9 @@ packages: license: LGPL-2.1-only license_family: GPL purls: [] + run_exports: + weak: + - libnsl >=2.0.1,<2.1.0a0 size: 33731 timestamp: 1750274110928 - conda: https://conda.anaconda.org/conda-forge/linux-64/libntlm-1.8-hb9d3cd8_0.conda @@ -14814,6 +16287,9 @@ packages: - libgcc >=13 license: LGPL-2.1-or-later purls: [] + run_exports: + weak: + - libntlm >=1.8,<2.0a0 size: 33418 timestamp: 1734670021371 - conda: https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.5-hd0c01bc_1.conda @@ -14825,6 +16301,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libogg >=1.3.5,<1.4.0a0 size: 218500 timestamp: 1745825989535 - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.33-openmp_hd680484_0.conda @@ -14859,6 +16338,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libopenblas >=0.3.33,<1.0a0 size: 5931919 timestamp: 1776993658641 - conda: https://conda.anaconda.org/conda-forge/linux-64/libopengl-1.7.0-ha4b6fd6_3.conda @@ -14869,6 +16351,7 @@ packages: - libglvnd 1.7.0 ha4b6fd6_3 license: LicenseRef-libglvnd purls: [] + run_exports: {} size: 51767 timestamp: 1779728204026 - conda: https://conda.anaconda.org/conda-forge/linux-64/libopengl-devel-1.7.0-ha4b6fd6_3.conda @@ -14879,6 +16362,9 @@ packages: - libopengl 1.7.0 ha4b6fd6_3 license: LicenseRef-libglvnd purls: [] + run_exports: + weak: + - libopengl >=1.7.0,<2.0a0 size: 16667 timestamp: 1779728214747 - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-2026.0.0-hb56ce9e_1.conda @@ -14895,6 +16381,22 @@ packages: purls: [] size: 6582302 timestamp: 1772727204779 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-2026.2.1-h1f0fae8_1.conda + sha256: 7941fb9ba8c3a5a0a2401dc4120e8fcb561b96d928c43374eb93f545019a2858 + md5: ea41753f926f73966629d81fdf20ec6f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - pugixml >=1.15,<1.16.0a0 + - tbb >=2023.0.0 + license: Apache-2.0 + license_family: APACHE + run_exports: + weak: + - libopenvino >=2026.2.1,<2026.2.2.0a0 + size: 6823841 + timestamp: 1782219077259 - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-auto-batch-plugin-2026.0.0-hd85de46_1.conda sha256: 286de85805dc69ce0bd25367ae2a20c8096ddef35eb2483474eb246dacd5387e md5: ee41df976413676f794af2785b291b0c @@ -14909,6 +16411,20 @@ packages: purls: [] size: 114431 timestamp: 1772727230331 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-auto-batch-plugin-2026.2.1-h7e124b3_1.conda + sha256: 3ac14d36fa890840ae8474b8a9f0a094b8542fd8fbc409faf3d465c68f20aff0 + md5: 5698a64698e14e8a2e9e16f8f0de0e2e + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libopenvino 2026.2.1 h1f0fae8_1 + - libstdcxx >=14 + - tbb >=2023.0.0 + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 114628 + timestamp: 1782219097820 - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-auto-plugin-2026.0.0-hd85de46_1.conda sha256: 9988ed6339a5eb044ae8d079e2b22f5a310c41e49a0cf716057f30b21ef9cec2 md5: ca025fa5c42ba94453636a2ae333de6b @@ -14923,6 +16439,20 @@ packages: purls: [] size: 249056 timestamp: 1772727247597 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-auto-plugin-2026.2.1-h7e124b3_1.conda + sha256: 499a472fc7b598ad3753b8f2afe60eb5a277d48eca9362e8aca094b2862587a7 + md5: 2ce088ef09292930d4cb3262ce7e144d + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libopenvino 2026.2.1 h1f0fae8_1 + - libstdcxx >=14 + - tbb >=2023.0.0 + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 250912 + timestamp: 1782219111223 - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-hetero-plugin-2026.0.0-hd41364c_1.conda sha256: c7db498aeda5b0f36b347f4211b93b66ba108faaf54157a08bae8fa3c3af5f81 md5: 07a23e96db38f63d9763f666b2db66aa @@ -14937,6 +16467,20 @@ packages: purls: [] size: 211582 timestamp: 1772727264950 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-hetero-plugin-2026.2.1-hd41364c_1.conda + sha256: bec24379598a4405de171ad151945e79743c6bd049aceabf190b753c3f7a11da + md5: 02e71250f7ca786c4b183d0a39ef63ab + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libopenvino 2026.2.1 h1f0fae8_1 + - libstdcxx >=14 + - pugixml >=1.15,<1.16.0a0 + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 215488 + timestamp: 1782219123433 - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-cpu-plugin-2026.0.0-hb56ce9e_1.conda sha256: 01a28c0bd1f205b3800e7759e30bc8e8a75836e0d5a73a745b4da42837bbb174 md5: b43b96578573ddbcc8d084ae6e44c964 @@ -14952,6 +16496,21 @@ packages: purls: [] size: 13173323 timestamp: 1772727282718 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-cpu-plugin-2026.2.1-h1f0fae8_1.conda + sha256: eecc040a7838752a2dff9b4435a4c59bbc67b83e0c880457935b968206cb20b5 + md5: 7288f979a74cfe3fd4b32d8a0dc7baa4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libopenvino 2026.2.1 h1f0fae8_1 + - libstdcxx >=14 + - pugixml >=1.15,<1.16.0a0 + - tbb >=2023.0.0 + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 13637410 + timestamp: 1782219135415 - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-gpu-plugin-2026.0.0-hb56ce9e_1.conda sha256: 720b87e1d5f1a10c577e040d4bf425072a978e925c6dfab8b1551bc848007c94 md5: 26e8e92c90d1a22af6eac8e9507d9b8f @@ -14968,6 +16527,22 @@ packages: purls: [] size: 11402462 timestamp: 1772727323957 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-gpu-plugin-2026.2.1-h1f0fae8_1.conda + sha256: a47442ce578b022e19a306f963536a108cc79385f4e09d57a14a849b6a864604 + md5: c0a258b12f0c18c476b8344dbd6db8d5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libopenvino 2026.2.1 h1f0fae8_1 + - libstdcxx >=14 + - ocl-icd >=2.3.4,<3.0a0 + - pugixml >=1.15,<1.16.0a0 + - tbb >=2023.0.0 + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 12367381 + timestamp: 1782219178219 - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-npu-plugin-2026.0.0-hb56ce9e_1.conda sha256: df7eb2b23a1af38f2cd2281353309f2e2a04da1374ecedc7c6745c2a67ba617c md5: 01ba8b179ac45b2b37fe2d4225dddcc7 @@ -14984,6 +16559,22 @@ packages: purls: [] size: 1994640 timestamp: 1772727360780 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-npu-plugin-2026.2.1-h1f0fae8_1.conda + sha256: 45a91feb68ccce90ad0fa86520572233ca20be56deae0c920f86133d020ad1e8 + md5: c214b149e108e92672e0ee097ebe16f7 + depends: + - __glibc >=2.17,<3.0.a0 + - level-zero >=1.29.0,<2.0a0 + - libgcc >=14 + - libopenvino 2026.2.1 h1f0fae8_1 + - libstdcxx >=14 + - pugixml >=1.15,<1.16.0a0 + - tbb >=2023.0.0 + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 2630818 + timestamp: 1782219217519 - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-ir-frontend-2026.0.0-hd41364c_1.conda sha256: 8e7356b0b80b3f180615e264694d6811d388b210155d419553ff64e42f78ffa0 md5: aa002c4d343b01cdcc458c95cd071d1b @@ -14998,6 +16589,22 @@ packages: purls: [] size: 192778 timestamp: 1772727380069 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-ir-frontend-2026.2.1-hd41364c_1.conda + sha256: ebeba9a3ac9505ee69b556865b7d1b9fbbad01ca1ebe6a4249ff62c3dc677b47 + md5: 2d946aebcf06e9ba438880987050e975 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libopenvino 2026.2.1 h1f0fae8_1 + - libstdcxx >=14 + - pugixml >=1.15,<1.16.0a0 + license: Apache-2.0 + license_family: APACHE + run_exports: + weak: + - libopenvino-ir-frontend >=2026.2.1,<2026.2.2.0a0 + size: 201061 + timestamp: 1782219232657 - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-onnx-frontend-2026.0.0-h7a07914_1.conda sha256: 35a68214201e807bd9a31f94e618cb6a5385198e89eef46dde6c122cff77da58 md5: 218084544c2e7e78e4b8877ec37b8cdb @@ -15014,6 +16621,24 @@ packages: purls: [] size: 1860687 timestamp: 1772727397981 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-onnx-frontend-2026.2.1-h607c73d_1.conda + sha256: 7b105c0102356352d6d9518a112ff6343dab6b8f32c837809117cd26cbf006df + md5: 3bd3599825189418ea14b2c9da3a6d87 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libgcc >=14 + - libopenvino 2026.2.1 h1f0fae8_1 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE + run_exports: + weak: + - libopenvino-onnx-frontend >=2026.2.1,<2026.2.2.0a0 + size: 1944558 + timestamp: 1782219246849 - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-paddle-frontend-2026.0.0-h7a07914_1.conda sha256: cb37b717480207a66443a93d4342cf88210a74c0820fc0edd70e4fc791a64779 md5: 74915e5e271ef76a89f711eff5959a75 @@ -15030,6 +16655,24 @@ packages: purls: [] size: 684224 timestamp: 1772727417276 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-paddle-frontend-2026.2.1-h607c73d_1.conda + sha256: af45c03d41ebe0b48c28b68be31ee919cb801ac5077164808a66db515ad6a316 + md5: 91e198085bff9d8fa02d4d947f026ba8 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libgcc >=14 + - libopenvino 2026.2.1 h1f0fae8_1 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE + run_exports: + weak: + - libopenvino-paddle-frontend >=2026.2.1,<2026.2.2.0a0 + size: 690240 + timestamp: 1782219261154 - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-pytorch-frontend-2026.0.0-hecca717_1.conda sha256: 086469e5cd8bfde48975fe8641a7d6924e3da00d75dd06c99e03a78df03a0568 md5: 559ef86008749861a53025f669004f18 @@ -15043,6 +16686,21 @@ packages: purls: [] size: 1185558 timestamp: 1772727435039 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-pytorch-frontend-2026.2.1-hecca717_1.conda + sha256: e6353874a36143ffb7db7ec2c3767fd5e3434a8eeff41a569bc46e68259f668f + md5: 152d6694f1d05b53319b8376cdd811e4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libopenvino 2026.2.1 h1f0fae8_1 + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE + run_exports: + weak: + - libopenvino-pytorch-frontend >=2026.2.1,<2026.2.2.0a0 + size: 1226625 + timestamp: 1782219274006 - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-frontend-2026.0.0-h78e8023_1.conda sha256: 3a9a404bc9fd39e7395d49f4bd8facb58a01a31aeceabe8723a9d4f8eb5cc381 md5: fb20f4234bc0e29af1baa13d35e36785 @@ -15060,6 +16718,25 @@ packages: purls: [] size: 1257870 timestamp: 1772727453738 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-frontend-2026.2.1-h21c0c73_1.conda + sha256: cffe112815b8eb57528fdfdf8b39f6a0915884291147dab5bc2066d2bf123031 + md5: 89d2455ec2f065786856b0cd2ac1c0c6 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libgcc >=14 + - libopenvino 2026.2.1 h1f0fae8_1 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - libstdcxx >=14 + - snappy >=1.2.2,<1.3.0a0 + license: Apache-2.0 + license_family: APACHE + run_exports: + weak: + - libopenvino-tensorflow-frontend >=2026.2.1,<2026.2.2.0a0 + size: 1284650 + timestamp: 1782219287644 - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-lite-frontend-2026.0.0-hecca717_1.conda sha256: e7cee37c92ed0b62c0458c13937b6ad66319f1879f236a31c3a67391a999f429 md5: 0f0281435478b981f672a44d0029018c @@ -15073,6 +16750,21 @@ packages: purls: [] size: 456585 timestamp: 1772727473378 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-lite-frontend-2026.2.1-hecca717_1.conda + sha256: 142e7b24173ca8c32dbdb29c60f33a56ffb21a4ed733c9d6ab160c3a213ff52e + md5: c1a50f20847df0a8cb462138153ab46f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libopenvino 2026.2.1 h1f0fae8_1 + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE + run_exports: + weak: + - libopenvino-tensorflow-lite-frontend >=2026.2.1,<2026.2.2.0a0 + size: 501906 + timestamp: 1782219300706 - conda: https://conda.anaconda.org/conda-forge/linux-64/libopus-1.6.1-h280c20c_0.conda sha256: f1061a26213b9653bbb8372bfa3f291787ca091a9a3060a10df4d5297aad74fd md5: 2446ac1fe030c2aa6141386c1f5a6aed @@ -15082,6 +16774,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libopus >=1.6.1,<2.0a0 size: 324993 timestamp: 1768497114401 - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.19-hb03c661_0.conda @@ -15093,6 +16788,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libpciaccess >=0.19,<0.20.0a0 size: 29147 timestamp: 1773533027610 - conda: https://conda.anaconda.org/conda-forge/linux-64/libplacebo-7.351.0-h9eeb4b2_2.conda @@ -15110,6 +16808,23 @@ packages: purls: [] size: 538365 timestamp: 1775415851386 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libplacebo-7.360.1-hc50b9dd_1.conda + sha256: 7fa90c06b81559cb56ea7806a6696fb4902a1acc20bbaff1bd3a4a75b3ffa0d5 + md5: 4a750e2ae0d52d003bb1e3421581585e + depends: + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libdovi >=3.4.0,<4.0a0 + - lcms2 >=2.19.1,<3.0a0 + - shaderc >=2026.3,<2026.4.0a0 + - libvulkan-loader >=1.4.341.0,<2.0a0 + license: LGPL-2.1-or-later + run_exports: + weak: + - libplacebo >=7.360.1,<7.361.0a0 + size: 550759 + timestamp: 1784287829706 - conda: https://conda.anaconda.org/conda-forge/linux-64/libpmix-5.0.8-h9b03745_3.conda sha256: 339fdb508599d406a126cd93fd0fb995551cc5b981fc66e2fc615d4d606f6260 md5: 935ac8861a784e8393474b1675f92b3f @@ -15132,6 +16847,9 @@ packages: - libzlib >=1.3.2,<2.0a0 license: zlib-acknowledgement purls: [] + run_exports: + weak: + - libpng >=1.6.58,<1.7.0a0 size: 317729 timestamp: 1776315175087 - conda: https://conda.anaconda.org/conda-forge/linux-64/libpq-18.4-hd5a49e9_0.conda @@ -15148,6 +16866,22 @@ packages: purls: [] size: 2754709 timestamp: 1778786234149 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libpq-18.4-hd5a49e9_1.conda + sha256: f7232cb79a31f5a5bcd499aea930b469cde8b96d26db9541022493fd274d2a6e + md5: 6c9103e7ea739a3bb3505da49a4708c1 + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=78.3,<79.0a0 + - krb5 >=1.22.2,<1.23.0a0 + - libgcc >=14 + - openldap >=2.6.13,<2.7.0a0 + - openssl >=3.5.7,<4.0a0 + license: PostgreSQL + run_exports: + weak: + - libpq >=18.4,<19.0a0 + size: 2709008 + timestamp: 1782580447454 - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-6.33.5-h6eeba95_1.conda sha256: a59aa3f076d5710c618ca8fd12d9cd8211d8b738f6b0e0c98517c0162f23a5de md5: 7a4b11f3dd7374f1991a4088390d07c1 @@ -15163,6 +16897,38 @@ packages: purls: [] size: 3675765 timestamp: 1780003831209 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-7.35.1-h2840a7c_2.conda + sha256: b5ac3938186516c1091b96414c34f90255da2a3bbd736a0712b22bbf4b5ebf02 + md5: 729db8acaadb9a5e5bb2dc3d9ac84ae4 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.2,<2.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - libprotobuf >=7.35.1,<7.35.2.0a0 + size: 3774596 + timestamp: 1783168720126 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libpsl-0.22.0-h49b2146_1.conda + sha256: 71118885feab91c61bea4e9532ec46e0653b24f02e563681f822915aee8435bb + md5: af5ddfb52ad25d833c70c7511478d4eb + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=78.3,<79.0a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + run_exports: + weak: + - libpsl >=0.22.0,<0.23.0a0 + size: 70092 + timestamp: 1783936855082 - conda: https://conda.anaconda.org/conda-forge/linux-64/libptscotch-7.0.11-int32_hdf7b106_3.conda sha256: 1c77300be0d21b395286c409ef535c3cb496a4d488b9552d0a26df68e6a3c76a md5: ce7f5be0f35001f96185b5f632cc810d @@ -15171,6 +16937,23 @@ packages: - openmpi >=5.0.10,<6.0a0 size: 194317 timestamp: 1776441238560 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libraqm-0.10.5-h6406941_1.conda + sha256: 7c9e562842f193f772ef0ba681f238a2d9e5ef637588b6e46eb30cc6aa1940c8 + md5: fa63517815747363c41b439ff9301db1 + depends: + - __glibc >=2.28,<3.0.a0 + - libgcc >=14 + - libharfbuzz >=14.2.1 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - fribidi >=1.0.16,<2.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - libraqm >=0.10.5,<0.11.0a0 + size: 29441 + timestamp: 1782807076031 - conda: https://conda.anaconda.org/conda-forge/linux-64/libraw-0.22.1-h074291d_0.conda sha256: 2d2eb9147efeec20bcc89313fa55d052829b7d23def0e8eed039d374ecaf785e md5: bbb55eb739ed4188e24904cafa939567 @@ -15187,6 +16970,24 @@ packages: purls: [] size: 752027 timestamp: 1775541726097 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libraw-0.22.2-h074291d_0.conda + sha256: fa3ccb18cf22f8ac94ec4f6bfcc9fd5805bf7af11cf56bf19c27fb051b36dd6a + md5: a11f92bd6dd6721cce01564c7c9fdb25 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - _openmp_mutex >=4.5 + - libzlib >=1.3.2,<2.0a0 + - lcms2 >=2.19.1,<3.0a0 + - jasper >=4.2.9,<5.0a0 + - libjpeg-turbo >=3.2.0,<4.0a0 + license: LGPL-2.1-only + run_exports: + weak: + - libraw >=0.22.2,<0.23.0a0 + size: 742828 + timestamp: 1784220858002 - conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.62.3-h4c96295_0.conda sha256: 5571bd8239d71961d4e3ce972f865b3ea95a91ce0b53d5749fe2dd24254ddbda md5: 492c8d9b1c564c2e948b6cb4ba0f8261 @@ -15205,6 +17006,9 @@ packages: - __glibc >=2.17 license: LGPL-2.1-or-later purls: [] + run_exports: + weak: + - librsvg >=2.62.3,<3.0a0 size: 3476570 timestamp: 1780450632624 - conda: https://conda.anaconda.org/conda-forge/linux-64/libscotch-7.0.11-int32_h9a00010_3.conda @@ -15251,6 +17055,9 @@ packages: license: LGPL-2.1-or-later license_family: LGPL purls: [] + run_exports: + weak: + - libsndfile >=1.2.2,<1.3.0a0 size: 355619 timestamp: 1765181778282 - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.22-h280c20c_1.conda @@ -15290,6 +17097,19 @@ packages: purls: [] size: 954962 timestamp: 1777986471789 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.3-h0c1763c_0.conda + sha256: 365376f4815e5e80def2b3462a2419708b7c292da0da85278386c2618621fff4 + md5: 4aed8e657e9ff156bdbe849b4df44389 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.2,<2.0a0 + license: blessing + run_exports: + weak: + - libsqlite >=3.53.3,<4.0a0 + size: 962119 + timestamp: 1782519076616 - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda sha256: fa39bfd69228a13e553bd24601332b7cfeb30ca11a3ca50bb028108fe90a7661 md5: eecce068c7e4eddeb169591baac20ac4 @@ -15301,6 +17121,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libssh2 >=1.11.1,<2.0a0 size: 304790 timestamp: 1745608545575 - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda @@ -15314,6 +17137,7 @@ packages: license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] + run_exports: {} size: 5852044 timestamp: 1778269036376 - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_19.conda @@ -15324,6 +17148,9 @@ packages: license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] + run_exports: + strong: + - libstdcxx size: 27776 timestamp: 1778269074600 - conda: https://conda.anaconda.org/conda-forge/linux-64/libsuitesparseconfig-7.10.1-h901830b_7100101.conda @@ -15349,6 +17176,7 @@ packages: - libgcc >=14 license: LGPL-2.1-or-later purls: [] + run_exports: {} size: 493022 timestamp: 1780084748140 - conda: https://conda.anaconda.org/conda-forge/linux-64/libtheora-1.1.1-h4ab18f5_1006.conda @@ -15363,6 +17191,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libtheora >=1.1.1,<1.2.0a0 size: 328924 timestamp: 1719667859099 - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda @@ -15383,6 +17214,26 @@ packages: purls: [] size: 435273 timestamp: 1762022005702 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.2-h9d88235_0.conda + sha256: b31346e1c01ab40a170e91147092ee8fd92b1dee3c66ee47ef025571c879b159 + md5: c1fcb4a88bc15a9f77ad8d27d7af1df9 + depends: + - __glibc >=2.17,<3.0.a0 + - lerc >=4.1.0,<5.0a0 + - libdeflate >=1.25,<1.26.0a0 + - libgcc >=14 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libstdcxx >=14 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: HPND + run_exports: + weak: + - libtiff >=4.7.2,<4.8.0a0 + size: 452337 + timestamp: 1783084902636 - conda: https://conda.anaconda.org/conda-forge/linux-64/libudev1-257.13-h084b8d7_1.conda sha256: 287d05680e49eea51b8145fbf34bc213c0618b04f32e450e9da5d715e5134e38 md5: 89e5671a076d99516a6acd72a35b1640 @@ -15392,6 +17243,7 @@ packages: - libgcc >=14 license: LGPL-2.1-or-later purls: [] + run_exports: {} size: 145969 timestamp: 1780084753104 - conda: https://conda.anaconda.org/conda-forge/linux-64/libumfpack-6.3.5-h873dde6_7100101.conda @@ -15418,6 +17270,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libunwind >=1.8.3,<1.9.0a0 size: 75995 timestamp: 1757032240102 - conda: https://conda.anaconda.org/conda-forge/linux-64/liburing-2.14-hb700be7_0.conda @@ -15430,6 +17285,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - liburing >=2.14,<2.15.0a0 size: 154203 timestamp: 1770566529700 - conda: https://conda.anaconda.org/conda-forge/linux-64/libusb-1.0.29-h73b1eb8_0.conda @@ -15441,6 +17299,9 @@ packages: - libudev1 >=257.4 license: LGPL-2.1-or-later purls: [] + run_exports: + weak: + - libusb >=1.0.29,<2.0a0 size: 89551 timestamp: 1748856210075 - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.1-h5347b49_0.conda @@ -15454,6 +17315,19 @@ packages: purls: [] size: 40163 timestamp: 1779118517630 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.2-h5347b49_0.conda + sha256: 9b1bdce27a7e31f7d241aeecff67a1f3101d52a2b1e33ccc2cdf2613072bf81f + md5: 01bb81d12c957de066ea7362007df642 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - libuuid >=2.42.2,<3.0a0 + size: 40017 + timestamp: 1781625522462 - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.52.1-h280c20c_0.conda sha256: e28e4519223f78b3163599ca89c3f2d80bfb53e907e7fc74e806e60d1efa578b md5: 4e33d49bf4fc853855a3b00643aa5484 @@ -15486,6 +17360,29 @@ packages: purls: [] size: 221308 timestamp: 1765652453244 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libva-2.24.1-he1eb515_0.conda + sha256: 16a76abbb4fd1de4516ac4a3d06cbf1f561bc8049ca72b04dcac395eee74d017 + md5: eb1b7f8bfdea40eef150c4a1d37df09e + depends: + - __glibc >=2.17,<3.0.a0 + - libdrm >=2.4.127,<2.5.0a0 + - libegl >=1.7.0,<2.0a0 + - libgcc >=14 + - libgl >=1.7.0,<2.0a0 + - libglx >=1.7.0,<2.0a0 + - libxcb >=1.17.0,<2.0a0 + - wayland >=1.25.0,<2.0a0 + - wayland-protocols + - xorg-libx11 >=1.8.13,<2.0a0 + - xorg-libxext >=1.3.7,<2.0a0 + - xorg-libxfixes >=6.0.2,<7.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - libva >=2.24.1,<3.0a0 + size: 222717 + timestamp: 1783519315031 - conda: https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h54a6638_2.conda sha256: ca494c99c7e5ecc1b4cd2f72b5584cef3d4ce631d23511184411abcbb90a21a5 md5: b4ecbefe517ed0157c37f8182768271c @@ -15499,6 +17396,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libvorbis >=1.3.7,<1.4.0a0 size: 285894 timestamp: 1753879378005 - conda: https://conda.anaconda.org/conda-forge/linux-64/libvpl-2.16.0-h54a6638_0.conda @@ -15513,6 +17413,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libvpl >=2.16.0,<2.17.0a0 size: 287992 timestamp: 1772980546550 - conda: https://conda.anaconda.org/conda-forge/linux-64/libvpx-1.15.2-hecca717_0.conda @@ -15525,6 +17428,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libvpx >=1.15.2,<1.16.0a0 size: 1070048 timestamp: 1762010217363 - conda: https://conda.anaconda.org/conda-forge/linux-64/libvulkan-loader-1.4.341.0-h5279c79_0.conda @@ -15541,6 +17447,9 @@ packages: license: Apache-2.0 license_family: APACHE purls: [] + run_exports: + weak: + - libvulkan-loader >=1.4.341.0,<2.0a0 size: 199795 timestamp: 1770077125520 - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda @@ -15554,6 +17463,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libwebp-base >=1.6.0,<2.0a0 size: 429011 timestamp: 1752159441324 - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda @@ -15568,6 +17480,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libxcb >=1.17.0,<2.0a0 size: 395888 timestamp: 1727278577118 - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda @@ -15577,6 +17492,9 @@ packages: - libgcc-ng >=12 license: LGPL-2.1-or-later purls: [] + run_exports: + weak: + - libxcrypt >=4.4.36 size: 100393 timestamp: 1702724383534 - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.13.2-hca5e8e5_0.conda @@ -15594,8 +17512,25 @@ packages: license: MIT/X11 Derivative license_family: MIT purls: [] + run_exports: + weak: + - libxkbcommon >=1.13.2,<2.0a0 size: 851166 timestamp: 1780213397575 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbfile-1.2.0-hb03c661_0.conda + sha256: 49a0a85e595d1d6c7f355e13793fbcb650d42182f1051f81b15ae754f7c5692a + md5: f330a1b107cb5861f26dbb72a26f33e8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - xorg-libx11 >=1.8.13,<2.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - libxkbfile >=1.2.0,<2.0a0 + size: 87066 + timestamp: 1778169480942 - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.3-hca6bf5a_0.conda sha256: 3d44f737c5ae52d5af32682cc1530df433f401f8e58a7533926536244127572a md5: e79d2c2f24b027aa8d5ab1b1ba3061e7 @@ -15611,6 +17546,7 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: {} size: 559775 timestamp: 1776376739004 - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.3-h49c6c72_0.conda @@ -15627,6 +17563,10 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libxml2 + - libxml2-16 >=2.15.3 size: 46810 timestamp: 1776376751152 - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-devel-2.15.3-h49c6c72_0.conda @@ -15670,6 +17610,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libzip >=1.11.2,<2.0a0 size: 109043 timestamp: 1730442108429 - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda @@ -15682,6 +17625,9 @@ packages: license: Zlib license_family: Other purls: [] + run_exports: + weak: + - libzlib >=1.3.2,<2.0a0 size: 63629 timestamp: 1774072609062 - conda: https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-22.1.7-h4922eb0_0.conda @@ -15737,6 +17683,9 @@ packages: license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - lz4-c >=1.10.0,<1.11.0a0 size: 167055 timestamp: 1733741040117 - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda @@ -15799,6 +17748,36 @@ packages: - pkg:pypi/matplotlib?source=hash-mapping size: 8336056 timestamp: 1777000573501 +- conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.11.0-py312hc1765e9_1.conda + sha256: d983b4ea15841f1fde4ccba4370485e89e696a447fcf5e90a1f75c8487699c68 + md5: 1d06d24525cf2e373310c1131acf12f8 + depends: + - __glibc >=2.17,<3.0.a0 + - contourpy >=1.0.1 + - cycler >=0.10 + - fonttools >=4.22.0 + - freetype + - kiwisolver >=1.3.1 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libgcc >=14 + - libraqm >=0.10.5,<0.11.0a0 + - libstdcxx >=14 + - numpy >=1.23 + - numpy >=1.25,<3 + - packaging >=20.0 + - pillow >=8 + - pyparsing >=2.3.1 + - python >=3.12,<3.13.0a0 + - python-dateutil >=2.7 + - python_abi 3.12.* *_cp312 + - qhull >=2020.2,<2020.3.0a0 + - tk >=8.6.13,<8.7.0a0 + license: PSF-2.0 + license_family: PSF + run_exports: {} + size: 8877024 + timestamp: 1782829532714 - conda: https://conda.anaconda.org/conda-forge/linux-64/medcoupling-9.14.0-py312_mpi_openmpi_h966300a_6.conda sha256: cded0e1c6a6a3638afd2445755323e1bbc36e3b83cb08da7c9f711f2d6d2f035 md5: b8b6d77055170180015fc70f2be50272 @@ -15867,9 +17846,33 @@ packages: - mesalib license: MIT license_family: MIT - purls: [] - size: 6350427 - timestamp: 1755729794084 + purls: [] + size: 6350427 + timestamp: 1755729794084 +- conda: https://conda.anaconda.org/conda-forge/linux-64/mesalib-26.0.3-h8cca3c9_0.conda + sha256: 3e6385e04e5a8f62599a5eb72b9a8c65ac143f0621d57f24688f103276d686eb + md5: 823e4ae1d60e97845773f7358585fc56 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - xorg-libxshmfence >=1.3.3,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + - xorg-libx11 >=1.8.13,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - xorg-libxrandr >=1.5.5,<2.0a0 + - libexpat >=2.7.4,<3.0a0 + - spirv-tools >=2026,<2027.0a0 + - libllvm22 >=22.1.1,<22.2.0a0 + - libxcb >=1.17.0,<2.0a0 + - libdrm >=2.4.125,<2.5.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - mesalib >=26.0.3,<26.1.0a0 + size: 2964576 + timestamp: 1773867966762 - conda: https://conda.anaconda.org/conda-forge/linux-64/metis-5.1.0-hd0bcaf9_1007.conda sha256: e8a00971e6d00bd49f375c5d8d005b37a9abba0b1768533aed0f90a422bf5cc7 md5: 28eb714416de4eb83e2cbc47e99a1b45 @@ -15938,6 +17941,9 @@ packages: license: LGPL-3.0-only license_family: LGPL purls: [] + run_exports: + weak: + - mpfr >=4.2.2,<5.0a0 size: 730422 timestamp: 1773413915171 - conda: https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.9-hc50e24c_0.conda @@ -15950,6 +17956,9 @@ packages: license: LGPL-2.1-only license_family: LGPL purls: [] + run_exports: + weak: + - mpg123 >=1.32.9,<1.33.0a0 size: 491140 timestamp: 1730581373280 - conda: https://conda.anaconda.org/conda-forge/linux-64/mpi4py-4.1.2-py312hd140a38_100.conda @@ -15980,6 +17989,20 @@ packages: - pkg:pypi/msgpack?source=hash-mapping size: 102525 timestamp: 1762504116832 +- conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.2.1-py312h0a2e395_1.conda + sha256: c9764c77dd7f9c581c1d8a24c3024edf777cacfb5a4180de5badc6638dc8590f + md5: a142257c1b69e37cfefc66dc228a4d93 + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - libgcc >=14 + - python_abi 3.12.* *_cp312 + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 112800 + timestamp: 1782460774570 - conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.7.1-py312h8a5da7c_0.conda sha256: 0da7e7f4e69bfd6c98eff92523e93a0eceeaec1c6d503d4a4cd0af816c3fe3dc md5: 17c77acc59407701b54404cfd3639cac @@ -15992,6 +18015,7 @@ packages: license_family: APACHE purls: - pkg:pypi/multidict?source=hash-mapping + run_exports: {} size: 100056 timestamp: 1771611023053 - conda: https://conda.anaconda.org/conda-forge/linux-64/mumps-include-5.8.2-h158ef2a_2.conda @@ -16059,6 +18083,9 @@ packages: - libgcc >=14 license: X11 AND BSD-3-Clause purls: [] + run_exports: + weak: + - ncurses >=6.6,<7.0a0 size: 918956 timestamp: 1777422145199 - conda: https://conda.anaconda.org/conda-forge/linux-64/netcdf4-1.7.4-nompi_py311ha0596eb_107.conda @@ -16095,6 +18122,7 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: {} size: 136216 timestamp: 1758194284857 - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-24.16.0-h3d65ac4_0.conda @@ -16180,6 +18208,27 @@ packages: license_family: BSD size: 8978806 timestamp: 1779169197952 +- conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.5.1-py312h33ff503_0.conda + sha256: 2941c4a5fee88fe1c91c0d95b65e3a292d078f6655b53ff60f4813a44349c83e + md5: 3b7525d598ec0d0365ebd2378160a02f + depends: + - python + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libcblas >=3.9.0,<4.0a0 + - python_abi 3.12.* *_cp312 + - liblapack >=3.9.0,<4.0a0 + - libblas >=3.9.0,<4.0a0 + constrains: + - numpy-base <0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - numpy >=1.25,<3 + size: 8929128 + timestamp: 1783206200235 - conda: https://conda.anaconda.org/conda-forge/linux-64/obstore-0.10.0-py312h0ccc70a_0.conda sha256: b1bcf3dafd8afe87a0567d1e3aff425642124803f637f05aee8c8f540509c40d md5: 9bb87e4232d169111de1bfbc4b841cf5 @@ -16262,6 +18311,36 @@ packages: purls: [] size: 25385099 timestamp: 1776668879469 +- conda: https://conda.anaconda.org/conda-forge/linux-64/occt-8.0.0-all_hbdb0871_202.conda + sha256: 25e8239b1f7e54937cdbee96bfc941fd4d9c0ce35a134f4ce4a14495c4041566 + md5: 50ad6a828477507be762879cbc046ee7 + depends: + - __glibc >=2.17,<3.0.a0 + - fontconfig >=2.18.1,<3.0a0 + - fonts-conda-ecosystem + - freeimage >=3.18.0,<3.19.0a0 + - freetype + - libexpat >=2.8.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libgcc >=14 + - libgl >=1.7.0,<2.0a0 + - libstdcxx >=14 + - rapidjson + - tbb >=2023.0.0 + - vtk + - vtk-base >=9.6.2,<9.6.3.0a0 + - xorg-libx11 >=1.8.13,<2.0a0 + - xorg-libxext >=1.3.7,<2.0a0 + - xorg-libxi >=1.8.3,<2.0a0 + - xorg-libxt >=1.3.1,<2.0a0 + license: LGPL-2.1-only + license_family: LGPL + run_exports: + weak: + - occt >=8.0.0,<8.0.1.0a0 + size: 26964419 + timestamp: 1781682677147 - conda: https://conda.anaconda.org/conda-forge/linux-64/ocl-icd-2.3.4-hb03c661_1.conda sha256: 75f3bf733523a338f73d6c276c4a26634877cd970edb558f2769d9fa52b100a9 md5: c2871ba95727fd1382c05db66048b64c @@ -16270,7 +18349,11 @@ packages: - libgcc >=14 - opencl-headers >=2025.6.13 license: BSD-2-Clause + license_family: BSD purls: [] + run_exports: + weak: + - ocl-icd >=2.3.4,<3.0a0 size: 109598 timestamp: 1780362789611 - conda: https://conda.anaconda.org/conda-forge/linux-64/opencl-headers-2025.06.13-hecca717_0.conda @@ -16283,6 +18366,7 @@ packages: license: Apache-2.0 license_family: APACHE purls: [] + run_exports: {} size: 55754 timestamp: 1773844383536 - conda: https://conda.anaconda.org/conda-forge/linux-64/openexr-3.4.12-h9f1635d_0.conda @@ -16301,6 +18385,38 @@ packages: purls: [] size: 1220558 timestamp: 1779680416588 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openexr-3.4.13-hf734b31_1.conda + sha256: 17e3d2769dfc018748f0c09757b95744a5e942f74325b605d06303a69ee7955d + md5: 9db8aff65eaea058afc9fe93569b4b14 + depends: + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - imath >=3.2.2,<3.2.3.0a0 + - openjph >=0.30.1,<0.31.0a0 + - libdeflate >=1.25,<1.26.0a0 + - libzlib >=1.3.2,<2.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - openexr >=3.4.13,<3.5.0a0 + size: 1223929 + timestamp: 1782198396418 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openh264-2.6.0-h65dd3cf_1.conda + sha256: 5317c5c23762f3fe1c8510565a2bb94c645e1470ff73b386315656404f7eb58a + md5: 69894a95220a17a66272daa701c387bc + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: BSD-2-Clause + license_family: BSD + run_exports: + weak: + - openh264 >=2.6.0,<2.6.1.0a0 + size: 726478 + timestamp: 1782685945856 - conda: https://conda.anaconda.org/conda-forge/linux-64/openh264-2.6.0-hc22cd8d_0.conda sha256: 3f231f2747a37a58471c82a9a8a80d92b7fece9f3fce10901a5ac888ce00b747 md5: b28cf020fd2dead0ca6d113608683842 @@ -16326,6 +18442,9 @@ packages: license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - openjpeg >=2.5.4,<3.0a0 size: 355400 timestamp: 1758489294972 - conda: https://conda.anaconda.org/conda-forge/linux-64/openjph-0.27.3-h8d634f6_0.conda @@ -16341,6 +18460,21 @@ packages: purls: [] size: 283239 timestamp: 1778775149306 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openjph-0.30.1-h8d634f6_0.conda + sha256: e405a62cc16604c4274d1d64387fa62ba5466cc65fa087ae45451d0150f4b698 + md5: 6143d4af035262f7e1b954e1cba9156d + depends: + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - libgcc >=14 + - libtiff >=4.7.1,<4.8.0a0 + license: BSD-2-Clause + license_family: BSD + run_exports: + weak: + - openjph >=0.30.1,<0.31.0a0 + size: 294315 + timestamp: 1782091151122 - conda: https://conda.anaconda.org/conda-forge/linux-64/openldap-2.6.13-hbde042b_0.conda sha256: 21c4f6c7f41dc9bec2ea2f9c80440d9a4d45a6f2ac13243e658f10dcf1044146 md5: 680608784722880fbfe1745067570b00 @@ -16354,6 +18488,9 @@ packages: license: OLDAP-2.8 license_family: BSD purls: [] + run_exports: + weak: + - openldap >=2.6.13,<2.7.0a0 size: 786149 timestamp: 1775741359582 - conda: https://conda.anaconda.org/conda-forge/linux-64/openmpi-5.0.10-h611b0e2_0.conda @@ -16421,6 +18558,20 @@ packages: purls: [] size: 3167099 timestamp: 1775587756857 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda + sha256: d48f5c22b9897c01e4dff3680f1f57ceb02711ab9c62f74339b080419dfad34b + md5: 79dd2074b5cd5c5c6b2930514a11e22d + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + run_exports: + weak: + - openssl >=3.6.3,<4.0a0 + size: 3159683 + timestamp: 1781069855778 - conda: https://conda.anaconda.org/conda-forge/linux-64/orjson-3.11.4-py314h2925d53_1.conda sha256: d20cf7ff40d4be0da3d639608c5058efb81d0be1f2df27492734a994875390ff md5: 3c2950491e23e3eeac75ebb8f0aa3f69 @@ -16600,6 +18751,9 @@ packages: - libzlib >=1.3.2,<2.0a0 license: LGPL-2.1-or-later purls: [] + run_exports: + weak: + - pango >=1.56.4,<2.0a0 size: 458036 timestamp: 1774281947855 - conda: https://conda.anaconda.org/conda-forge/linux-64/parmetis-4.0.3-h3a7d728_1009.conda @@ -16625,6 +18779,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - pcre2 >=10.47,<10.48.0a0 size: 1222481 timestamp: 1763655398280 - conda: https://conda.anaconda.org/conda-forge/linux-64/petsc-3.24.6-real_h94d380b_0.conda @@ -16721,6 +18878,28 @@ packages: - pkg:pypi/pillow?source=hash-mapping size: 1039561 timestamp: 1775060059882 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.3.0-py312h50c33e8_0.conda + sha256: 76b2e56c7a903a06be1210d35f35c2a8dcdc57912fda5c96b2e68acfd2b281fe + md5: d749d04e1965315078f29320565c595a + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libtiff >=4.7.1,<4.8.0a0 + - libxcb >=1.17.0,<2.0a0 + - tk >=8.6.13,<8.7.0a0 + - openjpeg >=2.5.4,<3.0a0 + - zlib-ng >=2.3.3,<2.4.0a0 + - python_abi 3.12.* *_cp312 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - lcms2 >=2.19.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libwebp-base >=1.6.0,<2.0a0 + license: HPND + run_exports: {} + size: 1064129 + timestamp: 1782912080163 - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda sha256: 43d37bc9ca3b257c5dd7bf76a8426addbdec381f6786ff441dc90b1a49143b6a md5: c01af13bdc553d1a8fbfff6e8db075f0 @@ -16734,6 +18913,19 @@ packages: purls: [] size: 450960 timestamp: 1754665235234 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_2.conda + sha256: 9e5b5be056820ade8b09ef73cf9f4bea037eb9887145d0297ac99e0add88878d + md5: 7cd77fef4da3e1ca9484394616cb71f1 + depends: + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: MIT + run_exports: + weak: + - pixman >=0.46.4,<1.0a0 + size: 376220 + timestamp: 1784286827180 - conda: https://conda.anaconda.org/conda-forge/linux-64/proj-9.8.1-he0df7b0_0.conda sha256: dff6f355025b9a510d9093e29fd970fa1091e758b848c9dec814d96ae63a09ba md5: b23619e5e9009eaa070ead0342034027 @@ -16752,6 +18944,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - proj >=9.8.1,<9.9.0a0 size: 3652144 timestamp: 1775840249166 - conda: https://conda.anaconda.org/conda-forge/linux-64/propcache-0.5.2-py312h8a5da7c_0.conda @@ -16766,6 +18961,7 @@ packages: license_family: APACHE purls: - pkg:pypi/propcache?source=compressed-mapping + run_exports: {} size: 51586 timestamp: 1780037816755 - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py312h5253ce2_0.conda @@ -16803,6 +18999,7 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: {} size: 8252 timestamp: 1726802366959 - conda: https://conda.anaconda.org/conda-forge/linux-64/pugixml-1.15-h3f63f65_0.conda @@ -16815,6 +19012,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - pugixml >=1.15,<1.16.0a0 size: 118488 timestamp: 1736601364156 - conda: https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-h9a6aba3_3.conda @@ -16834,6 +19034,9 @@ packages: license: LGPL-2.1-or-later license_family: LGPL purls: [] + run_exports: + weak: + - pulseaudio-client >=17.0,<17.1.0a0 size: 750785 timestamp: 1763148198088 - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.4-py312h868fb18_0.conda @@ -16922,6 +19125,11 @@ packages: - python_abi 3.12.* *_cp312 license: Python-2.0 purls: [] + run_exports: + weak: + - python_abi 3.12.* *_cp312 + noarch: + - python size: 31608571 timestamp: 1772730708989 - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.5-habeac84_100_cp314.conda @@ -17056,6 +19264,9 @@ packages: - libstdcxx-ng >=12 license: LicenseRef-Qhull purls: [] + run_exports: + weak: + - qhull >=2020.2,<2020.3.0a0 size: 552937 timestamp: 1720813982144 - conda: https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.11.1-pl5321h16c4a6b_0.conda @@ -17131,6 +19342,81 @@ packages: purls: [] size: 60185269 timestamp: 1778597122245 +- conda: https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.11.1-pl5321h16c4a6b_1.conda + sha256: aefbc43bde188ff4027d480da99c7fa9e8e6341e9762e065190239cb9b99bb1c + md5: 331d660aef48fec733a878dd1f8f4206 + depends: + - libxcb + - xcb-util + - xcb-util-wm + - xcb-util-keysyms + - xcb-util-image + - xcb-util-renderutil + - xcb-util-cursor + - libgl-devel + - libegl-devel + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - xcb-util >=0.4.1,<0.5.0a0 + - xorg-libx11 >=1.8.13,<2.0a0 + - pcre2 >=10.47,<10.48.0a0 + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - fontconfig >=2.18.1,<3.0a0 + - fonts-conda-ecosystem + - xorg-libxxf86vm >=1.1.7,<2.0a0 + - xorg-libxrandr >=1.5.5,<2.0a0 + - libsqlite >=3.53.2,<4.0a0 + - libpq >=18.4,<19.0a0 + - xorg-libice >=1.1.2,<2.0a0 + - libtiff >=4.7.1,<4.8.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - wayland >=1.25.0,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - libvulkan-loader >=1.4.341.0,<2.0a0 + - xorg-libxext >=1.3.7,<2.0a0 + - xcb-util-keysyms >=0.4.1,<0.5.0a0 + - libpng >=1.6.58,<1.7.0a0 + - harfbuzz >=14.2.1 + - xcb-util-cursor >=0.1.6,<0.2.0a0 + - xorg-libxcursor >=1.2.3,<2.0a0 + - libcups >=2.3.3,<2.4.0a0 + - libxcb >=1.17.0,<2.0a0 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - libdrm >=2.4.127,<2.5.0a0 + - xorg-libxcomposite >=0.4.7,<1.0a0 + - xcb-util-image >=0.4.0,<0.5.0a0 + - xcb-util-wm >=0.4.2,<0.5.0a0 + - zstd >=1.5.7,<1.6.0a0 + - krb5 >=1.22.2,<1.23.0a0 + - xcb-util-renderutil >=0.3.10,<0.4.0a0 + - icu >=78.3,<79.0a0 + - xorg-libxdamage >=1.1.6,<2.0a0 + - xorg-libsm >=1.2.6,<2.0a0 + - alsa-lib >=1.2.16,<1.3.0a0 + - openssl >=3.5.6,<4.0a0 + - libglib >=2.88.1,<3.0a0 + - libgl >=1.7.0,<2.0a0 + - libxkbcommon >=1.13.2,<2.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - double-conversion >=3.4.0,<3.5.0a0 + - dbus >=1.16.2,<2.0a0 + - xorg-libxtst >=1.2.5,<2.0a0 + - libegl >=1.7.0,<2.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + constrains: + - qt ==6.11.1 + license: LGPL-3.0-only + license_family: LGPL + run_exports: + weak: + - qt6-main >=6.11.1,<7.0a0 + size: 60185421 + timestamp: 1780593127053 - conda: https://conda.anaconda.org/conda-forge/linux-64/rapidjson-1.1.0.post20240409-h3f2d84a_2.conda sha256: f87f265263a1ddbc50b98e2c2bcaa2bac63da3acc09267815dd0f4bd614cd902 md5: 65e2f30d532b4ae2063a424c185cc678 @@ -17142,6 +19428,7 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: {} size: 156074 timestamp: 1742820732296 - conda: https://conda.anaconda.org/conda-forge/linux-64/rdma-core-63.0-h192683f_1.conda @@ -17168,6 +19455,9 @@ packages: license: GPL-3.0-only license_family: GPL purls: [] + run_exports: + weak: + - readline >=8.3,<9.0a0 size: 345073 timestamp: 1765813471974 - conda: https://conda.anaconda.org/conda-forge/linux-64/rocksdb-10.6.2-h13d676d_0_default.conda @@ -17186,6 +19476,9 @@ packages: license: Apache-2.0 license_family: APACHE purls: [] + run_exports: + weak: + - rocksdb >=10.6.2,<10.7.0a0 size: 10425906 timestamp: 1761164568248 - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-2026.5.1-py312h192e038_0.conda @@ -17294,6 +19587,9 @@ packages: - libegl >=1.7.0,<2.0a0 license: Zlib purls: [] + run_exports: + weak: + - sdl2 >=2.32.56,<3.0a0 size: 589145 timestamp: 1757842881000 - conda: https://conda.anaconda.org/conda-forge/linux-64/sdl3-3.4.10-hdeec2a5_0.conda @@ -17326,6 +19622,38 @@ packages: purls: [] size: 2148830 timestamp: 1780262823658 +- conda: https://conda.anaconda.org/conda-forge/linux-64/sdl3-3.4.12-hdeec2a5_0.conda + sha256: 2607340a7adbf7b7ec902892bce00c9fd35ccdb7f1e8d4ef1efd51641ad36a3c + md5: 1c36b749acf532314ad381f6c1663647 + depends: + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - liburing >=2.14,<2.15.0a0 + - xorg-libxfixes >=6.0.2,<7.0a0 + - libusb >=1.0.29,<2.0a0 + - xorg-libxext >=1.3.7,<2.0a0 + - xorg-libx11 >=1.8.13,<2.0a0 + - libgl >=1.7.0,<2.0a0 + - dbus >=1.16.2,<2.0a0 + - libdrm >=2.4.127,<2.5.0a0 + - pulseaudio-client >=17.0,<17.1.0a0 + - xorg-libxi >=1.8.3,<2.0a0 + - wayland >=1.25.0,<2.0a0 + - libegl >=1.7.0,<2.0a0 + - libxkbcommon >=1.13.2,<2.0a0 + - libudev1 >=257.13 + - libunwind >=1.8.3,<1.9.0a0 + - xorg-libxscrnsaver >=1.2.4,<2.0a0 + - libvulkan-loader >=1.4.341.0,<2.0a0 + - xorg-libxcursor >=1.2.3,<2.0a0 + - xorg-libxtst >=1.2.5,<2.0a0 + license: Zlib + run_exports: + weak: + - sdl3 >=3.4.12,<4.0a0 + size: 2156114 + timestamp: 1782948044627 - conda: https://conda.anaconda.org/conda-forge/linux-64/shaderc-2025.5-h3e344bc_0.conda sha256: fd4d20f8b74c473e3579f181c40687697777be7ac617ee62866a2fe3199745d9 md5: 6455b7f6e2c8caeb87b83cab7163bcb8 @@ -17334,12 +19662,28 @@ packages: - glslang >=16,<17.0a0 - libgcc >=14 - libstdcxx >=14 - - spirv-tools >=2025,<2026.0a0 + - spirv-tools >=2025,<2026.0a0 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 113361 + timestamp: 1764287965059 +- conda: https://conda.anaconda.org/conda-forge/linux-64/shaderc-2026.3-h1b60276_0.conda + sha256: 1325456e9cff1ec8a5e826f64b8eef5806162bfcceeed2e32817a3c280f0dfc9 + md5: ee5e719bbf258faa3b6533a1a621092b + depends: + - __glibc >=2.17,<3.0.a0 + - glslang >=16,<17.0a0 + - libgcc >=14 + - libstdcxx >=14 + - spirv-tools >=2026,<2027.0a0 license: Apache-2.0 license_family: Apache - purls: [] - size: 113361 - timestamp: 1764287965059 + run_exports: + weak: + - shaderc >=2026.3,<2026.4.0a0 + size: 114267 + timestamp: 1784251192959 - conda: https://conda.anaconda.org/conda-forge/linux-64/shapely-2.1.2-py312h383787d_2.conda sha256: da100ac0210f52399faf814f701165058fa2e2f65f5c036cdf2bf99a40223373 md5: 69e400d3deca12ee7afd4b73a5596905 @@ -17354,6 +19698,7 @@ packages: license_family: BSD purls: - pkg:pypi/shapely?source=hash-mapping + run_exports: {} size: 631649 timestamp: 1762523699384 - conda: https://conda.anaconda.org/conda-forge/linux-64/simplejson-4.1.1-py312h4c3975b_0.conda @@ -17412,6 +19757,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - snappy >=1.2.2,<1.3.0a0 size: 45829 timestamp: 1762948049098 - conda: https://conda.anaconda.org/conda-forge/linux-64/spirv-tools-2025.5-hb700be7_0.conda @@ -17428,6 +19776,21 @@ packages: purls: [] size: 2595788 timestamp: 1769406054481 +- conda: https://conda.anaconda.org/conda-forge/linux-64/spirv-tools-2026.2-hb700be7_1.conda + sha256: 65a4e2fc5fb0677cff340ca314b25226d7eb94b2c75f733c0cab6176a8f46421 + md5: 6c08f1aeca6e195eaa2b2e9ee18a9891 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + constrains: + - spirv-headers >=1.4.350.1,<1.4.350.2.0a0 + license: Apache-2.0 + run_exports: + weak: + - spirv-tools >=2026,<2027.0a0 + size: 2392774 + timestamp: 1784383717730 - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.53.1-hbc0de68_0.conda sha256: d167fa92781bcdcd3b9aaa6bb1cd50c5b108f6190c170098a118b5cf5df2f881 md5: 8e0b8654ead18e50af552e54b5a08a61 @@ -17442,6 +19805,22 @@ packages: purls: [] size: 205399 timestamp: 1777986477546 +- conda: https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.53.3-hbc0de68_0.conda + sha256: ef17725138fa72aa5a0f8ccace9727831c4b333e39575f78fb5ad1755826b687 + md5: b345ea7f13e4cae9809c69b1d1af2c99 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libsqlite 3.53.3 h0c1763c_0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.6,<7.0a0 + - readline >=8.3,<9.0a0 + license: blessing + run_exports: + weak: + - libsqlite >=3.53.3,<4.0a0 + size: 206481 + timestamp: 1782519082269 - conda: https://conda.anaconda.org/conda-forge/linux-64/superlu-7.0.1-h8f6e6c4_0.conda sha256: 4e748f877553c7ed42290420ba1e9aa0e80cf72b23b463f12dbb7927c16f0437 md5: 6bd14d1838657334b3c0eb02f85561e2 @@ -17487,6 +19866,20 @@ packages: purls: [] size: 2619743 timestamp: 1769664536467 +- conda: https://conda.anaconda.org/conda-forge/linux-64/svt-av1-4.2.0-hecca717_0.conda + sha256: c79f983a6bb4218bdef9064aec5821d59d744f9a98f8cf8437c7bd0351df0d95 + md5: 683f1b6d013bb1eb0d5c8025d2eb21a3 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: BSD-2-Clause + license_family: BSD + run_exports: + weak: + - svt-av1 >=4.2.0,<4.2.1.0a0 + size: 2666786 + timestamp: 1784069888521 - conda: https://conda.anaconda.org/conda-forge/linux-64/tbb-2023.0.0-hab88423_2.conda sha256: 30cb9355c2fefc20ff1a3d6566b9714d5614086a2524c07721fc344eb20515ae md5: 7073b15f9364ebc118998601ac6ca6a6 @@ -17498,6 +19891,7 @@ packages: license: Apache-2.0 license_family: APACHE purls: [] + run_exports: {} size: 182331 timestamp: 1778673758649 - conda: https://conda.anaconda.org/conda-forge/linux-64/tbb-devel-2023.0.0-hab88423_2.conda @@ -17509,6 +19903,9 @@ packages: - libstdcxx >=14 - tbb 2023.0.0 hab88423_2 purls: [] + run_exports: + weak: + - tbb >=2023.0.0 size: 1139342 timestamp: 1778673771784 - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda @@ -17525,6 +19922,22 @@ packages: purls: [] size: 3301196 timestamp: 1769460227866 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd70dff1_3.conda + build_number: 103 + sha256: 43624eab22f5f29df7d6ffe914cf442f28fd559b55b290906255492826e636e8 + md5: 48a1049e710857572fc2a832aa394d9f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.2,<2.0a0 + constrains: + - xorg-libx11 >=1.8.13,<2.0a0 + license: TCL + run_exports: + weak: + - tk >=8.6.13,<8.7.0a0 + size: 3550916 + timestamp: 1784229071544 - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.6-py312h4c3975b_0.conda sha256: 1a5f2eee536c5ea97dd9674b1b883d8d676ee346f10c8d4ae30626e20aa6d289 md5: dcbe46475eff6fb9d1adad473c984f39 @@ -17599,6 +20012,7 @@ packages: license_family: Apache purls: - pkg:pypi/unicodedata2?source=hash-mapping + run_exports: {} size: 410641 timestamp: 1770909099497 - conda: https://conda.anaconda.org/conda-forge/linux-64/utfcpp-4.09-ha770c72_0.conda @@ -17606,6 +20020,7 @@ packages: md5: 99884244028fe76046e3914f90d4ad05 license: BSL-1.0 purls: [] + run_exports: {} size: 14226 timestamp: 1767012219987 - conda: https://conda.anaconda.org/conda-forge/linux-64/uvloop-0.22.1-py312h4c3975b_1.conda @@ -17651,6 +20066,26 @@ packages: purls: [] size: 25057909 timestamp: 1765513310183 +- conda: https://conda.anaconda.org/conda-forge/linux-64/viskores-1.1.1-cpu_hc82bd48_1.conda + sha256: 8b46803c96a9d3ee5757372e03e80aed20563b3bd13ddb6bb121dbfd356057e0 + md5: 2a0d0433bed3e7f5caa3e0705cf17f3b + depends: + - _openmp_mutex >=4.5 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - zfp >=1.0.1,<2.0a0 + - hdf5 >=1.14.6,<1.14.7.0a0 + - glew >=2.3.0,<2.4.0a0 + - mesalib >=26.0.3,<26.1.0a0 + track_features: + - viskores-p-0 + license: BSD-3-Clause + run_exports: + weak: + - viskores >=1.1.1,<1.2.0a0 + size: 25122380 + timestamp: 1784250954707 - conda: https://conda.anaconda.org/conda-forge/linux-64/vtk-9.6.1-py312h244374b_4.conda sha256: 964d33bf2d534bfd5e450c962cc1f11edb6c93b433adb55324e0afe3dc6d546c md5: ac787c9c5e55a50c6f13500380d665d7 @@ -17670,6 +20105,27 @@ packages: purls: [] size: 28284 timestamp: 1778024918008 +- conda: https://conda.anaconda.org/conda-forge/linux-64/vtk-9.6.2-py312h244374b_5.conda + sha256: 8f919c00c1561664b7569a34a56418632a89c8c12cb765ea70d3fbc55b951315 + md5: 8a7b9d6d5fff6e4021bb5c07c8dc1849 + depends: + - vtk-base >=9.6.2,<9.6.3.0a0 + - vtk-io-ffmpeg >=9.6.2,<9.6.3.0a0 + - libgl-devel + - libopengl-devel + - libboost-devel + - liblzma-devel + - tbb-devel + - eigen + - expat + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - vtk-base >=9.6.2,<9.6.3.0a0 + size: 28638 + timestamp: 1783987046527 - conda: https://conda.anaconda.org/conda-forge/linux-64/vtk-base-9.6.1-py312hb626a2d_2.conda sha256: b4c8ef6d5504fcc6c3a01da90330aee86cf2191305b1295996ff70d1a79a6fb5 md5: 152844030eacee12907bae850d2e7fd1 @@ -17725,6 +20181,63 @@ packages: - pkg:pypi/vtk?source=hash-mapping size: 85638361 timestamp: 1776530985916 +- conda: https://conda.anaconda.org/conda-forge/linux-64/vtk-base-9.6.2-py312h3c33751_5.conda + sha256: c1b0ebe1b9a2826a24555a8375e74c26cdc3ce55336530e93cee5e149ff5696b + md5: 0dd483ce7ab8c981dbda1e9db044a5e8 + depends: + - python + - utfcpp + - nlohmann_json + - cli11 + - numpy + - wslink + - matplotlib-base >=2.0.0 + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - xorg-libxcursor >=1.2.3,<2.0a0 + - libpng >=1.6.58,<1.7.0a0 + - libjpeg-turbo >=3.2.0,<4.0a0 + - libglvnd >=1.7.0,<2.0a0 + - hdf5 >=1.14.6,<1.14.7.0a0 + - qt6-main >=6.11.1,<7.0a0 + - python_abi 3.12.* *_cp312 + - libopengl >=1.7.0,<2.0a0 + - libtheora >=1.1.1,<1.2.0a0 + - libzlib >=1.3.2,<2.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - jsoncpp >=1.9.8,<1.9.9.0a0 + - liblzma >=5.8.3,<6.0a0 + - libxkbfile >=1.2.0,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - proj >=9.8.1,<9.9.0a0 + - libsqlite >=3.53.3,<4.0a0 + - fmt >=12.1.0,<12.2.0a0 + - xorg-libx11 >=1.8.13,<2.0a0 + - tbb >=2023.0.0 + - gl2ps >=1.4.2,<1.4.3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libglu >=9.0.3,<9.1.0a0 + - eigen-abi >=5.0.1.80,<5.0.1.81.0a0 + - double-conversion >=3.4.0,<3.5.0a0 + - libtiff >=4.7.2,<4.8.0a0 + - libglx >=1.7.0,<2.0a0 + - viskores >=1.1.1,<1.2.0a0 + - libnetcdf >=4.10.1,<4.10.2.0a0 + - libogg >=1.3.5,<1.4.0a0 + - pugixml >=1.15,<1.16.0a0 + - libexpat >=2.8.1,<3.0a0 + constrains: + - libboost-headers >=1.90.0,<1.91.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - vtk-base >=9.6.2,<9.6.3.0a0 + size: 85983890 + timestamp: 1783987046527 - conda: https://conda.anaconda.org/conda-forge/linux-64/vtk-io-ffmpeg-9.6.1-py312h8d4cf9a_2.conda sha256: db20c91a7c588fcb0a25d888c3ef0d7b4f6d82a066136b3d59e099b6c0135040 md5: d6caba66f99c2e0b07c1736e9b589250 @@ -17738,6 +20251,21 @@ packages: purls: [] size: 112230 timestamp: 1776530985916 +- conda: https://conda.anaconda.org/conda-forge/linux-64/vtk-io-ffmpeg-9.6.2-py312h47b46e0_5.conda + sha256: 9a4a8d5402979db4966b962c5d294ac330481735087046e02aa01a5ad518dbe3 + md5: d6a3471972c3b5ab318d842fb3b9aa5c + depends: + - vtk-base ==9.6.2 py312h3c33751_5 + - ffmpeg + - ffmpeg >=8.1.2,<9.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - vtk-io-ffmpeg >=9.6.2,<9.6.3.0a0 + size: 113495 + timestamp: 1783987046527 - conda: https://conda.anaconda.org/conda-forge/linux-64/watchdog-6.0.0-py312h20c3967_3.conda sha256: b91e504d9bac172fd8f9f8902c8df85f65ceabd1e7e5a737183a494c49edbc15 md5: 67d76e548cb09501c0f22071c7d86b26 @@ -17808,6 +20336,21 @@ packages: purls: [] size: 334139 timestamp: 1773959575393 +- conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.26.0-hd6090a7_0.conda + sha256: 6b9e182021ef3a64ab3bf788ebdab6de6775035612237c639ccafc941639eb13 + md5: b34c5559f45d8996e3bc0b6250a6cc84 + depends: + - __glibc >=2.17,<3.0.a0 + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + run_exports: + weak: + - wayland >=1.26.0,<2.0a0 + size: 340543 + timestamp: 1784249169392 - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-16.0-py312h5253ce2_1.conda sha256: dd598cab9175a9ab11c8a1798c49ccabe923263d12aababa84a296cb18206464 md5: e35ffb48178b20ee1a43fbe7abc93746 @@ -17868,6 +20411,9 @@ packages: license: GPL-2.0-or-later license_family: GPL purls: [] + run_exports: + weak: + - x264 >=1!164.3095,<1!165 size: 897548 timestamp: 1660323080555 - conda: https://conda.anaconda.org/conda-forge/linux-64/x265-3.5-h924138e_3.tar.bz2 @@ -17879,6 +20425,9 @@ packages: license: GPL-2.0-or-later license_family: GPL purls: [] + run_exports: + weak: + - x265 >=3.5,<3.6.0a0 size: 3357188 timestamp: 1646609687141 - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda @@ -17891,6 +20440,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xcb-util >=0.4.1,<0.5.0a0 size: 20772 timestamp: 1750436796633 - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda @@ -17906,6 +20458,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xcb-util-cursor >=0.1.6,<0.2.0a0 size: 20829 timestamp: 1763366954390 - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda @@ -17918,6 +20473,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xcb-util-image >=0.4.0,<0.5.0a0 size: 24551 timestamp: 1718880534789 - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-keysyms-0.4.1-hb711507_0.conda @@ -17929,6 +20487,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xcb-util-keysyms >=0.4.1,<0.5.0a0 size: 14314 timestamp: 1718846569232 - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-renderutil-0.3.10-hb711507_0.conda @@ -17940,6 +20501,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xcb-util-renderutil >=0.3.10,<0.4.0a0 size: 16978 timestamp: 1718848865819 - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-wm-0.4.2-hb711507_0.conda @@ -17951,6 +20515,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xcb-util-wm >=0.4.2,<0.5.0a0 size: 51689 timestamp: 1718844051451 - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.47-hb03c661_0.conda @@ -17965,6 +20532,18 @@ packages: purls: [] size: 399291 timestamp: 1772021302485 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.48-h280c20c_0.conda + sha256: 3b04afd5d1a65d2d27ac2d49a63b01ab8bcd875776779ec63e337370ed38afdc + md5: b233b41be0bf210989d57160ed39b394 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - xorg-libx11 >=1.8.13,<2.0a0 + license: MIT + license_family: MIT + run_exports: {} + size: 441670 + timestamp: 1782027360439 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda sha256: c12396aabb21244c212e488bbdc4abcdef0b7404b15761d9329f5a4a39113c4b md5: fb901ff28063514abb6046c9ec2c4a45 @@ -17974,6 +20553,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libice >=1.1.2,<2.0a0 size: 58628 timestamp: 1734227592886 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda @@ -17987,6 +20569,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libsm >=1.2.6,<2.0a0 size: 27590 timestamp: 1741896361728 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.13-he1eb515_0.conda @@ -17999,6 +20584,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libx11 >=1.8.13,<2.0a0 size: 839652 timestamp: 1770819209719 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda @@ -18010,6 +20598,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxau >=1.0.12,<2.0a0 size: 15321 timestamp: 1762976464266 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcomposite-0.4.7-hb03c661_0.conda @@ -18023,6 +20614,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxcomposite >=0.4.7,<1.0a0 size: 14415 timestamp: 1770044404696 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda @@ -18037,6 +20631,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxcursor >=1.2.3,<2.0a0 size: 32533 timestamp: 1730908305254 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdamage-1.1.6-hb9d3cd8_0.conda @@ -18051,6 +20648,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxdamage >=1.1.6,<2.0a0 size: 13217 timestamp: 1727891438799 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda @@ -18062,6 +20662,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxdmcp >=1.1.5,<2.0a0 size: 20591 timestamp: 1762976546182 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.7-hb03c661_0.conda @@ -18074,6 +20677,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxext >=1.3.7,<2.0a0 size: 50326 timestamp: 1769445253162 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.2-hb03c661_0.conda @@ -18086,6 +20692,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxfixes >=6.0.2,<7.0a0 size: 20071 timestamp: 1759282564045 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxi-1.8.3-hb03c661_0.conda @@ -18100,6 +20709,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxi >=1.8.3,<2.0a0 size: 47717 timestamp: 1779111857071 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxinerama-1.1.6-hecca717_0.conda @@ -18142,6 +20754,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxrandr >=1.5.5,<2.0a0 size: 30456 timestamp: 1769445263457 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda @@ -18154,6 +20769,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxrender >=0.9.12,<0.10.0a0 size: 33005 timestamp: 1734229037766 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxscrnsaver-1.2.4-hb9d3cd8_0.conda @@ -18167,6 +20785,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxscrnsaver >=1.2.4,<2.0a0 size: 14412 timestamp: 1727899730073 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxshmfence-1.3.3-hb9d3cd8_0.conda @@ -18178,6 +20799,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxshmfence >=1.3.3,<2.0a0 size: 12302 timestamp: 1734168591429 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxt-1.3.1-hb9d3cd8_0.conda @@ -18192,6 +20816,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxt >=1.3.1,<2.0a0 size: 379686 timestamp: 1731860547604 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda @@ -18206,6 +20833,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxtst >=1.2.5,<2.0a0 size: 32808 timestamp: 1727964811275 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxxf86vm-1.1.7-hb03c661_0.conda @@ -18219,6 +20849,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxxf86vm >=1.1.7,<2.0a0 size: 18701 timestamp: 1769434732453 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-xorgproto-2025.1-hb03c661_0.conda @@ -18230,6 +20863,7 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: {} size: 570010 timestamp: 1766154256151 - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda @@ -18258,6 +20892,7 @@ packages: license_family: Apache purls: - pkg:pypi/yarl?source=hash-mapping + run_exports: {} size: 155061 timestamp: 1779246264888 - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h09e67af_11.conda @@ -18285,6 +20920,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - zfp >=1.0.1,<2.0a0 size: 277694 timestamp: 1766549572069 - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.3-hceb46e0_1.conda @@ -18297,6 +20935,9 @@ packages: license: Zlib license_family: Other purls: [] + run_exports: + weak: + - zlib-ng >=2.3.3,<2.4.0a0 size: 122618 timestamp: 1770167931827 - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda @@ -18308,6 +20949,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - zstd >=1.5.7,<1.6.0a0 size: 601375 timestamp: 1764777111296 - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda @@ -18342,6 +20986,16 @@ packages: - pkg:pypi/aiohappyeyeballs?source=compressed-mapping size: 20727 timestamp: 1779297825279 +- conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.7.1-pyhd8ed1ab_0.conda + sha256: 5ef589e5fd3736439781a9d48e68ce1e723b7081a471c02169908198eba4f448 + md5: e51e09bf3b91c62b7461a9f94e92c2c9 + depends: + - python >=3.10 + license: PSF-2.0 + license_family: PSF + run_exports: {} + size: 24690 + timestamp: 1782986823268 - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda sha256: 8dc149a6828d19bf104ea96382a9d04dae185d4a03cc6beb1bc7b84c428e3ca2 md5: 421a865222cd0c9d83ff08bc78bf3a61 @@ -18353,6 +21007,7 @@ packages: license_family: APACHE purls: - pkg:pypi/aiosignal?source=hash-mapping + run_exports: {} size: 13688 timestamp: 1751626573984 - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda @@ -18504,6 +21159,7 @@ packages: license_family: MIT purls: - pkg:pypi/attrs?source=hash-mapping + run_exports: {} size: 64927 timestamp: 1773935801332 - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda @@ -18603,6 +21259,24 @@ packages: purls: [] size: 129868 timestamp: 1779289852439 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-h4c7d964_0.conda + sha256: 7f458e4a82514d7bebbfef23d92817794a16aaf1c748a15f04870d4fb49aeab2 + md5: b9696b2cf00dfeec138c70cee38ed192 + depends: + - __win + license: ISC + run_exports: {} + size: 129352 + timestamp: 1781709016515 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + sha256: f8e3c730fa14ee3f170493779f06522c4acf89169f43db4f039727709b6419cf + md5: a9965dd99f683c5f444428f896635716 + depends: + - __unix + license: ISC + run_exports: {} + size: 128866 + timestamp: 1781708962055 - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 noarch: python sha256: 561e6660f26c35d137ee150187d89767c988413c978e1b712d53f27ddf70ea17 @@ -18706,6 +21380,7 @@ packages: license_family: BSD purls: - pkg:pypi/colorama?source=hash-mapping + run_exports: {} size: 27011 timestamp: 1733218222191 - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda @@ -18767,6 +21442,7 @@ packages: license_family: BSD purls: - pkg:pypi/cycler?source=hash-mapping + run_exports: {} size: 14778 timestamp: 1764466758386 - conda: https://conda.anaconda.org/conda-forge/noarch/datamodel-code-generator-0.30.1-pyhd8ed1ab_0.conda @@ -18896,6 +21572,7 @@ packages: license: MIT and PSF-2.0 purls: - pkg:pypi/exceptiongroup?source=hash-mapping + run_exports: {} size: 21333 timestamp: 1763918099466 - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda @@ -18977,6 +21654,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: {} size: 397370 timestamp: 1566932522327 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 @@ -18985,6 +21663,7 @@ packages: license: OFL-1.1 license_family: Other purls: [] + run_exports: {} size: 96530 timestamp: 1620479909603 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 @@ -18993,6 +21672,7 @@ packages: license: OFL-1.1 license_family: Other purls: [] + run_exports: {} size: 700814 timestamp: 1620479612257 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda @@ -19001,6 +21681,7 @@ packages: license: LicenseRef-Ubuntu-Font-Licence-Version-1.0 license_family: Other purls: [] + run_exports: {} size: 1620504 timestamp: 1727511233259 - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 @@ -19011,6 +21692,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: {} size: 3667 timestamp: 1566974674465 - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda @@ -19024,6 +21706,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: {} size: 4059 timestamp: 1762351264405 - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda @@ -19187,6 +21870,17 @@ packages: - pkg:pypi/idna?source=compressed-mapping size: 56858 timestamp: 1779999227630 +- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + sha256: c75632ea624aa450a394f570749420c5a2e0997d0216bc29d5d45b0f39df0426 + md5: 577b04680ae422adb86fc60d7b940659 + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + run_exports: {} + size: 163869 + timestamp: 1781620148226 - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.0-pyhd8ed1ab_0.conda sha256: 5a047f9eac290e679b4e6f6f4cbfcc5acdfbf031a4f06824d4ddb590cdbb850b md5: 92617c2ba2847cca7a6ed813b6f4ab79 @@ -19267,6 +21961,7 @@ packages: license_family: MIT purls: - pkg:pypi/iniconfig?source=hash-mapping + run_exports: {} size: 13387 timestamp: 1760831448842 - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda @@ -19904,6 +22599,7 @@ packages: license_family: Apache purls: - pkg:pypi/munkres?source=hash-mapping + run_exports: {} size: 15851 timestamp: 1749895533014 - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda @@ -20137,6 +22833,7 @@ packages: license_family: APACHE purls: - pkg:pypi/packaging?source=hash-mapping + run_exports: {} size: 91574 timestamp: 1777103621679 - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 @@ -20303,6 +23000,7 @@ packages: license_family: MIT purls: - pkg:pypi/pluggy?source=hash-mapping + run_exports: {} size: 25877 timestamp: 1764896838868 - conda: https://conda.anaconda.org/conda-forge/noarch/prance-25.4.8.0-pyhd8ed1ab_0.conda @@ -20486,6 +23184,7 @@ packages: license_family: BSD purls: - pkg:pypi/pygments?source=hash-mapping + run_exports: {} size: 893031 timestamp: 1774796815820 - conda: https://conda.anaconda.org/conda-forge/noarch/pyjwt-2.13.0-pyhcf101f3_0.conda @@ -20537,6 +23236,7 @@ packages: license_family: MIT purls: - pkg:pypi/pyparsing?source=hash-mapping + run_exports: {} size: 110893 timestamp: 1769003998136 - conda: https://conda.anaconda.org/conda-forge/noarch/pyquaternion-0.9.9-pyhd8ed1ab_1.tar.bz2 @@ -20606,6 +23306,26 @@ packages: - pkg:pypi/pytest?source=hash-mapping size: 299984 timestamp: 1775644472530 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.1.1-pyhc364b38_2.conda + sha256: 430051d80765207a7d782b2b188230ba1489d35c6e75fd9903f76cb9fda4af16 + md5: 64c98a12c4e23eb238bf66bbecafdf3c + depends: + - colorama + - pygments >=2.7.2 + - python >=3.10 + - iniconfig >=1.0.1 + - packaging >=22 + - pluggy >=1.5,<2 + - tomli >=1 + - exceptiongroup >=1 + - python + constrains: + - pytest-faulthandler >=2 + license: MIT + license_family: MIT + run_exports: {} + size: 306724 + timestamp: 1782127176429 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-1.4.0-pyhcf101f3_0.conda sha256: 1eddccab8f91f718f890a2aabfc25e68c37a6171fb120e4ec4df7a5a96a877e9 md5: 787319c0ace4c39b7e883a86332db6a4 @@ -20684,6 +23404,7 @@ packages: license_family: APACHE purls: - pkg:pypi/python-dateutil?source=hash-mapping + run_exports: {} size: 233310 timestamp: 1751104122689 - conda: https://conda.anaconda.org/conda-forge/noarch/python-docx-1.2.0-pyhff2d567_0.conda @@ -20835,6 +23556,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: {} size: 6958 timestamp: 1752805918820 - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda @@ -21107,6 +23829,7 @@ packages: license_family: MIT purls: - pkg:pypi/six?source=hash-mapping + run_exports: {} size: 18455 timestamp: 1753199211006 - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda @@ -21337,6 +24060,7 @@ packages: license_family: MIT purls: - pkg:pypi/tomli?source=hash-mapping + run_exports: {} size: 21561 timestamp: 1774492402955 - conda: https://conda.anaconda.org/conda-forge/noarch/toposort-1.10-pyhd8ed1ab_1.conda @@ -21460,6 +24184,17 @@ packages: - pkg:pypi/typing-extensions?source=hash-mapping size: 51692 timestamp: 1756220668932 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + sha256: 2d888f90af0686044882c74193ec80a90ec1943145d94a7b1b048958acda1848 + md5: c70ad746c22219b9700931707482992c + depends: + - python >=3.10 + - python + license: PSF-2.0 + license_family: PSF + run_exports: {} + size: 52631 + timestamp: 1783002732887 - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda sha256: 3088d5d873411a56bf988eee774559335749aed6f6c28e07bf933256afb9eb6c md5: f6d7aa696c67756a650e91e15e88223c @@ -21478,6 +24213,13 @@ packages: purls: [] size: 119135 timestamp: 1767016325805 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + sha256: b928c30ddcb0e3f544c6eade8352737e6e610e263276b90232db6a578ef899d8 + md5: fcb489df604d100968b737f2cb6076c6 + license: LicenseRef-Public-Domain + run_exports: {} + size: 118849 + timestamp: 1784250406640 - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda sha256: e0eb6c8daf892b3056f08416a96d68b0a358b7c46b99c8a50481b22631a4dfc0 md5: e7cb0f5745e4c5035a460248334af7eb @@ -21578,6 +24320,14 @@ packages: purls: [] size: 140476 timestamp: 1765821981856 +- conda: https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.49-hd8ed1ab_0.conda + sha256: 04ce686cd187d379344f9b2be7b4da5f431b265dc0944a6b764fab9da9171948 + md5: 0839a3421140d4a9ba93fb988698fc00 + license: MIT + license_family: MIT + run_exports: {} + size: 147954 + timestamp: 1780946721169 - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda sha256: 1ee2d8384972ecbf8630ce8a3ea9d16858358ad3e8566675295e66996d5352da md5: eb9538b8e55069434a18547f43b96059 @@ -21727,6 +24477,7 @@ packages: license_family: BSD purls: - pkg:pypi/wslink?source=compressed-mapping + run_exports: {} size: 36803 timestamp: 1778826669944 - conda: https://conda.anaconda.org/conda-forge/noarch/xsdata-26.2-pyhd8ed1ab_0.conda @@ -21768,29 +24519,33 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - _openmp_mutex >=4.5 size: 8328 timestamp: 1764092562779 -- conda: https://conda.anaconda.org/conda-forge/osx-64/ada-cpp-0.16.1-py312h9b4feea_0.conda - sha256: aef223b0a3775be566d0287eda9ccbedbf4b685c68eae4baca3cb23004611b76 - md5: b26b605b7e7266f0073b307a63e548d1 +- conda: https://conda.anaconda.org/conda-forge/osx-64/ada-cpp-0.18.0-py312h9b4feea_0.conda + sha256: 08ea1d4dbb99a75a893652ebbd523f2fb3783b2d4f799e8e410e47e6b21692d9 + md5: be2bd2d75840e14d56beb8b6449f02d7 depends: - python - gmsh >=4.15.2,<4.16.0a0 - libcxx >=19 - __osx >=11.0 - - python_abi 3.12.* *_cp312 + - occt >=7.9.3,<7.9.4.0a0 - rocksdb >=10.6.2,<10.7.0a0 - - libzlib >=1.3.2,<2.0a0 - ifcopenshell >=0.8.5,<0.8.6.0a0 - - occt >=7.9.3,<7.9.4.0a0 + - libzlib >=1.3.2,<2.0a0 + - python_abi 3.12.* *_cp312 license: GPL-3.0-or-later + license_family: GPL purls: - pkg:pypi/ada-cpp?source=hash-mapping run_exports: weak: - - ada-cpp >=0.16.1,<0.16.2.0a0 - size: 13128560 - timestamp: 1784298767674 + - ada-cpp >=0.18.0,<0.18.1.0a0 + size: 13330216 + timestamp: 1784533839265 - conda: https://conda.anaconda.org/conda-forge/osx-64/aiohttp-3.13.5-py312h0b1c2f0_0.conda sha256: feec9338ff6d56c28c06ec8176b90019d545d902c45132a218ca2320133f5538 md5: 88eef704008ae051b5a8c54119eab94e @@ -21811,6 +24566,39 @@ packages: - pkg:pypi/aiohttp?source=hash-mapping size: 1004069 timestamp: 1775000332248 +- conda: https://conda.anaconda.org/conda-forge/osx-64/aiohttp-3.14.1-py312h0b1c2f0_0.conda + sha256: 745c449c1dccfeafbd33b4068f7be35d8d6a6307946817414185a67f51538f9d + md5: bb2b52fed934237c452506f1e36a0cd9 + depends: + - __osx >=11.0 + - aiohappyeyeballs >=2.5.0 + - aiosignal >=1.4.0 + - attrs >=17.3.0 + - frozenlist >=1.1.1 + - multidict >=4.5,<7.0 + - propcache >=0.2.0 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - typing_extensions >=4.4 + - yarl >=1.17.0,<2.0 + license: MIT AND Apache-2.0 + license_family: Apache + run_exports: {} + size: 1048378 + timestamp: 1780913675043 +- conda: https://conda.anaconda.org/conda-forge/osx-64/aom-3.14.1-pl5321h3d58d69_1.conda + sha256: f4b61d6701205611c11e2dedaf398ebce30ecf05c193be98df9f7887e189129a + md5: ccde90806539cc31f781a667e4c080d1 + depends: + - __osx >=11.0 + - libcxx >=19 + license: BSD-2-Clause + license_family: BSD + run_exports: + weak: + - aom >=3.14.1,<3.15.0a0 + size: 3117313 + timestamp: 1780752528554 - conda: https://conda.anaconda.org/conda-forge/osx-64/aom-3.9.1-hf036a51_0.conda sha256: 3032f2f55d6eceb10d53217c2a7f43e1eac83603d91e21ce502e8179e63a75f5 md5: 3f17bc32cb7fcb2b4bf3d8d37f656eb8 @@ -21917,6 +24705,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - blosc >=1.21.6,<2.0a0 size: 46990 timestamp: 1733513422834 - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-1.2.0-hf139dec_1.conda @@ -21930,6 +24721,11 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 size: 20194 timestamp: 1764017661405 - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-bin-1.2.0-h8616949_1.conda @@ -21942,6 +24738,7 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: {} size: 18589 timestamp: 1764017635544 - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py312h4b46afd_1.conda @@ -21968,6 +24765,9 @@ packages: license: bzip2-1.0.6 license_family: BSD purls: [] + run_exports: + weak: + - bzip2 >=1.0.8,<2.0a0 size: 133427 timestamp: 1771350680709 - conda: https://conda.anaconda.org/conda-forge/osx-64/c-ares-1.34.6-hb5e19a0_0.conda @@ -21980,6 +24780,18 @@ packages: purls: [] size: 186122 timestamp: 1765215100384 +- conda: https://conda.anaconda.org/conda-forge/osx-64/c-ares-1.34.8-ha1e9b39_0.conda + sha256: b879a20c5b29237707db9eac7f99b2f5128bb0095a3dbe8449557350ea510af9 + md5: 99bc571aba2ddd7130cb315fe38f6dd0 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + run_exports: + weak: + - c-ares >=1.34.8,<2.0a0 + size: 188777 + timestamp: 1784091418806 - conda: https://conda.anaconda.org/conda-forge/osx-64/cairo-1.18.4-h7656bdc_1.conda sha256: 88e7e1efb6a0f6b1477e617338e0ed3d27d4572a3283f8341ce6143b7118e31a md5: 9917add2ab43df894b9bb6f5bf485975 @@ -21998,6 +24810,9 @@ packages: - pixman >=0.46.4,<1.0a0 license: LGPL-2.1-only or MPL-1.1 purls: [] + run_exports: + weak: + - cairo >=1.18.4,<2.0a0 size: 896676 timestamp: 1766416262450 - conda: https://conda.anaconda.org/conda-forge/osx-64/calculix-2.23-pl5321h380832f_3.conda @@ -22075,6 +24890,21 @@ packages: purls: [] size: 6058716 timestamp: 1777880504843 +- conda: https://conda.anaconda.org/conda-forge/osx-64/cgal-cpp-6.2-h2a11f3b_0.conda + sha256: a38bb4db566722f2e33335bd86b13bac2417d6dbefb1f2c42d3d8f142601265e + md5: 578907feec1626aa55732ed8b7969940 + depends: + - __osx >=11.0 + - eigen + - eigen-abi >=5.0.1.80,<5.0.1.81.0a0 + - libboost >=1.90.0,<1.91.0a0 + - libboost-devel + - libcxx >=19 + - mpfr >=4.2.2,<5.0a0 + license: GPL3/LGPL3 + run_exports: {} + size: 6215952 + timestamp: 1781697774489 - conda: https://conda.anaconda.org/conda-forge/osx-64/cli11-2.6.2-h2fb4741_0.conda sha256: c1db50f47724efe145e7c928834e95a93090e17a346c3e25a99678535b532a50 md5: e8c3b35cd9ff57b7c2b2857d5a06e6fc @@ -22084,6 +24914,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: {} size: 100244 timestamp: 1772207988632 - conda: https://conda.anaconda.org/conda-forge/osx-64/contourpy-1.3.3-py312hb0c38da_4.conda @@ -22099,6 +24930,7 @@ packages: license_family: BSD purls: - pkg:pypi/contourpy?source=hash-mapping + run_exports: {} size: 298198 timestamp: 1769156053873 - conda: https://conda.anaconda.org/conda-forge/osx-64/coverage-7.14.1-py312heb39f77_0.conda @@ -22171,6 +25003,9 @@ packages: license: BSD-3-Clause-Attribution license_family: BSD purls: [] + run_exports: + weak: + - cyrus-sasl >=2.1.28,<3.0a0 size: 198777 timestamp: 1771943943021 - conda: https://conda.anaconda.org/conda-forge/osx-64/dav1d-1.2.1-h0dc2134_0.conda @@ -22179,6 +25014,9 @@ packages: license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - dav1d >=1.2.1,<1.2.2.0a0 size: 668439 timestamp: 1685696184631 - conda: https://conda.anaconda.org/conda-forge/osx-64/dbus-1.16.2-h6e7f9a9_1.conda @@ -22192,6 +25030,9 @@ packages: - libzlib >=1.3.1,<2.0a0 license: AFL-2.1 OR GPL-2.0-or-later purls: [] + run_exports: + weak: + - dbus >=1.16.2,<2.0a0 size: 407670 timestamp: 1764536068038 - conda: https://conda.anaconda.org/conda-forge/osx-64/debugpy-1.8.21-py312h4075484_0.conda @@ -22216,6 +25057,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - double-conversion >=3.4.0,<3.5.0a0 size: 67904 timestamp: 1773480315381 - conda: https://conda.anaconda.org/conda-forge/osx-64/eigen-5.0.1-h4ff50a2_0.conda @@ -22224,6 +25068,7 @@ packages: license: MPL-2.0 license_family: MOZILLA purls: [] + run_exports: {} size: 1313286 timestamp: 1773745053527 - conda: https://conda.anaconda.org/conda-forge/osx-64/eigen-abi-5.0.1.80-h969a130_0.conda @@ -22234,6 +25079,7 @@ packages: license: MPL-2.0 license_family: MOZILLA purls: [] + run_exports: {} size: 13290 timestamp: 1773745053527 - conda: https://conda.anaconda.org/conda-forge/osx-64/expat-2.8.1-hcc62823_0.conda @@ -22247,6 +25093,19 @@ packages: purls: [] size: 140161 timestamp: 1779279080682 +- conda: https://conda.anaconda.org/conda-forge/osx-64/expat-2.8.1-hcc62823_1.conda + sha256: 5d738eb05771e181cf2ea1e63046caa61ccd9fe45ba4720399f5f0aeada372d5 + md5: c74610f2af492e29a28848880c048d09 + depends: + - __osx >=11.0 + - libexpat 2.8.1 hcc62823_1 + license: MIT + license_family: MIT + run_exports: + weak: + - libexpat >=2.8.1,<3.0a0 + size: 141090 + timestamp: 1781204342752 - conda: https://conda.anaconda.org/conda-forge/osx-64/fastar-0.11.0-py312he87df83_0.conda sha256: 55463118a958106c629fb95395029ba1d682fdf9efcfc0ea67c7d816eef31de1 md5: 69bb001baf75998550c140e356e4c6ea @@ -22331,6 +25190,62 @@ packages: purls: [] size: 11146490 timestamp: 1777749266411 +- conda: https://conda.anaconda.org/conda-forge/osx-64/ffmpeg-8.1.2-gpl_he601db1_104.conda + sha256: 8aad3817c595824e95389e1def364b8fb0b1d84dd4e45ce41804971bb542d21a + md5: 3fcf8fa80335f52ab20933a660987f20 + depends: + - __osx >=11.0 + - aom >=3.14.1,<3.15.0a0 + - bzip2 >=1.0.8,<2.0a0 + - dav1d >=1.2.1,<1.2.2.0a0 + - fontconfig >=2.18.1,<3.0a0 + - fonts-conda-ecosystem + - gmp >=6.3.0,<7.0a0 + - lame >=4.0,<4.1.0a0 + - libass >=0.17.5,<0.17.6.0a0 + - libcxx >=19 + - libexpat >=2.8.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libharfbuzz >=14.2.1 + - libiconv >=1.18,<2.0a0 + - libjxl >=0.12.0,<0.13.0a0 + - liblzma >=5.8.3,<6.0a0 + - libopenvino >=2026.2.1,<2026.2.2.0a0 + - libopenvino-auto-batch-plugin >=2026.2.1,<2026.2.2.0a0 + - libopenvino-auto-plugin >=2026.2.1,<2026.2.2.0a0 + - libopenvino-hetero-plugin >=2026.2.1,<2026.2.2.0a0 + - libopenvino-intel-cpu-plugin >=2026.2.1,<2026.2.2.0a0 + - libopenvino-ir-frontend >=2026.2.1,<2026.2.2.0a0 + - libopenvino-onnx-frontend >=2026.2.1,<2026.2.2.0a0 + - libopenvino-paddle-frontend >=2026.2.1,<2026.2.2.0a0 + - libopenvino-pytorch-frontend >=2026.2.1,<2026.2.2.0a0 + - libopenvino-tensorflow-frontend >=2026.2.1,<2026.2.2.0a0 + - libopenvino-tensorflow-lite-frontend >=2026.2.1,<2026.2.2.0a0 + - libopus >=1.6.1,<2.0a0 + - libplacebo >=7.360.1,<7.361.0a0 + - librsvg >=2.62.3,<3.0a0 + - libvorbis >=1.3.7,<1.4.0a0 + - libvpx >=1.15.2,<1.16.0a0 + - libvulkan-loader >=1.4.341.0,<2.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - libzlib >=1.3.2,<2.0a0 + - openh264 >=2.6.0,<2.6.1.0a0 + - openssl >=3.5.7,<4.0a0 + - sdl2 >=2.32.56,<3.0a0 + - shaderc >=2026.3,<2026.4.0a0 + - svt-av1 >=4.2.0,<4.2.1.0a0 + - x264 >=1!164.3095,<1!165 + - x265 >=3.5,<3.6.0a0 + license: GPL-2.0-or-later + license_family: GPL + run_exports: + weak: + - ffmpeg >=8.1.2,<9.0a0 + size: 11126124 + timestamp: 1784315041143 - conda: https://conda.anaconda.org/conda-forge/osx-64/flatbuffers-25.12.19-h06076ce_0.conda sha256: f23fb474996e6ff0375309ea32e41eacddc41cb717dee4afb6535d69058dd82c md5: ba143d1c0e58368ecd9a9985b1bc3dfb @@ -22384,6 +25299,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - fmt >=12.1.0,<12.2.0a0 size: 188352 timestamp: 1767681462452 - conda: https://conda.anaconda.org/conda-forge/osx-64/fontconfig-2.18.1-h7a4440b_0.conda @@ -22398,6 +25316,10 @@ packages: - libzlib >=1.3.2,<2.0a0 license: MIT purls: [] + run_exports: + weak: + - fontconfig >=2.18.1,<3.0a0 + - fonts-conda-ecosystem size: 247884 timestamp: 1780450811484 - conda: https://conda.anaconda.org/conda-forge/osx-64/fonttools-4.63.0-py312heb39f77_0.conda @@ -22414,6 +25336,7 @@ packages: license_family: MIT purls: - pkg:pypi/fonttools?source=compressed-mapping + run_exports: {} size: 2914614 timestamp: 1778770861388 - conda: https://conda.anaconda.org/conda-forge/osx-64/freeimage-3.18.0-h0f7f9a8_25.conda @@ -22434,6 +25357,9 @@ packages: - openjpeg >=2.5.4,<3.0a0 license: GPL-2.0-or-later OR GPL-3.0-or-later OR FreeImage purls: [] + run_exports: + weak: + - freeimage >=3.18.0,<3.19.0a0 size: 413129 timestamp: 1780003210724 - conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.14.3-h694c41f_0.conda @@ -22446,6 +25372,19 @@ packages: purls: [] size: 174060 timestamp: 1774298809296 +- conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.14.3-h694c41f_1.conda + sha256: c67130a919d3c7733fce056cc2ce8cec2935e295547d5d70bcbf35e4351d543b + md5: 48fc845b770770e9c7db8743f6d53d44 + depends: + - libfreetype 2.14.3 h694c41f_1 + - libfreetype6 2.14.3 h58fbd8d_1 + license: GPL-2.0-only OR FTL + run_exports: + weak: + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + size: 174300 + timestamp: 1780934162319 - conda: https://conda.anaconda.org/conda-forge/osx-64/fribidi-1.0.16-h8616949_0.conda sha256: 53dd0a6c561cf31038633aaa0d52be05da1f24e86947f06c4e324606c72c7413 md5: 4422491d30462506b9f2d554ab55e33d @@ -22453,6 +25392,9 @@ packages: - __osx >=10.13 license: LGPL-2.1-or-later purls: [] + run_exports: + weak: + - fribidi >=1.0.16,<2.0a0 size: 60923 timestamp: 1757438791418 - conda: https://conda.anaconda.org/conda-forge/osx-64/frozenlist-1.8.0-py312h90c63f8_0.conda @@ -22467,6 +25409,7 @@ packages: license_family: APACHE purls: - pkg:pypi/frozenlist?source=hash-mapping + run_exports: {} size: 52291 timestamp: 1780000343424 - conda: https://conda.anaconda.org/conda-forge/osx-64/gdk-pixbuf-2.44.6-hae309b2_0.conda @@ -22485,6 +25428,24 @@ packages: purls: [] size: 552947 timestamp: 1774986327487 +- conda: https://conda.anaconda.org/conda-forge/osx-64/gdk-pixbuf-2.44.7-hae309b2_0.conda + sha256: b936e714e8b449ea1fe3f9773392794795b3c3142666d56806ee3c4c9e8f70e4 + md5: 4b55a5953b14e83c7a52d864ddfac733 + depends: + - __osx >=11.0 + - libglib >=2.88.2,<3.0a0 + - libintl >=0.25.1,<1.0a0 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libpng >=1.6.58,<1.7.0a0 + - libtiff >=4.7.1,<4.8.0a0 + license: LGPL-2.1-or-later + license_family: LGPL + run_exports: + weak: + - gdk-pixbuf >=2.44.7,<3.0a0 + size: 556945 + timestamp: 1782591683373 - conda: https://conda.anaconda.org/conda-forge/osx-64/geos-3.14.1-he483b9e_0.conda sha256: 4d95fd55a9e649620b4e50ddafff064c4ec52d87e1ed64aa4cad13e643b32baf md5: d83030a79ce1276edc2332c1730efa17 @@ -22493,6 +25454,9 @@ packages: - libcxx >=19 license: LGPL-2.1-only purls: [] + run_exports: + weak: + - geos >=3.14.1,<3.14.2.0a0 size: 1631280 timestamp: 1761593838143 - conda: https://conda.anaconda.org/conda-forge/osx-64/gflags-2.2.2-hac325c4_1005.conda @@ -22504,6 +25468,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - gflags >=2.2.2,<2.3.0a0 size: 84946 timestamp: 1726600054963 - conda: https://conda.anaconda.org/conda-forge/osx-64/glew-2.2.0-h10021a6_0.conda @@ -22516,6 +25483,18 @@ packages: purls: [] size: 501528 timestamp: 1760370712856 +- conda: https://conda.anaconda.org/conda-forge/osx-64/glew-2.3.0-h19ec1ea_0.conda + sha256: 002f2d03eeed975055ce32e31b6f4a4c8677e357745126460ad8f11ad3bcfb4b + md5: 3cd13a2a3d2b762fcbc042c863a7be3b + depends: + - __osx >=10.13 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - glew >=2.3.0,<2.4.0a0 + size: 454633 + timestamp: 1766373718842 - conda: https://conda.anaconda.org/conda-forge/osx-64/glfw-3.4-h8616949_1.conda sha256: a611498bb63138d668a262e0189663518dded3c40ccc3413968737e9d42ea6ca md5: 300c09cdab141aa4d03b502c23f26a57 @@ -22537,6 +25516,20 @@ packages: purls: [] size: 945747 timestamp: 1769369974612 +- conda: https://conda.anaconda.org/conda-forge/osx-64/glslang-16.3.0-he78e680_0.conda + sha256: 7daad6c8e66ab4ced78305c3c55cd8fdfd4f694b1912b94e8420ae3993acd2df + md5: b7689ec7d60b8e2d537a697582592cd8 + depends: + - __osx >=11.0 + - libcxx >=19 + - spirv-tools >=2026,<2027.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - glslang >=16,<17.0a0 + size: 957702 + timestamp: 1777747326436 - conda: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-hf036a51_2.conda sha256: 75aa5e7a875afdcf4903b7dc98577672a3dc17b528ac217b915f9528f93c85fc md5: 427101d13f19c4974552a4e5b072eef1 @@ -22545,6 +25538,9 @@ packages: - libcxx >=16 license: GPL-2.0-or-later OR LGPL-3.0-or-later purls: [] + run_exports: + weak: + - gmp >=6.3.0,<7.0a0 size: 428919 timestamp: 1718981041839 - conda: https://conda.anaconda.org/conda-forge/osx-64/gmsh-4.15.2-h770d4d0_8.conda @@ -22579,6 +25575,9 @@ packages: - libcxx >=19 license: LGPL-2.0-or-later purls: [] + run_exports: + weak: + - graphite2 >=1.3.15,<2.0a0 size: 85964 timestamp: 1780454502704 - conda: https://conda.anaconda.org/conda-forge/osx-64/h5py-3.16.0-nompi_py312h53b4df1_102.conda @@ -22597,6 +25596,18 @@ packages: - pkg:pypi/h5py?source=hash-mapping size: 1183163 timestamp: 1775582252030 +- conda: https://conda.anaconda.org/conda-forge/osx-64/harfbuzz-14.2.1-h694c41f_1.conda + sha256: 41949302a347146509cf3734123753b508abc5518b4e4285b2977039ede6db43 + md5: f1d02a13025478e3eb3863d6c2c74f46 + depends: + - libharfbuzz-devel 14.2.1 h97ceea7_1 + license: MIT + license_family: MIT + run_exports: + weak: + - libharfbuzz >=14.2.1 + size: 11032 + timestamp: 1782801135854 - conda: https://conda.anaconda.org/conda-forge/osx-64/harfbuzz-14.2.1-hf0bc557_0.conda sha256: d329b82aab0681aada77dfcb709fb42ab59403339eb886df2b58695aeb7c6869 md5: d217d80acf915fd7af2bb416a7d57e5a @@ -22625,6 +25636,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - hdf4 >=4.2.15,<4.2.16.0a0 size: 724103 timestamp: 1695661907511 - conda: https://conda.anaconda.org/conda-forge/osx-64/hdf5-1.14.6-nompi_h13accda_109.conda @@ -22644,6 +25658,25 @@ packages: purls: [] size: 3523555 timestamp: 1777519851430 +- conda: https://conda.anaconda.org/conda-forge/osx-64/hdf5-1.14.6-nompi_h13accda_110.conda + sha256: 76fdcb1295eedd01a697d55e871415dd606adc7661b6acd893d073e95240961e + md5: 8fa7c7e9a3c1b57a37bc03f0183aaf17 + depends: + - __osx >=11.0 + - libaec >=1.1.5,<2.0a0 + - libcurl >=8.20.0,<9.0a0 + - libcxx >=19 + - libgfortran + - libgfortran5 >=14.3.0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.6,<4.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - hdf5 >=1.14.6,<1.14.7.0a0 + size: 3525015 + timestamp: 1780582823074 - conda: https://conda.anaconda.org/conda-forge/osx-64/httptools-0.8.0-py312h933eb07_0.conda sha256: 8399c7ac507dbb18c425684e362f58e76980bf8df48701ca2751f40f013d8d5c md5: 1e730af039a43e2a85ec730c326f6d89 @@ -22676,6 +25709,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - icu >=78.3,<79.0a0 size: 12273764 timestamp: 1773822733780 - conda: https://conda.anaconda.org/conda-forge/osx-64/ifcopenshell-0.8.5-py312h3d2a36b_4.conda @@ -22705,6 +25741,37 @@ packages: purls: [] size: 46610146 timestamp: 1780316965437 +- conda: https://conda.anaconda.org/conda-forge/osx-64/ifcopenshell-0.8.5-py312h743479f_6.conda + sha256: ff64da9c2efb6917bbf47742bd57f176e4a0da68af7f276470f98059d60a4578 + md5: 52a2bdde198772d84f02ae85c7d0f2ae + depends: + - python + - shapely + - typing_extensions + - hdf5 >=1.14.6,<1.15.0a0 + - occt >=8.0.0,<8.1.0a0 + - cgal-cpp >=6.2,<6.3.0a0 + - libcxx >=19 + - __osx >=11.0 + - libboost >=1.90.0,<1.91.0a0 + - occt >=8.0.0,<8.0.1.0a0 + - hdf5 >=1.14.6,<1.14.7.0a0 + - gmp >=6.3.0,<7.0a0 + - rocksdb >=10.6.2,<10.7.0a0 + - python_abi 3.12.* *_cp312 + - zstd >=1.5.7,<1.6.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - mpfr >=4.2.2,<5.0a0 + - libzlib >=1.3.2,<2.0a0 + license: LGPL-3.0-or-later + license_family: LGPL + run_exports: + weak: + - ifcopenshell >=0.8.5,<0.8.6.0a0 + - rocksdb >=10.6.2,<10.7.0a0 + size: 46771483 + timestamp: 1782250661214 - conda: https://conda.anaconda.org/conda-forge/osx-64/imath-3.2.2-h55e386d_0.conda sha256: a51a1e84cbcfde5ca8d519e899f0d1de925d56cfac0902682d2fdd89dc90b60c md5: 5c0ac598eca47c36d5a8962e45924c59 @@ -22717,6 +25784,20 @@ packages: purls: [] size: 158223 timestamp: 1759983589371 +- conda: https://conda.anaconda.org/conda-forge/osx-64/imath-3.2.2-h6337977_0.conda + sha256: d1fb13e40349261077c9fbd91c848513cf3e130ced9141496329f0f31d5e54d1 + md5: 9d1f8b4552ef3a8062177426da4c7a5d + depends: + - __osx >=11.0 + - libcxx >=19 + - libzlib >=1.3.2,<2.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - imath >=3.2.2,<3.2.3.0a0 + size: 157231 + timestamp: 1784330031372 - conda: https://conda.anaconda.org/conda-forge/osx-64/jasper-4.2.9-hbeb4536_1.conda sha256: dc013823ea75791ec93889455bf64680db4f756111f9ea791f4336f002973dd2 md5: ba331350354f69260f18733403411157 @@ -22725,6 +25806,9 @@ packages: - libjpeg-turbo >=3.1.2,<4.0a0 license: JasPer-2.0 purls: [] + run_exports: + weak: + - jasper >=4.2.9,<5.0a0 size: 574390 timestamp: 1773678288465 - conda: https://conda.anaconda.org/conda-forge/osx-64/jsoncpp-1.9.6-h466cfd8_1.conda @@ -22737,12 +25821,27 @@ packages: purls: [] size: 145556 timestamp: 1733780384512 +- conda: https://conda.anaconda.org/conda-forge/osx-64/jsoncpp-1.9.8-hebe015c_0.conda + sha256: 945fd6db391cc60c9536e53699a7f99fe9759d896d770d9ee96498371ede80c6 + md5: 76817c225d2456fc766a1df780f5294b + depends: + - __osx >=11.0 + - libcxx >=19 + license: LicenseRef-Public-Domain OR MIT + run_exports: + weak: + - jsoncpp >=1.9.8,<1.9.9.0a0 + size: 168266 + timestamp: 1782507276262 - conda: https://conda.anaconda.org/conda-forge/osx-64/jxrlib-1.1-h10d778d_3.conda sha256: a548a4be14a4c76d6d992a5c1feffcbb08062f5c57abc6e4278d40c2c9a7185b md5: cfaf81d843a80812fe16a68bdae60562 license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - jxrlib >=1.1,<1.2.0a0 size: 220376 timestamp: 1703334073774 - conda: https://conda.anaconda.org/conda-forge/osx-64/kiwisolver-1.5.0-py312hb1dc2e7_0.conda @@ -22757,6 +25856,7 @@ packages: license_family: BSD purls: - pkg:pypi/kiwisolver?source=hash-mapping + run_exports: {} size: 69432 timestamp: 1773067281295 - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.22.2-h207b36a_0.conda @@ -22773,6 +25873,22 @@ packages: purls: [] size: 1193620 timestamp: 1769770267475 +- conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.22.2-h3ddfcb2_1.conda + sha256: c6342c340b18651d14b6134e223904da6f6099665e45449efb683d4c68b28432 + md5: e070b249c4f9c6bddb7984a1a794e8df + depends: + - __osx >=11.0 + - libcxx >=19 + - libedit >=3.1.20250104,<3.2.0a0 + - libedit >=3.1.20250104,<4.0a0 + - openssl >=3.5.7,<4.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - krb5 >=1.22.2,<1.23.0a0 + size: 1195956 + timestamp: 1781860554632 - conda: https://conda.anaconda.org/conda-forge/osx-64/lame-3.100-hb7f2c08_1003.tar.bz2 sha256: 0f943b08abb4c748d73207594321b53bad47eea3e7d06b6078e0f6c59ce6771e md5: 3342b33c9a0921b22b767ed68ee25861 @@ -22781,6 +25897,19 @@ packages: purls: [] size: 542681 timestamp: 1664996421531 +- conda: https://conda.anaconda.org/conda-forge/osx-64/lame-4.0-h6c71e33_0.conda + sha256: b55357790e05d139e7be4470529ae5485d5c4237f2798fe4770a04bbe062d3fd + md5: dc1c02f46d13d733423fc6dd8882cf07 + depends: + - __osx >=11.0 + - mpg123 >=1.32.9,<1.33.0a0 + license: LGPL-2.0-only + license_family: LGPL + run_exports: + weak: + - lame >=4.0,<4.1.0a0 + size: 303663 + timestamp: 1783769970370 - conda: https://conda.anaconda.org/conda-forge/osx-64/lazy-object-proxy-1.12.0-py312h80b0991_1.conda sha256: 424911da8290fa4039266e0afea73a150979f924f89467bbd421e3fe0dc3a065 md5: 646a1b2dda40557b1b84fad1886a958f @@ -22802,6 +25931,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - lcms2 >=2.19.1,<3.0a0 size: 229477 timestamp: 1780211969520 - conda: https://conda.anaconda.org/conda-forge/osx-64/lerc-4.1.0-h35c7297_0.conda @@ -22813,6 +25945,9 @@ packages: license: Apache-2.0 license_family: Apache purls: [] + run_exports: + weak: + - lerc >=4.1.0,<5.0a0 size: 215089 timestamp: 1773114468701 - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20260107.1-cxx17_h7ed6875_0.conda @@ -22829,6 +25964,23 @@ packages: purls: [] size: 1217836 timestamp: 1770863510112 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20260526.0-cxx17_h1a06613_1.conda + sha256: 97f67b176c3b686d40e2501bb06ebf28cfeaa0af19ce7d33ecc9a988f690d8dc + md5: 89d5ad48e1c61baf6bb05ebc15556572 + depends: + - __osx >=11.0 + - libcxx >=19 + constrains: + - libabseil-static =20260526.0=cxx17* + - abseil-cpp =20260526.0 + license: Apache-2.0 + license_family: Apache + run_exports: + weak: + - libabseil >=20260526.0,<20260527.0a0 + - libabseil =*=cxx17* + size: 1271334 + timestamp: 1780525130242 - conda: https://conda.anaconda.org/conda-forge/osx-64/libaec-1.1.5-he7c3a48_0.conda sha256: b42ac9c684c730cb97cb3931a0a97aaf791da38bace4f6944eca10de609e5946 md5: 975f98248cde8d54884c6d1eb5184e13 @@ -22838,6 +25990,9 @@ packages: license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - libaec >=1.1.5,<2.0a0 size: 30555 timestamp: 1769222189944 - conda: https://conda.anaconda.org/conda-forge/osx-64/libass-0.17.4-h87c4fc2_0.conda @@ -22857,6 +26012,25 @@ packages: purls: [] size: 157712 timestamp: 1749329008301 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libass-0.17.5-hf78704f_0.conda + sha256: c34ab289380159fcc10234da036fbdc0be38925f42913a7e2c43b22ca7bd9125 + md5: 84932be0a9f1e535853601e1a6e1b36e + depends: + - __osx >=11.0 + - fribidi >=1.0.16,<2.0a0 + - libiconv >=1.18,<2.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libzlib >=1.3.2,<2.0a0 + - fontconfig >=2.18.1,<3.0a0 + - fonts-conda-ecosystem + - harfbuzz >=14.2.1 + license: ISC + run_exports: + weak: + - libass >=0.17.5,<0.17.6.0a0 + size: 158640 + timestamp: 1782298977130 - conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.11.0-8_he492b99_openblas.conda build_number: 8 sha256: 55cf9f92a2d07c33f8a32c44ff1528ea48fd69677cc003a4532d09b71cb8a316 @@ -22873,6 +26047,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libblas >=3.11.0,<4.0a0 size: 19048 timestamp: 1779860008916 - conda: https://conda.anaconda.org/conda-forge/osx-64/libboost-1.88.0-h5950822_7.conda @@ -22892,6 +26069,23 @@ packages: purls: [] size: 2091448 timestamp: 1766348915727 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libboost-1.90.0-h5950822_1.conda + sha256: 024acac2ca0106e47901544bee1b893a6f3be867610df0b62d4771daadb9dca6 + md5: 9cc11836b85562a34f5af56557eac600 + depends: + - __osx >=10.13 + - bzip2 >=1.0.8,<2.0a0 + - icu >=78.2,<79.0a0 + - libcxx >=19 + - liblzma >=5.8.2,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - boost-cpp <0.0a0 + license: BSL-1.0 + run_exports: {} + size: 2264821 + timestamp: 1770080982056 - conda: https://conda.anaconda.org/conda-forge/osx-64/libboost-devel-1.88.0-hd676150_7.conda sha256: 20bee22b24c8336d221a8e1b11fc0edef579a72427b3df28b82ffafff9799b4a md5: c0fccf2ccaa4e582cf44dde30d2fef67 @@ -22904,6 +26098,20 @@ packages: purls: [] size: 40785 timestamp: 1766349102357 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libboost-devel-1.90.0-hd676150_1.conda + sha256: 40e890d4f0590d50d4311bdfcae7e013c7db226647c7502bfc071304e49e449d + md5: fa685c42bcc92af8d554d0a316c13c9d + depends: + - libboost 1.90.0 h5950822_1 + - libboost-headers 1.90.0 h694c41f_1 + constrains: + - boost-cpp <0.0a0 + license: BSL-1.0 + run_exports: + weak: + - libboost >=1.90.0,<1.91.0a0 + size: 39690 + timestamp: 1770081209819 - conda: https://conda.anaconda.org/conda-forge/osx-64/libboost-headers-1.88.0-h694c41f_7.conda sha256: 011d06fd076ae118c36c248d31ef8283c2fe11ee812c58d77906ecea0095fe66 md5: 17f1ab8714d5c0e703752d7f78bbd9af @@ -22913,6 +26121,15 @@ packages: purls: [] size: 14667322 timestamp: 1766348958606 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libboost-headers-1.90.0-h694c41f_1.conda + sha256: e151c4d6bea342aa03597d868fb1a2f8ddadf18c3f1026bc819da40f968061dd + md5: 86efd3bf7c2b342ad3a1e6d6437c785a + constrains: + - boost-cpp <0.0a0 + license: BSL-1.0 + run_exports: {} + size: 14746172 + timestamp: 1770081031206 - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.2.0-h8616949_1.conda sha256: 4c19b211b3095f541426d5a9abac63e96a5045e509b3d11d4f9482de53efe43b md5: f157c098841474579569c85a60ece586 @@ -22921,6 +26138,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libbrotlicommon >=1.2.0,<1.3.0a0 size: 78854 timestamp: 1764017554982 - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.2.0-h8616949_1.conda @@ -22932,6 +26152,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libbrotlidec >=1.2.0,<1.3.0a0 size: 30835 timestamp: 1764017584474 - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.2.0-h8616949_1.conda @@ -22943,6 +26166,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libbrotlienc >=1.2.0,<1.3.0a0 size: 310355 timestamp: 1764017609985 - conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.11.0-8_h9b27e0a_openblas.conda @@ -22958,6 +26184,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libcblas >=3.11.0,<4.0a0 size: 19049 timestamp: 1779860025163 - conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.20.0-h8f0b9e4_0.conda @@ -22976,6 +26205,25 @@ packages: purls: [] size: 419676 timestamp: 1777462238769 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.21.0-h06afde3_2.conda + sha256: 1bebccfae840b14022b7f65e6fbc467bf7ab0ef82bbd34f52b19eb0996c1dde4 + md5: 38c8e5eae6090e0be9e2ed40e3fc4553 + depends: + - __osx >=11.0 + - krb5 >=1.22.2,<1.23.0a0 + - libnghttp2 >=1.68.1,<2.0a0 + - libpsl >=0.22.0,<0.23.0a0 + - libssh2 >=1.11.1,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.7,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: curl + license_family: MIT + run_exports: + weak: + - libcurl >=8.21.0,<9.0a0 + size: 430795 + timestamp: 1782912686349 - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.7-h19cb2f5_0.conda sha256: c03c298355dea54b729ed6c5f1e6dbd0e2426906039eba8aa2ba1254d005b7d8 md5: 423373b842c3861da6cfa8c8915798ce @@ -22986,6 +26234,16 @@ packages: purls: [] size: 564939 timestamp: 1780442565078 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.8-h19cb2f5_0.conda + sha256: 57ee997f1f800cf38abc743c0f0a9ddfe6a101c697c35510452ce6f4ddf96361 + md5: 0f600157f28fc7bc9549ecafdfa5bc12 + depends: + - __osx >=11.0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + run_exports: {} + size: 566717 + timestamp: 1781672189697 - conda: https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.25-h517ebb2_0.conda sha256: 025f8b1e85dd8254e0ca65f011919fb1753070eb507f03bca317871a884d24de md5: 31aa65919a729dc48180893f62c25221 @@ -22994,6 +26252,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libdeflate >=1.25,<1.26.0a0 size: 70840 timestamp: 1761980008502 - conda: https://conda.anaconda.org/conda-forge/osx-64/libdovi-3.3.2-h59a3c7f_4.conda @@ -23008,6 +26269,19 @@ packages: purls: [] size: 289445 timestamp: 1777839124213 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libdovi-3.4.0-h59a3c7f_0.conda + sha256: 405af0d2e61f51d575a4520b8be96138c43c69bcdcfc47b605bb29a5f87c5afd + md5: 29ca72282aea2052e2ef7bf35dd801e3 + depends: + - __osx >=11.0 + constrains: + - __osx >=11.0 + license: MIT + run_exports: + weak: + - libdovi >=3.4.0,<4.0a0 + size: 378627 + timestamp: 1784281769114 - conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda sha256: 6cc49785940a99e6a6b8c6edbb15f44c2dd6c789d9c283e5ee7bdfedd50b4cd6 md5: 1f4ed31220402fcddc083b4bff406868 @@ -23018,6 +26292,9 @@ packages: license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - libedit >=3.1.20250104,<3.2.0a0 size: 115563 timestamp: 1738479554273 - conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda @@ -23026,6 +26303,9 @@ packages: license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - libev >=4.33,<4.34.0a0 size: 106663 timestamp: 1702146352558 - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.1-hcc62823_0.conda @@ -23040,6 +26320,18 @@ packages: purls: [] size: 75654 timestamp: 1779279058576 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.1-hcc62823_1.conda + sha256: 9c96cc05e056e1bba5b545cbbd57b6e01db622dc2c82934caaaa25cfb22fe666 + md5: dcfdea7b7013beef0a4d744d776ea38f + depends: + - __osx >=11.0 + constrains: + - expat 2.8.1.* + license: MIT + license_family: MIT + run_exports: {} + size: 76020 + timestamp: 1781204303305 - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda sha256: 951958d1792238006fdc6fce7f71f1b559534743b26cc1333497d46e5903a2d6 md5: 66a0dc7464927d0853b590b6f53ba3ea @@ -23048,6 +26340,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libffi >=3.5.2,<3.6.0a0 size: 53583 timestamp: 1769456300951 - conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype-2.14.3-h694c41f_0.conda @@ -23059,6 +26354,15 @@ packages: purls: [] size: 8088 timestamp: 1774298785964 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype-2.14.3-h694c41f_1.conda + sha256: 9029ed0c940be8161c86f5338eacfad1f61af216cdc508e386a648f6ef893a28 + md5: 7cec36e11e7c5a674a1d8c1d5082479e + depends: + - libfreetype6 >=2.14.3 + license: GPL-2.0-only OR FTL + run_exports: {} + size: 8394 + timestamp: 1780934152050 - conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype6-2.14.3-h58fbd8d_0.conda sha256: 9d34b5b2be6ebdd3bcd9e21d6598d493afce4d3fcd2d419f3356022cb4d746fd md5: 27515b8ab8bf4abd8d3d90cf11212411 @@ -23072,6 +26376,19 @@ packages: purls: [] size: 364828 timestamp: 1774298783922 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype6-2.14.3-h58fbd8d_1.conda + sha256: cc94862c51e68626fadddf68b523e5f752149186ccc498fa37976504e2e7ff55 + md5: 112cb22521fa3abf19bc0c93938576f5 + depends: + - __osx >=11.0 + - libpng >=1.6.58,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + constrains: + - freetype >=2.14.3 + license: GPL-2.0-only OR FTL + run_exports: {} + size: 365107 + timestamp: 1780934149073 - conda: https://conda.anaconda.org/conda-forge/osx-64/libgcc-15.2.0-h08519bb_19.conda sha256: 17a5dcd818f89173db51d7d1acd77615cb77db7b4c2b5f571d4dafe559430ab5 md5: 4bf33d5ca73f4b89d3495285a42414a4 @@ -23083,6 +26400,7 @@ packages: license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] + run_exports: {} size: 424164 timestamp: 1778271183296 - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran-15.2.0-h7e5c614_19.conda @@ -23095,6 +26413,7 @@ packages: license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] + run_exports: {} size: 139925 timestamp: 1778271458366 - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-15.2.0-hd16e46c_19.conda @@ -23107,6 +26426,7 @@ packages: license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] + run_exports: {} size: 1063687 timestamp: 1778271196574 - conda: https://conda.anaconda.org/conda-forge/osx-64/libglib-2.88.1-hf28f236_2.conda @@ -23125,6 +26445,66 @@ packages: purls: [] size: 4519643 timestamp: 1778508940832 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libglib-2.88.2-hf28f236_0.conda + sha256: 445e6806480103c6411993e7c2fd5ad1c6cb14ef1fee9386b44adeb536834d07 + md5: 6ed62b59574adb4c9629ed6932a51de7 + depends: + - __osx >=11.0 + - libiconv >=1.18,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - libintl >=0.25.1,<1.0a0 + - pcre2 >=10.47,<10.48.0a0 + - libffi >=3.5.2,<3.6.0a0 + constrains: + - glib >2.66 + license: LGPL-2.1-or-later + run_exports: + weak: + - libglib >=2.88.2,<3.0a0 + size: 4510929 + timestamp: 1782464244345 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libharfbuzz-14.2.1-h97ceea7_1.conda + sha256: 33ec741f9f4bfe132c03e18c2a5822a9ad850c8813fa1a40b94f1f4df8fcde58 + md5: eeb8ef2f2d2ba6d3ff5ba4e7fdf4b73c + depends: + - __osx >=11.0 + - cairo >=1.18.4,<2.0a0 + - graphite2 >=1.3.15,<2.0a0 + - icu >=78.3,<79.0a0 + - libcxx >=19 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libglib >=2.88.2,<3.0a0 + - libpng >=1.6.58,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + license: MIT + license_family: MIT + run_exports: {} + size: 1013958 + timestamp: 1782801064703 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libharfbuzz-devel-14.2.1-h97ceea7_1.conda + sha256: 728387566192c8c57904256888edc71a95db68e3b07678046b39802d32a9ee3f + md5: 5c93ec50698ae52cb8701653b25cfcc4 + depends: + - __osx >=11.0 + - cairo >=1.18.4,<2.0a0 + - freetype + - graphite2 >=1.3.15,<2.0a0 + - icu >=78.3,<79.0a0 + - libcxx >=19 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libglib >=2.88.2,<3.0a0 + - libharfbuzz 14.2.1 h97ceea7_1 + - libpng >=1.6.58,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - libharfbuzz >=14.2.1 + size: 1503179 + timestamp: 1782801123566 - conda: https://conda.anaconda.org/conda-forge/osx-64/libhwloc-2.13.0-default_h4e3125e_1000.conda sha256: 8e051b990da280ca6eca2f025640572459a0ddfa2b3b71a113e4d14b4277aa33 md5: c74c64d67f55426bc24d333a84aed0e3 @@ -23136,8 +26516,23 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libhwloc >=2.13.0,<2.13.1.0a0 size: 2363468 timestamp: 1770954013361 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libhwy-1.4.0-h7747b51_0.conda + sha256: b028ef59ae5a84d40bbf5ad2d0dd2378808afd895ddd4b6a313c42aa7e850f90 + md5: 907c5677af8696394f85800a2599cb4f + depends: + - __osx >=11.0 + - libcxx >=19 + license: Apache-2.0 OR BSD-3-Clause + run_exports: + weak: + - libhwy >=1.4.0,<1.5.0a0 + size: 1002721 + timestamp: 1784326121662 - conda: https://conda.anaconda.org/conda-forge/osx-64/libhwy-1.4.0-hca42a69_0.conda sha256: fb82a974f5bc029963665a77a0d669cacecfd067e1f3b32fe427d806ec21d52b md5: b9e41e8946bb04aca90e181f29c5cf82 @@ -23155,6 +26550,9 @@ packages: - __osx >=10.13 license: LGPL-2.1-only purls: [] + run_exports: + weak: + - libiconv >=1.18,<2.0a0 size: 737846 timestamp: 1754908900138 - conda: https://conda.anaconda.org/conda-forge/osx-64/libintl-0.25.1-h3184127_1.conda @@ -23165,6 +26563,9 @@ packages: - libiconv >=1.18,<2.0a0 license: LGPL-2.1-or-later purls: [] + run_exports: + weak: + - libintl >=0.25.1,<1.0a0 size: 96909 timestamp: 1753343977382 - conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.1.4.1-ha1e9b39_0.conda @@ -23178,6 +26579,19 @@ packages: purls: [] size: 587997 timestamp: 1775963139212 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.2.0-ha1e9b39_0.conda + sha256: 5c48ea89073ba28eb93df264587973d3905827e5b536a5320604ae3baaa73e13 + md5: d86c1b9259377fb4dcd76fe7472bd2d2 + depends: + - __osx >=11.0 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + run_exports: + weak: + - libjpeg-turbo >=3.2.0,<4.0a0 + size: 613600 + timestamp: 1783732260943 - conda: https://conda.anaconda.org/conda-forge/osx-64/libjxl-0.11.2-h473410d_1.conda sha256: 5c59a02fcb345c49ef8bdd5e1889de8aa918bedd95917f9805d20659cec65da1 md5: 596810d804d0b2f2c54bfdd635025e92 @@ -23192,6 +26606,22 @@ packages: purls: [] size: 1692611 timestamp: 1777065242546 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libjxl-0.12.0-h473410d_1.conda + sha256: c95d90d1bc89f8a2dde719a830afe83e1c3b46957e6981ebe0fa427bd18b9b0e + md5: 29f47e6c574a21fa536224fe5674abbe + depends: + - libcxx >=19 + - __osx >=11.0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - libhwy >=1.4.0,<1.5.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - libjxl >=0.12.0,<0.13.0a0 + size: 1739055 + timestamp: 1783146209419 - conda: https://conda.anaconda.org/conda-forge/osx-64/liblapack-3.11.0-8_h859234e_openblas.conda build_number: 8 sha256: 56a68fce5a63d4583a42c212324d62ac292376b8bf05986a551bd640e7fa137d @@ -23205,6 +26635,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - liblapack >=3.11.0,<3.12.0a0 size: 19030 timestamp: 1779860046842 - conda: https://conda.anaconda.org/conda-forge/osx-64/libllvm20-20.1.8-h56e7563_1.conda @@ -23222,6 +26655,23 @@ packages: purls: [] size: 30758108 timestamp: 1757354844443 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libllvm22-22.1.8-hab754da_1.conda + sha256: c2bd652c5a6c4f0e6029786c4e59c54c09018049664006e94d674db4561238ea + md5: 8de4654ab7428c890ef09cee05e11b42 + depends: + - __osx >=11.0 + - libcxx >=19 + - libxml2 + - libxml2-16 >=2.14.6 + - libzlib >=1.3.2,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + run_exports: + weak: + - libllvm22 >=22.1.8,<22.2.0a0 + size: 31843736 + timestamp: 1781793214214 - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda sha256: d9e2006051529aec5578c6efeb13bb6a7200a014b2d5a77a579e83a8049d5f3c md5: becdfbfe7049fa248e52aa37a9df09e2 @@ -23231,6 +26681,9 @@ packages: - xz 5.8.3.* license: 0BSD purls: [] + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 size: 105724 timestamp: 1775826029494 - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-devel-5.8.3-hbb4bfdb_0.conda @@ -23241,6 +26694,9 @@ packages: - liblzma 5.8.3 hbb4bfdb_0 license: 0BSD purls: [] + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 size: 118185 timestamp: 1775826064340 - conda: https://conda.anaconda.org/conda-forge/osx-64/libmed-4.2-nompi_hb060774_28.conda @@ -23294,6 +26750,32 @@ packages: purls: [] size: 724400 timestamp: 1776686843833 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libnetcdf-4.10.1-nompi_h565eff6_201.conda + build_number: 101 + sha256: e5a1344a1da084f911fa8b43048dda37419e936077b013c7f891ea431bde4c8d + md5: 125297c2f681592f94743cc913afb0a6 + depends: + - __osx >=11.0 + - libcxx >=19 + - openssl >=3.5.7,<4.0a0 + - hdf5 >=1.14.6,<1.14.7.0a0 + - bzip2 >=1.0.8,<2.0a0 + - libcurl >=8.21.0,<9.0a0 + - hdf4 >=4.2.15,<4.2.16.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - zstd >=1.5.7,<1.6.0a0 + - libzlib >=1.3.2,<2.0a0 + - blosc >=1.21.6,<2.0a0 + - libaec >=1.1.5,<2.0a0 + - libzip >=1.11.2,<2.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - libnetcdf >=4.10.1,<4.10.2.0a0 + size: 861271 + timestamp: 1783982145760 - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.68.1-h70048d4_0.conda sha256: 899551e16aac9dfb85bfc2fd98b655f4d1b7fea45720ec04ccb93d95b4d24798 md5: dba4c95e2fe24adcae4b77ebf33559ae @@ -23308,6 +26790,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libnghttp2 >=1.68.1,<2.0a0 size: 606749 timestamp: 1773854765508 - conda: https://conda.anaconda.org/conda-forge/osx-64/libntlm-1.8-h6e16a3a_0.conda @@ -23317,6 +26802,9 @@ packages: - __osx >=10.13 license: LGPL-2.1-or-later purls: [] + run_exports: + weak: + - libntlm >=1.8,<2.0a0 size: 33742 timestamp: 1734670081910 - conda: https://conda.anaconda.org/conda-forge/osx-64/libogg-1.3.5-he3325bb_1.conda @@ -23327,6 +26815,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libogg >=1.3.5,<1.4.0a0 size: 215854 timestamp: 1745826006966 - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.33-openmp_h9e49c7b_0.conda @@ -23342,6 +26833,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libopenblas >=0.3.33,<1.0a0 size: 6287889 timestamp: 1776996499823 - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-2026.0.0-hb00a3bc_1.conda @@ -23357,6 +26851,21 @@ packages: purls: [] size: 4897830 timestamp: 1772720560472 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-2026.2.1-h96313a9_1.conda + sha256: 8d9eb80a21338c3cfd064966336b0194c472b3187827d885a47ea9f04ef95976 + md5: 36b4c8fb3760c662fe19cb5e84ad1f1c + depends: + - __osx >=12.0 + - libcxx >=19 + - pugixml >=1.15,<1.16.0a0 + - tbb >=2023.0.0 + license: Apache-2.0 + license_family: APACHE + run_exports: + weak: + - libopenvino >=2026.2.1,<2026.2.2.0a0 + size: 5087043 + timestamp: 1782220055930 - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-batch-plugin-2026.0.0-hb282e6e_1.conda sha256: 74449b53eddf09e3f5ac77d0d63c295408693eb35c2f3f06497739228c6fd43e md5: 41c5daada9baea67de876af759f56935 @@ -23370,6 +26879,19 @@ packages: purls: [] size: 107335 timestamp: 1772720630310 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-batch-plugin-2026.2.1-h4a57bf7_1.conda + sha256: b4cb9f1f1a9dd37730773b813d4cdd6d9fb58e60eab409c88b28e2703af54807 + md5: d81cdcfac84acafb0b56ad5d674c747b + depends: + - __osx >=12.0 + - libcxx >=19 + - libopenvino 2026.2.1 h96313a9_1 + - tbb >=2023.0.0 + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 106753 + timestamp: 1782220136856 - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-plugin-2026.0.0-hb282e6e_1.conda sha256: 7f6d0e5c59355d26349bcda3d6f93e87d80c4558c8d3a250c46828043be7d142 md5: 9f89a4401e096a68ec9b26e34db145d1 @@ -23383,6 +26905,19 @@ packages: purls: [] size: 217710 timestamp: 1772720676953 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-plugin-2026.2.1-h4a57bf7_1.conda + sha256: 2ec56959873145aaf6046db82045b26a065e6bcc48f4bf1017b671f6c8688e16 + md5: 2fbdeac17ea8660889ba226f2a62ac45 + depends: + - __osx >=12.0 + - libcxx >=19 + - libopenvino 2026.2.1 h96313a9_1 + - tbb >=2023.0.0 + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 217040 + timestamp: 1782220180881 - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-hetero-plugin-2026.0.0-hdd33d0b_1.conda sha256: 80f35bd1d5bb1e5576d21033dc45fd9bb4851b3c83a6fd038513e5ae2f5f9aea md5: f105028881f4f2c4609b5f045ba4e368 @@ -23396,6 +26931,19 @@ packages: purls: [] size: 192441 timestamp: 1772720730603 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-hetero-plugin-2026.2.1-h5d0de11_1.conda + sha256: 898e78330d16104fb788d5eb1e1898a176ccb4120c62f3cff1cefc5e6e3bbad9 + md5: 24bf998ffce4719c81e48b832d10e3e1 + depends: + - __osx >=12.0 + - libcxx >=19 + - libopenvino 2026.2.1 h96313a9_1 + - pugixml >=1.15,<1.16.0a0 + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 191871 + timestamp: 1782220222015 - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-intel-cpu-plugin-2026.0.0-hb00a3bc_1.conda sha256: 50a2239be28a1d25b2bfdb9db8544af7757ac5a7bfb03237e182a433540a47ac md5: 7b2b1a07ed63c3be4c57f674b69e0dfb @@ -23410,6 +26958,20 @@ packages: purls: [] size: 11652178 timestamp: 1772720779038 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-intel-cpu-plugin-2026.2.1-h96313a9_1.conda + sha256: 996beb5c8268a242a4ed85018c71a1d1563f361bc2935c53db5ba0a95d3961e6 + md5: 705ba7f945b459b90fe48beab221477d + depends: + - __osx >=12.0 + - libcxx >=19 + - libopenvino 2026.2.1 h96313a9_1 + - pugixml >=1.15,<1.16.0a0 + - tbb >=2023.0.0 + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 12223770 + timestamp: 1782220360398 - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-ir-frontend-2026.0.0-hdd33d0b_1.conda sha256: 3a11682b364affb7c9ef6d12834b58a5ca3062ab685667382273770a03a29e07 md5: 056aa7e551ae62a8a8f2ee213e8be10a @@ -23423,6 +26985,21 @@ packages: purls: [] size: 183235 timestamp: 1772720913359 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-ir-frontend-2026.2.1-h5d0de11_1.conda + sha256: 2b4026f5c34086682197affd728e4d21e8f90ae82a7c71d06c7e032ef5711e09 + md5: 8eaf23234d9076ece47cf656e9a41468 + depends: + - __osx >=12.0 + - libcxx >=19 + - libopenvino 2026.2.1 h96313a9_1 + - pugixml >=1.15,<1.16.0a0 + license: Apache-2.0 + license_family: APACHE + run_exports: + weak: + - libopenvino-ir-frontend >=2026.2.1,<2026.2.2.0a0 + size: 188569 + timestamp: 1782220471703 - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-onnx-frontend-2026.0.0-h4178b9d_1.conda sha256: 1ad9ceacfe4bb1b33ef5b233c68b445d00052972c5798a21576d29f76aa5cb4b md5: f6ad2f8906cb1708c77eb4548ac7a31d @@ -23438,6 +27015,23 @@ packages: purls: [] size: 1494216 timestamp: 1772720964125 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-onnx-frontend-2026.2.1-h8456301_1.conda + sha256: d8a734ae0e21857ad73201e80957a1c19f392a2f666a050a36c73f46de4f562b + md5: da4bd2abfe98b9a2d87e2d9af284356e + depends: + - __osx >=12.0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libcxx >=19 + - libopenvino 2026.2.1 h96313a9_1 + - libprotobuf >=7.35.1,<7.35.2.0a0 + license: Apache-2.0 + license_family: APACHE + run_exports: + weak: + - libopenvino-onnx-frontend >=2026.2.1,<2026.2.2.0a0 + size: 1549582 + timestamp: 1782220529699 - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-paddle-frontend-2026.0.0-h4178b9d_1.conda sha256: 7156655d855ea7c2a58fbfa8fe691cf9dcca546027fec360d8f9651ad15aa393 md5: 0d4afaa4358f5184a96a1e3a3ff4963d @@ -23453,6 +27047,23 @@ packages: purls: [] size: 447981 timestamp: 1772721011379 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-paddle-frontend-2026.2.1-h8456301_1.conda + sha256: 04338b752e0260893fa257e471d4dd7ed2c69aeebaf7407a3bb974eff86f977d + md5: 063f9eab4cc18fda27f316a6dd08179e + depends: + - __osx >=12.0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libcxx >=19 + - libopenvino 2026.2.1 h96313a9_1 + - libprotobuf >=7.35.1,<7.35.2.0a0 + license: Apache-2.0 + license_family: APACHE + run_exports: + weak: + - libopenvino-paddle-frontend >=2026.2.1,<2026.2.2.0a0 + size: 452281 + timestamp: 1782220588739 - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-pytorch-frontend-2026.0.0-hcc62823_1.conda sha256: b2488eb49b1b33bbe2a2ae4b544f4e816564ace190b15d69f81e47ed9c476591 md5: a964a2ca66287fa7fb95a7c186f2097f @@ -23465,6 +27076,20 @@ packages: purls: [] size: 849999 timestamp: 1772721055081 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-pytorch-frontend-2026.2.1-h409306b_1.conda + sha256: a657d15f95593389bad1761a4e429b9e855f73d5f0613faa4e26d5b4377e1e78 + md5: bd6d8b097407fe7999c2560c855ada9d + depends: + - __osx >=12.0 + - libcxx >=19 + - libopenvino 2026.2.1 h96313a9_1 + license: Apache-2.0 + license_family: APACHE + run_exports: + weak: + - libopenvino-pytorch-frontend >=2026.2.1,<2026.2.2.0a0 + size: 874619 + timestamp: 1782220631023 - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-frontend-2026.0.0-hc17a88c_1.conda sha256: 195cc0ffc34f4f5d1d0d05c52c312755224b068b3103bef728a50f50573dee26 md5: 2969c2a8aa6d3f21365e1d07eb5423fe @@ -23481,6 +27106,24 @@ packages: purls: [] size: 960235 timestamp: 1772721102522 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-frontend-2026.2.1-h10fe5cc_1.conda + sha256: 91afa52b5c96b0f0b54bfd83b9886dc6474653382debb405ed1768f37bd82287 + md5: 9fdc051ac0264b6feddb344153e462a9 + depends: + - __osx >=12.0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libcxx >=19 + - libopenvino 2026.2.1 h96313a9_1 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - snappy >=1.2.2,<1.3.0a0 + license: Apache-2.0 + license_family: APACHE + run_exports: + weak: + - libopenvino-tensorflow-frontend >=2026.2.1,<2026.2.2.0a0 + size: 971666 + timestamp: 1782220672257 - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-lite-frontend-2026.0.0-hcc62823_1.conda sha256: 53a85cb10d8add9072b8a38d8cb299a0c2cbc452aba4be16099d33cf8bef5570 md5: 707bd420c9403f75579fb3b67fa77d28 @@ -23493,6 +27136,20 @@ packages: purls: [] size: 376127 timestamp: 1772721164591 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-lite-frontend-2026.2.1-h409306b_1.conda + sha256: 1b52f62960f7f273177644aed63034380510de70dd96b0bffc93212dfb4608cc + md5: 512561e081ce8e07ab8b240177e6079c + depends: + - __osx >=12.0 + - libcxx >=19 + - libopenvino 2026.2.1 h96313a9_1 + license: Apache-2.0 + license_family: APACHE + run_exports: + weak: + - libopenvino-tensorflow-lite-frontend >=2026.2.1,<2026.2.2.0a0 + size: 409448 + timestamp: 1782220709551 - conda: https://conda.anaconda.org/conda-forge/osx-64/libopus-1.6.1-hc6ced15_0.conda sha256: 14389effc1a614456cfe013e4b34e0431f28c5e0047bb6fc80b7dbdab3df4d25 md5: c009362fc3b273d1a671507cff70a3da @@ -23501,6 +27158,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libopus >=1.6.1,<2.0a0 size: 346077 timestamp: 1768497213180 - conda: https://conda.anaconda.org/conda-forge/osx-64/libplacebo-7.351.0-he17f467_2.conda @@ -23517,6 +27177,22 @@ packages: purls: [] size: 535639 timestamp: 1775416046840 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libplacebo-7.360.1-h1ed201f_1.conda + sha256: 498086d67adc639e28f36185db7310ac6f2900baf842c3229172ba97f3bf5bcf + md5: 99d6bda5b07ada8527c9542113106b60 + depends: + - __osx >=11.0 + - libcxx >=19 + - libdovi >=3.4.0,<4.0a0 + - libvulkan-loader >=1.4.341.0,<2.0a0 + - lcms2 >=2.19.1,<3.0a0 + - shaderc >=2026.3,<2026.4.0a0 + license: LGPL-2.1-or-later + run_exports: + weak: + - libplacebo >=7.360.1,<7.361.0a0 + size: 545845 + timestamp: 1784288250095 - conda: https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.58-he930e7c_0.conda sha256: a669b22978e546484d18d99a210801b1823360a266d7035c713d8d1facd035f7 md5: 9744d43d5200f284260637304a069ddd @@ -23525,6 +27201,9 @@ packages: - libzlib >=1.3.2,<2.0a0 license: zlib-acknowledgement purls: [] + run_exports: + weak: + - libpng >=1.6.58,<1.7.0a0 size: 299206 timestamp: 1776315286816 - conda: https://conda.anaconda.org/conda-forge/osx-64/libpq-18.4-h5935a4f_0.conda @@ -23538,6 +27217,9 @@ packages: - openssl >=3.5.6,<4.0a0 license: PostgreSQL purls: [] + run_exports: + weak: + - libpq >=18.4,<19.0a0 size: 2559697 timestamp: 1778787549553 - conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-6.33.5-hff14b61_1.conda @@ -23554,6 +27236,52 @@ packages: purls: [] size: 2971082 timestamp: 1780005104925 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-7.35.1-h087ac9f_2.conda + sha256: d9199b69128d4337d7b34ebbf37372844ce2d257a53d39f4a41886a95ec97136 + md5: 51efaa75647f5c4e2b933503a4e545f7 + depends: + - __osx >=12.0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libcxx >=19 + - libzlib >=1.3.2,<2.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - libprotobuf >=7.35.1,<7.35.2.0a0 + size: 2875966 + timestamp: 1783169173397 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libpsl-0.22.0-ha9fe4b0_1.conda + sha256: fe9e283a3b404b5c11429b15a9628003033f015a275985956528b74aae5e3e85 + md5: 372870287bccae5d1696e6f5533f4937 + depends: + - __osx >=11.0 + - icu >=78.3,<79.0a0 + - libcxx >=19 + license: MIT + license_family: MIT + run_exports: + weak: + - libpsl >=0.22.0,<0.23.0a0 + size: 68134 + timestamp: 1783937592205 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libraqm-0.10.5-h1888d0e_1.conda + sha256: ad1bf7d798c5ea5ed246a3e47d5c11028a71f846f90dda8d1e3e53b32364b7a8 + md5: 4652bf9d34e607edca7e220d2df74ce4 + depends: + - __osx >=11.0 + - fribidi >=1.0.16,<2.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libharfbuzz >=14.2.1 + license: MIT + license_family: MIT + run_exports: + weak: + - libraqm >=0.10.5,<0.11.0a0 + size: 28241 + timestamp: 1782807441485 - conda: https://conda.anaconda.org/conda-forge/osx-64/libraw-0.22.1-h3650196_0.conda sha256: 2ebbafce23d66d1bf26de209cea018c040fed458e27ca52773ecc3f868fda229 md5: b2bc90c53d4b3ca0618cbe59ea0c81c4 @@ -23568,6 +27296,22 @@ packages: purls: [] size: 701053 timestamp: 1775541927685 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libraw-0.22.2-h3650196_0.conda + sha256: a39f49ab1c0f7fbb7598e573e14ce72214d8bc5805fc92888d9702633971e3fe + md5: 4de79049ad3edcb891330ccb8902d2d7 + depends: + - libcxx >=19 + - __osx >=11.0 + - jasper >=4.2.9,<5.0a0 + - libjpeg-turbo >=3.2.0,<4.0a0 + - lcms2 >=2.19.1,<3.0a0 + - libzlib >=1.3.2,<2.0a0 + license: LGPL-2.1-only + run_exports: + weak: + - libraw >=0.22.2,<0.23.0a0 + size: 703167 + timestamp: 1784221038155 - conda: https://conda.anaconda.org/conda-forge/osx-64/librsvg-2.62.3-h7321050_0.conda sha256: 4e6ceb25dcc7b67d550e2b6ce98da585b49dd4590f21a709dd6ec626df3b8c19 md5: 2d5f6b880486d5058c7eab0db04b1bc9 @@ -23585,6 +27329,9 @@ packages: - __osx >=10.13 license: LGPL-2.1-or-later purls: [] + run_exports: + weak: + - librsvg >=2.62.3,<3.0a0 size: 2511802 timestamp: 1780451204499 - conda: https://conda.anaconda.org/conda-forge/osx-64/libsodium-1.0.22-ha3d0635_1.conda @@ -23607,6 +27354,19 @@ packages: purls: [] size: 1007171 timestamp: 1777987093870 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.3-h8f8c405_0.conda + sha256: 84d4af77021ca28e02ff60f396575b921ed1747e459838daa0e73b4724b47953 + md5: 72d9eaef59342a3614c83d57a32f578b + depends: + - __osx >=11.0 + - icu >=78.3,<79.0a0 + - libzlib >=1.3.2,<2.0a0 + license: blessing + run_exports: + weak: + - libsqlite >=3.53.3,<4.0a0 + size: 1012648 + timestamp: 1782519619230 - conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.1-hed3591d_0.conda sha256: 00654ba9e5f73aa1f75c1f69db34a19029e970a4aeb0fa8615934d8e9c369c3c md5: a6cb15db1c2dc4d3a5f6cf3772e09e81 @@ -23617,6 +27377,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libssh2 >=1.11.1,<2.0a0 size: 284216 timestamp: 1745608575796 - conda: https://conda.anaconda.org/conda-forge/osx-64/libtheora-1.1.1-hfdf4475_1006.conda @@ -23631,6 +27394,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libtheora >=1.1.1,<1.2.0a0 size: 289472 timestamp: 1719667988764 - conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.1-ha0a348c_1.conda @@ -23650,6 +27416,25 @@ packages: purls: [] size: 404591 timestamp: 1762022511178 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.2-h95d6d7f_0.conda + sha256: 8e43d2a3538f45848c61cdda4baa6728ce0a48bc665b306de5a31dd3464a30c1 + md5: c92fd887c114aac4612bfc14b1516776 + depends: + - __osx >=11.0 + - lerc >=4.1.0,<5.0a0 + - libcxx >=19 + - libdeflate >=1.25,<1.26.0a0 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: HPND + run_exports: + weak: + - libtiff >=4.7.2,<4.8.0a0 + size: 418681 + timestamp: 1783085828393 - conda: https://conda.anaconda.org/conda-forge/osx-64/libusb-1.0.29-h2287256_0.conda sha256: b46c1c71d8be2d19615a10eaa997b3547848d1aee25a7e9486ad1ca8d61626a7 md5: e5d5fd6235a259665d7652093dc7d6f1 @@ -23657,6 +27442,9 @@ packages: - __osx >=10.13 license: LGPL-2.1-or-later purls: [] + run_exports: + weak: + - libusb >=1.0.29,<2.0a0 size: 85523 timestamp: 1748856209535 - conda: https://conda.anaconda.org/conda-forge/osx-64/libuv-1.52.1-ha3d0635_0.conda @@ -23680,6 +27468,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libvorbis >=1.3.7,<1.4.0a0 size: 279656 timestamp: 1753879393065 - conda: https://conda.anaconda.org/conda-forge/osx-64/libvpx-1.15.2-heffb93a_0.conda @@ -23691,6 +27482,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libvpx >=1.15.2,<1.16.0a0 size: 1299073 timestamp: 1762010520190 - conda: https://conda.anaconda.org/conda-forge/osx-64/libvulkan-loader-1.4.341.0-ha6bc089_0.conda @@ -23704,6 +27498,9 @@ packages: license: Apache-2.0 license_family: APACHE purls: [] + run_exports: + weak: + - libvulkan-loader >=1.4.341.0,<2.0a0 size: 182703 timestamp: 1770077140315 - conda: https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.6.0-hb807250_0.conda @@ -23716,6 +27513,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libwebp-base >=1.6.0,<2.0a0 size: 365086 timestamp: 1752159528504 - conda: https://conda.anaconda.org/conda-forge/osx-64/libxcb-1.17.0-hf1f96e2_0.conda @@ -23729,6 +27529,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libxcb >=1.17.0,<2.0a0 size: 323770 timestamp: 1727278927545 - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-16-2.15.3-h7a90416_0.conda @@ -23745,6 +27548,7 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: {} size: 496338 timestamp: 1776377250079 - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.15.3-h953d39d_0.conda @@ -23760,6 +27564,10 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libxml2 + - libxml2-16 >=2.15.3 size: 40836 timestamp: 1776377277986 - conda: https://conda.anaconda.org/conda-forge/osx-64/libxslt-1.1.43-h486b42e_1.conda @@ -23785,6 +27593,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libzip >=1.11.2,<2.0a0 size: 129542 timestamp: 1730442392952 - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda @@ -23797,6 +27608,9 @@ packages: license: Zlib license_family: Other purls: [] + run_exports: + weak: + - libzlib >=1.3.2,<2.0a0 size: 59000 timestamp: 1774073052242 - conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-22.1.7-h0d3cbff_0.conda @@ -23811,6 +27625,21 @@ packages: purls: [] size: 310879 timestamp: 1780456054580 +- conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-22.1.8-h0d3cbff_0.conda + sha256: 7e8dcf03c2ef5491405d6d86eb892d14e99902f50f4eeb250db0cbdc58dd5818 + md5: 9d5828c46147a47f828ca47a18407621 + depends: + - __osx >=11.0 + constrains: + - openmp 22.1.8|22.1.8.* + - intel-openmp <0.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE + run_exports: + strong: + - llvm-openmp >=22.1.8 + size: 311645 + timestamp: 1781737360942 - conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-6.1.1-py312h211e60a_0.conda sha256: 66cb6fe7c73ffb49a59631de03f50ebd20666a42c475dc9ebcf699630e2164ee md5: 120740d52f4092dd860416c5df1cb7dd @@ -23850,6 +27679,9 @@ packages: license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - lz4-c >=1.10.0,<1.11.0a0 size: 159500 timestamp: 1733741074747 - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.3-py312heb39f77_1.conda @@ -23908,6 +27740,34 @@ packages: - pkg:pypi/matplotlib?source=hash-mapping size: 8284805 timestamp: 1777000784713 +- conda: https://conda.anaconda.org/conda-forge/osx-64/matplotlib-base-3.11.0-py312ha85c1d5_1.conda + sha256: e240dc5c90632d1d2f256943db0ee1f06899f086e6e487121efd62e2f7832ddc + md5: bea49c7256083900aea9339de198b149 + depends: + - __osx >=11.0 + - contourpy >=1.0.1 + - cycler >=0.10 + - fonttools >=4.22.0 + - freetype + - kiwisolver >=1.3.1 + - libcxx >=19 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libraqm >=0.10.5,<0.11.0a0 + - numpy >=1.23 + - numpy >=1.25,<3 + - packaging >=20.0 + - pillow >=8 + - pyparsing >=2.3.1 + - python >=3.12,<3.13.0a0 + - python-dateutil >=2.7 + - python_abi 3.12.* *_cp312 + - qhull >=2020.2,<2020.3.0a0 + license: PSF-2.0 + license_family: PSF + run_exports: {} + size: 8821263 + timestamp: 1782830518616 - conda: https://conda.anaconda.org/conda-forge/osx-64/mesalib-25.0.5-hcf854c5_2.conda sha256: 5e9e8c713327a22107d89762275092b559a9b43d962cb484a3a6632cb4def43e md5: 94ac75b275962ce21d8cc201b3bb9e31 @@ -23930,6 +27790,24 @@ packages: purls: [] size: 5648090 timestamp: 1755729828785 +- conda: https://conda.anaconda.org/conda-forge/osx-64/mesalib-26.0.3-hd99e324_0.conda + sha256: 98f059d78e76411278e49fdb0cfd3cfdc283b0bb0e41a2a1ea65f2800ff50a7d + md5: 352200db1f0b6ce7080e1bb2c16bb8bb + depends: + - libcxx >=20 + - __osx >=12.3 + - spirv-tools >=2026,<2027.0a0 + - libllvm22 >=22.1.1,<22.2.0a0 + - libexpat >=2.7.4,<3.0a0 + - zstd >=1.5.7,<1.6.0a0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - mesalib >=26.0.3,<26.1.0a0 + size: 2803446 + timestamp: 1773868196019 - conda: https://conda.anaconda.org/conda-forge/osx-64/mpfr-4.2.2-h31caf2d_0.conda sha256: 0a238d8500b2206b04f780093c25d83694c8c9628ea50f4376463c608168bf95 md5: bc5ac4d19d24a6062f60560aab0e8976 @@ -23939,8 +27817,24 @@ packages: license: LGPL-3.0-only license_family: LGPL purls: [] + run_exports: + weak: + - mpfr >=4.2.2,<5.0a0 size: 374756 timestamp: 1773414598704 +- conda: https://conda.anaconda.org/conda-forge/osx-64/mpg123-1.32.9-h78e78a4_0.conda + sha256: 15e660430e9ed13c98c81c3f0333d6d8aaf1a40f703e2af68973d1c374842e60 + md5: 6bdfebc249f2466c2893bdf6d5faf484 + depends: + - __osx >=10.13 + - libcxx >=18 + license: LGPL-2.1-only + license_family: LGPL + run_exports: + weak: + - mpg123 >=1.32.9,<1.33.0a0 + size: 391294 + timestamp: 1730581456153 - conda: https://conda.anaconda.org/conda-forge/osx-64/msgpack-python-1.1.2-py312hd099df3_1.conda sha256: 77314afa123abe6c25a0b8a161763d7f624f432bff382b976e5f243c72082944 md5: 00597ae4dd073faaa9e6d2ca478f21c6 @@ -23955,6 +27849,19 @@ packages: - pkg:pypi/msgpack?source=hash-mapping size: 90666 timestamp: 1762504423797 +- conda: https://conda.anaconda.org/conda-forge/osx-64/msgpack-python-1.2.1-py312hb1dc2e7_1.conda + sha256: de3762675f153fd0217148fc319d069643f5de94cbbb89d7ca93f0f9ea19a5b8 + md5: ef5aaa098ccc4df7a8e0c22952312779 + depends: + - python + - libcxx >=19 + - __osx >=11.0 + - python_abi 3.12.* *_cp312 + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 103742 + timestamp: 1782460885527 - conda: https://conda.anaconda.org/conda-forge/osx-64/multidict-6.7.1-py312hfc03ebc_0.conda sha256: 5365f648eeb6770c9f05d01f95df9a050f28e3c6ad287178411560e8cdf12130 md5: 60ca4e1376742de9d637162802d16102 @@ -23966,6 +27873,7 @@ packages: license_family: APACHE purls: - pkg:pypi/multidict?source=hash-mapping + run_exports: {} size: 88773 timestamp: 1771610901609 - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda @@ -23975,6 +27883,9 @@ packages: - __osx >=11.0 license: X11 AND BSD-3-Clause purls: [] + run_exports: + weak: + - ncurses >=6.6,<7.0a0 size: 831711 timestamp: 1777423052277 - conda: https://conda.anaconda.org/conda-forge/osx-64/netcdf4-1.7.4-nompi_py311hd72b3df_107.conda @@ -24010,6 +27921,7 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: {} size: 137081 timestamp: 1768670842725 - conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-24.16.0-h6dcb475_0.conda @@ -24091,6 +28003,26 @@ packages: license_family: BSD size: 8155498 timestamp: 1779169315894 +- conda: https://conda.anaconda.org/conda-forge/osx-64/numpy-2.5.1-py312h746d82c_0.conda + sha256: e98ee4e0e15ee5ee8c82b9580de1d99a3e22a2d2f612a0573e6a8e4fc430a129 + md5: 34950d8877f08ad0a89ba0d603fcdf1f + depends: + - python + - __osx >=11.0 + - libcxx >=19 + - python_abi 3.12.* *_cp312 + - liblapack >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libblas >=3.9.0,<4.0a0 + constrains: + - numpy-base <0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - numpy >=1.25,<3 + size: 8125715 + timestamp: 1783206312597 - conda: https://conda.anaconda.org/conda-forge/osx-64/obstore-0.10.0-py312h424b2ee_0.conda sha256: 3a99176835ad137ee7daacebd4b91a8f43d51f24684a9f0e822de6827f51ca8f md5: b7deb6f15c00f54f491ff367c9f52417 @@ -24159,6 +28091,30 @@ packages: purls: [] size: 25335749 timestamp: 1776673553205 +- conda: https://conda.anaconda.org/conda-forge/osx-64/occt-8.0.0-all_hb6057d6_202.conda + sha256: c552326d3a7959324a1b325bd0d6fe7d7595da8b46b93288fb1d119d01e851fa + md5: 62b1e8bc867191f21308acc98c5c810b + depends: + - __osx >=11.0 + - fontconfig >=2.18.1,<3.0a0 + - fonts-conda-ecosystem + - freeimage >=3.18.0,<3.19.0a0 + - freetype + - libcxx >=19 + - libexpat >=2.8.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - rapidjson + - tbb >=2023.0.0 + - vtk + - vtk-base >=9.6.2,<9.6.3.0a0 + license: LGPL-2.1-only + license_family: LGPL + run_exports: + weak: + - occt >=8.0.0,<8.0.1.0a0 + size: 26878568 + timestamp: 1781685983170 - conda: https://conda.anaconda.org/conda-forge/osx-64/openexr-3.4.12-h23d5f1b_0.conda sha256: 2b185a87fc50edff11e48110ccbe443d1dc9a5d1353879f912f6cb2575684183 md5: 649b614e737d6df4329f3ae8ef46178c @@ -24174,6 +28130,23 @@ packages: purls: [] size: 1021088 timestamp: 1779680669303 +- conda: https://conda.anaconda.org/conda-forge/osx-64/openexr-3.4.13-h3ec6c1b_1.conda + sha256: d4d2c63407ecd21129d3c2d12fa7f770514e35c3f16d2a3b97787b34f9efcd0b + md5: 99c056eb2392ccec7f9d66221b3a508e + depends: + - __osx >=11.0 + - libcxx >=19 + - openjph >=0.30.1,<0.31.0a0 + - imath >=3.2.2,<3.2.3.0a0 + - libdeflate >=1.25,<1.26.0a0 + - libzlib >=1.3.2,<2.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - openexr >=3.4.13,<3.5.0a0 + size: 1022491 + timestamp: 1782198678800 - conda: https://conda.anaconda.org/conda-forge/osx-64/openh264-2.6.0-h4883158_0.conda sha256: a6d734ddbfed9b6b972e7564f5d5eeaab9db2ba128ef92677abd11d36192ff2f md5: 774f56cba369e2286e4922c8f143694a @@ -24185,6 +28158,19 @@ packages: purls: [] size: 660864 timestamp: 1739400822452 +- conda: https://conda.anaconda.org/conda-forge/osx-64/openh264-2.6.0-hd629203_1.conda + sha256: bf665eb898b6105fd87a3a908301b8216463d2ee963b8e719801d3b1032148fd + md5: d685cb1e808a140561319c447ba9eed6 + depends: + - __osx >=11.0 + - libcxx >=19 + license: BSD-2-Clause + license_family: BSD + run_exports: + weak: + - openh264 >=2.6.0,<2.6.1.0a0 + size: 666034 + timestamp: 1782686541058 - conda: https://conda.anaconda.org/conda-forge/osx-64/openjpeg-2.5.4-h52bb76a_0.conda sha256: 9a37ecf9c086f3a50d0132e6087dcbe7ea978d80e2da267fa3199c486529b311 md5: 46e628da6e796c948fa8ec9d6d10bda3 @@ -24197,6 +28183,9 @@ packages: license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - openjpeg >=2.5.4,<3.0a0 size: 335227 timestamp: 1772625294157 - conda: https://conda.anaconda.org/conda-forge/osx-64/openjph-0.27.3-h2c0e27e_0.conda @@ -24211,6 +28200,20 @@ packages: purls: [] size: 267852 timestamp: 1778775444580 +- conda: https://conda.anaconda.org/conda-forge/osx-64/openjph-0.30.1-h2c0e27e_0.conda + sha256: 90b68fc33e81033b7d5be79566d79e78bbd8c626d7a7a52d265e07d671a7f32a + md5: 1be7f2bf99a499c31b4dc1da6f79c40f + depends: + - __osx >=11.0 + - libcxx >=19 + - libtiff >=4.7.1,<4.8.0a0 + license: BSD-2-Clause + license_family: BSD + run_exports: + weak: + - openjph >=0.30.1,<0.31.0a0 + size: 279778 + timestamp: 1782091406701 - conda: https://conda.anaconda.org/conda-forge/osx-64/openldap-2.6.13-h2f5043c_0.conda sha256: 9b1b7fb7b6d3c98fd9b42313f575b93dba0fd299add27cbb7b1a3f26bbc62c85 md5: 59df11dee9461bb55a0d7bcf4f3b7b7b @@ -24223,6 +28226,9 @@ packages: license: OLDAP-2.8 license_family: BSD purls: [] + run_exports: + weak: + - openldap >=2.6.13,<2.7.0a0 size: 777355 timestamp: 1775742025810 - conda: https://conda.anaconda.org/conda-forge/osx-64/openpyxl-3.1.5-py312h35dbd26_3.conda @@ -24260,6 +28266,19 @@ packages: purls: [] size: 2776564 timestamp: 1775589970694 +- conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.3-hc881268_0.conda + sha256: 819d4368d6b5b298fa40d4bc836c1250842489002cacf3fb918a13ee2033b7c6 + md5: 46be42ab403712fd349d007d763bf767 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + run_exports: + weak: + - openssl >=3.6.3,<4.0a0 + size: 2775300 + timestamp: 1781071391999 - conda: https://conda.anaconda.org/conda-forge/osx-64/orjson-3.11.9-py312hbfa05d2_0.conda sha256: acd6de85bbbb0b4248984ad6607d84c2f2401f4af4b5e47a121265e94c06fb3f md5: b7651eb56e366bff1a37f5b71dade79f @@ -24434,6 +28453,9 @@ packages: - libzlib >=1.3.2,<2.0a0 license: LGPL-2.1-or-later purls: [] + run_exports: + weak: + - pango >=1.56.4,<2.0a0 size: 431801 timestamp: 1774282435173 - conda: https://conda.anaconda.org/conda-forge/osx-64/pcre2-10.47-h13923f0_0.conda @@ -24446,6 +28468,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - pcre2 >=10.47,<10.48.0a0 size: 1106584 timestamp: 1763655837207 - conda: https://conda.anaconda.org/conda-forge/osx-64/pigz-2.8-h380d223_2.conda @@ -24481,6 +28506,39 @@ packages: - pkg:pypi/pillow?source=hash-mapping size: 973346 timestamp: 1775060319565 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pillow-12.3.0-py312he84af14_0.conda + sha256: a92c88e6dddfbb70245faa6294fccb086fea23b2e39c784c1d908e3898f6fd46 + md5: 4bf7154c73081936ee50ecb4fb77dd0c + depends: + - python + - __osx >=11.0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libxcb >=1.17.0,<2.0a0 + - python_abi 3.12.* *_cp312 + - libwebp-base >=1.6.0,<2.0a0 + - tk >=8.6.13,<8.7.0a0 + - openjpeg >=2.5.4,<3.0a0 + - lcms2 >=2.19.1,<3.0a0 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - zlib-ng >=2.3.3,<2.4.0a0 + - libtiff >=4.7.1,<4.8.0a0 + license: HPND + run_exports: {} + size: 987687 + timestamp: 1782912253167 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pixman-0.46.4-h2fb4741_2.conda + sha256: 24fd53956b359b0cb79152522aadc2c216531880736e146cac3acaf137c48867 + md5: f8e55c2953f01b116eb61d764ed79095 + depends: + - __osx >=11.0 + - libcxx >=19 + license: MIT + run_exports: + weak: + - pixman >=0.46.4,<1.0a0 + size: 320575 + timestamp: 1784286990554 - conda: https://conda.anaconda.org/conda-forge/osx-64/pixman-0.46.4-ha059160_1.conda sha256: ff8b679079df25aa3ed5daf3f4e3a9c7ee79e7d4b2bd8a21de0f8e7ec7207806 md5: 742a8552e51029585a32b6024e9f57b4 @@ -24509,6 +28567,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - proj >=9.8.1,<9.9.0a0 size: 3290074 timestamp: 1775840330697 - conda: https://conda.anaconda.org/conda-forge/osx-64/propcache-0.5.2-py312heb39f77_0.conda @@ -24522,6 +28583,7 @@ packages: license_family: APACHE purls: - pkg:pypi/propcache?source=compressed-mapping + run_exports: {} size: 49489 timestamp: 1780038241106 - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.2.2-py312hf7082af_0.conda @@ -24556,6 +28618,7 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: {} size: 8364 timestamp: 1726802331537 - conda: https://conda.anaconda.org/conda-forge/osx-64/pugixml-1.15-h46091d4_0.conda @@ -24567,6 +28630,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - pugixml >=1.15,<1.16.0a0 size: 97559 timestamp: 1736601483485 - conda: https://conda.anaconda.org/conda-forge/osx-64/pydantic-core-2.46.4-py312hc1db7ba_0.conda @@ -24662,6 +28728,11 @@ packages: - python_abi 3.12.* *_cp312 license: Python-2.0 purls: [] + run_exports: + weak: + - python_abi 3.12.* *_cp312 + noarch: + - python size: 13672169 timestamp: 1772730464626 - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.14.5-h7c6738f_100_cp314.conda @@ -24758,6 +28829,9 @@ packages: - libcxx >=16 license: LicenseRef-Qhull purls: [] + run_exports: + weak: + - qhull >=2020.2,<2020.3.0a0 size: 528122 timestamp: 1720814002588 - conda: https://conda.anaconda.org/conda-forge/osx-64/qt6-main-6.11.1-pl5321h64d128d_1.conda @@ -24787,7 +28861,11 @@ packages: constrains: - qt ==6.11.1 license: LGPL-3.0-only + license_family: LGPL purls: [] + run_exports: + weak: + - qt6-main >=6.11.1,<7.0a0 size: 51408631 timestamp: 1780400802009 - conda: https://conda.anaconda.org/conda-forge/osx-64/rapidjson-1.1.0.post20240409-h92383a6_2.conda @@ -24799,6 +28877,7 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: {} size: 156314 timestamp: 1742820725403 - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda @@ -24810,6 +28889,9 @@ packages: license: GPL-3.0-only license_family: GPL purls: [] + run_exports: + weak: + - readline >=8.3,<9.0a0 size: 317819 timestamp: 1765813692798 - conda: https://conda.anaconda.org/conda-forge/osx-64/rocksdb-10.6.2-h5fc20da_0_default.conda @@ -24827,6 +28909,9 @@ packages: license: Apache-2.0 license_family: APACHE purls: [] + run_exports: + weak: + - rocksdb >=10.6.2,<10.7.0a0 size: 7829853 timestamp: 1761164812811 - conda: https://conda.anaconda.org/conda-forge/osx-64/rpds-py-2026.5.1-py312hb77ea7e_0.conda @@ -24903,6 +28988,19 @@ packages: license_family: BSD size: 15137152 timestamp: 1779876260804 +- conda: https://conda.anaconda.org/conda-forge/osx-64/sdl2-2.32.56-h2fb4741_0.conda + sha256: 22aacfc07fb6003992a281517514cbfc024fe52131bcb5645e7e13f3da28340b + md5: 1986c166d7888162b30cbe2f81c2f75c + depends: + - __osx >=11.0 + - libcxx >=19 + - sdl3 >=3.4.12,<4.0a0 + license: Zlib + run_exports: + weak: + - sdl2 >=2.32.56,<3.0a0 + size: 739868 + timestamp: 1783452029054 - conda: https://conda.anaconda.org/conda-forge/osx-64/sdl2-2.32.56-h53ec75d_0.conda sha256: 3f64f2cabdfe2f4ed8df6adf26a86bd9db07380cb8fa28d18a80040cc8b8b7d9 md5: 0a8a18995e507da927d1f8c4b7f15ca8 @@ -24927,6 +29025,21 @@ packages: purls: [] size: 1707538 timestamp: 1780262874049 +- conda: https://conda.anaconda.org/conda-forge/osx-64/sdl3-3.4.12-hf9078ff_0.conda + sha256: 1341772ad10c042d2486c5b0d8c2fd412591dadb83b5921c1838231197f5c661 + md5: f18696d6257c9648562e4319e8acbf23 + depends: + - libcxx >=19 + - __osx >=11.0 + - libvulkan-loader >=1.4.341.0,<2.0a0 + - dbus >=1.16.2,<2.0a0 + - libusb >=1.0.29,<2.0a0 + license: Zlib + run_exports: + weak: + - sdl3 >=3.4.12,<4.0a0 + size: 1713182 + timestamp: 1782948155190 - conda: https://conda.anaconda.org/conda-forge/osx-64/shaderc-2025.5-hda4194c_0.conda sha256: 321390c3afda2a53fdfbfffe68e8113e30e82d26c94ad37a6e17045f410078b7 md5: 1172d55d4dea85f134f42306dbc9c477 @@ -24940,6 +29053,21 @@ packages: purls: [] size: 114836 timestamp: 1764288205378 +- conda: https://conda.anaconda.org/conda-forge/osx-64/shaderc-2026.3-h3d334e4_0.conda + sha256: f1d9235299de62beb0ea7e17c1dbf9de5cb339f344e10422684d69d32dab91e0 + md5: e92e61b50d832f176ca187e708b630b0 + depends: + - __osx >=11.0 + - glslang >=16,<17.0a0 + - libcxx >=19 + - spirv-tools >=2026,<2027.0a0 + license: Apache-2.0 + license_family: Apache + run_exports: + weak: + - shaderc >=2026.3,<2026.4.0a0 + size: 115498 + timestamp: 1784251908351 - conda: https://conda.anaconda.org/conda-forge/osx-64/shapely-2.1.2-py312hd8edc82_2.conda sha256: 0ad376aee3a2fe149443af9345aadeb8ad82a95953bee74b59ca17997da03012 md5: eae9cbc6418de8f26e08f4fb255759e9 @@ -24953,6 +29081,7 @@ packages: license_family: BSD purls: - pkg:pypi/shapely?source=hash-mapping + run_exports: {} size: 603294 timestamp: 1762523892524 - conda: https://conda.anaconda.org/conda-forge/osx-64/simplejson-4.1.1-py312h933eb07_0.conda @@ -24988,6 +29117,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - snappy >=1.2.2,<1.3.0a0 size: 40023 timestamp: 1762948053450 - conda: https://conda.anaconda.org/conda-forge/osx-64/spirv-tools-2025.5-h06b67a2_0.conda @@ -25003,6 +29135,20 @@ packages: purls: [] size: 1684476 timestamp: 1769406116317 +- conda: https://conda.anaconda.org/conda-forge/osx-64/spirv-tools-2026.2-hc1436ee_1.conda + sha256: 435d5cfe2a73f3711b925d159267bf26e0943459ec5d371a3ad973a60d056c94 + md5: a3c4207daaa177daf2d9bed8d556a299 + depends: + - __osx >=11.0 + - libcxx >=19 + constrains: + - spirv-headers >=1.4.350.1,<1.4.350.2.0a0 + license: Apache-2.0 + run_exports: + weak: + - spirv-tools >=2026,<2027.0a0 + size: 1740832 + timestamp: 1784384306255 - conda: https://conda.anaconda.org/conda-forge/osx-64/sqlite-3.53.1-h6775aab_0.conda sha256: 0e956bcb99103410108808019f4c85267e1f4ba74250e9f966a29f3e5d854330 md5: 3056144f84229311f525e2dfdd51a775 @@ -25017,6 +29163,22 @@ packages: purls: [] size: 191591 timestamp: 1777987116542 +- conda: https://conda.anaconda.org/conda-forge/osx-64/sqlite-3.53.3-h6775aab_0.conda + sha256: 2c0e5f3def3cc45e883e98807a7d737ed03c301b0d5817c77db46a36593d2209 + md5: cd1962ee22e3f5d7064c310923638700 + depends: + - __osx >=11.0 + - icu >=78.3,<79.0a0 + - libsqlite 3.53.3 h8f8c405_0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.6,<7.0a0 + - readline >=8.3,<9.0a0 + license: blessing + run_exports: + weak: + - libsqlite >=3.53.3,<4.0a0 + size: 192637 + timestamp: 1782519638363 - conda: https://conda.anaconda.org/conda-forge/osx-64/svt-av1-4.0.1-h991f03e_0.conda sha256: 5f8c150f558437364bfe6dade1a81cf1b3fe8ba291d8d8db01f889c32a310f08 md5: 111339b9431e9de1e37ffa8e53040609 @@ -25028,6 +29190,19 @@ packages: purls: [] size: 2364161 timestamp: 1769664663364 +- conda: https://conda.anaconda.org/conda-forge/osx-64/svt-av1-4.2.0-hcc62823_0.conda + sha256: a213979ade4c9a46f3e96aa532a802d1aca1de60d61e30e665e338a601c18f4a + md5: ca3b1049f73c1a7df4cd503e9cdedf7f + depends: + - __osx >=11.0 + - libcxx >=19 + license: BSD-2-Clause + license_family: BSD + run_exports: + weak: + - svt-av1 >=4.2.0,<4.2.1.0a0 + size: 2385894 + timestamp: 1784073244000 - conda: https://conda.anaconda.org/conda-forge/osx-64/tbb-2023.0.0-he3dc2fa_2.conda sha256: f57b374fa5bbb1823a9e1b7e4a9db044c42964313c59d4c652b948f20b55d1a0 md5: 2867ad6f19cadc54675abd00a19d0268 @@ -25038,6 +29213,7 @@ packages: license: Apache-2.0 license_family: APACHE purls: [] + run_exports: {} size: 161879 timestamp: 1778674626809 - conda: https://conda.anaconda.org/conda-forge/osx-64/tbb-devel-2023.0.0-he3dc2fa_2.conda @@ -25048,6 +29224,9 @@ packages: - libcxx >=19 - tbb 2023.0.0 he3dc2fa_2 purls: [] + run_exports: + weak: + - tbb >=2023.0.0 size: 1138236 timestamp: 1778674693289 - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda @@ -25061,6 +29240,18 @@ packages: purls: [] size: 3282953 timestamp: 1769460532442 +- conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hb794df6_3.conda + sha256: 670a364b8285887e5738880bb026f721af2662cfefe9ef64aa9e93eff1981535 + md5: bc699b366e49399bf8e5c6de99bb8cfb + depends: + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: TCL + run_exports: + weak: + - tk >=8.6.13,<8.7.0a0 + size: 3516600 + timestamp: 1784229134070 - conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.6-py312h933eb07_0.conda sha256: 526a7d61358628a05d692c088125fa09c5b85a4ede5be1b9e998bb41c09f460f md5: fb813cf264c3fe67d9f48e719071196a @@ -25100,6 +29291,7 @@ packages: license_family: Apache purls: - pkg:pypi/unicodedata2?source=hash-mapping + run_exports: {} size: 406056 timestamp: 1770909495553 - conda: https://conda.anaconda.org/conda-forge/osx-64/utfcpp-4.09-h694c41f_0.conda @@ -25107,6 +29299,7 @@ packages: md5: 6cd5227f7138e460302f97d1a1549d84 license: BSL-1.0 purls: [] + run_exports: {} size: 14226 timestamp: 1767012401783 - conda: https://conda.anaconda.org/conda-forge/osx-64/uvloop-0.22.1-py312h80b0991_1.conda @@ -25149,6 +29342,25 @@ packages: purls: [] size: 22696759 timestamp: 1765513340482 +- conda: https://conda.anaconda.org/conda-forge/osx-64/viskores-1.1.1-cpu_h791b1e3_1.conda + sha256: 6d623cd9a395f823082897041844d4f6ae4f987a160cead48af457f357ce7d62 + md5: f8b2f1b76cd81fffe23c5bfffec0563d + depends: + - libcxx >=19 + - llvm-openmp >=19.1.7 + - __osx >=11.0 + - hdf5 >=1.14.6,<1.14.7.0a0 + - zfp >=1.0.1,<2.0a0 + - glew >=2.3.0,<2.4.0a0 + - mesalib >=26.0.3,<26.1.0a0 + track_features: + - viskores-p-0 + license: BSD-3-Clause + run_exports: + weak: + - viskores >=1.1.1,<1.2.0a0 + size: 22700639 + timestamp: 1784251231884 - conda: https://conda.anaconda.org/conda-forge/osx-64/vtk-9.6.1-py312h2320ed6_4.conda sha256: a1f1b1fd157f2986a3721b2c8806bb68810a3c8afe6ad4ae31731a731d312e37 md5: 1df7130295ab946f5e93c4a356a9658a @@ -25166,6 +29378,25 @@ packages: purls: [] size: 27212 timestamp: 1778025021461 +- conda: https://conda.anaconda.org/conda-forge/osx-64/vtk-9.6.2-py312h2320ed6_5.conda + sha256: 58540215250146578e76d79e266f76e27205a87d844387fb7fcabacbe1cb220b + md5: 26e2208a47680048c75de2e3707b15ac + depends: + - vtk-base >=9.6.2,<9.6.3.0a0 + - vtk-io-ffmpeg >=9.6.2,<9.6.3.0a0 + - libboost-devel + - liblzma-devel + - tbb-devel + - eigen + - expat + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - vtk-base >=9.6.2,<9.6.3.0a0 + size: 27568 + timestamp: 1783987209599 - conda: https://conda.anaconda.org/conda-forge/osx-64/vtk-base-9.6.1-py312h7fe9505_2.conda sha256: 38ff90211dfd6f7ff4530f50e70718a25f3a506a77f74c3564427804b6387cb9 md5: f2c603c73edc3e948fc291494e4ba60e @@ -25213,6 +29444,54 @@ packages: - pkg:pypi/vtk?source=hash-mapping size: 66060455 timestamp: 1776479686293 +- conda: https://conda.anaconda.org/conda-forge/osx-64/vtk-base-9.6.2-py312h972fd0c_5.conda + sha256: dbbb3f595bf087f347d3a8392c0e47df2dd0719ea98b869f7da56a5e325325c9 + md5: 2d2fb2986fe5864a9a3cd0bc4dfff962 + depends: + - python + - utfcpp + - nlohmann_json + - cli11 + - numpy + - wslink + - matplotlib-base >=2.0.0 + - libcxx >=18 + - __osx >=11.0 + - libtiff >=4.7.2,<4.8.0a0 + - libexpat >=2.8.1,<3.0a0 + - libogg >=1.3.5,<1.4.0a0 + - libzlib >=1.3.2,<2.0a0 + - hdf5 >=1.14.6,<1.14.7.0a0 + - viskores >=1.1.1,<1.2.0a0 + - libnetcdf >=4.10.1,<4.10.2.0a0 + - liblzma >=5.8.3,<6.0a0 + - eigen-abi >=5.0.1.80,<5.0.1.81.0a0 + - libjpeg-turbo >=3.2.0,<4.0a0 + - fmt >=12.1.0,<12.2.0a0 + - proj >=9.8.1,<9.9.0a0 + - double-conversion >=3.4.0,<3.5.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - tbb >=2023.0.0 + - libtheora >=1.1.1,<1.2.0a0 + - qt6-main >=6.11.1,<7.0a0 + - python_abi 3.12.* *_cp312 + - pugixml >=1.15,<1.16.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - libsqlite >=3.53.3,<4.0a0 + - libpng >=1.6.58,<1.7.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - jsoncpp >=1.9.8,<1.9.9.0a0 + constrains: + - libboost-headers >=1.90.0,<1.91.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - vtk-base >=9.6.2,<9.6.3.0a0 + size: 66093339 + timestamp: 1783987209599 - conda: https://conda.anaconda.org/conda-forge/osx-64/vtk-io-ffmpeg-9.6.1-py312hdd75ea5_2.conda sha256: d71f79ef2d3c3b458189874e6e06b820cfd2b09ac6ad719df79c41dbac0e18bd md5: 80e23a2156bedcc5d52af33ca2956a22 @@ -25226,6 +29505,21 @@ packages: purls: [] size: 95454 timestamp: 1776479686293 +- conda: https://conda.anaconda.org/conda-forge/osx-64/vtk-io-ffmpeg-9.6.2-py312haeed73e_5.conda + sha256: 463aa218ecbec711cdb85761853bdc4218f3341b83fac8dff605bc99c28654f0 + md5: fd9afda66c75241fcbf32da7aa64eb85 + depends: + - vtk-base ==9.6.2 py312h972fd0c_5 + - ffmpeg + - ffmpeg >=8.1.2,<9.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - vtk-io-ffmpeg >=9.6.2,<9.6.3.0a0 + size: 96733 + timestamp: 1783987209599 - conda: https://conda.anaconda.org/conda-forge/osx-64/watchdog-6.0.0-py312hba6025d_3.conda sha256: dc852510fc797797a475bf39c7e1b1a0d890c4bc10710d7d6781b2a6ef55c360 md5: 555736af86bab70bbf128cb933e74140 @@ -25325,6 +29619,9 @@ packages: license: GPL-2.0-or-later license_family: GPL purls: [] + run_exports: + weak: + - x264 >=1!164.3095,<1!165 size: 937077 timestamp: 1660323305349 - conda: https://conda.anaconda.org/conda-forge/osx-64/x265-3.5-hbb4e6a2_3.tar.bz2 @@ -25335,6 +29632,9 @@ packages: license: GPL-2.0-or-later license_family: GPL purls: [] + run_exports: + weak: + - x265 >=3.5,<3.6.0a0 size: 3433205 timestamp: 1646610148268 - conda: https://conda.anaconda.org/conda-forge/osx-64/xorg-libice-1.1.2-h6e16a3a_0.conda @@ -25377,6 +29677,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxau >=1.0.12,<2.0a0 size: 13810 timestamp: 1762977180568 - conda: https://conda.anaconda.org/conda-forge/osx-64/xorg-libxdmcp-1.1.5-h8616949_1.conda @@ -25387,6 +29690,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxdmcp >=1.1.5,<2.0a0 size: 19067 timestamp: 1762977101974 - conda: https://conda.anaconda.org/conda-forge/osx-64/xorg-libxext-1.3.7-hf3981d6_0.conda @@ -25469,6 +29775,7 @@ packages: license_family: Apache purls: - pkg:pypi/yarl?source=hash-mapping + run_exports: {} size: 147968 timestamp: 1779246634448 - conda: https://conda.anaconda.org/conda-forge/osx-64/zeromq-4.3.5-h84953be_11.conda @@ -25494,6 +29801,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - zfp >=1.0.1,<2.0a0 size: 211942 timestamp: 1766458562226 - conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-ng-2.3.3-h8bce59a_1.conda @@ -25505,6 +29815,9 @@ packages: license: Zlib license_family: Other purls: [] + run_exports: + weak: + - zlib-ng >=2.3.3,<2.4.0a0 size: 120464 timestamp: 1770168263684 - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda @@ -25516,6 +29829,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - zstd >=1.5.7,<1.6.0a0 size: 528148 timestamp: 1764777156963 - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-20_gnu.conda @@ -25536,28 +29852,29 @@ packages: - _openmp_mutex >=4.5 size: 52252 timestamp: 1770943776666 -- conda: https://conda.anaconda.org/conda-forge/win-64/ada-cpp-0.16.1-py312h710a262_0.conda - sha256: ace289c75628f15996f08965dd7a231036275fc2089bf3736b4f8aeff1768da2 - md5: c907f1e6ca2b120f073328597232c94c +- conda: https://conda.anaconda.org/conda-forge/win-64/ada-cpp-0.18.0-py312h710a262_0.conda + sha256: 7fd6029e384ad3abb25ad4553a17d30eecdf795f400a1dbb80455baacfc4d3c5 + md5: fc12f13dbc2ad00e85e022aa8f26fe74 depends: - python - gmsh >=4.15.2,<4.16.0a0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - - occt >=7.9.3,<7.9.4.0a0 - - python_abi 3.12.* *_cp312 - ifcopenshell >=0.8.5,<0.8.6.0a0 - rocksdb >=10.6.2,<10.7.0a0 + - occt >=7.9.3,<7.9.4.0a0 - libzlib >=1.3.2,<2.0a0 + - python_abi 3.12.* *_cp312 license: GPL-3.0-or-later + license_family: GPL purls: - pkg:pypi/ada-cpp?source=hash-mapping run_exports: weak: - - ada-cpp >=0.16.1,<0.16.2.0a0 - size: 9579187 - timestamp: 1784298715172 + - ada-cpp >=0.18.0,<0.18.1.0a0 + size: 9728156 + timestamp: 1784533822335 - conda: https://conda.anaconda.org/conda-forge/win-64/aiohttp-3.13.5-py312h6b91d65_0.conda sha256: 539e8fba2b4835ee4437fa8b51056be2cf033ac4155f64a19e87855c2ae2af94 md5: f5474e6e952ff964319851b1c68b7301 @@ -25580,6 +29897,42 @@ packages: - pkg:pypi/aiohttp?source=hash-mapping size: 974294 timestamp: 1774999837374 +- conda: https://conda.anaconda.org/conda-forge/win-64/aiohttp-3.14.1-py312h6b91d65_0.conda + sha256: 8b22d894d8642a42f5c0c9a70293dfed7efcc431552d3746e148897e5e6880c2 + md5: 19164d6ac3875e2dfcb278a9d126af8c + depends: + - aiohappyeyeballs >=2.5.0 + - aiosignal >=1.4.0 + - attrs >=17.3.0 + - frozenlist >=1.1.1 + - multidict >=4.5,<7.0 + - propcache >=0.2.0 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - typing_extensions >=4.4 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - yarl >=1.17.0,<2.0 + license: MIT AND Apache-2.0 + license_family: Apache + run_exports: {} + size: 1026227 + timestamp: 1780913508178 +- conda: https://conda.anaconda.org/conda-forge/win-64/aom-3.14.1-pl5321h06fc181_1.conda + sha256: 3033fa8953f7f0c1bb5b89b5af77253badc14a89ba94d743dde3c9159e10fd5e + md5: 7a8ace8100a48355a34d87386012c57b + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: BSD-2-Clause + license_family: BSD + run_exports: + weak: + - aom >=3.14.1,<3.15.0a0 + size: 2214571 + timestamp: 1780752497150 - conda: https://conda.anaconda.org/conda-forge/win-64/aom-3.9.1-he0c23c2_0.conda sha256: 0524d0c0b61dacd0c22ac7a8067f977b1d52380210933b04141f5099c5b6fec7 md5: 3d7c14285d3eb3239a76ff79063f27a5 @@ -25697,6 +30050,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - blosc >=1.21.6,<2.0a0 size: 49840 timestamp: 1733513605730 - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.2.0-h2d644bc_1.conda @@ -25712,6 +30068,11 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 size: 20342 timestamp: 1764017988883 - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.2.0-hfd05255_1.conda @@ -25726,6 +30087,7 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: {} size: 22714 timestamp: 1764017952449 - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py312hc6d9e41_1.conda @@ -25755,6 +30117,9 @@ packages: license: bzip2-1.0.6 license_family: BSD purls: [] + run_exports: + weak: + - bzip2 >=1.0.8,<2.0a0 size: 56115 timestamp: 1771350256444 - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h477c42c_1.conda @@ -25776,6 +30141,9 @@ packages: - vc14_runtime >=14.44.35208 license: LGPL-2.1-only or MPL-1.1 purls: [] + run_exports: + weak: + - cairo >=1.18.4,<2.0a0 size: 1537783 timestamp: 1766416059188 - conda: https://conda.anaconda.org/conda-forge/win-64/calculix-2.23-pl5321h8f7b585_3.conda @@ -25858,6 +30226,22 @@ packages: purls: [] size: 6037069 timestamp: 1777880163915 +- conda: https://conda.anaconda.org/conda-forge/win-64/cgal-cpp-6.2-h29dcab7_0.conda + sha256: c4ed1e4ef2ee43eaf356cc670f76a68fc962431f6899190a34f7a2d283e6791c + md5: 3a2630839585c0ecd0923191a4476359 + depends: + - eigen + - eigen-abi >=5.0.1.100,<5.0.1.101.0a0 + - libboost >=1.90.0,<1.91.0a0 + - libboost-devel + - mpfr >=4.2.2,<5.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: GPL3/LGPL3 + run_exports: {} + size: 6224638 + timestamp: 1781697153627 - conda: https://conda.anaconda.org/conda-forge/win-64/cli11-2.6.2-h5112557_0.conda sha256: f7ea861e84b6ae645ed851103d5a4f75fae737aba41e21c1df943b1dfa668743 md5: 3e925c6db80edb4ff8a2e6b42a4d3737 @@ -25868,6 +30252,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: {} size: 96777 timestamp: 1772207749358 - conda: https://conda.anaconda.org/conda-forge/win-64/contourpy-1.3.3-py312h78d62e6_4.conda @@ -25884,6 +30269,7 @@ packages: license_family: BSD purls: - pkg:pypi/contourpy?source=hash-mapping + run_exports: {} size: 244035 timestamp: 1769155978578 - conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.14.1-py312h05f76fc_0.conda @@ -25958,6 +30344,9 @@ packages: license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - dav1d >=1.2.1,<1.2.2.0a0 size: 618643 timestamp: 1685696352968 - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.21-py312ha1a9051_0.conda @@ -25984,6 +30373,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - double-conversion >=3.4.0,<3.5.0a0 size: 70943 timestamp: 1765193243911 - conda: https://conda.anaconda.org/conda-forge/win-64/eigen-5.0.1-h5112557_0.conda @@ -25992,6 +30384,7 @@ packages: license: MPL-2.0 license_family: MOZILLA purls: [] + run_exports: {} size: 1308183 timestamp: 1773744771265 - conda: https://conda.anaconda.org/conda-forge/win-64/eigen-abi-5.0.1.100-hc11354d_0.conda @@ -26002,6 +30395,7 @@ packages: license: MPL-2.0 license_family: MOZILLA purls: [] + run_exports: {} size: 13278 timestamp: 1773744771265 - conda: https://conda.anaconda.org/conda-forge/win-64/expat-2.8.1-hac47afa_0.conda @@ -26017,6 +30411,21 @@ packages: purls: [] size: 133069 timestamp: 1779278800236 +- conda: https://conda.anaconda.org/conda-forge/win-64/expat-2.8.1-hac47afa_1.conda + sha256: 8eff79bb4637f916eb15eb0af588958294383cbd61efb876c623fc0e05633165 + md5: d63696fc7573fd16ed8e6a48cd7e124b + depends: + - libexpat 2.8.1 hac47afa_1 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + run_exports: + weak: + - libexpat >=2.8.1,<3.0a0 + size: 132855 + timestamp: 1781203738759 - conda: https://conda.anaconda.org/conda-forge/win-64/fastar-0.11.0-py312hb482819_0.conda sha256: e3313f29adf05bb0482a181d4be7670f4ca08bd9d899685e572fdd91a9b2f392 md5: 6959c616e2d197a149e48ef45f133e9e @@ -26089,6 +30498,50 @@ packages: purls: [] size: 10417668 timestamp: 1776634255891 +- conda: https://conda.anaconda.org/conda-forge/win-64/ffmpeg-8.1.2-gpl_he504192_904.conda + sha256: a31772d389fa14ca3eec690a4f07b8308116302392a48f6affab01cbcaa97e23 + md5: 9a9e4bdb63c2a32c82f97c168c9602a3 + depends: + - aom >=3.14.1,<3.15.0a0 + - bzip2 >=1.0.8,<2.0a0 + - dav1d >=1.2.1,<1.2.2.0a0 + - fontconfig >=2.18.1,<3.0a0 + - fonts-conda-ecosystem + - lame >=4.0,<4.1.0a0 + - libexpat >=2.8.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libharfbuzz >=14.2.1 + - libiconv >=1.18,<2.0a0 + - libjxl >=0.12.0,<0.13.0a0 + - liblzma >=5.8.3,<6.0a0 + - libopus >=1.6.1,<2.0a0 + - librsvg >=2.62.3,<3.0a0 + - libvorbis >=1.3.7,<1.4.0a0 + - libvulkan-loader >=1.4.341.0,<2.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - libzlib >=1.3.2,<2.0a0 + - openh264 >=2.6.0,<2.6.1.0a0 + - openssl >=3.5.7,<4.0a0 + - sdl2 >=2.32.56,<3.0a0 + - shaderc >=2026.3,<2026.4.0a0 + - svt-av1 >=4.2.0,<4.2.1.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - x264 >=1!164.3095,<1!165 + - x265 >=3.5,<3.6.0a0 + constrains: + - __cuda >=12.8 + license: GPL-2.0-or-later + license_family: GPL + run_exports: + weak: + - ffmpeg >=8.1.2,<9.0a0 + size: 11022986 + timestamp: 1784315529510 - conda: https://conda.anaconda.org/conda-forge/win-64/flatbuffers-25.12.19-h5112557_0.conda sha256: e155e1d61041e0f4391bfbbbf8b7331afbbea364823525cc421c8bd5c3b887fc md5: 6070d5165220ed7f30c8511917c77cf9 @@ -26145,6 +30598,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - fmt >=12.1.0,<12.2.0a0 size: 186390 timestamp: 1767681264793 - conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.18.1-hd47e2ca_0.conda @@ -26162,6 +30618,10 @@ packages: - vc14_runtime >=14.44.35208 license: MIT purls: [] + run_exports: + weak: + - fontconfig >=2.18.1,<3.0a0 + - fonts-conda-ecosystem size: 202630 timestamp: 1780450217840 - conda: https://conda.anaconda.org/conda-forge/win-64/fonttools-4.63.0-py312h05f76fc_0.conda @@ -26180,6 +30640,7 @@ packages: license_family: MIT purls: - pkg:pypi/fonttools?source=compressed-mapping + run_exports: {} size: 2533681 timestamp: 1778770493020 - conda: https://conda.anaconda.org/conda-forge/win-64/freeglut-3.2.2-hac47afa_4.conda @@ -26192,6 +30653,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - freeglut >=3.2.2,<4.0a0 size: 113837 timestamp: 1776928077923 - conda: https://conda.anaconda.org/conda-forge/win-64/freeimage-3.18.0-h81f0cfa_25.conda @@ -26213,6 +30677,9 @@ packages: - vc14_runtime >=14.44.35208 license: GPL-2.0-or-later OR GPL-3.0-or-later OR FreeImage purls: [] + run_exports: + weak: + - freeimage >=3.18.0,<3.19.0a0 size: 469775 timestamp: 1780003051597 - conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.3-h57928b3_0.conda @@ -26225,6 +30692,20 @@ packages: purls: [] size: 185640 timestamp: 1774300487600 +- conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.3-h57928b3_1.conda + sha256: a0e419e96146159f12344c870dca608d11bca36841f228092b986ffc2e1e0f02 + md5: e77293b32225b136a8be300f93d0e89f + depends: + - libfreetype 2.14.3 h57928b3_1 + - libfreetype6 2.14.3 hdbac1cb_1 + - zlib + license: GPL-2.0-only OR FTL + run_exports: + weak: + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + size: 185584 + timestamp: 1780934817461 - conda: https://conda.anaconda.org/conda-forge/win-64/fribidi-1.0.16-hfd05255_0.conda sha256: 15011071ee56c216ffe276c8d734427f1f893f275ef733f728d13f610ed89e6e md5: c27bd87e70f970010c1c6db104b88b18 @@ -26234,6 +30715,9 @@ packages: - vc14_runtime >=14.44.35208 license: LGPL-2.1-or-later purls: [] + run_exports: + weak: + - fribidi >=1.0.16,<2.0a0 size: 64394 timestamp: 1757438741305 - conda: https://conda.anaconda.org/conda-forge/win-64/frozenlist-1.8.0-py312hfdf67e6_0.conda @@ -26249,6 +30733,7 @@ packages: license_family: APACHE purls: - pkg:pypi/frozenlist?source=hash-mapping + run_exports: {} size: 50861 timestamp: 1779999886062 - conda: https://conda.anaconda.org/conda-forge/win-64/gdk-pixbuf-2.44.6-h1f5b9c4_0.conda @@ -26269,6 +30754,26 @@ packages: purls: [] size: 576065 timestamp: 1774986034812 +- conda: https://conda.anaconda.org/conda-forge/win-64/gdk-pixbuf-2.44.7-h1f5b9c4_0.conda + sha256: 4965bf7c84c9b7c970f1f7bc475962e43441018cf9551682a4a22960c5090588 + md5: 5daba86dbe8072c7e49f74013ef308cd + depends: + - libglib >=2.88.2,<3.0a0 + - libintl >=0.22.5,<1.0a0 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libpng >=1.6.58,<1.7.0a0 + - libtiff >=4.7.1,<4.8.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: LGPL-2.1-or-later + license_family: LGPL + run_exports: + weak: + - gdk-pixbuf >=2.44.7,<3.0a0 + size: 579329 + timestamp: 1782591520147 - conda: https://conda.anaconda.org/conda-forge/win-64/geos-3.14.1-hdade9fe_0.conda sha256: 032a16d78e69a20ffae6216191a66977bc50f6c21cb75f9853b37298b95308c4 md5: 8c75d7e401a4d799ce8d4bb922320967 @@ -26278,6 +30783,9 @@ packages: - vc14_runtime >=14.44.35208 license: LGPL-2.1-only purls: [] + run_exports: + weak: + - geos >=3.14.1,<3.14.2.0a0 size: 1772787 timestamp: 1761593910217 - conda: https://conda.anaconda.org/conda-forge/win-64/gflags-2.2.2-he0c23c2_1005.conda @@ -26290,6 +30798,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - gflags >=2.2.2,<2.3.0a0 size: 76765 timestamp: 1726600357514 - conda: https://conda.anaconda.org/conda-forge/win-64/gl2ps-1.4.2-h26c196c_2.conda @@ -26304,6 +30815,9 @@ packages: license: LGPL-2.0-or-later license_family: LGPL purls: [] + run_exports: + weak: + - gl2ps >=1.4.2,<1.4.3.0a0 size: 73164 timestamp: 1773985484479 - conda: https://conda.anaconda.org/conda-forge/win-64/glew-2.2.0-hcc5dbe9_0.conda @@ -26318,6 +30832,20 @@ packages: purls: [] size: 784982 timestamp: 1760370568105 +- conda: https://conda.anaconda.org/conda-forge/win-64/glew-2.3.0-hdfd1c44_0.conda + sha256: a582ff0418d9b0bbe13d7f42c363506ebfc026b487569885a5cb01c88e2d8641 + md5: 993d65db21f7a63a21da83f0bc9adab4 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - glew >=2.3.0,<2.4.0a0 + size: 544026 + timestamp: 1766373555913 - conda: https://conda.anaconda.org/conda-forge/win-64/glfw-3.4-hfd05255_1.conda sha256: b71b2cdaf2b781608a1b5b3fb772c713032057cb79bc1343d9f30b8f7f65daa1 md5: 74d8bf762b7fe17e02b586b2d8bc1da3 @@ -26329,6 +30857,39 @@ packages: purls: [] size: 119345 timestamp: 1758809778618 +- conda: https://conda.anaconda.org/conda-forge/win-64/glib-2.88.2-h395db07_0.conda + sha256: b67107959a71dbf9f5c2e95511f18095d9ac6f0001f0de4a1b6e14282162989e + md5: 7d203837b88a2255b32dca555d37ca50 + depends: + - python * + - packaging + - libglib ==2.88.2 h7ce1215_0 + - glib-tools ==2.88.2 h74ecf4c_0 + - libintl-devel + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libintl >=0.22.5,<1.0a0 + license: LGPL-2.1-or-later + run_exports: + weak: + - libglib >=2.88.2,<3.0a0 + size: 75973 + timestamp: 1782463965040 +- conda: https://conda.anaconda.org/conda-forge/win-64/glib-tools-2.88.2-h74ecf4c_0.conda + sha256: c604b6ca42c6271f281b1c0616e0d50bd800f8fc913a91a2753c2551b3b6b16c + md5: 63c615f0c525ee64f72e557e583b6257 + depends: + - libglib ==2.88.2 h7ce1215_0 + - libffi + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libintl >=0.22.5,<1.0a0 + license: LGPL-2.1-or-later + run_exports: {} + size: 251644 + timestamp: 1782463965040 - conda: https://conda.anaconda.org/conda-forge/win-64/glslang-16.2.0-h5b34520_0.conda sha256: 07dfe93944a9354b86caa93b02acffe2f32886e0b648a4c927407e6c57285c7f md5: b3c42da233547957551b5327ec403007 @@ -26342,6 +30903,21 @@ packages: purls: [] size: 5012459 timestamp: 1769369856199 +- conda: https://conda.anaconda.org/conda-forge/win-64/glslang-16.3.0-h294ba9c_0.conda + sha256: d80276b89d8aeab6ff0d8d7d4b9af336b368fc0b8fa28ea8cde6f6f2aa07bacf + md5: 7d6fed8a6ebeeebd6362790e22e56bb3 + depends: + - spirv-tools >=2026,<2027.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - glslang >=16,<17.0a0 + size: 5074630 + timestamp: 1777747167205 - conda: https://conda.anaconda.org/conda-forge/win-64/gmp-6.3.0-hfeafd45_2.conda sha256: 664311ea9164898f72d43b7ebf591b1a337d574cbb0d5026b4a8f21000dc8b29 md5: 74558de25a206a7dff062fd4f5ff2d8b @@ -26353,6 +30929,9 @@ packages: - mpir <0.0a0 license: GPL-2.0-or-later OR LGPL-3.0-or-later purls: [] + run_exports: + weak: + - gmp >=6.3.0,<7.0a0 size: 567053 timestamp: 1718982076982 - conda: https://conda.anaconda.org/conda-forge/win-64/gmsh-4.15.2-hb112770_llvm_8.conda @@ -26388,6 +30967,9 @@ packages: - vc14_runtime >=14.44.35208 license: LGPL-2.0-or-later purls: [] + run_exports: + weak: + - graphite2 >=1.3.15,<2.0a0 size: 97308 timestamp: 1780454389458 - conda: https://conda.anaconda.org/conda-forge/win-64/h5py-3.16.0-nompi_py312h03cd2ba_102.conda @@ -26408,6 +30990,18 @@ packages: - pkg:pypi/h5py?source=hash-mapping size: 1085071 timestamp: 1775581446982 +- conda: https://conda.anaconda.org/conda-forge/win-64/harfbuzz-14.2.1-h57928b3_1.conda + sha256: c8bf564f2c415b82f056eff98b8967fb75c86821398998f20289397b5acf1ef7 + md5: e706de885f817f9832c56f1c9ad7ce71 + depends: + - libharfbuzz-devel 14.2.1 h03b5201_1 + license: MIT + license_family: MIT + run_exports: + weak: + - libharfbuzz >=14.2.1 + size: 11542 + timestamp: 1782801047786 - conda: https://conda.anaconda.org/conda-forge/win-64/harfbuzz-14.2.1-h5a1b470_0.conda sha256: 55d6d483e089afe68bdbb38a003d7b76002e65341665b80f38e6ce4b494beef6 md5: 0bcbb7f911590beec914555c6b82050d @@ -26439,6 +31033,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - hdf4 >=4.2.15,<4.2.16.0a0 size: 779637 timestamp: 1695662145568 - conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.6-nompi_hae35d4c_109.conda @@ -26457,6 +31054,24 @@ packages: purls: [] size: 2377680 timestamp: 1777518205631 +- conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.6-nompi_hae35d4c_110.conda + sha256: 54f5211e698a468fd74def162e277d834e956c5b24a9ba4f9cf6298209328475 + md5: 4c94504a694781771108b07bf4a9370f + depends: + - libaec >=1.1.5,<2.0a0 + - libcurl >=8.20.0,<9.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.6,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - hdf5 >=1.14.6,<1.14.7.0a0 + size: 2354049 + timestamp: 1780581446500 - conda: https://conda.anaconda.org/conda-forge/win-64/httptools-0.8.0-py312he06e257_0.conda sha256: 7a681d208c0b86ba38d754b6bcf6d4768158224563e6f838cfc25aea3a7d294d md5: 54f018cf4a90780967493f77caa5f204 @@ -26495,8 +31110,43 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - icu >=78.3,<79.0a0 size: 14954024 timestamp: 1773822508646 +- conda: https://conda.anaconda.org/conda-forge/win-64/ifcopenshell-0.8.5-py312h35b9407_6.conda + sha256: 4d774a9fe889df3882ca9f514f72fcabec53a8775d42497141c0a68fda11ba27 + md5: e1fd7036efd08e950a87b6d00d3d318e + depends: + - python + - shapely + - typing_extensions + - hdf5 >=1.14.6,<1.15.0a0 + - occt >=8.0.0,<8.1.0a0 + - cgal-cpp >=6.2,<6.3.0a0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - zstd >=1.5.7,<1.6.0a0 + - hdf5 >=1.14.6,<1.14.7.0a0 + - occt >=8.0.0,<8.0.1.0a0 + - libboost >=1.90.0,<1.91.0a0 + - gmp >=6.3.0,<7.0a0 + - python_abi 3.12.* *_cp312 + - libxml2 + - libxml2-16 >=2.14.6 + - libzlib >=1.3.2,<2.0a0 + - rocksdb >=10.6.2,<10.7.0a0 + - mpfr >=4.2.2,<5.0a0 + license: LGPL-3.0-or-later + license_family: LGPL + run_exports: + weak: + - ifcopenshell >=0.8.5,<0.8.6.0a0 + - rocksdb >=10.6.2,<10.7.0a0 + size: 78014503 + timestamp: 1782250772495 - conda: https://conda.anaconda.org/conda-forge/win-64/ifcopenshell-0.8.5-py312h989904b_4.conda sha256: 3e161438cd246377dce4fea6afc13d1563f26c46c8204ca75b245edca35ae76f md5: e7d5c95d490fab86b0895fd3c01443fd @@ -26536,6 +31186,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - imath >=3.2.2,<3.2.3.0a0 size: 162099 timestamp: 1759983547586 - conda: https://conda.anaconda.org/conda-forge/win-64/jasper-4.2.9-h8ad263b_1.conda @@ -26549,6 +31202,9 @@ packages: - vc14_runtime >=14.44.35208 license: JasPer-2.0 purls: [] + run_exports: + weak: + - jasper >=4.2.9,<5.0a0 size: 447034 timestamp: 1773677819715 - conda: https://conda.anaconda.org/conda-forge/win-64/jsoncpp-1.9.6-hda1637e_1.conda @@ -26562,6 +31218,19 @@ packages: purls: [] size: 342126 timestamp: 1733780675474 +- conda: https://conda.anaconda.org/conda-forge/win-64/jsoncpp-1.9.8-h477610d_0.conda + sha256: d38d6bcef68250ed0ab06621246108ad1f7180588358436dc77d7b4c0a1aa250 + md5: 54bd50036261e85cf239fa6b8c2b24dd + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: LicenseRef-Public-Domain OR MIT + run_exports: + weak: + - jsoncpp >=1.9.8,<1.9.9.0a0 + size: 363209 + timestamp: 1782507130995 - conda: https://conda.anaconda.org/conda-forge/win-64/jxrlib-1.1-hcfcfb64_3.conda sha256: a9ac265bcf65fce57cfb6512a1b072d5489445d14aa1b60c9bdf73370cf261b2 md5: a9dff8432c11dfa980346e934c29ca3f @@ -26572,6 +31241,9 @@ packages: license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - jxrlib >=1.1,<1.2.0a0 size: 355340 timestamp: 1703334132631 - conda: https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.5.0-py312h78d62e6_0.conda @@ -26587,6 +31259,7 @@ packages: license_family: BSD purls: - pkg:pypi/kiwisolver?source=hash-mapping + run_exports: {} size: 73357 timestamp: 1773067062006 - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h0ea6238_0.conda @@ -26602,6 +31275,21 @@ packages: purls: [] size: 751055 timestamp: 1769769688841 +- conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h719d79b_1.conda + sha256: c55745796e762ba9e817ab1fc0f21f1a049e202f90fa762df39578f37923f6c2 + md5: 00335c2c4a98656554771aaf6f1a7400 + depends: + - openssl >=3.5.7,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + run_exports: + weak: + - krb5 >=1.22.2,<1.23.0a0 + size: 750320 + timestamp: 1781859644591 - conda: https://conda.anaconda.org/conda-forge/win-64/lame-3.100-hcfcfb64_1003.tar.bz2 sha256: 824988a396b97bb9138823a1b3aabd8326e06da5834b3011253d72bb45fd3a88 md5: d92e64077c44c9e32c72d4b5799d47e4 @@ -26614,6 +31302,21 @@ packages: purls: [] size: 570583 timestamp: 1664996824680 +- conda: https://conda.anaconda.org/conda-forge/win-64/lame-4.0-h4a41cd3_0.conda + sha256: ed10e660e52e7180cef2e2e90d912c21b8e84fed4f0bccf03dfd42a79b113846 + md5: fe5aa7c5c8c387db5fadce4d814a8d04 + depends: + - mpg123 >=1.32.9,<1.33.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: LGPL-2.0-only + license_family: LGPL + run_exports: + weak: + - lame >=4.0,<4.1.0a0 + size: 357834 + timestamp: 1783769697203 - conda: https://conda.anaconda.org/conda-forge/win-64/lazy-object-proxy-1.12.0-py312he06e257_1.conda sha256: e46612d6fa5e7329eac3f928c70239f04bfe8ca4d05f123efe93b494edc075c8 md5: 2555df745af0159219c4d2e1f2fa0d3a @@ -26639,6 +31342,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - lcms2 >=2.19.1,<3.0a0 size: 523194 timestamp: 1780211799997 - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.1.0-hd936e49_0.conda @@ -26651,6 +31357,9 @@ packages: license: Apache-2.0 license_family: Apache purls: [] + run_exports: + weak: + - lerc >=4.1.0,<5.0a0 size: 172395 timestamp: 1773113455582 - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.5-haf901d7_0.conda @@ -26663,6 +31372,9 @@ packages: license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - libaec >=1.1.5,<2.0a0 size: 34463 timestamp: 1769221960556 - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-8_h8455456_mkl.conda @@ -26679,6 +31391,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libblas >=3.11.0,<4.0a0 size: 68103 timestamp: 1779859688049 - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-1.88.0-h9dfe17d_7.conda @@ -26700,6 +31415,25 @@ packages: purls: [] size: 2404502 timestamp: 1766348533008 +- conda: https://conda.anaconda.org/conda-forge/win-64/libboost-1.90.0-h9dfe17d_1.conda + sha256: 37cbb374215ac3573bcb3aace8a19ab7527e5e366b02f3d28ada441756570903 + md5: bbf7cb9964cef65e88b48388b22979dd + depends: + - bzip2 >=1.0.8,<2.0a0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - __win >=10 + - boost-cpp <0.0a0 + license: BSL-1.0 + run_exports: {} + size: 2519001 + timestamp: 1766348188389 - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-devel-1.88.0-h1c1089f_7.conda sha256: b68a3d2d10f1837f8c4c1029a70e4beeab8740e9f22de081caac8efcd7b0b8d3 md5: 1706ad0abc99f425edac178d4bd91f8d @@ -26712,6 +31446,20 @@ packages: purls: [] size: 42557 timestamp: 1766348731376 +- conda: https://conda.anaconda.org/conda-forge/win-64/libboost-devel-1.90.0-h1c1089f_1.conda + sha256: fb658e49bfcae883a2d00edb0586893d6695b06c001194721be777b86c0a33d5 + md5: 7df553bf336a77fc14b387b001ff0f53 + depends: + - libboost 1.90.0 h9dfe17d_1 + - libboost-headers 1.90.0 h57928b3_1 + constrains: + - boost-cpp <0.0a0 + license: BSL-1.0 + run_exports: + weak: + - libboost >=1.90.0,<1.91.0a0 + size: 41030 + timestamp: 1766348400124 - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-headers-1.88.0-h57928b3_7.conda sha256: 64109e62262e5b4a2125a2a44edc087d0443b2dbd2dd1cabf7442b3e32a1be35 md5: e7fb10a0c6fa8639eaa61ee453dfe90f @@ -26721,6 +31469,15 @@ packages: purls: [] size: 14701109 timestamp: 1766348593737 +- conda: https://conda.anaconda.org/conda-forge/win-64/libboost-headers-1.90.0-h57928b3_1.conda + sha256: 532af7c91e6d1b51aadb377331f9da0f6f1d5d8bf7a7bde1319a5de669300e24 + md5: c985e6c438127242f35f8b25817a1a57 + constrains: + - boost-cpp <0.0a0 + license: BSL-1.0 + run_exports: {} + size: 14792005 + timestamp: 1766348254349 - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.2.0-hfd05255_1.conda sha256: 5097303c2fc8ebf9f9ea9731520aa5ce4847d0be41764edd7f6dee2100b82986 md5: 444b0a45bbd1cb24f82eedb56721b9c4 @@ -26731,6 +31488,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libbrotlicommon >=1.2.0,<1.3.0a0 size: 82042 timestamp: 1764017799966 - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.2.0-hfd05255_1.conda @@ -26744,6 +31504,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libbrotlidec >=1.2.0,<1.3.0a0 size: 34449 timestamp: 1764017851337 - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.2.0-hfd05255_1.conda @@ -26757,6 +31520,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libbrotlienc >=1.2.0,<1.3.0a0 size: 252903 timestamp: 1764017901735 - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-8_h2a3cdd5_mkl.conda @@ -26772,6 +31538,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libcblas >=3.11.0,<4.0a0 size: 68443 timestamp: 1779859701498 - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.20.0-h8206538_0.conda @@ -26789,6 +31558,24 @@ packages: purls: [] size: 393114 timestamp: 1777461635732 +- conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.21.0-h51a1c48_2.conda + sha256: e9a9231e6c04b82979a6a1a4f90b8a697dc3a42884e709dee8ba5d0355b45296 + md5: 88c875a8f34785d6f6185ceb646154fa + depends: + - krb5 >=1.22.2,<1.23.0a0 + - libpsl >=0.22.0,<0.23.0a0 + - libssh2 >=1.11.1,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: curl + license_family: MIT + run_exports: + weak: + - libcurl >=8.21.0,<9.0a0 + size: 404786 + timestamp: 1782911887650 - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda sha256: 834e4881a18b690d5ec36f44852facd38e13afe599e369be62d29bd675f107ee md5: e77030e67343e28b084fabd7db0ce43e @@ -26799,6 +31586,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libdeflate >=1.25,<1.26.0a0 size: 156818 timestamp: 1761979842440 - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_0.conda @@ -26815,6 +31605,20 @@ packages: purls: [] size: 71280 timestamp: 1779278786150 +- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_1.conda + sha256: 1a54d874addda73b6f7164d5f3905821277a1831bcc05edd74b3085391688571 + md5: ccc490c81ffe14181861beac0e8f3169 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - expat 2.8.1.* + license: MIT + license_family: MIT + run_exports: {} + size: 71631 + timestamp: 1781203724164 - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda sha256: 59d01f2dfa8b77491b5888a5ab88ff4e1574c9359f7e229da254cdfe27ddc190 md5: 720b39f5ec0610457b725eb3f396219a @@ -26825,6 +31629,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libffi >=3.5.2,<3.6.0a0 size: 45831 timestamp: 1769456418774 - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.3-h57928b3_0.conda @@ -26836,6 +31643,15 @@ packages: purls: [] size: 8379 timestamp: 1774300468411 +- conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.3-h57928b3_1.conda + sha256: 035d0c67bf9f7a16f4a1764f420c120f1a995d071bb265fcc66ef688ef709d7b + md5: e45b52fb9a81c9e2708465a706e05952 + depends: + - libfreetype6 >=2.14.3 + license: GPL-2.0-only OR FTL + run_exports: {} + size: 8711 + timestamp: 1780934891782 - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.3-hdbac1cb_0.conda sha256: 497e9ab7c80f579e1b2850523740d6a543b8020f6b43be6bd6e83b3a6fb7fb32 md5: f9975a0177ee6cdda10c86d1db1186b0 @@ -26851,6 +31667,21 @@ packages: purls: [] size: 340180 timestamp: 1774300467879 +- conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.3-hdbac1cb_1.conda + sha256: 0bbd19c9f7c4d0232b31892e6a4d1f82b8d19d1b84d89725f1f491b336447758 + md5: 4e4d54f9f98383d977ba56ef39ebf46d + depends: + - libpng >=1.6.58,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - freetype >=2.14.3 + license: GPL-2.0-only OR FTL + run_exports: {} + size: 340411 + timestamp: 1780934813224 - conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.2.0-h8ee18e1_19.conda sha256: 80e80ef5e31b00b12539db3c5aaecde60dab91381abfc1060e323d5c3b016dce md5: cc5d690fc1c629038f13c68e88e65f44 @@ -26908,6 +31739,26 @@ packages: purls: [] size: 4522891 timestamp: 1778508851933 +- conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.88.2-h7ce1215_0.conda + sha256: 20d4a182b8aa1d71b331579fae281bb3ccb1a199257ce15fadc53786031a7408 + md5: 5be116480ef34a5646894d7f7cd7ae41 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libintl >=0.22.5,<1.0a0 + - libzlib >=1.3.2,<2.0a0 + - libiconv >=1.18,<2.0a0 + - pcre2 >=10.47,<10.48.0a0 + - libffi >=3.5.2,<3.6.0a0 + constrains: + - glib >2.66 + license: LGPL-2.1-or-later + run_exports: + weak: + - libglib >=2.88.2,<3.0a0 + size: 4518265 + timestamp: 1782463965040 - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h8ee18e1_19.conda sha256: 4dc958ced2fc7f42bc675b07e2c9abe3e150875ffdf62ca551d94fc6facf1fd7 md5: f1147651e3fdd585e2f442c0c2fc8f2d @@ -26919,11 +31770,58 @@ packages: license_family: GPL purls: [] run_exports: - strong: - - _openmp_mutex >=4.5 - - libgomp >=15.2.0 - size: 664640 - timestamp: 1778272979661 + strong: + - _openmp_mutex >=4.5 + - libgomp >=15.2.0 + size: 664640 + timestamp: 1778272979661 +- conda: https://conda.anaconda.org/conda-forge/win-64/libharfbuzz-14.2.1-h03b5201_1.conda + sha256: 634a64cf43f1ce5a4139334bcdbde54d6854ae33d881ae1774377965e21051a5 + md5: 005469a341088900ca235892d3154c24 + depends: + - cairo >=1.18.4,<2.0a0 + - graphite2 >=1.3.15,<2.0a0 + - icu >=78.3,<79.0a0 + - libexpat >=2.8.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libglib >=2.88.2,<3.0a0 + - libpng >=1.6.58,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + run_exports: {} + size: 1008194 + timestamp: 1782801000396 +- conda: https://conda.anaconda.org/conda-forge/win-64/libharfbuzz-devel-14.2.1-h03b5201_1.conda + sha256: d04bac65245bf76ae23a18148b941d8215085d38a6d391971966ccda8a996b99 + md5: de077ebf9cbc0c1da6510fdf1bbc6baa + depends: + - cairo >=1.18.4,<2.0a0 + - freetype + - glib + - graphite2 >=1.3.15,<2.0a0 + - icu >=78.3,<79.0a0 + - libexpat >=2.8.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libglib >=2.88.2,<3.0a0 + - libharfbuzz 14.2.1 h03b5201_1 + - libpng >=1.6.58,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + run_exports: + weak: + - libharfbuzz >=14.2.1 + size: 321750 + timestamp: 1782801035822 - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.13.0-default_h049141e_1000.conda sha256: 2ee12e37223dfcd0acd050c80a91150c482b6e2899198521e1800dce66662467 md5: 6a01c986e30292c715038d2788aa1385 @@ -26937,6 +31835,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libhwloc >=2.13.0,<2.13.1.0a0 size: 2396128 timestamp: 1770954127918 - conda: https://conda.anaconda.org/conda-forge/win-64/libhwy-1.4.0-h172a326_0.conda @@ -26950,6 +31851,19 @@ packages: purls: [] size: 562583 timestamp: 1776989522919 +- conda: https://conda.anaconda.org/conda-forge/win-64/libhwy-1.4.0-h2419aca_0.conda + sha256: ade3e4e8e09051bad9c75ea9e96b09e3c635216c409c87c369e6f8566a528cb1 + md5: 430378e206cf5a148fc8da7603b2466e + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 OR BSD-3-Clause + run_exports: + weak: + - libhwy >=1.4.0,<1.5.0a0 + size: 564121 + timestamp: 1784325614001 - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda sha256: 0dcdb1a5f01863ac4e8ba006a8b0dc1a02d2221ec3319b5915a1863254d7efa7 md5: 64571d1dd6cdcfa25d0664a5950fdaa2 @@ -26959,6 +31873,9 @@ packages: - vc14_runtime >=14.44.35208 license: LGPL-2.1-only purls: [] + run_exports: + weak: + - libiconv >=1.18,<2.0a0 size: 696926 timestamp: 1754909290005 - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda @@ -26968,8 +31885,23 @@ packages: - libiconv >=1.17,<2.0a0 license: LGPL-2.1-or-later purls: [] + run_exports: + weak: + - libintl >=0.22.5,<1.0a0 size: 95568 timestamp: 1723629479451 +- conda: https://conda.anaconda.org/conda-forge/win-64/libintl-devel-0.22.5-h5728263_3.conda + sha256: be1f3c48bc750bca7e68955d57180dfd826d6f9fa7eb32994f6cb61b813f9a6a + md5: 7537784e9e35399234d4007f45cdb744 + depends: + - libiconv >=1.17,<2.0a0 + - libintl 0.22.5 h5728263_3 + license: LGPL-2.1-or-later + run_exports: + weak: + - libintl >=0.22.5,<1.0a0 + size: 40746 + timestamp: 1723629745649 - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.4.1-hfd05255_0.conda sha256: 698d57b5b90120270eaa401298319fcb25ea186ae95b340c2f4813ed9171083d md5: 25a127bad5470852b30b239f030ec95b @@ -26983,6 +31915,21 @@ packages: purls: [] size: 842806 timestamp: 1775962811457 +- conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.2.0-hfd05255_0.conda + sha256: 3d6635efa9497b9c5ba7957df8067c0a980d5cb10a6ea136931809eb410f8a99 + md5: cf219146d5bf2fee5907409ff9f5ac89 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + run_exports: + weak: + - libjpeg-turbo >=3.2.0,<4.0a0 + size: 991057 + timestamp: 1783731990693 - conda: https://conda.anaconda.org/conda-forge/win-64/libjxl-0.11.2-h932607e_1.conda sha256: 4715e22c602526c85da09f73865676add67e0995a944b821fbff84547a9db533 md5: 327bce3eb1ef1875c7145e915d25bcd3 @@ -26998,6 +31945,23 @@ packages: purls: [] size: 1194926 timestamp: 1777065171989 +- conda: https://conda.anaconda.org/conda-forge/win-64/libjxl-0.12.0-h932607e_1.conda + sha256: 141310ec747808be57ba9e7501fcfab75b260803233dd9ba818fb85f6080aad9 + md5: adb20d060513fe9655d28a780bafafaa + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libhwy >=1.4.0,<1.5.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - libjxl >=0.12.0,<0.13.0a0 + size: 1199695 + timestamp: 1783146089449 - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-8_hf9ab0e9_mkl.conda build_number: 8 sha256: 44999ed04bc0a56de44ee0ac8bd5b3702efd411a8b29491c0e3d3deb8619c94e @@ -27011,6 +31975,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - liblapack >=3.11.0,<3.12.0a0 size: 81027 timestamp: 1779859714698 - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda @@ -27024,6 +31991,9 @@ packages: - xz 5.8.3.* license: 0BSD purls: [] + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 size: 106486 timestamp: 1775825663227 - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-devel-5.8.3-hfd05255_0.conda @@ -27036,6 +32006,9 @@ packages: - vc14_runtime >=14.44.35208 license: 0BSD purls: [] + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 size: 130960 timestamp: 1775825690054 - conda: https://conda.anaconda.org/conda-forge/win-64/libmed-4.2-nompi_hbaf3de0_28.conda @@ -27090,6 +32063,32 @@ packages: purls: [] size: 662406 timestamp: 1776686500276 +- conda: https://conda.anaconda.org/conda-forge/win-64/libnetcdf-4.10.1-nompi_he1c4404_201.conda + build_number: 101 + sha256: 7ee6dfde20dbbe52511d670816465c1d31db1534f552c90be5991117083d501b + md5: 0f934d3f1d9edc88e1a476613123ed83 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - bzip2 >=1.0.8,<2.0a0 + - libaec >=1.1.5,<2.0a0 + - libzip >=1.11.2,<2.0a0 + - blosc >=1.21.6,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - libzlib >=1.3.2,<2.0a0 + - hdf4 >=4.2.15,<4.2.16.0a0 + - hdf5 >=1.14.6,<1.14.7.0a0 + - libcurl >=8.21.0,<9.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - libnetcdf >=4.10.1,<4.10.2.0a0 + size: 786533 + timestamp: 1783982110706 - conda: https://conda.anaconda.org/conda-forge/win-64/libogg-1.3.5-h2466b09_1.conda sha256: c63e5fb169dbd192aacdcee6e37235407f106b8ca9c9036942a25e0366cbc73c md5: b67ed8c9ca072695ff482e50d888a523 @@ -27103,6 +32102,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libogg >=1.3.5,<1.4.0a0 size: 35040 timestamp: 1745826086628 - conda: https://conda.anaconda.org/conda-forge/win-64/libopus-1.6.1-h6a83c73_0.conda @@ -27115,6 +32117,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libopus >=1.6.1,<2.0a0 size: 307373 timestamp: 1768497136248 - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda @@ -27127,8 +32132,44 @@ packages: - libzlib >=1.3.2,<2.0a0 license: zlib-acknowledgement purls: [] + run_exports: + weak: + - libpng >=1.6.58,<1.7.0a0 size: 385227 timestamp: 1776315248638 +- conda: https://conda.anaconda.org/conda-forge/win-64/libpsl-0.22.0-h25e0afd_1.conda + sha256: 040d4adabae2634b13183203e383c21f74ed98028b5d50bf9bae4968dd05aaa5 + md5: ba200e33e10ccf0d730c4ce87badbdf1 + depends: + - icu >=78.3,<79.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + run_exports: + weak: + - libpsl >=0.22.0,<0.23.0a0 + size: 72405 + timestamp: 1783937048984 +- conda: https://conda.anaconda.org/conda-forge/win-64/libraqm-0.10.5-h50d6d30_1.conda + sha256: 79ca55f7202daa560bffafcf7aef435e48618fe91a5243b8e4a6ed4edfbe63bc + md5: e5ab537cdd3462f26be2803209142913 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libharfbuzz >=14.2.1 + - fribidi >=1.0.16,<2.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - libraqm >=0.10.5,<0.11.0a0 + size: 29296 + timestamp: 1782807186901 - conda: https://conda.anaconda.org/conda-forge/win-64/libraw-0.22.1-h345c428_0.conda sha256: b5f50ec5914a2a66b5cca95710152d6cb97e09b8a834ce859974aa60fffa7770 md5: fa0e0ded35a0ce9a41a7619ba6445182 @@ -27144,6 +32185,23 @@ packages: purls: [] size: 572711 timestamp: 1775541781709 +- conda: https://conda.anaconda.org/conda-forge/win-64/libraw-0.22.2-h345c428_0.conda + sha256: 347df3aa8db67e51702d3478bcba7a85745dde82c6824b252744e8de676e5e23 + md5: 901729097d423c69247ba6bed85be4e4 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - jasper >=4.2.9,<5.0a0 + - libjpeg-turbo >=3.2.0,<4.0a0 + - lcms2 >=2.19.1,<3.0a0 + - libzlib >=1.3.2,<2.0a0 + license: LGPL-2.1-only + run_exports: + weak: + - libraw >=0.22.2,<0.23.0a0 + size: 581445 + timestamp: 1784220908733 - conda: https://conda.anaconda.org/conda-forge/win-64/librsvg-2.62.3-h15cfe45_0.conda sha256: 6f678be6074b79fe754660d16857a6edba73dd197ad92086250dc38c11b179ab md5: 3fffc63af7b943cde57aa72f5ffe6048 @@ -27159,6 +32217,9 @@ packages: - vc14_runtime >=14.44.35208 license: LGPL-2.1-or-later purls: [] + run_exports: + weak: + - librsvg >=2.62.3,<3.0a0 size: 3361405 timestamp: 1780451179155 - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.22-h6a83c73_1.conda @@ -27183,6 +32244,19 @@ packages: purls: [] size: 1304178 timestamp: 1777986510497 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.3-hf5d6505_0.conda + sha256: 692dfb73a22c873656d5e393b8f1e2b019a3c8a6486c97cb6900552e64e38c25 + md5: 051f1b2228e7517a2ef8cca5146c8967 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: blessing + run_exports: + weak: + - libsqlite >=3.53.3,<4.0a0 + size: 1315909 + timestamp: 1782519131898 - conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda sha256: cbdf93898f2e27cefca5f3fe46519335d1fab25c4ea2a11b11502ff63e602c09 md5: 9dce2f112bfd3400f4f432b3d0ac07b2 @@ -27195,6 +32269,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libssh2 >=1.11.1,<2.0a0 size: 292785 timestamp: 1745608759342 - conda: https://conda.anaconda.org/conda-forge/win-64/libstdcxx-15.2.0-hae5796f_19.conda @@ -27221,6 +32298,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libtheora >=1.1.1,<1.2.0a0 size: 160440 timestamp: 1719668116346 - conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.1-h8f73337_1.conda @@ -27240,6 +32320,25 @@ packages: purls: [] size: 993166 timestamp: 1762022118895 +- conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.2-h8f73337_0.conda + sha256: ec6d66308a6d6abaf3225f2f185113e6172e77eb0fa8622af982d7a5d6d47a2c + md5: e83f459471905a04ebe15e21d063c49d + depends: + - lerc >=4.1.0,<5.0a0 + - libdeflate >=1.25,<1.26.0a0 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zstd >=1.5.7,<1.6.0a0 + license: HPND + run_exports: + weak: + - libtiff >=4.7.2,<4.8.0a0 + size: 1014598 + timestamp: 1783085017197 - conda: https://conda.anaconda.org/conda-forge/win-64/libusb-1.0.29-h1839187_0.conda sha256: 9837f8e8de20b6c9c033561cd33b4554cd551b217e3b8d2862b353ed2c23d8b8 md5: a656b2c367405cd24988cf67ff2675aa @@ -27252,6 +32351,9 @@ packages: - ucrt >=10.0.20348.0 license: LGPL-2.1-or-later purls: [] + run_exports: + weak: + - libusb >=1.0.29,<2.0a0 size: 118204 timestamp: 1748856290542 - conda: https://conda.anaconda.org/conda-forge/win-64/libvorbis-1.3.7-h5112557_2.conda @@ -27269,6 +32371,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libvorbis >=1.3.7,<1.4.0a0 size: 243401 timestamp: 1753879416570 - conda: https://conda.anaconda.org/conda-forge/win-64/libvulkan-loader-1.4.341.0-h477610d_0.conda @@ -27283,6 +32388,9 @@ packages: license: Apache-2.0 license_family: APACHE purls: [] + run_exports: + weak: + - libvulkan-loader >=1.4.341.0,<2.0a0 size: 282251 timestamp: 1770077165680 - conda: https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.6.0-h4d5522a_0.conda @@ -27297,6 +32405,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libwebp-base >=1.6.0,<2.0a0 size: 279176 timestamp: 1752159543911 - conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_10.conda @@ -27325,6 +32436,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libxcb >=1.17.0,<2.0a0 size: 1208687 timestamp: 1727279378819 - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.3-h3cfd58e_0.conda @@ -27343,6 +32457,7 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: {} size: 519871 timestamp: 1776376969852 - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.3-h692994f_0.conda @@ -27377,6 +32492,10 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - libxml2 + - libxml2-16 >=2.15.3 size: 43684 timestamp: 1776376992865 - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.3-hbc0d294_0.conda @@ -27423,6 +32542,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - libzip >=1.11.2,<2.0a0 size: 146856 timestamp: 1730442305774 - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda @@ -27437,6 +32559,9 @@ packages: license: Zlib license_family: Other purls: [] + run_exports: + weak: + - libzlib >=1.3.2,<2.0a0 size: 58347 timestamp: 1774072851498 - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-22.1.7-h4fa8253_0.conda @@ -27453,6 +32578,23 @@ packages: purls: [] size: 347536 timestamp: 1780456277495 +- conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-22.1.8-h4fa8253_0.conda + sha256: 50c02902bb516eeb56680358f052be38b5bf74b40e78ea4b2a675e84957e7307 + md5: de3551bf6508d45ca46b714639e52823 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - openmp 22.1.8|22.1.8.* + - intel-openmp <0.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE + run_exports: + strong: + - llvm-openmp >=22.1.8 + size: 348002 + timestamp: 1781737042070 - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.1.1-py312h2f35c63_0.conda sha256: a23ee72837fa9606b5931b64c3d272303895b07bd654f291b3fb49f35ca1a3dc md5: f23b7c90ac0cb5366be9715af546cfa1 @@ -27497,6 +32639,9 @@ packages: license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - lz4-c >=1.10.0,<1.11.0a0 size: 139891 timestamp: 1733741168264 - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda @@ -27560,6 +32705,35 @@ packages: - pkg:pypi/matplotlib?source=hash-mapping size: 8033620 timestamp: 1777000825433 +- conda: https://conda.anaconda.org/conda-forge/win-64/matplotlib-base-3.11.0-py312hcbcab24_1.conda + sha256: 10e4c3e9d56daa97503e8ef5e478d1ed1031f06c72febcf915839ef55a064804 + md5: fb202688665dd29a61750aa34161fee4 + depends: + - contourpy >=1.0.1 + - cycler >=0.10 + - fonttools >=4.22.0 + - freetype + - kiwisolver >=1.3.1 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libraqm >=0.10.5,<0.11.0a0 + - numpy >=1.23 + - numpy >=1.25,<3 + - packaging >=20.0 + - pillow >=8 + - pyparsing >=2.3.1 + - python >=3.12,<3.13.0a0 + - python-dateutil >=2.7 + - python_abi 3.12.* *_cp312 + - qhull >=2020.2,<2020.3.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: PSF-2.0 + license_family: PSF + run_exports: {} + size: 8621787 + timestamp: 1782829733178 - conda: https://conda.anaconda.org/conda-forge/win-64/mesalib-25.0.5-hf8ad13a_2.conda sha256: 89b6f9af9a50f0b551e28ada27ac8d5370ef1d880a4632d2abefabb837afc790 md5: 628c6d8c8d3311dfe4599175011df796 @@ -27577,6 +32751,25 @@ packages: purls: [] size: 51415172 timestamp: 1755730200200 +- conda: https://conda.anaconda.org/conda-forge/win-64/mesalib-26.0.3-h667bac9_0.conda + sha256: 5e2e0d6ebf20c1c6fb59dd8ef5b720b31adf459310994013cd36af7d9e2504c9 + md5: fc1ac6efe8938419dd7f92b28a7f4647 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libxml2 + - libxml2-16 >=2.14.6 + - spirv-tools >=2026,<2027.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - mesalib >=26.0.3,<26.1.0a0 + size: 43614883 + timestamp: 1773867984858 - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_908.conda sha256: f997bfc9bc4d4e14261cdcd1ad195d64a72ee44dca3145d24c1349f8d1311aa5 md5: 36ea6e1292e9d5e89374201da79646ef @@ -27592,6 +32785,21 @@ packages: purls: [] size: 114354729 timestamp: 1779293121860 +- conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.1.0-hac47afa_233.conda + sha256: ff355522fb0b6e33841167d9ca749147c8734d8be07b63b2ce25b0db043f42ed + md5: 5afbf28c4ca3e05b5469dbbd78c8e704 + depends: + - llvm-openmp >=22.1.8 + - onemkl-license 2026.1.0 h57928b3_233 + - tbb >=2023.0.0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: LicenseRef-IntelSimplifiedSoftwareOct2022 + license_family: Proprietary + run_exports: {} + size: 114592547 + timestamp: 1783700769546 - conda: https://conda.anaconda.org/conda-forge/win-64/mpfr-4.2.2-h883a981_0.conda sha256: 59d10b8dcfa2b899726556835b4546a2e1054f104c938b951ed9821fff81fad2 md5: 7bcb237f435cb77e5a68d6e528ec65ae @@ -27603,8 +32811,25 @@ packages: license: LGPL-3.0-only license_family: LGPL purls: [] + run_exports: + weak: + - mpfr >=4.2.2,<5.0a0 size: 734113 timestamp: 1773415369651 +- conda: https://conda.anaconda.org/conda-forge/win-64/mpg123-1.32.9-h01009b0_0.conda + sha256: a1d7d25f2c448f5c47d1678cca1f6ae5deadb38e176ea0c76ea5c688589dfd7a + md5: 1ed1580d4211223b285787eff05560f9 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: LGPL-2.1-only + license_family: LGPL + run_exports: + weak: + - mpg123 >=1.32.9,<1.33.0a0 + size: 274386 + timestamp: 1730581654395 - conda: https://conda.anaconda.org/conda-forge/win-64/msgpack-python-1.1.2-py312hf90b1b7_1.conda sha256: 0408cc0868e0963922c76940d618266df88518a7b58b5d28da8378911916b998 md5: 3272249c8d0f9cb7693e189611b9943f @@ -27620,6 +32845,20 @@ packages: - pkg:pypi/msgpack?source=hash-mapping size: 87478 timestamp: 1762504274037 +- conda: https://conda.anaconda.org/conda-forge/win-64/msgpack-python-1.2.1-py312h78d62e6_1.conda + sha256: f43cc72ae92759d661187cb4bc4fb4721b97128d150f336b9b0ebf24b8600a27 + md5: 0ac1766b278a8547f22cd0820b8a7e96 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.12.* *_cp312 + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 89054 + timestamp: 1782460807428 - conda: https://conda.anaconda.org/conda-forge/win-64/multidict-6.7.1-py312h05f76fc_0.conda sha256: 3a33346421defe3bd5dfdd10bed8a7055a2ce3b696aafa47f9ec5b1749d38c54 md5: 3b54ef7993acc98becf31e0016e0404d @@ -27633,6 +32872,7 @@ packages: license_family: APACHE purls: - pkg:pypi/multidict?source=hash-mapping + run_exports: {} size: 91620 timestamp: 1771610831076 - conda: https://conda.anaconda.org/conda-forge/win-64/netcdf4-1.7.4-nompi_py311h7adff93_107.conda @@ -27670,6 +32910,7 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: {} size: 134870 timestamp: 1758194302226 - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-24.16.0-h80d1838_0.conda @@ -27723,6 +32964,27 @@ packages: license_family: BSD size: 7318163 timestamp: 1779169232086 +- conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.5.1-py312ha3f287d_0.conda + sha256: e9df7b016c6910a9d4b1894e53499de79a871292e2f88f338070f0fa0080fcaa + md5: 8ae08a63662c4b2d46af3f2917e749b4 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - liblapack >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - python_abi 3.12.* *_cp312 + - libblas >=3.9.0,<4.0a0 + constrains: + - numpy-base <0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - numpy >=1.25,<3 + size: 7301536 + timestamp: 1783206237609 - conda: https://conda.anaconda.org/conda-forge/win-64/obstore-0.10.0-py312hb0142fd_0.conda sha256: 9e778dd60a36d6fb43786ef25be04581b262a5cc8f2f24bc1456b6f5de2439a8 md5: 16b7222c978a1cd83a334399c1c5f509 @@ -27793,6 +33055,31 @@ packages: purls: [] size: 24503850 timestamp: 1776672932602 +- conda: https://conda.anaconda.org/conda-forge/win-64/occt-8.0.0-all_h8ecc14b_202.conda + sha256: 92f9089e8e97414fafcc4af0f9d0113683e6b43774fc4883b277c3de3d6cc132 + md5: 19e5090f840e8688508abe98ae36ddc4 + depends: + - fontconfig >=2.18.1,<3.0a0 + - fonts-conda-ecosystem + - freeimage >=3.18.0,<3.19.0a0 + - freetype + - libexpat >=2.8.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - rapidjson + - tbb >=2023.0.0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - vtk + - vtk-base >=9.6.2,<9.6.3.0a0 + license: LGPL-2.1-only + license_family: LGPL + run_exports: + weak: + - occt >=8.0.0,<8.0.1.0a0 + size: 25108055 + timestamp: 1781686481495 - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_908.conda sha256: 42ad15cbb3bf31830efa04d4b86dd2d5c0dd590c86f98adcd3c8c1f75acf5dd5 md5: 9c9303e08b50e09f5c23e1dac99d0936 @@ -27801,6 +33088,14 @@ packages: purls: [] size: 41580 timestamp: 1779292867015 +- conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.1.0-h57928b3_233.conda + sha256: 96dec1c878d6e6f835be8ed57152a37be9a5104ac79c2425815d71c64cf13ee4 + md5: 1566e2ce8c3bc23a2feb866c4d9e91e3 + license: LicenseRef-IntelSimplifiedSoftwareOct2022 + license_family: Proprietary + run_exports: {} + size: 53362 + timestamp: 1783700628723 - conda: https://conda.anaconda.org/conda-forge/win-64/openexr-3.4.12-h7faf11f_0.conda sha256: 08ef1d8a85b74a9e7fc3010008e9a4ce41196f841d26c51892d5cc5f39602863 md5: bbcf24b8aa69a457c92fffb58392ea05 @@ -27817,6 +33112,38 @@ packages: purls: [] size: 948711 timestamp: 1779680445353 +- conda: https://conda.anaconda.org/conda-forge/win-64/openexr-3.4.13-hd20f895_1.conda + sha256: a00088687b2eb0c22041be2e74fa3222c44b42667415e10e8312365a245d061c + md5: cb83fa215048c5da5dc8a14c7fe37485 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - imath >=3.2.2,<3.2.3.0a0 + - libdeflate >=1.25,<1.26.0a0 + - openjph >=0.30.1,<0.31.0a0 + - libzlib >=1.3.2,<2.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - openexr >=3.4.13,<3.5.0a0 + size: 949849 + timestamp: 1782198449274 +- conda: https://conda.anaconda.org/conda-forge/win-64/openh264-2.6.0-h1eab103_1.conda + sha256: 8d7e4a2dcd68afcc87c1e875a19600d980ca8f792f00105a622efd5faed6b05a + md5: 91c186a483e5491170156399b2850804 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-2-Clause + license_family: BSD + run_exports: + weak: + - openh264 >=2.6.0,<2.6.1.0a0 + size: 422904 + timestamp: 1782686043511 - conda: https://conda.anaconda.org/conda-forge/win-64/openh264-2.6.0-hb17fa0b_0.conda sha256: 914702d9a64325ff3afb072c8bc0f8cbea3f19955a8395a8c190e45604f83c76 md5: ad4cac6ceb9e4c8e01802e3f15e87bb2 @@ -27842,6 +33169,9 @@ packages: license: BSD-2-Clause license_family: BSD purls: [] + run_exports: + weak: + - openjpeg >=2.5.4,<3.0a0 size: 245594 timestamp: 1772624841727 - conda: https://conda.anaconda.org/conda-forge/win-64/openjph-0.27.3-hf13a347_0.conda @@ -27857,6 +33187,21 @@ packages: purls: [] size: 217660 timestamp: 1778775186038 +- conda: https://conda.anaconda.org/conda-forge/win-64/openjph-0.30.1-hf13a347_0.conda + sha256: 5deaf995e476e6928dedced3254025b568a70a905f04ad23fd150d9abe558b91 + md5: 47ed976bdc702ab527737d6dc0d7e6d7 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libtiff >=4.7.1,<4.8.0a0 + license: BSD-2-Clause + license_family: BSD + run_exports: + weak: + - openjph >=0.30.1,<0.31.0a0 + size: 226246 + timestamp: 1782091209917 - conda: https://conda.anaconda.org/conda-forge/win-64/openpyxl-3.1.5-py312h83acffa_3.conda sha256: 824eeb546a08c990eb461706280d3064c0437f67eb320018986fa439f468e43a md5: ff342a314798173eaaf2753a22f044fa @@ -27900,6 +33245,21 @@ packages: purls: [] size: 9410183 timestamp: 1775589779763 +- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.3-hf411b9b_0.conda + sha256: cb6e7ba0d010ee0d3249ce9886de3d7613d26d9965d4c95666fa66b9c4c31001 + md5: e99f95734a326c0fd4d02bbd995150d4 + depends: + - ca-certificates + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + run_exports: + weak: + - openssl >=3.6.3,<4.0a0 + size: 9414790 + timestamp: 1781071745579 - conda: https://conda.anaconda.org/conda-forge/win-64/orjson-3.11.9-py312hfb36fc7_0.conda sha256: 0f90e4718fafc3eef76a6dd05a8161b30b28ba5d1584865f87466cbeb2e567ed md5: a8e62725b0997291831240eaef29cae6 @@ -28080,6 +33440,9 @@ packages: - vc14_runtime >=14.29.30139 license: LGPL-2.1-or-later purls: [] + run_exports: + weak: + - pango >=1.56.4,<2.0a0 size: 454919 timestamp: 1774282149607 - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.47-hd2b5f0e_0.conda @@ -28094,6 +33457,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - pcre2 >=10.47,<10.48.0a0 size: 995992 timestamp: 1763655708300 - conda: https://conda.anaconda.org/conda-forge/win-64/pigz-2.8-h005e9e7_2.conda @@ -28133,6 +33499,29 @@ packages: - pkg:pypi/pillow?source=hash-mapping size: 944998 timestamp: 1775060119774 +- conda: https://conda.anaconda.org/conda-forge/win-64/pillow-12.3.0-py312h31f0997_0.conda + sha256: 19e982ab2e3e43ca1506bd34ce516fd309d78068692e4770a1bb59cbda2d2710 + md5: 7e163dfd8d118658c0ca0ebffe80ad7d + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - openjpeg >=2.5.4,<3.0a0 + - python_abi 3.12.* *_cp312 + - libxcb >=1.17.0,<2.0a0 + - lcms2 >=2.19.1,<3.0a0 + - zlib-ng >=2.3.3,<2.4.0a0 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - tk >=8.6.13,<8.7.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libtiff >=4.7.1,<4.8.0a0 + - libwebp-base >=1.6.0,<2.0a0 + license: HPND + run_exports: {} + size: 949403 + timestamp: 1782912129930 - conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.4-h5112557_1.conda sha256: 246fce4706b3f8b247a7d6142ba8d732c95263d3c96e212b9d63d6a4ab4aff35 md5: 08c8fa3b419df480d985e304f7884d35 @@ -28148,6 +33537,19 @@ packages: purls: [] size: 542795 timestamp: 1754665193489 +- conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.4-h5112557_2.conda + sha256: f37fb21952bd4297f8c7d78e2f256647da2b4ad4e1df097d49ebff8a40275cab + md5: df8da7fe89bdc91b880df69a8eb1c37b + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: MIT + run_exports: + weak: + - pixman >=0.46.4,<1.0a0 + size: 257105 + timestamp: 1784286884376 - conda: https://conda.anaconda.org/conda-forge/win-64/proj-9.8.1-hd30e2cd_0.conda sha256: 10ac4a9cdd99d3c2dc4695c5db51d9aa263a3b7551604f57801679b81c785d12 md5: bbf5692c63b2f280975b3430ed3e37fd @@ -28166,6 +33568,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - proj >=9.8.1,<9.9.0a0 size: 3129512 timestamp: 1775840349774 - conda: https://conda.anaconda.org/conda-forge/win-64/propcache-0.5.2-py312h05f76fc_0.conda @@ -28181,6 +33586,7 @@ packages: license_family: APACHE purls: - pkg:pypi/propcache?source=compressed-mapping + run_exports: {} size: 49101 timestamp: 1780037831647 - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py312he5662c2_0.conda @@ -28221,6 +33627,7 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: {} size: 9389 timestamp: 1726802555076 - conda: https://conda.anaconda.org/conda-forge/win-64/pugixml-1.15-h372dad0_0.conda @@ -28233,6 +33640,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - pugixml >=1.15,<1.16.0a0 size: 113967 timestamp: 1736601565527 - conda: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.46.4-py312hdabe01f_0.conda @@ -28300,6 +33710,11 @@ packages: - python_abi 3.12.* *_cp312 license: Python-2.0 purls: [] + run_exports: + weak: + - python_abi 3.12.* *_cp312 + noarch: + - python size: 15840187 timestamp: 1772728877265 - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.14.5-h4b44e0e_100_cp314.conda @@ -28447,6 +33862,9 @@ packages: - vc14_runtime >=14.29.30139 license: LicenseRef-Qhull purls: [] + run_exports: + weak: + - qhull >=2020.2,<2020.3.0a0 size: 1377020 timestamp: 1720814433486 - conda: https://conda.anaconda.org/conda-forge/win-64/qt6-main-6.11.1-pl5321hfcac499_1.conda @@ -28479,7 +33897,11 @@ packages: constrains: - qt ==6.11.1 license: LGPL-3.0-only + license_family: LGPL purls: [] + run_exports: + weak: + - qt6-main >=6.11.1,<7.0a0 size: 89576886 timestamp: 1780400596481 - conda: https://conda.anaconda.org/conda-forge/win-64/rapidjson-1.1.0.post20240409-he0c23c2_2.conda @@ -28495,6 +33917,7 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: {} size: 153477 timestamp: 1742820803681 - conda: https://conda.anaconda.org/conda-forge/win-64/rocksdb-10.6.2-h6b9cf66_0_default.conda @@ -28513,6 +33936,9 @@ packages: license: Apache-2.0 license_family: APACHE purls: [] + run_exports: + weak: + - rocksdb >=10.6.2,<10.7.0a0 size: 54418605 timestamp: 1761165754119 - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-2026.5.1-py312hd944d65_0.conda @@ -28603,6 +34029,9 @@ packages: - sdl3 >=3.2.22,<4.0a0 license: Zlib purls: [] + run_exports: + weak: + - sdl2 >=2.32.56,<3.0a0 size: 572101 timestamp: 1757842925694 - conda: https://conda.anaconda.org/conda-forge/win-64/sdl3-3.4.10-h5112557_0.conda @@ -28618,6 +34047,21 @@ packages: purls: [] size: 1677765 timestamp: 1780262836463 +- conda: https://conda.anaconda.org/conda-forge/win-64/sdl3-3.4.12-h5112557_0.conda + sha256: cd70a95559fdcaa9809d7c697b1d2e283c2d61eb184f91f7b03f8c2291e206a8 + md5: 5f80121d90de6623ae8e0eee34da16ff + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libvulkan-loader >=1.4.341.0,<2.0a0 + - libusb >=1.0.29,<2.0a0 + license: Zlib + run_exports: + weak: + - sdl3 >=3.4.12,<4.0a0 + size: 1680362 + timestamp: 1782948074728 - conda: https://conda.anaconda.org/conda-forge/win-64/shaderc-2025.5-haa9a63f_0.conda sha256: ed369ab2775ed7ff0476cedf8926a326d8d93068ee9c3e5f0e8836393390d5b0 md5: c7159a672b77c38a0e8f88491ca9e9b2 @@ -28632,6 +34076,22 @@ packages: purls: [] size: 1516952 timestamp: 1764288127996 +- conda: https://conda.anaconda.org/conda-forge/win-64/shaderc-2026.3-hbcac29b_0.conda + sha256: ae69be2f9a0828e35397b4c0d25bad3eb9b395535bc635154e872a6fd5b440aa + md5: 77b44ad4a137a273aeff5962764cfa58 + depends: + - glslang >=16,<17.0a0 + - spirv-tools >=2026,<2027.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + run_exports: + weak: + - shaderc >=2026.3,<2026.4.0a0 + size: 1600332 + timestamp: 1784251308130 - conda: https://conda.anaconda.org/conda-forge/win-64/shapely-2.1.2-py312h37f46ab_2.conda sha256: 06f2e00ea487689ad23de7a9f791378ad00d69d464cd809ccce1ad39486263a9 md5: 674ce7b99e43ac450b79d6c9c8a11705 @@ -28647,6 +34107,7 @@ packages: license_family: BSD purls: - pkg:pypi/shapely?source=hash-mapping + run_exports: {} size: 596670 timestamp: 1762523748692 - conda: https://conda.anaconda.org/conda-forge/win-64/simplejson-4.1.1-py312he06e257_0.conda @@ -28690,6 +34151,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - snappy >=1.2.2,<1.3.0a0 size: 67417 timestamp: 1762948090450 - conda: https://conda.anaconda.org/conda-forge/win-64/spirv-tools-2025.5-h49e36cd_0.conda @@ -28706,6 +34170,21 @@ packages: purls: [] size: 14484778 timestamp: 1769406457280 +- conda: https://conda.anaconda.org/conda-forge/win-64/spirv-tools-2026.2-h49e36cd_1.conda + sha256: 30a87a710c1deed93371e6ff365ebd3b578aeefad68cc89bedff85ffb2fea9d2 + md5: 5a929a4b1f6c82d05f0a1283d4a14e45 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - spirv-headers >=1.4.350.1,<1.4.350.2.0a0 + license: Apache-2.0 + run_exports: + weak: + - spirv-tools >=2026,<2027.0a0 + size: 14643364 + timestamp: 1784384374991 - conda: https://conda.anaconda.org/conda-forge/win-64/sqlite-3.53.1-hdb435a2_0.conda sha256: cdac82f7ff491ac4915161e560094ad830606b1316099046e565ef3dda9f58ce md5: 5833ae18b609bf0790e0bfe6f95b3351 @@ -28718,6 +34197,20 @@ packages: purls: [] size: 425712 timestamp: 1777986518363 +- conda: https://conda.anaconda.org/conda-forge/win-64/sqlite-3.53.3-hdb435a2_0.conda + sha256: 700391bf405117eb5c766c4b506f23d3a31f56191ca6619d1b8e5c715376bb7f + md5: 7a28cb97617f0bd6a650b4b88f1f44cb + depends: + - libsqlite 3.53.3 hf5d6505_0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: blessing + run_exports: + weak: + - libsqlite >=3.53.3,<4.0a0 + size: 426871 + timestamp: 1782519137996 - conda: https://conda.anaconda.org/conda-forge/win-64/svt-av1-4.0.1-hac47afa_0.conda sha256: 4d77eec06ee4c5de38d330fb7dfd6dac2f867ec007123acb901be9942e12c08a md5: d9714a97bc69f98fd5032f675ae1b0b5 @@ -28730,6 +34223,20 @@ packages: purls: [] size: 1808810 timestamp: 1769664619287 +- conda: https://conda.anaconda.org/conda-forge/win-64/svt-av1-4.2.0-hac47afa_0.conda + sha256: 6dc6ed0e651e99d03ae25b6a358064247c96b1cb436fdbf973ab25c1c6aedcd4 + md5: c49fb5bcbd2f2537875e4b2f62c0de3b + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-2-Clause + license_family: BSD + run_exports: + weak: + - svt-av1 >=4.2.0,<4.2.1.0a0 + size: 1833926 + timestamp: 1784070033860 - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-hd3d4ead_2.conda sha256: 8a4053839b8e997a5965e2dff7d6cf3c77be62d82c0e48c8a04a5ed2d2e73035 md5: 8ee01a693aecff5432069eaaf1183c45 @@ -28741,6 +34248,7 @@ packages: license: Apache-2.0 license_family: APACHE purls: [] + run_exports: {} size: 156515 timestamp: 1778673901757 - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-devel-2023.0.0-h7c1ad13_2.conda @@ -28752,6 +34260,9 @@ packages: - vc >=14.3,<15 - vc14_runtime >=14.44.35208 purls: [] + run_exports: + weak: + - tbb >=2023.0.0 size: 1147696 timestamp: 1778673916862 - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda @@ -28766,6 +34277,19 @@ packages: purls: [] size: 3526350 timestamp: 1769460339384 +- conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h967ab96_3.conda + sha256: 13fa29257d43f8e630a1e591ed77fae9bbbb236b011432f01e2034cf36e6bf03 + md5: aaf79e2af50a151fb5b5a3e3f38b7a69 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: TCL + run_exports: + weak: + - tk >=8.6.13,<8.7.0a0 + size: 3782314 + timestamp: 1784229072899 - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.6-py312he06e257_0.conda sha256: b28d89d6e848032ff22d451b74ff081112f0ab0745c9c399f68cbe5f0ce89989 md5: 75924126d32422a3a327f2be9f74e267 @@ -28789,6 +34313,7 @@ packages: - vs2015_runtime >=14.29.30037 license: LicenseRef-MicrosoftWindowsSDK10 purls: [] + run_exports: {} size: 694692 timestamp: 1756385147981 - conda: https://conda.anaconda.org/conda-forge/win-64/uharfbuzz-0.53.2-py312h6d735b1_0.conda @@ -28820,6 +34345,7 @@ packages: license_family: Apache purls: - pkg:pypi/unicodedata2?source=hash-mapping + run_exports: {} size: 405896 timestamp: 1770909182518 - conda: https://conda.anaconda.org/conda-forge/win-64/utfcpp-4.09-h57928b3_0.conda @@ -28827,6 +34353,7 @@ packages: md5: 6930bd6bf8290dbf83b2fecf42931971 license: BSL-1.0 purls: [] + run_exports: {} size: 14650 timestamp: 1767012267501 - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_38.conda @@ -28841,6 +34368,18 @@ packages: purls: [] size: 20187 timestamp: 1780005880049 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_39.conda + sha256: 17693b60cb54f80c60275f003f3bfc1b128af56dbfd65c4fae37c64eeb755ce1 + md5: 2eacea63f545b97342da520df6854276 + depends: + - vc14_runtime >=14.51.36231 + track_features: + - vc14 + license: BSD-3-Clause + license_family: BSD + run_exports: {} + size: 20362 + timestamp: 1781320968457 - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.51.36231-h1b9f54f_38.conda sha256: 957c7c65583c7107a5e76f39756c6361fcb7b0dc101ac7c0aea86e7ca09fe49c md5: 2cdcd8ea1010920911bb2eacb4c61227 @@ -28854,6 +34393,19 @@ packages: purls: [] size: 740997 timestamp: 1780005875753 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.51.36231-h1b9f54f_39.conda + sha256: 8153ed849c92e891eacac0f2f8d7ecb79f9b5fd7f7917fbb896f252a60a40390 + md5: 06a5bf5a1ca16cce0df6eaa91fc42bc2 + depends: + - ucrt >=10.0.20348.0 + - vcomp14 14.51.36231 h1b9f54f_39 + constrains: + - vs2015_runtime 14.51.36231.* *_39 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + run_exports: {} + size: 737434 + timestamp: 1781320964561 - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_38.conda sha256: c645fdc1f0f47718431d973386e946754a10200e7ba2c32032560913a970cacd md5: 63ee70d69d7540e821940dac5d4d9ba2 @@ -28866,6 +34418,20 @@ packages: purls: [] size: 123561 timestamp: 1780005858779 +- conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_39.conda + sha256: 07fb14713c4bc62e2533a2e23a363abfb0e65650681fba0ae4c840e2219350f3 + md5: 8b53a83fda40ec679e4d63fa32fae989 + depends: + - ucrt >=10.0.20348.0 + constrains: + - vs2015_runtime 14.51.36231.* *_39 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + run_exports: + strong: + - vcomp14 >=14.51.36231 + size: 120684 + timestamp: 1781320948530 - conda: https://conda.anaconda.org/conda-forge/win-64/viskores-1.1.0-h509acf7_0.conda sha256: 5043578c280c230c5526bd31206f8366c016f659a22d124fcfee04b0109d8445 md5: e58f3f51196ff6364d892ba67b458bf2 @@ -28882,6 +34448,25 @@ packages: purls: [] size: 11273482 timestamp: 1765513303522 +- conda: https://conda.anaconda.org/conda-forge/win-64/viskores-1.1.1-cpu_h4b717ef_1.conda + sha256: 474bbb189c29d85f5a5337968ab8c9e219258d4f5052f48b3fb23ac3842cd322 + md5: 9d8eb42dd4d26dc29b3b6e4f939ee30d + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - mesalib >=26.0.3,<26.1.0a0 + - zfp >=1.0.1,<2.0a0 + - glew >=2.3.0,<2.4.0a0 + - hdf5 >=1.14.6,<1.14.7.0a0 + track_features: + - viskores-p-0 + license: BSD-3-Clause + run_exports: + weak: + - viskores >=1.1.1,<1.2.0a0 + size: 11271493 + timestamp: 1784251236791 - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.51.36231-h84cd919_38.conda sha256: c4f38268563dc6b8322b0191481e5d20002fc6e37b076c15e0b955a553c8b4a0 md5: 6033851d921b6c33f1c3018205fcba6a @@ -28892,6 +34477,16 @@ packages: purls: [] size: 20170 timestamp: 1780005880423 +- conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.51.36231-h84cd919_39.conda + sha256: 6de6c2cf008fc2dce61060b583f2d8494c83883106952b201381b6b0505f03d7 + md5: 2ccc63d7b7d066a814ed9f99072832d7 + depends: + - vc14_runtime >=14.51.36231 + license: BSD-3-Clause + license_family: BSD + run_exports: {} + size: 20355 + timestamp: 1781320968804 - conda: https://conda.anaconda.org/conda-forge/win-64/vtk-9.6.1-py312h5ad3caf_3.conda sha256: 705919a0eaf4b7860b4f02bc0f12de4ca7346afd1f238bae6fa73bfdaaa42cf8 md5: f968484b1fc38288cbb4a9986648bea5 @@ -28908,6 +34503,25 @@ packages: purls: [] size: 27252 timestamp: 1777052930391 +- conda: https://conda.anaconda.org/conda-forge/win-64/vtk-9.6.2-py312h5ad3caf_5.conda + sha256: c7fcad74c5e4898d21487383108270bdf66f5f1ad8d5f0052268f16d493c433c + md5: afc0ab88fdddd99dad214f4a7704bbcc + depends: + - vtk-base >=9.6.2,<9.6.3.0a0 + - vtk-io-ffmpeg >=9.6.2,<9.6.3.0a0 + - libboost-devel + - liblzma-devel + - tbb-devel + - eigen + - expat + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - vtk-base >=9.6.2,<9.6.3.0a0 + size: 27889 + timestamp: 1783987235976 - conda: https://conda.anaconda.org/conda-forge/win-64/vtk-base-9.6.1-py312hb1e1833_2.conda sha256: bae80ff25d65ace05398283943474e9f5de55fa548b129d2d4dbf618b6a9b2ee md5: 873bb8f0ae1e13e4372974d0e7326c61 @@ -28958,6 +34572,71 @@ packages: - pkg:pypi/vtk?source=hash-mapping size: 56633863 timestamp: 1776479702685 +- conda: https://conda.anaconda.org/conda-forge/win-64/vtk-base-9.6.2-py312h3243052_5.conda + sha256: 9a6e001943dc0f6e71e632351351065b7f7a9429e09251d0d200546aac12e3d1 + md5: 7c1852eaade049e749a600ee17921849 + depends: + - python + - utfcpp + - nlohmann_json + - cli11 + - numpy + - wslink + - matplotlib-base >=2.0.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - hdf5 >=1.14.6,<1.14.7.0a0 + - gl2ps >=1.4.2,<1.4.3.0a0 + - python_abi 3.12.* *_cp312 + - jsoncpp >=1.9.8,<1.9.9.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libjpeg-turbo >=3.2.0,<4.0a0 + - fmt >=12.1.0,<12.2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libtiff >=4.7.2,<4.8.0a0 + - libpng >=1.6.58,<1.7.0a0 + - pugixml >=1.15,<1.16.0a0 + - libnetcdf >=4.10.1,<4.10.2.0a0 + - proj >=9.8.1,<9.9.0a0 + - qt6-main >=6.11.1,<7.0a0 + - eigen-abi >=5.0.1.100,<5.0.1.101.0a0 + - libogg >=1.3.5,<1.4.0a0 + - viskores >=1.1.1,<1.2.0a0 + - libtheora >=1.1.1,<1.2.0a0 + - libexpat >=2.8.1,<3.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - libzlib >=1.3.2,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - tbb >=2023.0.0 + - libsqlite >=3.53.3,<4.0a0 + - double-conversion >=3.4.0,<3.5.0a0 + constrains: + - libboost-headers >=1.90.0,<1.91.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - vtk-base >=9.6.2,<9.6.3.0a0 + size: 56694636 + timestamp: 1783987235976 +- conda: https://conda.anaconda.org/conda-forge/win-64/vtk-io-ffmpeg-9.6.2-py312h858e5c0_5.conda + sha256: c1a5f691ba693bbf843cd27b0437daebd1c5c0e273dbbfc24f81bbba012707b1 + md5: 78b7866c11c5c1ae57dadf600075680f + depends: + - vtk-base ==9.6.2 py312h3243052_5 + - ffmpeg + - python_abi 3.12.* *_cp312 + - ffmpeg >=8.1.2,<9.0a0 + license: BSD-3-Clause + license_family: BSD + run_exports: + weak: + - vtk-io-ffmpeg >=9.6.2,<9.6.3.0a0 + size: 99328 + timestamp: 1783987235976 - conda: https://conda.anaconda.org/conda-forge/win-64/watchdog-6.0.0-py312h209ec74_3.conda sha256: ce118b4eecd52c2b16664524b9a7a1c283e479ac1754587268df03ba732e15ac md5: bf3401836efb5fa8fc02fe1d19310fdd @@ -29069,6 +34748,9 @@ packages: license: GPL-2.0-or-later license_family: GPL purls: [] + run_exports: + weak: + - x264 >=1!164.3095,<1!165 size: 1041889 timestamp: 1660323726084 - conda: https://conda.anaconda.org/conda-forge/win-64/x265-3.5-h2d74725_3.tar.bz2 @@ -29080,6 +34762,9 @@ packages: license: GPL-2.0-or-later license_family: GPL purls: [] + run_exports: + weak: + - x265 >=3.5,<3.6.0a0 size: 5517425 timestamp: 1646611941216 - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libice-1.1.2-h0e40799_0.conda @@ -29131,6 +34816,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxau >=1.0.12,<2.0a0 size: 109246 timestamp: 1762977105140 - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.5-hba3369d_1.conda @@ -29143,6 +34831,9 @@ packages: license: MIT license_family: MIT purls: [] + run_exports: + weak: + - xorg-libxdmcp >=1.1.5,<2.0a0 size: 70691 timestamp: 1762977015220 - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxext-1.3.7-hba3369d_0.conda @@ -29215,6 +34906,7 @@ packages: license_family: Apache purls: - pkg:pypi/yarl?source=compressed-mapping + run_exports: {} size: 151702 timestamp: 1779246206775 - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h3a581c9_11.conda @@ -29241,8 +34933,26 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - zfp >=1.0.1,<2.0a0 size: 238850 timestamp: 1766549439919 +- conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.2-hfd05255_2.conda + sha256: ef408f85f664a4b9c9dac3cb2e36154d9baa15a88984ea800e11060e0f2394a1 + md5: 5187ecf958be3c39110fe691cbd6873e + depends: + - libzlib 1.3.2 hfd05255_2 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Zlib + license_family: Other + run_exports: + weak: + - libzlib >=1.3.2,<2.0a0 + size: 850351 + timestamp: 1774072891049 - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-ng-2.3.3-h0261ad2_1.conda sha256: 71332532332d13b5dbe57074ddcf82ae711bdc132affa5a2982a29ffa06dc234 md5: 46a21c0a4e65f1a135251fc7c8663f83 @@ -29253,6 +34963,9 @@ packages: license: Zlib license_family: Other purls: [] + run_exports: + weak: + - zlib-ng >=2.3.3,<2.4.0a0 size: 124542 timestamp: 1770167984883 - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda @@ -29266,6 +34979,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] + run_exports: + weak: + - zstd >=1.5.7,<1.6.0a0 size: 388453 timestamp: 1764777142545 - pypi: https://files.pythonhosted.org/packages/22/7b/1d679f4fced4ea94efadd17103856d8c565384f68382a1681264e46f5925/playwright-1.60.0-py3-none-manylinux1_x86_64.whl diff --git a/pixi.toml b/pixi.toml index dd0c41dae..6bf691d63 100644 --- a/pixi.toml +++ b/pixi.toml @@ -95,6 +95,7 @@ lint = { features = ["lint"], no-default-feature = true } profile = { features = ["profile", "prod", "pyocc", "desktop"], solve-group = "prod", no-default-feature = true } frontend = { features = ["frontend"], no-default-feature = true } jupyterlite = { features = ["jupyterlite"], no-default-feature = true } +ifc-validation = { features = ["ifc-validation"], no-default-feature = true } tests-core = { features = ["tests-core"], no-default-feature = true } test-pyodide = { features = ["test-pyodide"], no-default-feature = true } tests = { features = ["tests", "tests-core", "prod", "pyocc", "desktop"], solve-group = "prod", no-default-feature = true } @@ -130,6 +131,35 @@ meshio-bridge-tests = { features = ["meshio-bridge", "tests-core", "prod", "pyoc [feature.jupyterlite.dependencies] jupyterlite-core = "*" +# Official buildingSMART-aligned IFC validation, kept in a dedicated feature so +# its closure stays out of the main envs. Covers the OFFLINE, installable parts +# of the buildingSMART IFC Validation Service (https://github.com/buildingSMART/validate): +# * Syntax check — ifcopenshell.open + log_internal_cpp_errors +# * Schema check — ifcopenshell.validate (attribute/select/inverse rules) +# * Express/where-rules — ifcopenshell.validate(..., express_rules=True) +# * IDS check (opt-in) — ifctester, when an .ids is passed to the task +# The service's Gherkin normative rules (buildingSMART/ifc-gherkin-rules) are a +# git repo of behave feature files, not a pip package, and pull a heavy closure +# (Django/pyproj/rtree/shapely); they are deliberately NOT wired here — see +# scripts/validate_ifc.py for how to run them separately. +# +# ifctester (the optional IDS pillar) is PyPI-only and NOT declared here on +# purpose: it depends on `ifcopenshell`, and the conda-forge ifcopenshell ships +# no .dist-info, so pixi/uv can't see it as satisfying that pypi requirement and +# tries to pull a second (pypi) ifcopenshell — which has no wheel for this +# manylinux baseline, breaking the solve. The env therefore stays conda-only and +# resolves cleanly; enable IDS validation when needed with: +# pixi run -e ifc-validation python -m pip install --no-deps ifctester +[feature.ifc-validation.dependencies] +python = "3.12.*" +ifcopenshell = "*" +# ifcopenshell's generated EXPRESS where-rule modules `import pytest` (they use +# pytest.approx for numeric tolerance), so express_rules=True needs pytest present. +pytest = "*" + +[feature.ifc-validation.tasks] +ifc-validate = { cmd = "python scripts/validate_ifc.py", env = { "PYTHONPATH" = "$PIXI_PROJECT_ROOT/src" } } + [feature.meshio-bridge.dependencies] meshio = "*" @@ -153,7 +183,7 @@ occt = { version = "7.9.3.*", build = "novtk_*" } # for every other path (IFC/XML/FEM, occ-builtin fallback). Shares occt 7.9.3 with # pyocc (see above) so both coexist in one env. [feature.adacpp-runtime.dependencies] -ada-cpp = "0.16.1.*" +ada-cpp = "0.18.0.*" # ada-cpp CAD backend (AdacppBackend) — native CPython build of the # wasm-capable kernel. Brings its own occt 7.9.3 + gmsh + ifcopenshell, @@ -175,8 +205,10 @@ ada-cpp = "0.16.1.*" # imprint_planar_faces plus win-64 build and cad-API-generator encoding fixes. 0.16 # adds native OCC-free STEP/IFC→GLB with tessellation tracks and colour/curved-surface # fidelity; 0.16.1 fixes per-face STEP colours without --face-regions plus CLI positional -# args and pipeline validation. -ada-cpp = "0.16.1.*" +# args and pipeline validation. 0.18 adds AP242 mapped instancing, closed-B-spline +# tessellation fixes, and the NGEOM record-stream emitters (stream_ngeom_to_step/ifc/glb/mesh +# + ngeom_to_ifc_body_spf) — adapy's native Genie-XML export legs call these directly. +ada-cpp = "0.18.0.*" [feature.adacpp.activation.env] ADAPY_CAD_BACKEND = "adacpp" diff --git a/scripts/validate_ifc.py b/scripts/validate_ifc.py new file mode 100644 index 000000000..2971a5965 --- /dev/null +++ b/scripts/validate_ifc.py @@ -0,0 +1,157 @@ +"""Official buildingSMART-aligned IFC validation for adapy-produced files. + +Runs the offline, pip/conda-installable parts of the buildingSMART IFC +Validation Service (https://github.com/buildingSMART/validate) against one or +more IFC files (or directories of IFCs): + +* **Syntax check** — the file is opened with ``ifcopenshell.open`` and any + low-level STEP/parse errors reported by the IfcOpenShell C++ core are + captured (mirrors the service's "Syntax Check"). +* **Schema check** — attribute cardinality/type/selects and inverse + attributes are checked by ``ifcopenshell.validate`` (mirrors "Schema + Check"). +* **Express/where-rules** — the schema's WHERE rules and global rules are + evaluated with ``express_rules=True`` (part of "Schema Check"). +* **IDS check** (optional) — when ``--ids FILE.ids`` is supplied, ``ifctester`` + validates the model against an Information Delivery Specification. + +The service's third pillar, the **Gherkin normative rules** +(``buildingSMART/ifc-gherkin-rules``), is intentionally NOT run here: it is a +git repository of behave feature files rather than a pip package, and it drags +in a heavy closure (Django, pyproj, rtree, shapely, ...). See the module notes +and the accompanying task documentation for how to run it separately. + +Exit code is non-zero if any file produces validation errors, so this is +usable as a CI gate. Run via ``pixi run -e ifc-validation ifc-validate ``. +""" + +from __future__ import annotations + +import argparse +import pathlib +import sys +from typing import Iterable + +import ifcopenshell +import ifcopenshell.validate + + +def _iter_ifc_files(paths: Iterable[str]) -> list[pathlib.Path]: + out: list[pathlib.Path] = [] + for p in paths: + path = pathlib.Path(p) + if path.is_dir(): + out.extend(sorted(path.rglob("*.ifc"))) + else: + out.append(path) + # de-duplicate while preserving order + seen: set[pathlib.Path] = set() + uniq: list[pathlib.Path] = [] + for path in out: + rp = path.resolve() + if rp not in seen: + seen.add(rp) + uniq.append(path) + return uniq + + +def _validate_file(path: pathlib.Path, express_rules: bool) -> list[dict]: + """Return a list of error statements ({'level','message',...}) for one IFC.""" + logger = ifcopenshell.validate.json_logger() + try: + f = ifcopenshell.open(str(path)) + except Exception as exc: # syntax / unreadable file + return [{"level": "error", "message": f"Could not open file: {exc}", "type": "syntax"}] + + # Capture low-level C++ parse/geometry errors (syntax pillar). + try: + ifcopenshell.validate.log_internal_cpp_errors(f, str(path), logger) + except Exception as exc: # pragma: no cover - defensive + logger.error("internal error running syntax check: %s", exc) + + # Schema + express/where-rule check. + try: + ifcopenshell.validate.validate(f, logger, express_rules=express_rules) + except Exception as exc: # pragma: no cover - defensive + logger.error("internal error running schema check: %s", exc) + + return [s for s in logger.statements if s.get("level") == "error"] + + +def _validate_ids(path: pathlib.Path, ids_path: pathlib.Path) -> tuple[bool, str]: + """Validate one IFC against an IDS. Returns (passed, summary).""" + try: + import ifctester + import ifctester.ids + except ModuleNotFoundError as exc: + raise RuntimeError( + "ifctester is not installed. Install it (IDS pillar is optional) with:\n" + " pixi run -e ifc-validation python -m pip install --no-deps ifctester" + ) from exc + + ids = ifctester.ids.open(str(ids_path)) + model = ifcopenshell.open(str(path)) + ids.validate(model) + total = failed = 0 + for spec in ids.specifications: + for req in spec.requirements: + for res in getattr(req, "failed_entities", []): + failed += 1 + total += 1 + passed = all(spec.status is not False for spec in ids.specifications) + n_failed_specs = sum(1 for spec in ids.specifications if spec.status is False) + return passed, f"{n_failed_specs} of {len(ids.specifications)} specification(s) failed" + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("paths", nargs="+", help="IFC file(s) and/or directory(ies) to validate.") + ap.add_argument( + "--no-rules", + action="store_true", + help="Skip express/where-rules; run syntax + schema attribute checks only (faster).", + ) + ap.add_argument("--ids", default=None, help="Optional IDS file to additionally validate against (ifctester).") + ap.add_argument("--max-errors", type=int, default=10, help="Max error lines to print per file (default 10).") + args = ap.parse_args(argv) + + files = _iter_ifc_files(args.paths) + if not files: + print("No .ifc files found in the given path(s).", file=sys.stderr) + return 2 + + ids_path = pathlib.Path(args.ids) if args.ids else None + any_fail = False + print(f"Validating {len(files)} IFC file(s) with ifcopenshell {ifcopenshell.version}\n") + + for path in files: + errors = _validate_file(path, express_rules=not args.no_rules) + status = "PASS" if not errors else "FAIL" + if errors: + any_fail = True + print(f"[{status}] {path} ({len(errors)} error(s))") + for stmt in errors[: args.max_errors]: + msg = " ".join(str(stmt.get("message", "")).split()) + attr = stmt.get("attribute") or stmt.get("type") or "" + head = f" - {attr}: " if attr else " - " + print(f"{head}{msg[:400]}") + if len(errors) > args.max_errors: + print(f" ... {len(errors) - args.max_errors} more error(s) suppressed") + + if ids_path is not None: + try: + ok, summary = _validate_ids(path, ids_path) + except Exception as exc: + ok, summary = False, f"ifctester error: {exc}" + print(f" [IDS {'PASS' if ok else 'FAIL'}] {summary}") + if not ok: + any_fail = True + print() + + print("=" * 60) + print("RESULT:", "FAIL" if any_fail else "PASS", f"({len(files)} file(s) checked)") + return 1 if any_fail else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/ada/api/containers/materials.py b/src/ada/api/containers/materials.py index 8f170e956..0fab4c686 100644 --- a/src/ada/api/containers/materials.py +++ b/src/ada/api/containers/materials.py @@ -180,7 +180,7 @@ def add(self, material: Material) -> Material: # calls funneling into the same ``existing`` material. # Rebuilding ``set(existing_refs)`` on every call was the # dominant O(N²) cost (5.8 BILLION FemSection.__hash__ - # calls on the JackethybridFEM → Genie XML conversion). + # calls on a jacket FEM model → Genie XML conversion). # Cache an ``id()``-keyed set on the material itself so: # * membership tests are pointer-equality, no __hash__ # overhead on FemSection / Beam / Plate refs diff --git a/src/ada/api/curves.py b/src/ada/api/curves.py index fe1821e68..f21242e9c 100644 --- a/src/ada/api/curves.py +++ b/src/ada/api/curves.py @@ -1,5 +1,6 @@ from __future__ import annotations +from dataclasses import dataclass from typing import TYPE_CHECKING, Iterable, Optional import numpy as np @@ -26,7 +27,38 @@ if TYPE_CHECKING: from ada import Beam from ada.cad import ShapeHandle - from ada.geom.curves import ArcLine, Edge, IndexedPolyCurve + from ada.geom.curves import ArcLine, BSplineCurveWithKnots, Edge, IndexedPolyCurve + + +@dataclass +class PlateEdgeCurve: + """Base: an analytic curve carried on ONE boundary edge of a ``CurvePoly2d``, between corners ``a``/``b``. + + A reader (e.g. the ACIS/SAT plate reader) emits these — as the concrete :class:`ArcEdge` / + :class:`SplineEdge` — so a curved plate boundary stays analytic: :meth:`CurvePoly2d.build_edge_segments` + turns ordered corners + these specs into the real segment list (arc/spline where a spec's ``a``/``b`` + match a corner pair, else a line), which ``Plate.from_segments`` carries verbatim — instead of the + reader sampling the curve into extra outline points at read time (slow, and lower fidelity: it reaches + IFC/STEP as a polyline). + """ + + a: tuple[float, float, float] + b: tuple[float, float, float] + + +@dataclass +class ArcEdge(PlateEdgeCurve): + """A circle/ellipse boundary edge. ``midpoint`` is the point on the arc halfway between ``a`` and + ``b`` — the only datum OCC/NGEOM/IFC/STEP need to reconstruct the arc.""" + + midpoint: tuple[float, float, float] + + +@dataclass +class SplineEdge(PlateEdgeCurve): + """A B-spline boundary edge carrying the analytic ``ada.geom.curves.BSplineCurveWithKnots``.""" + + curve: BSplineCurveWithKnots class CurveRevolve: @@ -226,7 +258,7 @@ def _points_to_segments(self, local_points2d, tol=1e-3): self._segments = seg_list2d self._segments3d = seg_list3d - self._seg_global_points, self._seg_index = segments_to_indexed_lists(seg_list3d) + self._seg_global_points, self._seg_index = segments_to_indexed_lists(self._segments3d) self._nodes = [ Node(p, r=self.radiis[i]) if i in self.radiis.keys() else Node(p) for i, p in enumerate(self._points3d) ] @@ -303,21 +335,17 @@ def curve_geom(self, use_3d_segments=False) -> IndexedPolyCurve | Edge | ArcLine poly_segments = self.segments3d if use_3d_segments else self.segments - if len(poly_segments) == 1: - seg = poly_segments[0] + def _to_geom(seg): if isinstance(seg, ArcSegment): return ArcLine(seg.p1, seg.midpoint, seg.p2) - else: - return Edge(seg.p1, seg.p2) + if isinstance(seg, SplineSegment): + return seg.curve_geom() # BSplineCurveWithKnots (2D-local or 3D per the segment) + return Edge(seg.p1, seg.p2) - segments = [] - for seg in poly_segments: - if isinstance(seg, ArcSegment): - segments.append(ArcLine(seg.p1, seg.midpoint, seg.p2)) - else: - segments.append(Edge(seg.p1, seg.p2)) + if len(poly_segments) == 1: + return _to_geom(poly_segments[0]) - return IndexedPolyCurve(segments) + return IndexedPolyCurve([_to_geom(seg) for seg in poly_segments]) def occ_wire(self) -> ShapeHandle: from ada.occ.utils import make_wire @@ -518,6 +546,90 @@ def from_fem_shells_batch(cls, pts: np.ndarray, parent=None, tol=1e-3) -> list[C out.append(self) return out + @staticmethod + def build_edge_segments(points3d, edge_curves=None) -> list[LineSegment]: + """Ordered, closed loop of 3D segments from ordered corner points + optional ``PlateEdgeCurve``. + + Consecutive corners form each edge; an edge whose endpoints match a spec becomes an + ``ArcSegment`` (circle/ellipse) or ``SplineSegment``, otherwise a ``LineSegment``. Endpoint + match is winding-agnostic; an unmatched spec (e.g. a corner pruned as collinear) just leaves + that edge straight. Feed the result to :meth:`from_segments` / ``Plate.from_segments``. + """ + pts = [Point(np.asarray(p, dtype=float)[:3]) for p in points3d] + n = len(pts) + spec_by_key = {_endpoint_key(s.a, s.b): s for s in (edge_curves or [])} + segs: list[LineSegment] = [] + for i in range(n): + a, b = pts[i], pts[(i + 1) % n] + spec = spec_by_key.get(_endpoint_key(a, b)) + if isinstance(spec, ArcEdge): + segs.append(ArcSegment(a, b, midpoint=Point(np.asarray(spec.midpoint, dtype=float)))) + elif isinstance(spec, SplineEdge): + segs.append(SplineSegment(a, b, curve=spec.curve)) + else: + segs.append(LineSegment(a, b)) + return segs + + @classmethod + def from_segments(cls, segments, tol=1e-3, parent=None, xdir=None, flip_n=False) -> CurvePoly2d: + """Construct directly from an ordered, closed loop of 3D segments (line/arc/spline). + + Unlike :meth:`from_3d_points` this neither samples nor rebuilds the boundary via + ``build_polycurve``: each segment is carried through to both the 3D and the projected 2D + outline as-is (arc midpoints, spline curves preserved), so analytic edges survive to IFC/STEP + and are discretized only downstream at tessellation. Orientation is derived from the corner + points exactly like :meth:`from_3d_points` (``Placement.from_co_linear_points``), so a + segments-built plate sits in the same frame a points-built one would. + """ + segments = list(segments) + if len(segments) < 3: + raise ValueError("At least three segments are required to define a plate outline.") + + points3d = np.array([np.asarray(seg.p1, dtype=float)[:3] for seg in segments]) + place = Placement.from_co_linear_points(points3d, xdir=xdir, flip_n=flip_n) + points2d_arr = place.transform_global_points_to_local(points3d) + points2d = [Point(p[0], p[1]) for p in points2d_arr] + points3d_pts = [Point(p) for p in points3d] + + def _to2d(p) -> Point: + q = place.transform_global_points_to_local(np.array([np.asarray(p, dtype=float)[:3]]))[0] + return Point(q[0], q[1]) + + segs2d: list[LineSegment] = [] + for seg in segments: + if isinstance(seg, ArcSegment): + segs2d.append(ArcSegment(_to2d(seg.p1), _to2d(seg.p2), midpoint=_to2d(seg.midpoint))) + elif isinstance(seg, SplineSegment): + # The extruded profile is 2D in the plate's local frame, so the B-spline's control + # points must be projected into that frame too (affine -> knots/degree/weights unchanged). + p1_2d, p2_2d = _to2d(seg.p1), _to2d(seg.p2) + curve2d = _project_bspline_2d(seg.curve, place) + # Snap the endpoints to the exact corner vertices so the outline wire closes: a plate + # boundary spline is clamped (end knot mult = degree+1), so its curve endpoints ARE its + # first/last control points, and the adjacent line edges join at these corners. + curve2d.control_points_list[0] = p1_2d + curve2d.control_points_list[-1] = p2_2d + segs2d.append(SplineSegment(p1_2d, p2_2d, curve=curve2d)) + else: + segs2d.append(LineSegment(_to2d(seg.p1), _to2d(seg.p2))) + + seg_global_points, seg_index = segments_to_indexed_lists(segments) + + self = cls.__new__(cls) + self._tol = tol + self._parent = parent + self._orientation = place + self._placement = Placement() + self._radiis = {} + self._points2d = points2d + self._points3d = points3d_pts + self._segments = segs2d + self._segments3d = segments + self._seg_global_points = seg_global_points + self._seg_index = seg_index + self._nodes = [Node(p) for p in points3d_pts] + return self + def get_face_geom(self) -> ArbitraryProfileDef: outer_curve = self.curve_geom() return ArbitraryProfileDef(ProfileType.AREA, outer_curve, []) @@ -774,3 +886,66 @@ def curve_geom(self) -> ArcLine: def __repr__(self): return f"ArcSegment({self.p1}, {self.midpoint}, {self.p2})" + + +class SplineSegment(LineSegment): + """A boundary edge that follows an analytic B-spline (``ada.geom.curves.BSplineCurveWithKnots``). + + Closes the gap the ``CurvePoly2d`` docstring notes: the outline had only line and arc segments, so + a spline plate edge had nowhere analytic to live and was sampled into straight outline points at + read time. The spline is carried here verbatim; discretization is a downstream (tessellation) + concern via :meth:`sample`, so the analytic form survives to IFC/STEP export. + """ + + def __init__(self, p1, p2, curve: BSplineCurveWithKnots = None, edge_geom=None, placement: Placement = None): + super().__init__(p1, p2, edge_geom=edge_geom, placement=placement) + self._curve = curve + + @property + def curve(self) -> BSplineCurveWithKnots: + return self._curve + + def curve_geom(self) -> BSplineCurveWithKnots: + return self._curve + + def sample(self, n: int) -> list[Point]: + """Discretize the spline into ``n`` points along the curve (endpoints included).""" + return [Point(p) for p in self._curve.sample(n)] + + def __repr__(self): + return f"SplineSegment({self.p1}, {self.p2})" + + +def _project_bspline_2d(curve, place): + """Project a ``BSplineCurveWithKnots``'s control points into ``place``'s local frame (as in-plane + 3D points ``(x, y, 0)``), returning a same-kind curve with knots/degree/weights untouched — the + profile a plate extrudes is 2D-local, so its analytic B-spline edge must live there too.""" + from ada.geom.curves import BSplineCurveWithKnots, RationalBSplineCurveWithKnots + + cps3d = np.array([np.asarray(p, dtype=float)[:3] for p in curve.control_points_list]) + local = place.transform_global_points_to_local(cps3d) + # 2D control points, matching the 2D line/arc points of the extruded profile (a 3D (x,y,0) here + # would make the outline point list inhomogeneous for get_unique / point3d). + cps2d = [Point(float(p[0]), float(p[1])) for p in local] + common = dict( + degree=curve.degree, + control_points_list=cps2d, + curve_form=curve.curve_form, + closed_curve=curve.closed_curve, + self_intersect=curve.self_intersect, + knot_multiplicities=curve.knot_multiplicities, + knots=curve.knots, + knot_spec=curve.knot_spec, + ) + if isinstance(curve, RationalBSplineCurveWithKnots): + return RationalBSplineCurveWithKnots(weights_data=curve.weights_data, **common) + return BSplineCurveWithKnots(**common) + + +def _endpoint_key(a, b, ndigits: int = 6): + """Order-independent key for an edge's two endpoints, so a ``PlateEdgeCurve`` spec can be matched to + the corner pair that bounds it regardless of winding. Rounded to microns — plate corners are far + enough apart that no two distinct corners collide.""" + ka = tuple(round(float(v), ndigits) for v in np.asarray(a, dtype=float)[:3]) + kb = tuple(round(float(v), ndigits) for v in np.asarray(b, dtype=float)[:3]) + return frozenset((ka, kb)) diff --git a/src/ada/api/loft.py b/src/ada/api/loft.py index 91bd6086f..eefdcb351 100644 --- a/src/ada/api/loft.py +++ b/src/ada/api/loft.py @@ -27,7 +27,7 @@ # / wire_points), so it works under adacpp as well as pythonocc with no OCC import # anywhere in this module. The transform helpers compose a 4x4 affine matrix and # hand it to ``CadBackend.transform``; ``planar_face_from_poly_loop`` builds a -# ``CurveBoundedPlane`` through ``CadBackend.build``. See dap plan/v3 Phase 1/2. +# ``CurveBoundedPlane`` through ``CadBackend.build``. See the internal design notes Phase 1/2. if TYPE_CHECKING: from ada.api.spatial.part import Part from ada.cad import ShapeHandle diff --git a/src/ada/api/plates/base_pl.py b/src/ada/api/plates/base_pl.py index eba992b9e..ca569155c 100644 --- a/src/ada/api/plates/base_pl.py +++ b/src/ada/api/plates/base_pl.py @@ -9,7 +9,7 @@ from ada.api.nodes import Node from ada.base.physical_objects import BackendGeom from ada.base.units import Units -from ada.config import Config +from ada.config import Config, logger from ada.core.vector_utils import poly2d_center_of_gravity from ada.geom import Geometry from ada.geom.direction import Direction @@ -101,6 +101,18 @@ def from_3d_points( poly = CurvePoly2d.from_3d_points(points, xdir=xdir, flip_n=flip_normal, **kwargs) return Plate(name, poly, t, mat=mat, color=color, metadata=metadata, **kwargs) + @staticmethod + def from_segments(name, segments, t, mat="S420", color=None, metadata=None, flip_normal=False, **kwargs) -> Plate: + """Build a plate whose outline is an ordered list of ``LineSegment``/``ArcSegment``/``SplineSegment``. + + Use this instead of ``from_3d_points`` when the boundary is genuinely a mix of line and analytic + curve edges (e.g. an ACIS/SAT plate with a circular or spline boundary): the segments are carried + verbatim rather than sampled into a point cloud and rebuilt, so arcs/splines survive analytically + into IFC/STEP and are discretized only downstream at tessellation. + """ + poly = CurvePoly2d.from_segments(segments, flip_n=flip_normal, **kwargs) + return Plate(name, poly, t, mat=mat, color=color, metadata=metadata, **kwargs) + @staticmethod def from_fem_shell( name, points, t, mat="S420", color=None, metadata=None, parent=None, detached=False, **kwargs @@ -196,6 +208,14 @@ def solid_geom(self) -> Geometry: origin = place_abs.origin + origin + # Global thickness anchor: offset the extrusion BASE along the plate normal + # ("as_is" = 0 keeps the historical output byte-identical). + from ada.geom.primitive_brep import thickness_anchor_base_offset + + base_off = thickness_anchor_base_offset(Config().geom_thickness_anchor, self.t) + if base_off: + origin = Point(*(np.asarray(origin, dtype=float) + base_off * np.asarray(normal, dtype=float))) + # Origin location is already included in the outer_curve definition place = Axis2Placement3D(location=origin, axis=normal, ref_direction=xdir) solid = geo_so.ExtrudedAreaSolid(profile, place, self.t, Direction(0, 0, 1)) @@ -410,6 +430,7 @@ def __init__( # (loft tool / gxml advanced faces render as the surface). self._extrude_as_solid = bool(extrude_as_solid) self._nodes_cache: list[Node] | None = None + self._thick_shell_cache = None self._bbox = None self._hash = None # Optional raw-OCC-face override; populated by @@ -451,6 +472,7 @@ def from_occ_face(cls, name: str, occ_face, t: float, mat: str | Material = "S42 instance._t = t instance._extrude_as_solid = False instance._nodes_cache = None + instance._thick_shell_cache = None instance._bbox = None instance._hash = None instance._occ_face_override = occ_face @@ -530,7 +552,9 @@ def nodes(self) -> list[Node]: shape = self._occ_face_override if shape is None: - shape = active_backend().build(self.solid_geom()) + # Always the bare face — boundary nodes are the shell's outer wire + # regardless of whether the SOLID representation is thickened. + shape = active_backend().build(self.geom) nodes = [Node(p) for p in boundary_points(shape)] except Exception: nodes = [] @@ -542,7 +566,74 @@ def bbox(self) -> BoundingBox: self._bbox = BoundingBox(self) return self._bbox + def gxml_sense_flag(self) -> bool: + """The gxml ``curved_shell`` sense flag (does the desired shell normal agree + with the wrapped face's own normal). Authored data preserved by the gxml + reader in ``metadata["props"]["gxml_sense_flag"]``; defaults to True.""" + props = self.metadata.get("props", {}) if isinstance(self.metadata, dict) else {} + return bool(props.get("gxml_sense_flag", True)) + + def thickness_direction(self) -> Direction | None: + """Sense-corrected unit thickness direction: the wrapped face's own oriented + normal (probed kernel-free at a representative parameter), flipped when the + gxml sense flag is false. None when the surface has no kernel-free probe.""" + if self._geom is None: + return None + from ada.geom import surfaces as geo_su + from ada.geom.primitive_brep import face_mid_normal + + geometry = self._geom.geometry + if not isinstance(geometry, (geo_su.AdvancedFace, geo_su.FaceSurface)): + return None + n = face_mid_normal(geometry) + if n is None: + return None + if not self.gxml_sense_flag(): + n = (-n[0], -n[1], -n[2]) + return Direction(*n) + + def _thick_shell_geom(self) -> Geometry | None: + """Thickness-``t`` analytic ClosedShell Geometry (kernel-free, cached), or None + when thickening is disabled / not buildable for this face.""" + cfg = Config() + if not cfg.geom_thicken_curved_shells or not self.t or self._geom is None: + return None + anchor = cfg.geom_thickness_anchor + key = (anchor, float(self.t)) + cached = self._thick_shell_cache + if cached is not None and cached[0] == key: + return cached[1] + + from ada.geom import surfaces as geo_su + from ada.geom.primitive_brep import face_to_thick_shell + + shell = None + geometry = self._geom.geometry + if isinstance(geometry, (geo_su.AdvancedFace, geo_su.FaceSurface)): + direction = self.thickness_direction() + if direction is not None: + try: + shell = face_to_thick_shell( + geometry, + direction, + self.t, + anchor=anchor, + direction_agrees_with_face=self.gxml_sense_flag(), + ) + except Exception: # noqa: BLE001 - malformed face data -> bare-face fallback + shell = None + result = Geometry(self.guid, shell, self.color) if shell is not None else None + self._thick_shell_cache = (key, result) + return result + def solid_geom(self) -> Geometry: + """The plate's SOLID geometry: a thickness-``t`` analytic ClosedShell (built + kernel-free by :func:`ada.geom.primitive_brep.face_to_thick_shell`, honouring + ``Config().geom_thickness_anchor``) when ``Config().geom_thicken_curved_shells`` + is on and the face is thickenable — else the bare face Geometry as before.""" + thick = self._thick_shell_geom() + if thick is not None: + return thick return self.geom def _face_occ(self) -> ShapeHandle: @@ -551,15 +642,25 @@ def _face_occ(self) -> ShapeHandle: return self._occ_face_override from ada.cad import active_backend - return active_backend().build(self.solid_geom()) + return active_backend().build(self.geom) def solid_occ(self) -> ShapeHandle: # Reconstructed panels opt into a true thickness extrusion for the SOLID - # representation (so STEP/SOLID-repr export is a real solid); everything - # else keeps the historical bare-face behaviour. No recursion: + # representation (so STEP/SOLID-repr export is a real solid). No recursion: # extruded_solid_occ builds the face via _face_occ, not solid_occ. if self._extrude_as_solid and self.t: return self.extruded_solid_occ() + # Thickened curved shells build the SAME ada.geom ClosedShell through the + # backend's normal geom conversion (never the OCC prism, which stays for the + # FEM surface-reconstruction opt-in above). Bare face on any build failure. + thick = self._thick_shell_geom() + if thick is not None: + try: + from ada.cad import active_backend + + return active_backend().build(thick) + except Exception as e: # noqa: BLE001 - backend can't build this shell + logger.debug("PlateCurved %r: thick-shell backend build failed (%s); using bare face", self.name, e) return self._face_occ() def extruded_solid_occ(self) -> ShapeHandle: diff --git a/src/ada/api/spatial/assembly.py b/src/ada/api/spatial/assembly.py index ac4869d66..73898c783 100644 --- a/src/ada/api/spatial/assembly.py +++ b/src/ada/api/spatial/assembly.py @@ -402,13 +402,23 @@ def to_genie_xml( the object-free vectorized FEM-shell face engine — streaming path only. """ if merge_strategy is not None: - if embed_sat: + from ada.cadit.gxml.write.stream_xml import _analytic_merge_strategy + + is_analytic = _analytic_merge_strategy(merge_strategy) is not None + if is_analytic: + # The analytic strategies (surface/panel/cylinder/analytic) author + # their recognised cylinder/panel patches into the SAT body and + # reference them as — so embed_sat is not only + # compatible, it is required for the curved faces. Default it on. + if embed_sat is None: + embed_sat = True + elif embed_sat: raise ValueError( "to_genie_xml: embed_sat=True is incompatible with merge_strategy=" f"{merge_strategy!r} — the SAT body is built from Plate objects, which the " "merge_strategy face source never materialises. Pass one or the other." ) - if embed_sat is None: + elif embed_sat is None: embed_sat = False logger.info("to_genie_xml: merge_strategy set, so plates are written as polygons (no SAT)") elif embed_sat is None: diff --git a/src/ada/api/spatial/part.py b/src/ada/api/spatial/part.py index b3b536337..5c26e14be 100644 --- a/src/ada/api/spatial/part.py +++ b/src/ada/api/spatial/part.py @@ -995,10 +995,7 @@ def iter_objects_from_fem( materials so the streamed plates share the exporter's material identity (else a post-consolidation ``materials.add`` would mint a fresh copy). """ - from ada.fem.formats.utils import ( - convert_shell_elem_to_plates, - line_elem_to_beam, - ) + from ada.fem.formats.utils import line_elem_to_beam if self.fem is None: return @@ -1008,12 +1005,33 @@ def iter_objects_from_fem( if not plates: return if merge_strategy is None: - mat_dict: dict = {} if mat_cache is None else mat_cache - for elem in self.fem.elements.shell: - yield from convert_shell_elem_to_plates(elem, self, mat_dict, detached=detached) + yield from self._iter_scalar_plates_from_fem(detached, mat_cache) else: yield from self._iter_merged_plates_from_fem(merge_strategy, detached, mat_cache) + def _iter_scalar_plates_from_fem(self, detached: bool, mat_cache: dict | None, chunk: int = 2048): + """Vectorised 1:1 element→plate stream (``merge_strategy=None``): gather one + shell element's plate entries at a time and batch-build the CurvePoly2d per + polygon arity in bounded chunks (never the whole set resident), yielding in + element order. Bitwise-identical plate outlines to the scalar per-element + ``convert_shell_elem_to_plates`` path — the orientation math is just run once + over arrays (:meth:`CurvePoly2d.from_fem_shells_batch`) instead of per element, + cutting the FEM→concept CPU cost dominated by ``from_fem_shell``.""" + from ada.fem.formats.utils import ( + build_plates_from_entries, + shell_elem_to_plate_entries, + ) + + mat_dict: dict = {} if mat_cache is None else mat_cache + buf: list = [] + for elem in self.fem.elements.shell: + buf.extend(shell_elem_to_plate_entries(elem, self, mat_dict)) + if len(buf) >= chunk: + yield from build_plates_from_entries(buf, self, detached=detached) + buf = [] + if buf: + yield from build_plates_from_entries(buf, self, detached=detached) + def _iter_merged_plates_from_fem(self, merge_strategy, detached: bool, mat_cache: dict | None, chunk: int = 2048): """Wrap the object-free merged :class:`FaceData` records in transient Plates. @@ -1221,7 +1239,7 @@ def consolidate_materials(self, include_self=True): # FEM → IFC / XML exports: every call re-scanned the target # material's (growing, tens-of-thousands-long) ``refs`` list, # turning consolidation into an O(sections × refs) blow-up - # (~7.4 billion id() calls, ~11 min on Ship1T1.FEM with 66k + # (~7.4 billion id() calls, ~11 min on a large ship FEM with 66k # sections sharing 2 materials). One add per distinct material is # identical in effect — repeat adds of an already-registered # material are no-ops. @@ -1275,7 +1293,11 @@ def get_all_parts_in_assembly(self, include_self=False, by_type=None) -> list[Pa parent = self.get_assembly() list_of_ps = [] self._flatten_list_of_subparts(parent, list_of_ps) - if include_self: + # The flatten walks the whole tree from the assembly root, so a non-root + # ``self`` is already in the list — appending it again on ``include_self`` + # duplicates it (and made every FEM consumer that iterates these parts + # process that part's mesh twice). Only add it when the walk didn't. + if include_self and not any(p is self for p in list_of_ps): list_of_ps += [self] if by_type is not None: @@ -1711,6 +1733,17 @@ def to_stp( fuse_fem: bool = True, merge_strategy=None, ): + # The "native" writer serializes each object's solid_geom() to an NGEOM + # blob and emits AP242 through adacpp's C++ record-stream writer + # (stream_ngeom_to_step) — no OCC and no per-entity Python text writer + # (~ms/face -> ~µs/face). Full ada.geom coverage (B-rep shells incl. + # thickened curved shells, extrusions, revolves, sweeps, booleans); + # raises NativeExportUnsupported if adacpp is absent or any object + # fails to serialize, so callers can fall back to "occ"/"stream". + if writer == "native": + from ada.cadit.ngeom.export import native_to_stp + + return native_to_stp(self, destination_file) # The "stream" writer authors AP242 B-rep text directly from parametric # geometry without building any OCC/adacpp shapes — constant memory, so # it does not OOM on large FEM models the way the OCC XCAF path does. It @@ -1731,7 +1764,7 @@ def to_stp( merge_strategy=merge_strategy, ) if writer != "occ": - raise ValueError(f"unknown writer {writer!r}; expected 'occ' or 'stream'") + raise ValueError(f"unknown writer {writer!r}; expected 'occ', 'stream' or 'native'") from ada.cad.doc import active_doc_backend from ada.occ.geom.cache import invalidate diff --git a/src/ada/base/physical_objects.py b/src/ada/base/physical_objects.py index c58d00969..a731ec0bb 100644 --- a/src/ada/base/physical_objects.py +++ b/src/ada/base/physical_objects.py @@ -275,8 +275,8 @@ def solid_occ(self) -> ShapeHandle: clash, bbox, FEM all consume it). Treat it as opaque — its concrete type is backend-private (a ``TopoDS_Solid``/``TopoDS_Compound`` under the default OCC backend). Operate on it via the CAD backend verbs, - not by importing kernel types. See dap plan/v3 - notes_occ_backend_abstraction (Phase 2).""" + not by importing kernel types. See the internal design notes + the internal design notes (Phase 2).""" raise NotImplementedError() def shell_occ(self) -> ShapeHandle: diff --git a/src/ada/cad/__init__.py b/src/ada/cad/__init__.py index 20533e5f3..6c0692273 100644 --- a/src/ada/cad/__init__.py +++ b/src/ada/cad/__init__.py @@ -353,7 +353,7 @@ def build(self, geometry: "Geometry") -> ShapeHandle: # where pythonocc does not exist). The ada.geom construction funnel is # being ported to adacpp C++ incrementally; types not yet ported raise # NotImplementedError rather than borrowing pythonocc. End goal: full - # parity with OccBackend. See dap plan/v3 Phase 7. + # parity with OccBackend. See the internal design notes Phase 7. import ada.geom.curves as gcu import ada.geom.solids as so import ada.geom.surfaces as su @@ -773,6 +773,12 @@ def _encode_curve(self, curve) -> list[list[float]]: for seg in curve.segments: if isinstance(seg, cu.ArcLine): edges.append([1.0, *self._xyz(seg.start), *self._xyz(seg.midpoint), *self._xyz(seg.end)]) + elif isinstance(seg, cu.BSplineCurveWithKnots): + # adacpp's edge-record vocabulary is line/arc/circle — an analytic B-spline + # boundary edge is sampled here, at the tessellation boundary (same policy as + # the NGEOM serializer's _loop_points_3d). + pts = seg.sample(max(16, int(seg.degree) * 8)) + edges.extend([0.0, *self._xyz(a), *self._xyz(b)] for a, b in zip(pts[:-1], pts[1:])) else: # Edge — straight line edges.append([0.0, *self._xyz(seg.start), *self._xyz(seg.end)]) return edges @@ -1427,7 +1433,7 @@ def extrude_face_along_normal(self, face: ShapeHandle, thickness: float) -> Shap def build_bspline_advanced_face_from_grid(self, grid: "list", tol: float): # Native grid→NURBS fit not yet ported to adacpp. Raising here makes the # surface-reconstruction caller fall back to flat plates (the safe - # default) rather than borrowing pythonocc. See dap plan/v3 Phase 7. + # default) rather than borrowing pythonocc. See the internal design notes Phase 7. raise NotImplementedError("adacpp.cad grid→bspline surface fit is not available yet") def face_to_advanced_face(self, shape: ShapeHandle): diff --git a/src/ada/cad/doc.py b/src/ada/cad/doc.py index f3e329d75..d9eb628c6 100644 --- a/src/ada/cad/doc.py +++ b/src/ada/cad/doc.py @@ -14,7 +14,7 @@ ``write_glb_bytes`` + client-side scene assembly path instead. Use :func:`require_capability` to gate the OCAF-only operations explicitly. -See dap plan/v3 notes_occ_backend_abstraction (Phase 6). +See the internal design notes (Phase 6). """ from __future__ import annotations diff --git a/src/ada/cadit/gxml/read/helpers.py b/src/ada/cadit/gxml/read/helpers.py index d557792ba..66a1db11e 100644 --- a/src/ada/cadit/gxml/read/helpers.py +++ b/src/ada/cadit/gxml/read/helpers.py @@ -85,27 +85,66 @@ def _project_to_best_fit_plane(pts): wrong" — the failure mode the user reports for the swept-surface fallbacks (exppc → exactcur / parcur / unresolvable ref chains). """ + plane = _fit_best_fit_plane(pts) + if plane is None: + return list(pts) + return _project_onto_plane(pts, plane) + + +def _fit_best_fit_plane(pts): + """``(centroid, unit_normal)`` of the SVD best-fit plane of ``pts``, or ``None`` if degenerate.""" import numpy as _np arr = _np.asarray([list(p)[:3] for p in pts], dtype=float) if arr.shape[0] < 3: - return list(pts) + return None centroid = arr.mean(axis=0) centred = arr - centroid # SVD: smallest singular value corresponds to the plane normal. try: _, _, vt = _np.linalg.svd(centred, full_matrices=False) except _np.linalg.LinAlgError: - return list(pts) + return None normal = vt[-1] n_len = float(_np.linalg.norm(normal)) if n_len < 1e-12: - return list(pts) - normal = normal / n_len - # Project: p_proj = p - dot(p - centroid, n) * n - offsets = (centred @ normal)[:, None] * normal - projected = arr - offsets - return [tuple(p) for p in projected] + return None + return centroid, normal / n_len + + +def _project_onto_plane(pts, plane): + import numpy as _np + + centroid, normal = plane + arr = _np.asarray([list(p)[:3] for p in pts], dtype=float) + # p_proj = p - dot(p - centroid, n) * n + offsets = ((arr - centroid) @ normal)[:, None] * normal + return [tuple(p) for p in arr - offsets] + + +def _project_edge_curves_onto_plane(edge_curves, plane): + """Reproject arc ``PlateEdgeCurve`` endpoints + midpoint onto ``plane`` so they still match their + (now projected) segment in ``CurvePoly2d``. + + Used only on the curved-shell OCC-failure FALLBACK (a flat best-fit-plane approximation of a curved + plate). Arcs reproject cleanly and stay analytic. B-spline edges are DROPPED to a chord here: a + spline lives on the plate's curved surface, so forcing it onto the flat fallback plane produces + sliver/spike triangles — the flat-plate path (where the spline really is in-plane) keeps it analytic. + """ + from ada.api.curves import ArcEdge, SplineEdge + + if not edge_curves or plane is None: + return edge_curves + out = [] + for ec in edge_curves: + if isinstance(ec, ArcEdge): + a, b, m = _project_onto_plane([ec.a, ec.b, ec.midpoint], plane) + out.append(ArcEdge(a=a, b=b, midpoint=m)) + elif isinstance(ec, SplineEdge): + continue # spline on a flat fallback -> chord (avoids off-plane tessellation slivers) + else: + out.append(ec) + return out def _plate_from_3d_points(name, points, t, desired_normal, **kwargs): @@ -135,6 +174,33 @@ def _plate_from_3d_points(name, points, t, desired_normal, **kwargs): return plate +def _plate_from_face(name, points, edge_curves, t, desired_normal, **kwargs): + """Build a plate from SAT-face corner points, keeping analytic arc/spline boundary edges. + + When ``edge_curves`` (arc/spline :class:`~ada.api.curves.PlateEdgeCurve` specs) are present, the + outline is assembled as real segments and built via ``Plate.from_segments`` so the curves survive + analytically; otherwise it falls back to the plain point-based ``_plate_from_3d_points``. Winding is + resolved the same way in both: build, compare to the desired normal, rebuild flipped on disagreement. + """ + if not edge_curves: + return _plate_from_3d_points(name, points, t, desired_normal, **kwargs) + + from ada import Plate + from ada.api.curves import CurvePoly2d + + segments = CurvePoly2d.build_edge_segments(points, edge_curves) + plate = Plate.from_segments(name, segments, t, **kwargs) + if desired_normal is None: + return plate + import numpy as _np + + got = _np.asarray(plate.poly.normal, dtype=float) + want = _np.asarray(desired_normal, dtype=float) + if float(_np.dot(got, want)) < 0: + plate = Plate.from_segments(name, segments, t, flip_normal=True, **kwargs) + return plate + + def _sense_against_face(sat_data, desired_normal, authored_sense: bool) -> bool: """The curved_shell sense flag: does ``desired_normal`` agree with the face? @@ -163,13 +229,15 @@ def _sense_against_face(sat_data, desired_normal, authored_sense: bool) -> bool: def yield_plate_elems_to_plate( - plate_elem, parent, sat_ref_d, thick_map, flat_fallback_d=None, face_normal_resolver=None + plate_elem, parent, sat_ref_d, thick_map, flat_fallback_d=None, face_normal_resolver=None, edge_curves_d=None ): base_name = plate_elem.attrib["name"] mat = parent.materials.get_by_name(plate_elem.attrib["material_ref"]) t = thick_map.get(plate_elem.attrib.get("thickness_ref")) if flat_fallback_d is None: flat_fallback_d = {} + if edge_curves_d is None: + edge_curves_d = {} # A curved shell has no single normal to state as a vector, so Genie # orients it with a flag against its face's own surface normal. It is @@ -336,9 +404,11 @@ def normal_for_face(face_ref): name, mismatch, ) - yield _plate_from_3d_points( + plane = _fit_best_fit_plane(fallback_pts) + yield _plate_from_face( name, - _project_to_best_fit_plane(fallback_pts), + _project_onto_plane(fallback_pts, plane) if plane else list(fallback_pts), + _project_edge_curves_onto_plane(edge_curves_d.get(face_ref), plane), t, normal_for_face(face_ref), mat=mat, @@ -372,6 +442,10 @@ def normal_for_face(face_ref): # strict pcurve guard. if fallback_pts is not None: pc._flat_fallback_pts = fallback_pts + # Carry the analytic arc specs too, so if the tessellator degrades this curved + # shell to its flat fallback it still draws the boundary arcs as real arcs rather + # than chords. Materialized only if the fallback fires — never sampled up front. + pc._flat_fallback_edge_curves = edge_curves_d.get(face_ref) yield pc continue @@ -391,10 +465,12 @@ def normal_for_face(face_ref): fb = flat_fallback_d.get(face_ref) if fb and len(fb) >= 3: try: - projected = _project_to_best_fit_plane(fb) - yield _plate_from_3d_points( + plane = _fit_best_fit_plane(fb) + projected = _project_onto_plane(fb, plane) if plane else list(fb) + yield _plate_from_face( name, projected, + _project_edge_curves_onto_plane(edge_curves_d.get(face_ref), plane), t, normal_for_face(face_ref), mat=mat, @@ -408,9 +484,10 @@ def normal_for_face(face_ref): continue try: - yield _plate_from_3d_points( + yield _plate_from_face( name, sat_data, + edge_curves_d.get(face_ref), t, normal_for_face(face_ref), mat=mat, diff --git a/src/ada/cadit/gxml/store.py b/src/ada/cadit/gxml/store.py index 74fb5828e..c78dc6083 100644 --- a/src/ada/cadit/gxml/store.py +++ b/src/ada/cadit/gxml/store.py @@ -74,7 +74,15 @@ def iter_plates_from_xml(self): # then fail the strict ``p-curve update incomplete`` guard # in surfaces.py — vanishing from output instead of falling # back. - flat_d = {name: points for name, points in self.sat_factory.iter_flat_plates()} + # ``edge_curves_d`` carries the analytic arc segments (circle/ellipse plate boundaries) parallel + # to the corner points, so the flat-plate build can keep them analytic instead of the reader + # sampling them into straight outline points. Keyed by face name, same as ``flat_d``. + flat_d = {} + edge_curves_d = {} + for name, points, edge_curves in self.sat_factory.iter_flat_plates(): + flat_d[name] = points + if edge_curves: + edge_curves_d[name] = edge_curves sat_d = dict(flat_d) if Config().gxml_import_advanced_faces is True: sat_faces = {name: geom for name, geom in self.sat_factory.iter_curved_face()} @@ -88,12 +96,24 @@ def iter_plates_from_xml(self): resolver = self.sat_factory.get_named_face_normal for fp in self.xml_root.iterfind(".//flat_plate"): yield from yield_plate_elems_to_plate( - fp, self.p, sat_d, thick_map, flat_fallback_d=flat_d, face_normal_resolver=resolver + fp, + self.p, + sat_d, + thick_map, + flat_fallback_d=flat_d, + face_normal_resolver=resolver, + edge_curves_d=edge_curves_d, ) for fp in self.xml_root.iterfind(".//curved_shell"): yield from yield_plate_elems_to_plate( - fp, self.p, sat_d, thick_map, flat_fallback_d=flat_d, face_normal_resolver=resolver + fp, + self.p, + sat_d, + thick_map, + flat_fallback_d=flat_d, + face_normal_resolver=resolver, + edge_curves_d=edge_curves_d, ) def to_part(self, extract_joints=False) -> Part: diff --git a/src/ada/cadit/gxml/write/stream_xml.py b/src/ada/cadit/gxml/write/stream_xml.py index 40c962c21..6ddf78a57 100644 --- a/src/ada/cadit/gxml/write/stream_xml.py +++ b/src/ada/cadit/gxml/write/stream_xml.py @@ -36,6 +36,7 @@ from .write_masses import add_masses from .write_materials import add_materials from .write_plates import ( + add_curved_shell_sat_data, add_plate_curved_polygon, add_plate_polygon, add_plate_polygon_data, @@ -57,6 +58,26 @@ _MARKER = "ADA__STREAMED_STRUCTURES__" +def _analytic_merge_strategy(merge_strategy): + """The :class:`MergeStrategy` for an analytic request, or ``None``. + + ``surface``/``panel`` are the analytic strategies; ``cylinder``/``analytic`` + are the aliases the STEP/IFC FEM writers accept (both mean ``surface``). Any + other value (``coplanar``, ``none``, ``planar``) is not analytic — those + stream flat polygons — so this returns ``None`` for them. + """ + from ada.fem.formats.mesh_faces import MergeStrategy + + if merge_strategy is None or isinstance(merge_strategy, bool): + return None + s = str(merge_strategy).lower() + if s in ("surface", "cylinder", "analytic"): + return MergeStrategy.SURFACE + if s == "panel": + return MergeStrategy.PANEL + return None + + def write_xml_stream( part, xml_file, @@ -84,20 +105,62 @@ def write_xml_stream( if not isinstance(xml_file, pathlib.Path): xml_file = pathlib.Path(xml_file) + # The analytic strategies (surface/panel/cylinder/analytic) author their + # recognised cylinder/panel patches into the embedded SAT body and reference + # them as — the only Genie-XML form that carries a curved + # surface. This is the one case where embed_sat composes with a face source. + analytic_strategy = _analytic_merge_strategy(merge_strategy) if embed_sat else None + analytic_mode = analytic_strategy is not None + use_faces = merge_strategy is not None - if embed_sat and use_faces: + if embed_sat and use_faces and not analytic_mode: raise ValueError("write_xml_stream: embed_sat is incompatible with merge_strategy") if use_faces: # plate materials live on the FEM shell sections; register them so # add_materials emits them and the streamed face material_refs resolve. _register_shell_materials(part) + else: + # merge_strategy is None but a part may still carry only a FEM mesh (no + # materialised Beam/Plate) — the streamer then fuses concepts straight from + # the mesh (mirroring the IFC/STEP streaming writers), so register the + # sections + materials those fused objects reference here, before the + # scaffolding + consolidation, so add_sections/add_materials emit them. + _register_fem_fused_scaffolding(part) part.consolidate_sections() part.consolidate_materials() + # embed_sat builds one shared ACIS body from the part's Plate objects — but a + # part that fuses its plates from the FEM mesh has none to build it from (and + # materialising them all would defeat the streaming). Fall back to polygon plates + # in that case rather than emitting s that reference absent SAT faces. + # (The analytic path builds its SAT straight from the mesh faces, so it is + # exempt from this.) + if ( + embed_sat + and not analytic_mode + and any(_fuses_from_fem(p) for p in part.get_all_parts_in_assembly(include_self=True)) + ): + from ada.config import logger as _logger + + _logger.info("write_xml_stream: a part streams plates from its FEM mesh; embed_sat disabled (polygon plates)") + embed_sat = False + # The ACIS body must exist before the plates stream: each references # its faces by the name the SAT writer minted for them. - sw = part_to_sat_writer(part) if embed_sat else None + analytic_records = None + if analytic_mode: + from ada.cadit.sat.write import sat_entities as se + + from .write_analytic_faces import analytic_faces_to_sat_writer + + sw, analytic_records = analytic_faces_to_sat_writer(part, analytic_strategy) + # No curved faces authored (an all-flat model): drop the empty body so the + # records stream as plain flat_plate polygons with no dangling refs. + if not sw.get_entities_by_type(se.Face): + sw = None + else: + sw = part_to_sat_writer(part) if embed_sat else None tree = ET.parse(_XML_TEMPLATE) root = tree.getroot() @@ -117,11 +180,13 @@ def write_xml_stream( thicknesses_elem = ET.SubElement(properties, "thicknesses") from ada.api.plates import PlateCurved - distinct_thicknesses = ( - _shell_thicknesses(part) - if use_faces - else [p.t for p in part.get_all_physical_objects(by_type=(Plate, PlateCurved))] - ) + if use_faces: + distinct_thicknesses = _shell_thicknesses(part) + else: + # Materialised plates, plus the FEM shell thicknesses of any part that fuses + # its plates straight from the mesh (no-op unless a part fuses). + distinct_thicknesses = [p.t for p in part.get_all_physical_objects(by_type=(Plate, PlateCurved))] + distinct_thicknesses += _shell_thicknesses(part) for t in distinct_thicknesses: if t not in thickness_map: name = thickness_name(t) @@ -163,7 +228,9 @@ def write_xml_stream( # Match the DOM writer byte-for-byte: tree.write(encoding="utf-8") # suppresses the XML declaration, so we emit none either. fh.write(head) - _stream_structures(part, fh, thickness_map, Beam, BeamTapered, Plate, merge_strategy, sw) + _stream_structures( + part, fh, thickness_map, Beam, BeamTapered, Plate, merge_strategy, sw, analytic_records=analytic_records + ) fh.write(tail) @@ -183,6 +250,36 @@ def _shell_thicknesses(part) -> list: return out +def _fuses_from_fem(p) -> bool: + """True for a part whose Beam/Plate haven't been materialised but whose FEM mesh + carries elements — the streamer fuses its concepts straight from the mesh (same + predicate the IFC/STEP streaming writers use).""" + from ada.cadit.step.write.ap242_stream import _part_fuses_from_fem + + return _part_fuses_from_fem(p) + + +def _register_fem_fused_scaffolding(part) -> None: + """Register the sections + materials referenced by the Beam/Plate concepts a part + fuses from its FEM mesh, so add_sections/add_materials emit them and the streamed + section_ref/material_ref resolve. Sourced from the FEM sections (a handful of + distinct entries), so nothing geometry-bearing is materialised. No-op for a + part that already carries built concepts.""" + for p in part.get_all_parts_in_assembly(include_self=True): + if not _fuses_from_fem(p): + continue + for fem_sec in p.fem.sections.lines: + sec = getattr(fem_sec, "section", None) + mat = getattr(fem_sec, "material", None) + if sec is not None and sec.name not in p.sections.name_map: + sec.parent = p + p.sections.add(sec) + if mat is not None and mat.name not in p.materials.name_map: + mat.parent = p + p.materials.add(mat) + _register_shell_materials(part) + + def _register_shell_materials(part) -> None: """Add every FEM shell-section material to its part's material container so add_materials emits them and the streamed face ``material_ref`` resolves.""" @@ -199,7 +296,9 @@ def _register_shell_materials(part) -> None: p.materials.add(mat) -def _stream_structures(part, fh, thickness_map, Beam, BeamTapered, Plate, merge_strategy, sw=None) -> None: +def _stream_structures( + part, fh, thickness_map, Beam, BeamTapered, Plate, merge_strategy, sw=None, analytic_records=None +) -> None: """Serialise one ```` subtree at a time and write it out. Beams precede plates, matching the add_beams → add_plates order of the DOM @@ -243,6 +342,36 @@ def _stream_structures(part, fh, thickness_map, Beam, BeamTapered, Plate, merge_ for child in list(tmp): fh.write(ET.tostring(child, encoding="unicode")) + # Beams fused straight from the FEM mesh (parts carrying only a mesh, no built + # concepts) — one transient Beam at a time, bounded memory, mirroring the + # IFC/STEP streaming writers. Emitted after the materialised beams so all beams + # still precede all plates. + for p in part.get_all_parts_in_assembly(include_self=True): + if not _fuses_from_fem(p): + continue + for beam in p.iter_objects_from_fem(beams=True, plates=False): + tmp = ET.Element("structures") + add_straight_beam(beam, tmp, sw) + for child in list(tmp): + fh.write(ET.tostring(child, encoding="unicode")) + + if analytic_records is not None: + # Analytic FEM path: each record is a curved shell (SAT face refs, already + # authored into `sw`) or a flat plate (boundary polygon). This is what + # lets a tube's shell mesh arrive as a handful of s instead + # of thousands of coplanar facets. + for rec in analytic_records: + tmp = ET.Element("structures") + if rec.face_refs: + add_curved_shell_sat_data(rec.name, thickness_map[rec.thickness], rec.material, tmp, rec.face_refs) + else: + add_plate_polygon_data( + rec.name, rec.outline, rec.normal, thickness_map[rec.thickness], rec.material, tmp + ) + for child in list(tmp): + fh.write(ET.tostring(child, encoding="unicode")) + return + if merge_strategy is None: from ada.api.plates import PlateCurved from ada.config import logger @@ -265,6 +394,19 @@ def _stream_structures(part, fh, thickness_map, Beam, BeamTapered, Plate, merge_ continue for child in list(tmp): fh.write(ET.tostring(child, encoding="unicode")) + # Plates fused straight from the FEM mesh (1:1 element→plate, no merge) for + # parts that carry only a mesh — bounded, one transient Plate at a time. + for p in part.get_all_parts_in_assembly(include_self=True): + if not _fuses_from_fem(p): + continue + for plate in p.iter_objects_from_fem(beams=False, plates=True, merge_strategy=None): + tmp = ET.Element("structures") + if sw is not None: + add_plate_sat(plate, thickness_map[plate.t], tmp, sw) + else: + add_plate_polygon(plate, thickness_map[plate.t], tmp) + for child in list(tmp): + fh.write(ET.tostring(child, encoding="unicode")) else: from ada.fem.formats.mesh_faces import iter_faces diff --git a/src/ada/cadit/gxml/write/write_analytic_faces.py b/src/ada/cadit/gxml/write/write_analytic_faces.py new file mode 100644 index 000000000..2b183ce2a --- /dev/null +++ b/src/ada/cadit/gxml/write/write_analytic_faces.py @@ -0,0 +1,428 @@ +"""Author FEM-shell *analytic* faces into an embedded ACIS body for Genie XML. + +The coplanar merge strategy folds a FEM shell mesh into flat ```` +polygons — faithful, but a curved skin (a tubular member, a bent panel) then +arrives as thousands of tiny per-plane facets, and the XML balloons. The STEP +and IFC FEM writers avoid this with the *analytic* face source +(:func:`ada.fem.formats.mesh_faces.iter_faces` on the ``surface`` / ``panel`` +strategy): each region-grown patch is fitted and emitted as a recognised +cylinder or B-spline surface, so a whole tube collapses to a handful of curved +faces. + +Genie XML expresses a curved shell as a ```` that names SAT faces +in the embedded ACIS body — the same mechanism a Genie-authored hull export +uses. This module bridges the two: it takes the analytic ``ada.geom`` faces and +authors each into a :class:`~ada.cadit.sat.write.writer.SatWriter`, recording +per face whether it became a curved shell (SAT face refs) or a flat plate +(boundary polygon). The streaming XML writer then emits one ```` per +record. + +The ACIS SAT reader (and Genie) carries only ``plane-surface`` and +``spline-surface`` faces, so an analytic cylinder is re-expressed as a degree-1 +B-spline surface sampling the tube — a curved patch that reads back as a curved +shell rather than degrading to flats. A patch that cannot be authored falls back +to its boundary polygon as a ```` — geometry is never dropped. +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass, field + +import numpy as np + +from ada.config import logger + + +@dataclass +class AnalyticFaceRecord: + """One FEM face, ready for the streaming XML writer. + + ``face_refs`` non-empty → a ```` naming those SAT faces. + Otherwise ``outline`` + ``normal`` → a ```` polygon. + """ + + name: str + material: str + thickness: float + face_refs: list[str] = field(default_factory=list) + outline: np.ndarray | None = None + normal: np.ndarray | None = None + + +def _edge_loop_from_points(points): + """A closed :class:`EdgeLoop` of straight edges through ``points`` (drops + coincident/duplicate points and a repeated closing point).""" + from ada.geom import curves as geo_cu + from ada.geom.direction import Direction + from ada.geom.points import Point + + pts: list = [] + for p in points: + p = np.asarray(p, dtype=float) + if not pts or float(np.linalg.norm(p - pts[-1])) > 1e-9: + pts.append(p) + if len(pts) >= 2 and float(np.linalg.norm(pts[0] - pts[-1])) < 1e-9: + pts = pts[:-1] + if len(pts) < 3: + return None + edges = [] + for i, a in enumerate(pts): + b = pts[(i + 1) % len(pts)] + d = b - a + line = geo_cu.Line(Point(*a), Direction(*d)) + edges.append( + geo_cu.OrientedEdge( + start=Point(*a), + end=Point(*b), + edge_element=geo_cu.EdgeCurve(start=Point(*a), end=Point(*b), edge_geometry=line, same_sense=True), + orientation=True, + ) + ) + return geo_cu.EdgeLoop(edge_list=edges) + + +def _boundary_points(geom_face) -> list[np.ndarray]: + """Every boundary vertex of a geom face, across all its loops.""" + from ada.geom import curves as geo_cu + + pts: list[np.ndarray] = [] + for bound in geom_face.bounds: + loop = bound.bound + if isinstance(loop, geo_cu.EdgeLoop): + pts.extend(np.asarray(oe.start, dtype=float) for oe in loop.edge_list) + elif isinstance(loop, geo_cu.PolyLoop): + pts.extend(np.asarray(p, dtype=float) for p in loop.polygon) + return pts + + +def _cylinder_to_bspline_faces(geom_face, *, target_deg: float = 15.0): + """Re-express a trimmed ``CylindricalSurface`` face as degree-1 B-spline + surface faces (a curved patch the SAT reader carries). + + The tube's angular sweep is recovered from the face's boundary vertices and + split into ≤120° segments (a full tube into at least two) so each patch is a + clean, non-degenerate single loop. Each segment samples the cylinder on a + (θ × z) grid whose control points lie exactly on the tube, so the degree-1 + surface follows it. Returns a list of authorable ``AdvancedFace``. + """ + surf = geom_face.face_surface + origin = np.array(surf.position.location, dtype=float) + axis = np.array(surf.position.axis, dtype=float) + axis /= np.linalg.norm(axis) or 1.0 + ref = np.array(surf.position.ref_direction, dtype=float) + ref /= np.linalg.norm(ref) or 1.0 + e2 = np.cross(axis, ref) + radius = float(surf.radius) + + pts = _boundary_points(geom_face) + if len(pts) < 3 or radius <= 1e-9: + return [] + d = np.asarray(pts, dtype=float) - origin + z = d @ axis + theta = np.mod(np.arctan2(d @ e2, d @ ref), 2.0 * np.pi) + z0, z1 = float(z.min()), float(z.max()) + if z1 - z0 < 1e-9: + return [] + + th = np.sort(theta) + gaps = np.diff(np.concatenate([th, th[:1] + 2.0 * np.pi])) + gmax_i = int(np.argmax(gaps)) + gmax = float(gaps[gmax_i]) + full = gmax <= math.radians(25.0) + if full: + t0, span = 0.0, 2.0 * np.pi + else: + t0 = float(th[(gmax_i + 1) % len(th)]) + span = 2.0 * np.pi - gmax + + n_seg = max(2 if full else 1, math.ceil(span / math.radians(120.0))) + faces = [] + for k in range(n_seg): + ta = t0 + span * k / n_seg + tb = t0 + span * (k + 1) / n_seg + n_th = max(2, math.ceil((tb - ta) / math.radians(target_deg))) + thetas = np.linspace(ta, tb, n_th + 1) + radial = np.outer(np.cos(thetas), ref) + np.outer(np.sin(thetas), e2) + row0 = origin + radius * radial + z0 * axis + row1 = origin + radius * radial + z1 * axis + af = _bspline_grid_face([row0, row1]) + if af is not None: + faces.append(af) + return faces + + +def _bspline_grid_face(grid): + """A degree-1 B-spline surface ``AdvancedFace`` through an ``nu × nv`` grid of + points, with a straight-edge ``EdgeLoop`` perimeter carrying a UV **pcurve** + per edge. + + The pcurves are what make the read-back face tessellate as a curved surface: + the tessellator trims a spline face in its own parameter space, and without a + pcurve the boundary can't be placed there — the face collapses to a + degenerate sliver and rendering falls back to flat (exactly what the analytic + cylinders must avoid). The surface's parameter domain is ``u ∈ [0, nu-1]`` × + ``v ∈ [0, nv-1]`` (clamped degree-1 knots), so each perimeter vertex has an + integer ``(u, v)`` and every edge is an axis-aligned segment in UV.""" + from ada.geom import curves as geo_cu + from ada.geom import surfaces as geo_su + from ada.geom.direction import Direction + from ada.geom.points import Point + + nu = len(grid) + nv = len(grid[0]) if nu else 0 + if nu < 2 or nv < 2: + return None + + def _clamped(n): + return [float(k) for k in range(n)], [2] + [1] * (n - 2) + [2] + + uk, um = _clamped(nu) + vk, vm = _clamped(nv) + surf = geo_su.BSplineSurfaceWithKnots( + u_degree=1, + v_degree=1, + control_points_list=[[Point(*grid[i][j]) for j in range(nv)] for i in range(nu)], + surface_form=geo_su.BSplineSurfaceForm.UNSPECIFIED, + u_closed=False, + v_closed=False, + self_intersect=False, + u_multiplicities=um, + v_multiplicities=vm, + u_knots=uk, + v_knots=vk, + knot_spec=geo_cu.KnotType.UNSPECIFIED, + ) + + # perimeter of the grid (CCW), each vertex tagged with its (u, v) parameter + perim: list[tuple[tuple[float, float], np.ndarray]] = [] + for j in range(nv): + perim.append(((0.0, float(j)), np.asarray(grid[0][j], dtype=float))) + for i in range(1, nu): + perim.append(((float(i), float(nv - 1)), np.asarray(grid[i][nv - 1], dtype=float))) + for j in range(nv - 2, -1, -1): + perim.append(((float(nu - 1), float(j)), np.asarray(grid[nu - 1][j], dtype=float))) + for i in range(nu - 2, 0, -1): + perim.append(((float(i), 0.0), np.asarray(grid[i][0], dtype=float))) + + edges = [] + for k in range(len(perim)): + (uv0, p0) = perim[k] + (uv1, p1) = perim[(k + 1) % len(perim)] + if float(np.linalg.norm(p1 - p0)) < 1e-12: + continue + line = geo_cu.Line(Point(*p0), Direction(*(p1 - p0))) + pcurve = geo_cu.Pcurve2dBSpline( + degree=1, + control_points_2d=[list(uv0), list(uv1)], + knots=[0.0, 1.0], + knot_multiplicities=[2, 2], + same_sense=True, + ) + edges.append( + geo_cu.OrientedEdge( + start=Point(*p0), + end=Point(*p1), + edge_element=geo_cu.EdgeCurve(start=Point(*p0), end=Point(*p1), edge_geometry=line, same_sense=True), + orientation=True, + pcurve=pcurve, + ) + ) + if len(edges) < 3: + return None + return geo_su.AdvancedFace( + bounds=[geo_su.FaceBound(bound=geo_cu.EdgeLoop(edge_list=edges), orientation=True)], + face_surface=surf, + same_sense=True, + ) + + +def analytic_face_to_authorable_faces(geom_face) -> list: + """Convert one analytic ``ada.geom`` face into ``AdvancedFace`` records the + SAT curved-plate writer can author (spline surface + straight-edge loop). + + A ``CylindricalSurface`` becomes one or more degree-1 B-spline patches; a + B-spline surface keeps its surface and gets a straight-edge loop rebuilt from + its boundary polygon. Anything else yields nothing (the caller falls back to + a flat polygon).""" + from ada.geom import curves as geo_cu + from ada.geom import surfaces as geo_su + + surf = geom_face.face_surface + if isinstance(surf, geo_su.CylindricalSurface): + return _cylinder_to_bspline_faces(geom_face) + if isinstance(surf, geo_su.BSplineSurfaceWithKnots): + bound = geom_face.bounds[0].bound + if isinstance(bound, geo_cu.EdgeLoop): + return [geom_face] + loop = _edge_loop_from_points(_boundary_points(geom_face)) + if loop is None: + return [] + return [ + geo_su.AdvancedFace( + bounds=[geo_su.FaceBound(bound=loop, orientation=True)], + face_surface=surf, + same_sense=geom_face.same_sense, + ) + ] + return [] + + +def _flat_polygon_from_geom_face(geom_face): + """Boundary polygon + normal for a geom face whose analytic authoring failed + — the never-drop fallback (position/extent faithful, curvature lost).""" + pts = _boundary_points(geom_face) + if len(pts) < 3: + return None, None + arr = np.asarray(pts, dtype=float) + nrm = np.zeros(3) + for i in range(len(arr)): + a, b = arr[i], arr[(i + 1) % len(arr)] + nrm[0] += (a[1] - b[1]) * (a[2] + b[2]) + nrm[1] += (a[2] - b[2]) * (a[0] + b[0]) + nrm[2] += (a[0] - b[0]) * (a[1] + b[1]) + length = float(np.linalg.norm(nrm)) + if length < 1e-12: + return None, None + return arr, nrm / length + + +def analytic_faces_to_sat_writer(part, strategy): + """Author every FEM analytic face under ``part`` into a SAT body. + + Returns ``(SatWriter, list[AnalyticFaceRecord])``. Sourced from the strong + analytic merge (:func:`ada.fem.formats.mesh_faces.iter_fem_analytic_faces`, + the exact face set FEM→STEP/IFC emit) tagged with each face's (material, + thickness): a **planar** patch becomes a compact ```` boundary + polygon (holes, where present, are noted and the outer boundary kept); a + **cylinder** (or B-spline) patch is authored into the ACIS body and + referenced by a ````. No face is ever dropped. + """ + from ada.cadit.sat.utils import make_ints_if_possible + from ada.cadit.sat.write import sat_entities as se + from ada.cadit.sat.write.write_curved_plate import ( + TopologyWeld, + UnsupportedCurvedFace, + advanced_face_to_sat_entities, + link_partner_rings, + ) + from ada.cadit.sat.write.writer import SatWriter, _assign_faces_to_shells + from ada.fem.formats.mesh_faces import MergeStrategy, iter_fem_analytic_faces + from ada.geom import curves as geo_cu + from ada.geom import surfaces as geo_su + + sw = SatWriter(part) + idg = sw.id_generator + weld = TopologyWeld(idg) + + records: list[AnalyticFaceRecord] = [] + face_id = 1 + n_curved = 0 + n_flat = 0 + n_holed = 0 + n_fallback = 0 + + # Author the faces first (collecting bbox points), then create the + # body/lump/shell around them — advanced_face_to_sat_entities needs sw.shell, + # so the shell is created up front with a provisional box that is finalised + # once every face vertex is known. + body = se.Body(idg.next_id(), None, [0.0] * 6) + lump = se.Lump(idg.next_id(), None, body, [0.0] * 6) + shell = se.Shell(idg.next_id(), None, lump, [0.0] * 6) + body.lump = lump + lump.shell = shell + sw.body, sw.lump, sw.shell = body, lump, shell + for e in (body, lump, shell): + sw.add_entity(e) + + reconstruct = strategy == MergeStrategy.PANEL + all_pts: list[np.ndarray] = [] + struct_id = 0 + + def _outer_polygon(geom_face): + """The outer boundary polygon + plane normal of a planar geom face.""" + bound = geom_face.bounds[0].bound + if isinstance(bound, geo_cu.PolyLoop): + outline = np.asarray([[p[0], p[1], p[2]] for p in bound.polygon], dtype=float) + else: + outline, _ = _flat_polygon_from_geom_face(geom_face) + if outline is None: + return None, None + pos = getattr(geom_face.face_surface, "position", None) + if pos is not None: + normal = np.asarray(pos.axis, dtype=float) + else: + _, normal = _flat_polygon_from_geom_face(geom_face) + return outline, normal + + for geom_face, material, thickness in iter_fem_analytic_faces(part, with_meta=True, reconstruct_curved=reconstruct): + struct_id += 1 + surf = geom_face.face_surface + + if isinstance(surf, geo_su.Plane): + outline, normal = _outer_polygon(geom_face) + if outline is None or len(outline) < 3 or normal is None: + logger.warning("analytic-xml: a planar face has no usable boundary; dropped") + continue + if len(geom_face.bounds) > 1: + # Genie is a single polygon, so an inner void loop + # cannot be carried here — keep the outer boundary (never drop the + # face). Rare (a handful per model); logged for visibility. + n_holed += 1 + records.append( + AnalyticFaceRecord(f"pl{struct_id}", material, float(thickness), outline=outline, normal=normal) + ) + n_flat += 1 + continue + + # Curved patch → author to the ACIS body, reference by . + refs: list[str] = [] + for af in analytic_face_to_authorable_faces(geom_face): + name = f"FACE{face_id:08d}" + try: + entities = advanced_face_to_sat_entities(af, name, sw, weld) + except UnsupportedCurvedFace as ex: + logger.debug(f"analytic-xml: curved face not authored to SAT: {ex}") + continue + except Exception as ex: # noqa: BLE001 - a bad face must not sink the whole write + logger.debug(f"analytic-xml: curved face SAT author error: {ex}") + continue + for entity in entities: + sw.add_entity(entity) + all_pts.extend(np.asarray(oe.start, dtype=float) for oe in af.bounds[0].bound.edge_list) + refs.append(name) + face_id += 1 + + if refs: + records.append(AnalyticFaceRecord(f"cs{struct_id}", material, float(thickness), face_refs=refs)) + n_curved += 1 + else: + outline, normal = _flat_polygon_from_geom_face(geom_face) + if outline is None: + logger.warning("analytic-xml: a curved face has no usable geometry; dropped") + continue + records.append( + AnalyticFaceRecord(f"cf{struct_id}", material, float(thickness), outline=outline, normal=normal) + ) + n_fallback += 1 + + link_partner_rings(weld) + for entity in weld.entities: + sw.add_entity(entity) + + if all_pts: + arr = np.asarray(all_pts, dtype=float) + bbox = make_ints_if_possible([*arr.min(axis=0), *arr.max(axis=0)]) + sw.bbox = bbox + for e in (body, lump, shell): + e.bbox = list(bbox) + + if sw.get_entities_by_type(se.Face): + _assign_faces_to_shells(sw, shell) + sw.renumber() + + logger.info( + f"analytic-xml: {n_curved} curved shells ({face_id - 1} SAT faces), " + f"{n_flat} flat plates ({n_holed} with holes kept outer-only), {n_fallback} curved→flat fallbacks" + ) + return sw, records diff --git a/src/ada/cadit/gxml/write/write_plates.py b/src/ada/cadit/gxml/write/write_plates.py index 2124e5b4b..e73d4bf0a 100644 --- a/src/ada/cadit/gxml/write/write_plates.py +++ b/src/ada/cadit/gxml/write/write_plates.py @@ -105,6 +105,32 @@ def add_curved_shell_sat(plate, thck_name: str, structures_elem: ET.Element, fac ET.SubElement(sat_reference, "face", {"face_ref": face_ref}) +def add_curved_shell_sat_data( + name: str, thck_name: str, material_name: str, structures_elem: ET.Element, face_refs: list[str] +): + """Emit a ```` from raw data (no Plate object). + + The object-free analytic FEM-shell path (:mod:`ada.cadit.gxml.write.write_analytic_faces`) + authors each curved patch into the embedded SAT body and hands back the face + names it minted; this writes the ```` that references them, the + same element shape as :func:`add_curved_shell_sat`. A FEM-derived face carries + no authored sense flag, so it defaults to ``true``.""" + structure = ET.SubElement(structures_elem, "structure") + curved_shell = ET.SubElement( + structure, "curved_shell", {"name": name, "thickness_ref": thck_name, "material_ref": material_name} + ) + ET.SubElement(curved_shell, "segmentation") + ET.SubElement(curved_shell, "front") + ET.SubElement(curved_shell, "back") + local_sys = ET.SubElement(curved_shell, "local_system") + ET.SubElement(local_sys, "sense_flag", {"sense": "true"}) + geometry = ET.SubElement(curved_shell, "geometry") + sheet = ET.SubElement(geometry, "sheet") + sat_reference = ET.SubElement(sheet, "sat_reference") + for face_ref in face_refs: + ET.SubElement(sat_reference, "face", {"face_ref": face_ref}) + + def add_plate_curved_polygon(plate, thck_name: str, structures_elem: ET.Element) -> bool: """Emit a :class:`PlateCurved` as a ```` boundary polygon. diff --git a/src/ada/cadit/ifc/read/geom/curves.py b/src/ada/cadit/ifc/read/geom/curves.py index 2988ba57f..34f924448 100644 --- a/src/ada/cadit/ifc/read/geom/curves.py +++ b/src/ada/cadit/ifc/read/geom/curves.py @@ -223,23 +223,36 @@ def trimmed_curve(ifc_entity: ifcopenshell.entity_instance) -> geo_cu.TrimmedCur ) -def pcurve_2d_from_surface_curve(surface_curve: ifcopenshell.entity_instance) -> geo_cu.Pcurve2dBSpline | None: - """Recover the UV p-curve (2D B-spline) attached to an IfcSurfaceCurve.""" +def pcurve_2d_from_surface_curve( + surface_curve: ifcopenshell.entity_instance, + basis_surface: ifcopenshell.entity_instance | None = None, +) -> geo_cu.Pcurve2dBSpline | None: + """Recover the UV p-curve (2D B-spline) attached to an IfcSurfaceCurve. + + ``basis_surface`` (the parent face's FaceSurface entity) filters the associated + p-curves: a p-curve's UV coordinates are meaningful ONLY on its own BasisSurface. + In a closed shell every edge is shared by two faces — without the filter the + bottom-patch p-curve leaks onto e.g. the ruled side face using the same edge, and + the backend then builds the side wire from foreign UV data (wire build failed).""" associated = getattr(surface_curve, "AssociatedGeometry", None) if not associated: return None - ref = associated[0].ReferenceCurve # IfcPcurve.ReferenceCurve - if not ref.is_a("IfcBSplineCurveWithKnots"): - return None - weights = list(ref.WeightsData) if ref.is_a("IfcRationalBSplineCurveWithKnots") else None - return geo_cu.Pcurve2dBSpline( - degree=ref.Degree, - control_points_2d=[(float(p.Coordinates[0]), float(p.Coordinates[1])) for p in ref.ControlPointsList], - knots=list(ref.Knots), - knot_multiplicities=list(ref.KnotMultiplicities), - weights=weights, - closed=bool(ref.ClosedCurve), - ) + for pc in associated: + if basis_surface is not None and getattr(pc, "BasisSurface", None) != basis_surface: + continue + ref = pc.ReferenceCurve # IfcPcurve.ReferenceCurve + if not ref.is_a("IfcBSplineCurveWithKnots"): + continue + weights = list(ref.WeightsData) if ref.is_a("IfcRationalBSplineCurveWithKnots") else None + return geo_cu.Pcurve2dBSpline( + degree=ref.Degree, + control_points_2d=[(float(p.Coordinates[0]), float(p.Coordinates[1])) for p in ref.ControlPointsList], + knots=list(ref.Knots), + knot_multiplicities=list(ref.KnotMultiplicities), + weights=weights, + closed=bool(ref.ClosedCurve), + ) + return None def b_spline_curve_with_knots(ifc_entity: ifcopenshell.entity_instance) -> geo_cu.BSplineCurveWithKnots: @@ -310,14 +323,18 @@ def edge(ifc_entity: ifcopenshell.entity_instance) -> geo_cu.Edge: return geo_cu.Edge(start=start, end=end) -def oriented_edge(ifc_entity: ifcopenshell.entity_instance) -> geo_cu.OrientedEdge: +def oriented_edge( + ifc_entity: ifcopenshell.entity_instance, + basis_surface: ifcopenshell.entity_instance | None = None, +) -> geo_cu.OrientedEdge: ee = ifc_entity.EdgeElement # Recover the UV p-curve when the edge geometry is an IfcSurfaceCurve — without - # it the trimmed B-spline face tessellates degenerate (near-zero area). + # it the trimmed B-spline face tessellates degenerate (near-zero area). Only the + # p-curve lying on the parent face's own surface is taken (see the filter above). pcurve = None if ee.is_a("IfcEdgeCurve") and ee.EdgeGeometry is not None and ee.EdgeGeometry.is_a("IfcSurfaceCurve"): - pcurve = pcurve_2d_from_surface_curve(ee.EdgeGeometry) + pcurve = pcurve_2d_from_surface_curve(ee.EdgeGeometry, basis_surface=basis_surface) return geo_cu.OrientedEdge( start=Point(ifc_entity.EdgeStart.VertexGeometry.Coordinates), @@ -328,5 +345,8 @@ def oriented_edge(ifc_entity: ifcopenshell.entity_instance) -> geo_cu.OrientedEd ) -def edge_loop(ifc_entity: ifcopenshell.entity_instance) -> geo_cu.EdgeLoop: - return geo_cu.EdgeLoop([oriented_edge(e) for e in ifc_entity.EdgeList]) +def edge_loop( + ifc_entity: ifcopenshell.entity_instance, + basis_surface: ifcopenshell.entity_instance | None = None, +) -> geo_cu.EdgeLoop: + return geo_cu.EdgeLoop([oriented_edge(e, basis_surface=basis_surface) for e in ifc_entity.EdgeList]) diff --git a/src/ada/cadit/ifc/read/geom/surfaces.py b/src/ada/cadit/ifc/read/geom/surfaces.py index 08de2d5fd..62f2dc650 100644 --- a/src/ada/cadit/ifc/read/geom/surfaces.py +++ b/src/ada/cadit/ifc/read/geom/surfaces.py @@ -245,11 +245,16 @@ def poly_loop(ifc_entity: ifcopenshell.entity_instance) -> geo_cu.PolyLoop: return geo_cu.PolyLoop(polygon=[Point(p.Coordinates) for p in ifc_entity.Polygon]) -def face_bound(ifc_entity: ifcopenshell.entity_instance) -> geo_su.FaceBound: +def face_bound( + ifc_entity: ifcopenshell.entity_instance, + basis_surface: ifcopenshell.entity_instance | None = None, +) -> geo_su.FaceBound: + """``basis_surface`` is the parent face's FaceSurface entity, used to pick only the + p-curves that live on THAT surface (shared edges carry p-curves of both faces).""" ifc_bound = ifc_entity.Bound if ifc_bound.is_a("IfcEdgeLoop"): - bound = edge_loop(ifc_bound) + bound = edge_loop(ifc_bound, basis_surface=basis_surface) elif ifc_bound.is_a("IfcPolyLoop"): bound = poly_loop(ifc_bound) else: @@ -342,15 +347,90 @@ def face_surface_geom(ifc_surface: ifcopenshell.entity_instance) -> geo_su.SURFA return toroidal_surface(ifc_surface) elif ifc_surface.is_a("IfcSurfaceOfRevolution"): return surface_of_revolution(ifc_surface) + elif ifc_surface.is_a("IfcSurfaceOfLinearExtrusion"): + return surface_of_linear_extrusion(ifc_surface) elif ifc_surface.is_a("IfcPlane"): return plane(ifc_surface) raise NotImplementedError(f"Face surface type {ifc_surface.is_a()} is not implemented") +def surface_of_linear_extrusion(ifc_entity: ifcopenshell.entity_instance) -> geo_su.SurfaceOfLinearExtrusion: + """IfcSurfaceOfLinearExtrusion -> SurfaceOfLinearExtrusion. + + The IFC form carries a TWO-dimensional profile curve in the XY plane of Position + plus an extrusion direction in the Position frame (the writer's inverse — see + ``ada.cadit.ifc.write.geom.surfaces.create_surface_of_linear_extrusion``). The + adapy form models a WORLD-space swept curve with ``position=None``, so the 2D + circle/ellipse is lifted through the Position frame here.""" + import numpy as np + + from ada.geom.direction import Direction + from ada.geom.placement import Axis2Placement3D + + from .curves import get_curve + from .placement import axis3d + + swept = ifc_entity.SweptCurve + curve_ent = swept.Curve if swept.is_a("IfcArbitraryOpenProfileDef") else swept + depth = float(ifc_entity.Depth) if ifc_entity.Depth is not None else 1.0 + d = np.asarray(list(ifc_entity.ExtrudedDirection.DirectionRatios), dtype=float) + if d.shape[0] == 2: + d = np.append(d, 0.0) + + pos = axis3d(ifc_entity.Position) if ifc_entity.Position is not None else None + if pos is None: + return geo_su.SurfaceOfLinearExtrusion( + swept_curve=get_curve(curve_ent), + position=None, + extrusion_direction=Direction(*(d / np.linalg.norm(d))), + depth=depth, + ) + + z = np.asarray(list(pos.axis), dtype=float) + x = np.asarray(list(pos.ref_direction), dtype=float) + z = z / np.linalg.norm(z) + x = x / np.linalg.norm(x) + y = np.cross(z, x) + loc = np.asarray(list(pos.location), dtype=float) + d_world = d[0] * x + d[1] * y + d[2] * z + d_world = d_world / np.linalg.norm(d_world) + + if curve_ent.is_a("IfcCircle") or curve_ent.is_a("IfcEllipse"): + p2 = curve_ent.Position + c2 = np.asarray(list(p2.Location.Coordinates), dtype=float) if p2 is not None else np.zeros(2) + r2 = ( + np.asarray(list(p2.RefDirection.DirectionRatios), dtype=float) + if p2 is not None and p2.RefDirection is not None + else np.array([1.0, 0.0]) + ) + center = loc + c2[0] * x + c2[1] * y + ref3 = r2[0] * x + r2[1] * y + placement = Axis2Placement3D( + location=Point(*center), axis=Direction(*z), ref_direction=Direction(*(ref3 / np.linalg.norm(ref3))) + ) + if curve_ent.is_a("IfcCircle"): + curve = geo_cu.Circle(position=placement, radius=float(curve_ent.Radius)) + else: + curve = geo_cu.Ellipse( + position=placement, + semi_axis1=float(curve_ent.SemiAxis1), + semi_axis2=float(curve_ent.SemiAxis2), + ) + else: + raise NotImplementedError( + f"IfcSurfaceOfLinearExtrusion profile curve {curve_ent.is_a()} with a Position frame is not implemented" + ) + + return geo_su.SurfaceOfLinearExtrusion( + swept_curve=curve, position=None, extrusion_direction=Direction(*d_world), depth=depth + ) + + def advanced_face(ifc_entity: ifcopenshell.entity_instance) -> geo_su.AdvancedFace: return geo_su.AdvancedFace( - bounds=[face_bound(x) for x in ifc_entity.Bounds], + bounds=[face_bound(x, basis_surface=ifc_entity.FaceSurface) for x in ifc_entity.Bounds], face_surface=face_surface_geom(ifc_entity.FaceSurface), + same_sense=bool(ifc_entity.SameSense) if ifc_entity.SameSense is not None else True, ) diff --git a/src/ada/cadit/ifc/read/read_plates.py b/src/ada/cadit/ifc/read/read_plates.py index c992441a5..99a4d4b29 100644 --- a/src/ada/cadit/ifc/read/read_plates.py +++ b/src/ada/cadit/ifc/read/read_plates.py @@ -75,6 +75,116 @@ def _import_curved_plate(ifc_elem, name, advanced_face: geo_su.AdvancedFace, ifc return pc +def _arc_midpoint(circle, p_start, p_end, same_sense: bool) -> tuple[float, float, float]: + """The point on the arc of ``circle`` halfway between the two trim points (positive direction).""" + import math + + import numpy as np + + center = np.asarray(circle.position.location, dtype=float)[:3] + axis = np.asarray(circle.position.axis, dtype=float)[:3] + axis = axis / np.linalg.norm(axis) + if not same_sense: + axis = -axis + xd = np.asarray(circle.position.ref_direction, dtype=float)[:3] + xd = xd - axis * float(xd @ axis) + xd = xd / np.linalg.norm(xd) + yd = np.cross(axis, xd) + r = float(circle.radius) + + def _ang(p): + d = np.asarray(p, dtype=float)[:3] - center + return math.atan2(float(d @ yd), float(d @ xd)) + + a0, a1 = _ang(p_start), _ang(p_end) + while a1 <= a0: + a1 += 2.0 * math.pi + am = 0.5 * (a0 + a1) + m = center + r * math.cos(am) * xd + r * math.sin(am) * yd + return (float(m[0]), float(m[1]), float(m[2])) + + +def _plate_from_extruded_brep(ifc_elem, name, shell: geo_su.ClosedShell, ifc_store: IfcStore) -> Plate | None: + """Reconstruct the parametric ``Plate`` from the analytic B-rep the writer emits for a + spline-boundary plate (``extruded_loop_to_shell``): planar caps + planar/cylindrical sides + one + or more B-spline side faces whose v-direction IS the extrusion vector. Returns None for any shell + that doesn't match that shape — the caller then falls back to a generic geometry Shape.""" + import numpy as np + + from ada.api.curves import ArcSegment, CurvePoly2d, LineSegment, SplineSegment + from ada.geom import curves as geo_cu + + spline_faces = [fc for fc in shell.cfs_faces if isinstance(fc.face_surface, geo_su.BSplineSurfaceWithKnots)] + if not spline_faces: + return None + grid = spline_faces[0].face_surface.control_points_list + if len(grid[0]) != 2: # the writer's surface is degree-1 in v: exactly two control columns + return None + dvec = np.asarray(grid[0][1], dtype=float)[:3] - np.asarray(grid[0][0], dtype=float)[:3] + depth = float(np.linalg.norm(dvec)) + if depth < 1e-9: + return None + ez = dvec / depth + + def _plane_axis(fc): + a = np.asarray(fc.face_surface.position.axis, dtype=float)[:3] + return a / np.linalg.norm(a) + + caps = [fc for fc in shell.cfs_faces if isinstance(fc.face_surface, geo_su.Plane)] + bottom = next((fc for fc in caps if float(_plane_axis(fc) @ ez) < -0.999), None) + if bottom is None or not bottom.bounds: + return None + loop = bottom.bounds[0].bound + edge_list = getattr(loop, "edge_list", None) + if not edge_list: + return None + + segments = [] + for oe in edge_list: + ec = oe.edge_element + geom = getattr(ec, "edge_geometry", None) + p_start, p_end = (ec.start, ec.end) if oe.orientation else (ec.end, ec.start) + if isinstance(geom, geo_cu.BSplineCurveWithKnots): + curve = geom if oe.orientation else _reversed_bspline_curve(geom) + segments.append(SplineSegment(p_start, p_end, curve=curve)) + elif isinstance(geom, geo_cu.Circle): + mid = _arc_midpoint(geom, p_start, p_end, getattr(ec, "same_sense", True)) + segments.append(ArcSegment(p_start, p_end, midpoint=mid)) + elif isinstance(geom, (geo_cu.Line, geo_cu.PolyLine)) or geom is None: + segments.append(LineSegment(p_start, p_end)) + else: + return None + # contiguity sanity: each segment must start where the previous one ended + for i, seg in enumerate(segments): + nxt = segments[(i + 1) % len(segments)] + if float(np.linalg.norm(np.asarray(seg.p2, dtype=float) - np.asarray(nxt.p1, dtype=float))) > 1e-6: + return None + + try: + poly = CurvePoly2d.from_segments(segments) + if float(np.asarray(poly.normal, dtype=float) @ ez) < 0.0: + poly = CurvePoly2d.from_segments(segments, flip_n=True) + except Exception as exc: # noqa: BLE001 - fall back to the generic shape import + logger.debug(f"plate {name}: from_segments reconstruction failed ({exc})") + return None + + return Plate( + name, + poly, + depth, + mat=_read_plate_material(ifc_elem, name, ifc_store), + guid=ifc_elem.GlobalId, + ifc_store=ifc_store, + units=ifc_store.assembly.units, + ) + + +def _reversed_bspline_curve(c): + from ada.geom.primitive_brep import _reversed_bspline + + return _reversed_bspline(c) + + def import_ifc_plate(ifc_elem: ifcopenshell.entity_instance, name, ifc_store: IfcStore): logger.info(f"importing {name}") geometries = get_product_definitions(ifc_elem) @@ -83,6 +193,13 @@ def import_ifc_plate(ifc_elem: ifcopenshell.entity_instance, name, ifc_store: If if len(geometries) == 1 and isinstance(geometries[0], geo_su.AdvancedFace): return _import_curved_plate(ifc_elem, name, geometries[0], ifc_store) + # A spline-boundary plate is written as an analytic IfcAdvancedBrep (ClosedShell); rebuild the + # parametric Plate from its bottom cap loop. + if len(geometries) == 1 and isinstance(geometries[0], geo_su.ClosedShell): + pl = _plate_from_extruded_brep(ifc_elem, name, geometries[0], ifc_store) + if pl is not None: + return pl + # Only an extruded arbitrary profile maps to a parametric Plate. Anything else # (e.g. another BREP form) is imported as a generic geometry-backed Shape so it # still renders and round-trips. diff --git a/src/ada/cadit/ifc/write/geom/curves.py b/src/ada/cadit/ifc/write/geom/curves.py index 6b6c12cc7..e494028fc 100644 --- a/src/ada/cadit/ifc/write/geom/curves.py +++ b/src/ada/cadit/ifc/write/geom/curves.py @@ -1,5 +1,7 @@ from __future__ import annotations +import weakref + import ifcopenshell from ada.cadit.ifc.write.geom.placement import ifc_placement_from_axis3d, vector @@ -7,6 +9,11 @@ from ada.config import Config, logger from ada.geom import curves as geo_cu +# Per-file EdgeCurve dedupe: {id(EdgeCurve): (EdgeCurve, entity_id)} — see create_edge_curve. +# The value stores the entity ID (an int), never the entity_instance: an entity holds a strong +# reference back to its file, which would pin the WeakKeyDictionary key forever (a leak). +_edge_curve_cache: "weakref.WeakKeyDictionary[ifcopenshell.file, dict]" = weakref.WeakKeyDictionary() + def indexed_poly_curve_from_points_and_segments( points: list[list[float]], f: ifcopenshell.file, segment_indices: list[list[int]] = None @@ -28,6 +35,64 @@ def indexed_poly_curve_from_points_and_segments( return f.create_entity("IfcIndexedPolyCurve", Points=ifc_point_list, Segments=None, SelfIntersect=None) +def _ipc_has_spline(ipc: geo_cu.IndexedPolyCurve) -> bool: + return any(isinstance(seg, geo_cu.BSplineCurveWithKnots) for seg in ipc.segments) + + +def _ipc_to_composite_curve(ipc: geo_cu.IndexedPolyCurve) -> geo_cu.CompositeCurve: + """Lift an ``IndexedPolyCurve`` (that contains an analytic B-spline) to a ``CompositeCurve``. + + ``IfcIndexedPolyCurve`` can only index line/arc segments, so a spline-bearing outline must be an + ``IfcCompositeCurve`` instead: each straight edge becomes a bounded ``PolyLine``, each arc a + ``TrimmedCurve`` on a ``Circle``, and each B-spline passes straight through (``write_curve`` emits + ``IfcBSplineCurveWithKnots``). + """ + import numpy as np + + from ada.core.curve_utils import calc_arc_radius_center_from_3points + from ada.geom.direction import Direction + from ada.geom.placement import Axis2Placement3D + from ada.geom.points import Point + + def _arc_parent(seg: geo_cu.ArcLine): + s, m, e = (np.asarray(p, dtype=float) for p in (seg.start, seg.midpoint, seg.end)) + center2d, radius = calc_arc_radius_center_from_3points(s[:2], m[:2], e[:2]) + center = Point(float(center2d[0]), float(center2d[1]), 0.0) + ccw = float(np.cross((m - s)[:2], (e - s)[:2])) >= 0.0 # winding in the profile plane + axis = Direction(0.0, 0.0, 1.0 if ccw else -1.0) + ref = Direction(*(s - np.asarray(center, dtype=float))).get_normalized() + circle = geo_cu.Circle(Axis2Placement3D(center, axis=axis, ref_direction=ref), float(radius)) + return geo_cu.TrimmedCurve( + basis_curve=circle, + trim1=Point(*s), + trim2=Point(*e), + sense_agreement=True, + master_representation="CARTESIAN", + ) + + segments = [] + for seg in ipc.segments: + if isinstance(seg, geo_cu.BSplineCurveWithKnots): + # A bare bounded B-spline is the schema-valid ParentCurve (an IfcTrimmedCurve wrapper would + # render in ifcopenshell but violates IfcTrimmedCurve.NoTrimOfBoundedCurves). ada's reader + # samples it back; ifcopenshell's own geometry engine currently can't build a wire from a + # B-spline composite segment (an upstream engine limitation, not an IFC validity issue). + parent = seg + elif isinstance(seg, geo_cu.ArcLine): + parent = _arc_parent(seg) + else: # Edge / straight + parent = geo_cu.PolyLine(points=[seg.start, seg.end]) + segments.append(geo_cu.CompositeCurveSegment(parent_curve=parent, same_sense=True, transition="CONTINUOUS")) + return geo_cu.CompositeCurve(segments=segments, self_intersect=ipc.self_intersect) + + +def indexed_poly_curve_or_composite(ipc: geo_cu.IndexedPolyCurve, f: ifcopenshell.file) -> ifcopenshell.entity_instance: + """``IfcCompositeCurve`` when the outline carries an analytic B-spline, else ``IfcIndexedPolyCurve``.""" + if _ipc_has_spline(ipc): + return composite_curve(_ipc_to_composite_curve(ipc), f) + return indexed_poly_curve(ipc, f) + + def indexed_poly_curve(ipc: geo_cu.IndexedPolyCurve, f: ifcopenshell.file) -> ifcopenshell.entity_instance: """Converts an IndexedPolyCurve to an IFC representation""" has_arclines = any([isinstance(seg, geo_cu.ArcLine) for seg in ipc.segments]) @@ -165,19 +230,31 @@ def create_edge_curve( When a UV ``pcurve`` and its ``basis_surface`` are supplied, the edge geometry is written as an IfcSurfaceCurve so the p-curve survives the - round-trip; otherwise just the bare 3D curve is written.""" + round-trip; otherwise just the bare 3D curve is written. + + The SAME ``EdgeCurve`` python object writes to the SAME IfcEdgeCurve entity: a manifold B-rep + uses each edge in exactly two faces, and the EXPRESS topology rules compare by instance — a + duplicated edge entity would break shell closure (see ``vrtx`` for the vertex analogue).""" + cache = _edge_curve_cache.setdefault(f, {}) + hit = cache.get(id(ec)) + if hit is not None: + return f.by_id(hit[1]) + if pcurve is not None and basis_surface is not None: edge_geometry = create_surface_curve(ec, pcurve, basis_surface, f) else: edge_geometry = _edge_geometry_3d(ec.edge_geometry, f) - return f.create_entity( + entity = f.create_entity( "IfcEdgeCurve", EdgeStart=vrtx(f, ec.start), EdgeEnd=vrtx(f, ec.end), EdgeGeometry=edge_geometry, SameSense=ec.same_sense, ) + # Keep ``ec`` alive alongside the id so the id() key can never be reused. + cache[id(ec)] = (ec, entity.id()) + return entity def create_ellipse(ellipse: geo_cu.Ellipse, f: ifcopenshell.file) -> ifcopenshell.entity_instance: @@ -245,7 +322,8 @@ def _trim_select(trim, f: ifcopenshell.file) -> tuple: if isinstance(trim, Point): return (cpt(f, trim),) - return (float(trim),) + # A parameter trim is an IfcParameterValue (a defined type) inside the IfcTrimmingSelect aggregate. + return (f.create_entity("IfcParameterValue", float(trim)),) def create_trimmed_curve(tc: geo_cu.TrimmedCurve, f: ifcopenshell.file) -> ifcopenshell.entity_instance: diff --git a/src/ada/cadit/ifc/write/geom/points.py b/src/ada/cadit/ifc/write/geom/points.py index b637b4df3..88b4d6ee3 100644 --- a/src/ada/cadit/ifc/write/geom/points.py +++ b/src/ada/cadit/ifc/write/geom/points.py @@ -1,13 +1,29 @@ from __future__ import annotations +import weakref + import ifcopenshell from ada.core.utils import to_real +# Per-file vertex dedupe. B-rep EXPRESS rules compare topology by INSTANCE — e.g. +# IfcEdgeLoop.IsClosed requires edge N's EdgeEnd to be the SAME IfcVertexPoint as edge N+1's +# EdgeStart — so coincident-but-duplicate vertices make an otherwise perfect shell invalid. +# The cache stores entity IDs (plain ints), never entity_instances: an entity holds a strong +# reference back to its file, which would pin the WeakKeyDictionary key forever (a leak). +_vrtx_cache: "weakref.WeakKeyDictionary[ifcopenshell.file, dict]" = weakref.WeakKeyDictionary() + def cpt(f: ifcopenshell.file, p): return f.create_entity("IfcCartesianPoint", to_real(p)) def vrtx(f: ifcopenshell.file, p): - return f.create_entity("IfcVertexPoint", VertexGeometry=cpt(f, p)) + cache = _vrtx_cache.setdefault(f, {}) + key = tuple(to_real(p)) + vid = cache.get(key) + if vid is None: + v = f.create_entity("IfcVertexPoint", VertexGeometry=cpt(f, p)) + cache[key] = v.id() + return v + return f.by_id(vid) diff --git a/src/ada/cadit/ifc/write/geom/surfaces.py b/src/ada/cadit/ifc/write/geom/surfaces.py index 95d6d2dc0..5640a98be 100644 --- a/src/ada/cadit/ifc/write/geom/surfaces.py +++ b/src/ada/cadit/ifc/write/geom/surfaces.py @@ -5,11 +5,13 @@ from ada import BoolHalfSpace from ada.geom import curves as geo_cu from ada.geom import surfaces as geo_su +from ada.geom.direction import Direction +from ada.geom.placement import Axis2Placement3D from .curves import ( circle_curve, edge_loop, - indexed_poly_curve, + indexed_poly_curve_or_composite, poly_line, poly_loop, write_curve, @@ -21,7 +23,7 @@ def arbitrary_profile_def(apd: geo_su.ArbitraryProfileDef, f: ifcopenshell.file) -> ifcopenshell.entity_instance: """Converts an ArbitraryProfileDefWithVoids to an IFC representation""" if isinstance(apd.outer_curve, geo_cu.IndexedPolyCurve): - outer_curve = indexed_poly_curve(apd.outer_curve, f) + outer_curve = indexed_poly_curve_or_composite(apd.outer_curve, f) elif isinstance(apd.outer_curve, geo_cu.Circle): outer_curve = circle_curve(apd.outer_curve, f) else: @@ -30,7 +32,7 @@ def arbitrary_profile_def(apd: geo_su.ArbitraryProfileDef, f: ifcopenshell.file) inner_curves = [] for ic in apd.inner_curves: if isinstance(ic, geo_cu.IndexedPolyCurve): - inner_curves.append(indexed_poly_curve(ic, f)) + inner_curves.append(indexed_poly_curve_or_composite(ic, f)) elif isinstance(ic, geo_cu.Circle): inner_curves.append(circle_curve(ic, f)) else: @@ -237,6 +239,12 @@ def advanced_face(af: geo_su.AdvancedFace, f: ifcopenshell.file) -> ifcopenshell # nothing about the boundary, which is why the face is an AdvancedFace # at all rather than a polygon. face_surface = create_plane(af.face_surface, f) + elif isinstance(af.face_surface, geo_su.CylindricalSurface): + # An arc boundary edge extruded through the plate thickness (extruded_loop_to_shell). + face_surface = create_cylindrical_surface(af.face_surface, f) + elif isinstance(af.face_surface, geo_su.SurfaceOfLinearExtrusion): + # A curved boundary edge extruded through the plate thickness (face_to_thick_shell). + face_surface = create_surface_of_linear_extrusion(af.face_surface, f) else: raise NotImplementedError(f"Unsupported face surface type: {type(af.face_surface)}") @@ -256,6 +264,64 @@ def advanced_face(af: geo_su.AdvancedFace, f: ifcopenshell.file) -> ifcopenshell ) +def create_surface_of_linear_extrusion( + sle: geo_su.SurfaceOfLinearExtrusion, f: ifcopenshell.file +) -> ifcopenshell.entity_instance: + """Converts a SurfaceOfLinearExtrusion to an IfcSurfaceOfLinearExtrusion. + + The IFC form types SweptCurve as an IfcProfileDef, and IfcArbitraryOpenProfileDef.WR12 + requires the profile curve to be TWO-dimensional — so the swept curve must be expressed + in the XY plane of the surface Position. That is only possible for a planar swept curve + with a known frame: a Circle/Ellipse (its own placement becomes the Position, the curve + a 2D circle/ellipse at the origin, and the extrusion direction is rewritten in that + frame). Non-planar swept curves (the general 3D case) cannot be written as a VALID + IfcSurfaceOfLinearExtrusion — callers emit the exact ruled B-spline surface instead.""" + import numpy as np + + c = sle.swept_curve + if not isinstance(c, (geo_cu.Circle, geo_cu.Ellipse)): + raise NotImplementedError( + f"IfcSurfaceOfLinearExtrusion requires a 2D-expressible swept curve; got {type(c).__name__}" + ) + pos = c.position + z = np.asarray(list(pos.axis), dtype=float) + z = z / np.linalg.norm(z) + if pos.ref_direction is not None: + x = np.asarray(list(pos.ref_direction), dtype=float) + else: + # A circle is rotation-invariant about its axis — any orthonormal ref works. + seed = np.array([1.0, 0.0, 0.0]) if abs(z[0]) < 0.9 else np.array([0.0, 1.0, 0.0]) + x = np.cross(seed, z) + pos = Axis2Placement3D(location=pos.location, axis=pos.axis, ref_direction=Direction(*(x / np.linalg.norm(x)))) + x = x / np.linalg.norm(x) + y = np.cross(z, x) + placement_2d = f.create_entity( + "IfcAxis2Placement2D", + Location=f.create_entity("IfcCartesianPoint", (0.0, 0.0)), + RefDirection=f.create_entity("IfcDirection", (1.0, 0.0)), + ) + if isinstance(c, geo_cu.Circle): + curve_2d = f.create_entity("IfcCircle", Position=placement_2d, Radius=float(c.radius)) + else: + curve_2d = f.create_entity( + "IfcEllipse", + Position=placement_2d, + SemiAxis1=float(c.semi_axis1), + SemiAxis2=float(c.semi_axis2), + ) + profile = f.create_entity("IfcArbitraryOpenProfileDef", ProfileType="CURVE", Curve=curve_2d) + d = np.asarray(list(sle.extrusion_direction), dtype=float) + d = d / np.linalg.norm(d) + d_local = (float(d @ x), float(d @ y), float(d @ z)) + return f.create_entity( + "IfcSurfaceOfLinearExtrusion", + SweptCurve=profile, + Position=ifc_placement_from_axis3d(pos, f), + ExtrudedDirection=f.create_entity("IfcDirection", d_local), + Depth=float(sle.depth or 1.0), + ) + + def create_half_space_geom(bool_half_space: BoolHalfSpace, f: ifcopenshell.file) -> ifcopenshell.entity_instance: """Converts a Half Space object to Plane to an IFC representation""" half_space = bool_half_space.solid_geom() @@ -316,7 +382,7 @@ def _bounded_curve(curve: geo_cu.CURVE_GEOM_TYPES, f: ifcopenshell.file) -> ifco """Write a boundary curve to an IfcCurve (IfcCurveBoundedPlane boundaries are IfcCurve, not topological loops).""" if isinstance(curve, geo_cu.IndexedPolyCurve): - return indexed_poly_curve(curve, f) + return indexed_poly_curve_or_composite(curve, f) elif isinstance(curve, geo_cu.PolyLine): return poly_line(curve, f) elif isinstance(curve, geo_cu.Circle): diff --git a/src/ada/cadit/ifc/write/native_ifc_writer.py b/src/ada/cadit/ifc/write/native_ifc_writer.py index 5b7453a10..8d3053c84 100644 --- a/src/ada/cadit/ifc/write/native_ifc_writer.py +++ b/src/ada/cadit/ifc/write/native_ifc_writer.py @@ -1,61 +1,106 @@ -"""Native IFC writer: emit via adacpp's ``blobs_to_ifc`` (C++ ifc_emit), no ifcopenshell/OCC. +"""Native IFC writer: emit via adacpp's C++ ifc_emit (no ifcopenshell/OCC). Feeds each shape's NGEOM blob + out-of-band record (colour, world transforms, spatial paths) to the native emitter, which decodes the blob and writes the analytic IFC solid (IfcExtrudedAreaSolid / IfcSweptDiskSolid / IfcBooleanResult / IfcAdvancedBrep / ...) + IfcStyledItem + the spatial tree — the inverse of the native IFC reader, so a native round-trip keeps the CSG analytic (never tessellated). -Only shapes that carry an NGEOM blob (lazy ShapeProxy) are emittable; others are skipped (the caller -falls back to the ifcopenshell writer). Best paired with ``from_ifc(reader="native")``. +Blob source per shape: a lazy ``ShapeProxy`` hands over its stored blob as-is (zero re-encode); any +other physical object is serialized from its parametric ``solid_geom()`` through the NGEOM encoder +(the same wire), so freshly-built models — e.g. a Genie-XML import with thickened curved shells — +take the native path too. Objects with neither (raw-OCC imports) are skipped and logged (the caller +falls back to the ifcopenshell writer for full coverage). Best paired with ``from_ifc(reader="native")``. + +Emits through ``stream_ngeom_to_ifc`` (records pulled lazily, bounded memory) when the adacpp build +has it, else the older parallel-list ``blobs_to_ifc``. """ from __future__ import annotations import pathlib +from ada.config import logger + def native_ifc_writer_available() -> bool: """True if adacpp's native NGEOM-blobs->IFC emitter is importable.""" try: import adacpp # noqa: F401 - return hasattr(adacpp.cad, "blobs_to_ifc") + return hasattr(adacpp.cad, "blobs_to_ifc") or hasattr(adacpp.cad, "stream_ngeom_to_ifc") except Exception: return False +def _shape_records(assembly): + """(name, blob, rgba|None, transforms|None, paths|None) per emittable shape + a skip count.""" + import numpy as np + + from ada.api.mass import MassPoint + from ada.api.shapes import ShapeProxy + from ada.cadit.ngeom.serialize import _Encoder, _Unsupported + + records: list[tuple] = [] + skipped = 0 + for shp in assembly.get_all_physical_objects(pipe_to_segments=True): + if isinstance(shp, MassPoint): + continue # no geometry + name = str(shp.name) + if isinstance(shp, ShapeProxy): + blob = shp.ngeom_blob() + if blob is not None: + rec = shp._shape_store.record(shp._store_index) + col = rec.color if rec.color is not None else shp.color + rgba = [float(c) for c in col][:4] if col is not None else None + # ShapeRecord transforms are 4x4 (column-major-decoded); flatten back to 16-float + # column-major for the wire. + mats = rec.transforms or None + tfs = [list(np.asarray(m, dtype="float32").flatten(order="F")) for m in mats] if mats else None + ip = rec.instance_paths or None + paths = [[(int(r), str(nm)) for (r, nm) in lvl] for lvl in ip] if ip else None + records.append((name, bytes(blob), rgba, tfs, paths)) + continue + # Non-proxy (or a proxy without a stored blob): serialize the parametric solid_geom(). + try: + geom = shp.solid_geom() + enc = _Encoder() + idx = enc.root(geom) + blob = enc.finish([(idx, name)]) + except _Unsupported as ex: + skipped += 1 + logger.debug( + "native IFC writer: %s (%s) unsupported by the NGEOM encoder: %s", name, type(shp).__name__, ex + ) + continue + except Exception as ex: + skipped += 1 + logger.debug("native IFC writer: %s (%s) not serializable: %s", name, type(shp).__name__, ex) + continue + col = getattr(shp, "color", None) + rgba = [float(c) for c in col][:4] if col is not None else None + records.append((name, blob, rgba, None, None)) + return records, skipped + + def native_write_ifc(assembly, out_path: str | pathlib.Path, schema: str = "IFC4X3_ADD2") -> dict: """Write ``assembly`` to an IFC file natively. Returns the losslessness audit dict - (solids_in/out, faces_in/out/dropped, drop_reasons). Raises if no shape carries an NGEOM blob.""" + (solids_in/out, faces_in/out/dropped, drop_reasons). Raises if no shape yields an NGEOM blob.""" import adacpp - import numpy as np - from ada.api.shapes import ShapeProxy from ada.base.units import Units - blobs: list[bytes] = [] - colors: list[list[float]] = [] - transforms: list[list[list[float]]] = [] - paths: list[list[list[tuple]]] = [] - - for shp in assembly.get_all_physical_objects(): - if not isinstance(shp, ShapeProxy): - continue # native writer needs the raw NGEOM blob (lazy store) - blob = shp.ngeom_blob() - if blob is None: - continue - rec = shp._shape_store.record(shp._store_index) - blobs.append(bytes(blob)) - col = rec.color if rec.color is not None else shp.color - colors.append([float(c) for c in col][:4] if col is not None else [0.0, 0.0, 0.0, -1.0]) - # ShapeRecord transforms are 4x4 (column-major-decoded); flatten back to 16-float column-major. - mats = rec.transforms or [] - transforms.append([list(np.asarray(m, dtype="float32").flatten(order="F")) for m in mats]) - ip = rec.instance_paths or [] - paths.append([[(int(r), str(nm)) for (r, nm) in lvl] for lvl in ip]) - - if not blobs: - raise RuntimeError("native IFC writer: no shape carries an NGEOM blob (need lazy ShapeProxy shapes)") + records, skipped = _shape_records(assembly) + if not records: + raise RuntimeError("native IFC writer: no shape yields an NGEOM blob (stored or via solid_geom())") + if skipped: + logger.warning("native IFC writer: %d object(s) skipped (no NGEOM-serializable geometry)", skipped) unit_scale = 0.001 if assembly.units == Units.MM else 1.0 + if hasattr(adacpp.cad, "stream_ngeom_to_ifc"): + return adacpp.cad.stream_ngeom_to_ifc(records, str(out_path), schema=schema, unit_scale=unit_scale) + # Older adacpp: the parallel-list form (colour sentinel alpha=-1 for "no colour"). + blobs = [r[1] for r in records] + colors = [r[2] if r[2] is not None else [0.0, 0.0, 0.0, -1.0] for r in records] + transforms = [r[3] or [] for r in records] + paths = [r[4] or [] for r in records] return adacpp.cad.blobs_to_ifc(blobs, colors, transforms, paths, str(out_path), schema, unit_scale) diff --git a/src/ada/cadit/ifc/write/stream_ifc.py b/src/ada/cadit/ifc/write/stream_ifc.py index c60d18b87..2d3b39f77 100644 --- a/src/ada/cadit/ifc/write/stream_ifc.py +++ b/src/ada/cadit/ifc/write/stream_ifc.py @@ -19,7 +19,12 @@ The converter leaves plates unbuilt (``create_objects_from_fem(skip_plates`` ``=True)``) so this path is taken. * **pre-built**: a non-FEM model whose ``part.plates`` already exist (e.g. a - parametric CAD model) streams those objects as text. + parametric CAD model) streams those objects as text. Curved plates + (``PlateCurved``) and spline-boundary flat plates stream too when adacpp's + ``ngeom_to_ifc_body_spf`` is importable: their analytic B-rep body graphs + are emitted as C++ SPF fragments (~µs/face) under hand-authored TYPED + ``IfcPlate`` wrappers, replacing the ~ms/face per-entity ifcopenshell + writer that dominated large curved-shell hulls. Entry point: :func:`stream_assembly_to_ifc`, used by ``Assembly.to_ifc(streaming=True)``. @@ -52,6 +57,21 @@ def _p(vec) -> str: _ID_LINE = re.compile(r"^#(\d+)=") +def _ngeom_body_fragment(): + """adacpp's C++ IFC geometry-body fragment emitter (``ngeom_to_ifc_body_spf``), or None. + + When available, curved plates (and spline-boundary flat plates) stream their heavy B-rep + body graphs at C++ speed while the TYPED product wrapper (IfcPlate + placement + style) + stays hand-authored here — the per-entity ifcopenshell writer costs ~ms/face and dominated + large curved-shell models.""" + try: + import adacpp + + return getattr(adacpp.cad, "ngeom_to_ifc_body_spf", None) + except Exception: + return None + + class _Emitter: """Hand-authors the SPF lines for one Plate, allocating ids from a counter.""" @@ -77,9 +97,18 @@ def _surface_style(self, key: tuple) -> int: return s[2] def _curve(self, lines: list[str], curve) -> int: - """Emit IfcCartesianPointList2D + IfcIndexedPolyCurve (with segment - indices, incl. IfcArcIndex for fillets) — the form ada's reader expects. - Returns the IfcIndexedPolyCurve id.""" + """Emit the profile curve and return its id. + + A B-spline-bearing outline is emitted analytically as an ``IfcCompositeCurve`` (line -> + IfcPolyline, arc -> IfcTrimmedCurve, spline -> IfcBSplineCurveWithKnots) — ``IfcIndexedPolyCurve`` + can only index line/arc. Otherwise the compact ``IfcIndexedPolyCurve`` form ada's reader + expects (with ``IfcArcIndex`` for fillets).""" + from ada.geom.curves import BSplineCurveWithKnots + + segs = getattr(curve, "segments", None) + if segs is not None and any(isinstance(s, BSplineCurveWithKnots) for s in segs): + return self._composite_curve(lines, curve) + pts, seg_idx = curve.get_unique_points_and_segment_indices() pts = pts.tolist() if hasattr(pts, "tolist") else pts ptlist = self.nid @@ -94,6 +123,93 @@ def _curve(self, lines: list[str], curve) -> int: self.nid = cid + 1 return cid + def _pt2d(self, lines: list[str], xy) -> int: + pid = self.nid + self.nid += 1 + lines.append(f"#{pid}=IfcCartesianPoint(({_f(xy[0])},{_f(xy[1])}));") + return pid + + def _polyline_parent(self, lines: list[str], a, b) -> int: + p1, p2 = self._pt2d(lines, a), self._pt2d(lines, b) + pid = self.nid + self.nid += 1 + lines.append(f"#{pid}=IfcPolyline((#{p1},#{p2}));") + return pid + + def _bspline_parent(self, lines: list[str], sp) -> int: + cps = ",".join(f"#{self._pt2d(lines, cp)}" for cp in sp.control_points_list) + mult = "(" + ",".join(str(int(m)) for m in sp.knot_multiplicities) + ")" + kn = "(" + ",".join(_f(k) for k in sp.knots) + ")" + form, spec = sp.curve_form.value, sp.knot_spec.value + closed = ".T." if sp.closed_curve else ".F." + si = ".T." if sp.self_intersect else ".F." + bid = self.nid + self.nid += 1 + weights = getattr(sp, "weights_data", None) + if weights: + w = "(" + ",".join(_f(x) for x in weights) + ")" + lines.append( + f"#{bid}=IfcRationalBSplineCurveWithKnots({sp.degree},({cps}),.{form}.,{closed},{si}," + f"{mult},{kn},.{spec}.,{w});" + ) + else: + lines.append( + f"#{bid}=IfcBSplineCurveWithKnots({sp.degree},({cps}),.{form}.,{closed},{si},{mult},{kn},.{spec}.);" + ) + # A bare bounded B-spline is the schema-valid ParentCurve; an IfcTrimmedCurve wrapper would + # violate IfcTrimmedCurve.NoTrimOfBoundedCurves. Mirrors the normal writer. + return bid + + def _arc_parent(self, lines: list[str], arc) -> int: + import numpy as np + + from ada.core.curve_utils import calc_arc_radius_center_from_3points + + s = np.asarray(arc.start, dtype=float)[:2] + m = np.asarray(arc.midpoint, dtype=float)[:2] + e = np.asarray(arc.end, dtype=float)[:2] + center, radius = calc_arc_radius_center_from_3points(s, m, e) + center = np.asarray(center, dtype=float)[:2] + ccw = float(np.cross(m - s, e - s)) >= 0.0 + cpid = self._pt2d(lines, (center[0], center[1])) + ref = s - center + rn = np.linalg.norm(ref) + ref = ref / rn if rn else np.array([1.0, 0.0]) + rdir = self.nid + lines.append(f"#{rdir}=IfcDirection(({_f(ref[0])},{_f(ref[1])}));") + a2p = rdir + 1 + lines.append(f"#{a2p}=IfcAxis2Placement2D(#{cpid},#{rdir});") + circ = a2p + 1 + lines.append(f"#{circ}=IfcCircle(#{a2p},{_f(radius)});") + self.nid = circ + 1 + t1, t2 = self._pt2d(lines, (s[0], s[1])), self._pt2d(lines, (e[0], e[1])) + tc = self.nid + self.nid += 1 + sense = ".T." if ccw else ".F." + lines.append(f"#{tc}=IfcTrimmedCurve(#{circ},(#{t1}),(#{t2}),{sense},.CARTESIAN.);") + return tc + + def _composite_curve(self, lines: list[str], curve) -> int: + """Hand-author an IfcCompositeCurve for a spline-bearing outline (analytic, not sampled).""" + import ada.geom.curves as cu + + seg_ids = [] + for seg in curve.segments: + if isinstance(seg, cu.BSplineCurveWithKnots): + parent = self._bspline_parent(lines, seg) + elif isinstance(seg, cu.ArcLine): + parent = self._arc_parent(lines, seg) + else: # Edge / straight + parent = self._polyline_parent(lines, seg.start, seg.end) + sid = self.nid + self.nid += 1 + lines.append(f"#{sid}=IfcCompositeCurveSegment(.CONTINUOUS.,.T.,#{parent});") + seg_ids.append(sid) + cc = self.nid + self.nid += 1 + lines.append(f"#{cc}=IfcCompositeCurve(({','.join(f'#{s}' for s in seg_ids)}),.F.);") + return cc + def plate(self, pl, lines: list[str]) -> int: """Append the SPF lines for ``pl``; return its IfcPlate id.""" g = pl.solid_geom().geometry # ExtrudedAreaSolid @@ -144,6 +260,49 @@ def plate(self, pl, lines: list[str]) -> int: self.nid = sitem + 1 return pid + def typed_plate_wrapper(self, pl, body_item: int, rep_type: str, lines: list[str]) -> int: + """Hand-author the typed product entities around an already-emitted body item: the SAME + wrapper the flat-plate path writes (placement + IfcShapeRepresentation + + IfcProductDefinitionShape + IfcPlate + shared-style IfcStyledItem), but referencing an + external body graph (C++ fragment or the Python B-rep emitter). Returns the IfcPlate id.""" + op = pl.placement.to_axis2placement3d() + a = self.nid + lines.append(f"#{a}=IfcCartesianPoint({_p(op.location)});") + lines.append(f"#{a + 1}=IfcDirection({_p(op.axis)});") + lines.append(f"#{a + 2}=IfcDirection({_p(op.ref_direction)});") + lines.append(f"#{a + 3}=IfcAxis2Placement3D(#{a},#{a + 1},#{a + 2});") + lines.append(f"#{a + 4}=IfcLocalPlacement($,#{a + 3});") + body = a + 5 + lines.append(f"#{body}=IfcShapeRepresentation(#{self.body_ctx_id},'Body','{rep_type}',(#{body_item}));") + pds = body + 1 + lines.append(f"#{pds}=IfcProductDefinitionShape($,$,(#{body}));") + pid = pds + 1 + nm = _spf_str(pl.name) + lines.append(f"#{pid}=IfcPlate('{pl.guid}',#{self.owner_id},{nm},{nm},$,#{a + 4},#{pds},$,$);") + self.nid = pid + 1 + col = getattr(pl, "color", None) + if col is not None: + rgb = col.rgb + key = (float(rgb[0]), float(rgb[1]), float(rgb[2]), float(getattr(col, "transparency", 0.0) or 0.0)) + style_id = self._surface_style(key) + sitem = self.nid + lines.append(f"#{sitem}=IfcStyledItem(#{body_item},(#{style_id}),$);") + self.nid = sitem + 1 + return pid + + def brep_plate(self, pl, blob: bytes, frag_fn, lines: list[str]) -> int: + """Emit ``pl`` as a typed IfcPlate whose geometry-body graph comes from adacpp's + ``ngeom_to_ifc_body_spf`` fragment emitter (~µs/face vs the ~ms/face per-entity + ifcopenshell writer). Raises when the fragment emit fails (unrepresentable root or any + dropped face) so the caller can take its Python fallback — a partial body is never + written.""" + spf, next_id, body_item, rep_type = frag_fn(blob, self.nid) + if not body_item: + raise ValueError("adacpp body-fragment emit failed (unrepresentable geometry or dropped faces)") + lines.append(spf.rstrip("\n")) + self.nid = next_id + return self.typed_plate_wrapper(pl, body_item, rep_type, lines) + def _spf_str(s) -> str: """Quote a Python string as an SPF string literal (apostrophes doubled).""" @@ -210,13 +369,18 @@ def stream_assembly_to_ifc( fused: list = [] # (part, n_shells) — plates streamed from the FEM mesh for part in assembly.get_all_parts_in_assembly(include_self=True): fem = getattr(part, "fem", None) - if fem is None or len(part.plates): + if fem is None: + continue + # Fuse beams straight from the mesh whenever the part has line elements but no + # built beams — independent of whether it also carries shells, so a beam-only + # FEM part still emits its beams (1:1 line→beam, matching the STEP/XML stream). + if not len(part.beams) and len(list(fem.elements.lines)): + part._beams = convert_part_elem_bm_to_beams(part) + if len(part.plates): continue shells = list(fem.elements.shell) if not shells: continue - if not len(part.beams) and len(list(fem.elements.lines)): - part._beams = convert_part_elem_bm_to_beams(part) _register_plate_materials(part, shells, GeomRepr) fused.append((part, len(shells))) @@ -232,10 +396,80 @@ def stream_assembly_to_ifc( body_ctx_id = store.get_context("Body").id() # Pre-built plain Plates stream as text; everything else (beams, shapes, …) - # is built once via the normal writer. - prebuilt_plates, others = [], [] + # is built once via the normal writer. Curved plates (PlateCurved) and + # spline-boundary flat plates carry analytic IfcAdvancedBrep bodies — when + # adacpp's fragment emitter is available their body graphs stream as + # C++-emitted SPF text under a hand-authored typed IfcPlate wrapper (the + # per-entity ifcopenshell writer is ~ms/face and dominated large hulls); + # otherwise they keep the normal-writer path. + from ada import PlateCurved + from ada.api.curves import SplineSegment + from ada.geom import Geometry + from ada.geom import surfaces as geo_su + + def _streams_as_text(obj) -> bool: + if type(obj) is not Plate: + return False + return not any(isinstance(s, SplineSegment) for s in obj.poly.segments3d) + + frag_fn = _ngeom_body_fragment() + + def _brep_stream_record(obj) -> tuple | None: + """``(shell Geometry, ngeom blob)`` when ``obj`` can stream as a typed B-rep plate + (body graph via the C++ fragment emitter), else None -> the normal ifcopenshell + writer keeps it. Serialization happens here, BEFORE the preamble is built, so a + non-serializable object falls back to the existing per-entity path untouched.""" + if frag_fn is None: + return None + try: + if isinstance(obj, PlateCurved): + if obj.geom is None: # from_occ_face plates carry no ada.geom tree + return None + g = obj.solid_geom() + if g is None or not isinstance(g.geometry, geo_su.ClosedShell): + # thickening off -> the Python writer's bare-face body is what keeps the + # PlateCurved round-trip; don't wrap an open face in a "closed" shell. + return None + elif type(obj) is Plate: + # spline-boundary flat plate: the same analytic extruded shell the normal + # writer emits (write_plates._plate_body), so the parametric Plate round-trip + # via _plate_from_extruded_brep is preserved. + from ada.config import Config + from ada.geom.primitive_brep import ( + extruded_loop_to_shell, + thickness_anchor_base_offset, + ) + + base_off = thickness_anchor_base_offset(Config().geom_thickness_anchor, obj.t) + shell = extruded_loop_to_shell(obj.poly.segments3d, obj.poly.normal, obj.t, base_offset=base_off) + if shell is None: + return None + g = Geometry(obj.guid, shell, getattr(obj, "color", None)) + else: + return None + from ada.cadit.ngeom.serialize import _Encoder + + enc = _Encoder() + idx = enc.root(g) + return g, enc.finish([(idx, str(obj.name))]) + except Exception as exc: # noqa: BLE001 — any failure -> the existing writer path + logger.debug( + "plate %r: B-rep stream serialize failed (%s); using the ifcopenshell writer", + getattr(obj, "name", "?"), + exc, + ) + return None + + prebuilt_plates, brep_stream, others = [], [], [] for obj in assembly.get_all_physical_objects(): - (prebuilt_plates if type(obj) is Plate else others).append(obj) + if _streams_as_text(obj): + prebuilt_plates.append(obj) + continue + rec = _brep_stream_record(obj) + if rec is not None: + brep_stream.append((obj, *rec)) + else: + others.append(obj) spatial_id = {} for part in assembly.get_all_parts_in_assembly(include_self=True): @@ -334,6 +568,51 @@ def _emit(pl) -> None: out.write("\n".join(lines) + "\n") lines.clear() + # 1b) pre-built curved / spline-boundary plates: typed IfcPlate wrappers around + # C++-emitted B-rep body fragments (per-object Python B-rep-emitter fallback). + if brep_stream: + from ada.cadit.step.write.stream_step_to_ifc import _IfcBrepEmitter + + for i, (pl, shell_geom, blob) in enumerate(brep_stream, 1): + try: + try: + pid = emitter.brep_plate(pl, blob, frag_fn, lines) + except Exception as frag_exc: # noqa: BLE001 — per-object Python fallback + logger.debug( + "plate %r: C++ body fragment failed (%s); Python B-rep emitter", + getattr(pl, "name", "?"), + frag_exc, + ) + be = _IfcBrepEmitter(emitter.nid - 1) + try: + brep, rep_type = be.solid(lines, shell_geom.geometry) + finally: + # Sync the id counter even when solid() RAISES mid-emit: a partial body + # leaves already-appended (orphan, harmless) entities whose ids must + # never be re-issued by later plates. + emitter.nid = be.nid + 1 + if brep is None: + raise ValueError("B-rep body not emittable") from frag_exc + pid = emitter.typed_plate_wrapper(pl, brep, rep_type, lines) + except Exception as exc: # noqa: BLE001 — a bad plate shouldn't sink the file + skipped += 1 + if skipped <= 5: + logger.warning(f"streaming IFC: skipped B-rep plate {getattr(pl, 'name', '?')!r}: {exc}") + continue + total += 1 + _record_spatial(pl.parent.guid, pid) + mat = getattr(pl, "material", None) + if mat is not None: + _record_material(mat.guid, pid) + if i % 64 == 0 or i == len(brep_stream): + out.write("\n".join(lines) + "\n") + lines.clear() + if progress_callback is not None: + progress_callback(i, len(brep_stream)) + if lines: + out.write("\n".join(lines) + "\n") + lines.clear() + # 2) fused FEM shells. if analytic: # One recognised-surface B-rep shell per FEM part (cylinders + flat faces), @@ -382,8 +661,6 @@ def _emit(pl) -> None: # hand-authored flat-plate text — they buffer (tens of objects by construction) # and emit as B-rep products after the flat stream, since the B-rep emitter # must own the id counter while it runs. - from ada import PlateCurved - curved_pending: list = [] # (owning part guid, PlateCurved) for part, n_shells in fused: cnt = 0 diff --git a/src/ada/cadit/ifc/write/write_ifc.py b/src/ada/cadit/ifc/write/write_ifc.py index a7fed76b5..5a1b92177 100644 --- a/src/ada/cadit/ifc/write/write_ifc.py +++ b/src/ada/cadit/ifc/write/write_ifc.py @@ -103,6 +103,20 @@ def sync_added_physical_objects(self) -> int: ) } + # A previous sync's prune_empty_relationships may have dropped a material's (then-empty) + # eager rel — e.g. a beam-only material, since beams associate via their profile-set usage + # and never populate the bare rel. The rel doubles as the material's guid-lookup handle + # (add_material_assignment resolves the IfcMaterial through f.by_guid(mat.guid)), so + # recreate it here; the final prune removes it again if it stays empty. + missing = {guid: mat for guid, mat in mat_map.items() if guid not in rel_mats_map} + if missing: + ifc_mats_by_name = {im.Name: im for im in self.ifc_store.f.by_type("IfcMaterial")} + for guid, mat in missing.items(): + ifc_mat = ifc_mats_by_name.get(mat.name) + if ifc_mat is None: + continue # material itself never written; eval_validity reports it properly + rel_mats_map[guid] = self.create_rel_associates_material(guid, ifc_mat) + new_objects = list(filter(is_added, list(a.get_all_physical_objects()))) num_new_objects = len(new_objects) @@ -144,10 +158,18 @@ def sync_added_physical_objects(self) -> int: self.callback(i, num_new_objects) # Create relationships between materials and physical objects + from ada import Beam + obj_map = defaultdict(list) for obj in new_objects: if not hasattr(obj, "material"): continue + if isinstance(obj, Beam): + # A beam's material association is its IfcMaterialProfileSetUsage rel (written in + # add_material_assignment). Adding it to the bare per-material rel as well gives it + # TWO IfcRelAssociatesMaterial, violating the EXPRESS where-rule + # IfcBuiltElement.MaxOneMaterialAssociation (caught by ifcopenshell.validate). + continue obj_map[obj.material].append(obj) f = self.ifc_store.f @@ -183,7 +205,12 @@ def sync_added_physical_objects(self) -> int: except RuntimeError: pass - rel_mat.RelatedObjects = [*rel_mat.RelatedObjects, *ifc_elems, *ifc_elems_pipe_seg] + # De-dup: pipe segments are already associated directly by write_ifc_pipe + # (associate_elem_with_material), and RelatedObjects is a SET — a repeated + # entry is a schema violation, not a harmless append. + existing = set(rel_mat.RelatedObjects) + fresh = [e for e in [*ifc_elems, *ifc_elems_pipe_seg] if e not in existing] + rel_mat.RelatedObjects = [*rel_mat.RelatedObjects, *fresh] for spatial_elem_guid, relating_elements in contained_in_spatial.items(): if len(relating_elements) == 0: diff --git a/src/ada/cadit/ifc/write/write_plates.py b/src/ada/cadit/ifc/write/write_plates.py index 781a6535d..d93651b4f 100644 --- a/src/ada/cadit/ifc/write/write_plates.py +++ b/src/ada/cadit/ifc/write/write_plates.py @@ -11,7 +11,6 @@ if TYPE_CHECKING: from ada.cadit.ifc.store import IfcStore - from ada.config import logger @@ -19,6 +18,33 @@ def update_ifc_plate(ifc_store: IfcStore, plate: Plate): logger.warning("Updating IFC plate not implemented yet") +def _plate_body(plate: Plate, f) -> "ifcopenshell.entity_instance": # noqa: F821 - typing-only name + """The plate's IFC body: an IfcExtrudedAreaSolid, or an IfcAdvancedBrep when the outline carries an + analytic B-spline edge. + + IfcExtrudedAreaSolid can't take a spline boundary through the tools that matter — + IfcIndexedPolyCurve is line/arc-only, and ifcopenshell's geometry engine won't build a wire from a + B-spline IfcCompositeCurve segment — so those plates emit the exact analytic B-rep instead + (planar caps/sides + the spline side face as the degree-1-in-v extrusion surface). + """ + from ada.api.curves import SplineSegment + from ada.cadit.ifc.write.geom.surfaces import create_closed_shell + from ada.config import Config + from ada.geom.primitive_brep import ( + extruded_loop_to_shell, + thickness_anchor_base_offset, + ) + + segs = plate.poly.segments3d + if any(isinstance(s, SplineSegment) for s in segs): + base_off = thickness_anchor_base_offset(Config().geom_thickness_anchor, plate.t) + shell = extruded_loop_to_shell(segs, plate.poly.normal, plate.t, base_offset=base_off) + if shell is not None: + return f.create_entity("IfcAdvancedBrep", Outer=create_closed_shell(shell, f)) + logger.warning("plate %r: analytic B-rep build failed; falling back to the extruded solid", plate.name) + return extruded_area_solid(plate.solid_geom().geometry, f) + + def write_ifc_plate(ifc_store: IfcStore, plate: Plate): if plate.parent is None: raise ValueError("Ifc element cannot be built without any parent element") @@ -31,7 +57,7 @@ def write_ifc_plate(ifc_store: IfcStore, plate: Plate): plate_placement = f.create_entity("IfcLocalPlacement", PlacementRelTo=None, RelativePlacement=axis2placement) - solid = extruded_area_solid(plate.solid_geom().geometry, f) + solid = _plate_body(plate, f) body = f.createIfcShapeRepresentation(ifc_store.get_context("Body"), "Body", "SolidModel", [solid]) product_shape = f.create_entity("IfcProductDefinitionShape", None, None, [body]) @@ -55,6 +81,30 @@ def write_ifc_plate(ifc_store: IfcStore, plate: Plate): return ifc_plate +def _plate_curved_body(plate: PlateCurved, f) -> "ifcopenshell.entity_instance": # noqa: F821 - typing-only name + """The curved plate's IFC body: an IfcAdvancedBrep of the thickness-t analytic + ClosedShell when curved-shell thickening is active (``solid_geom`` returns the + shell built by ``face_to_thick_shell``), else the historical bare IfcAdvancedFace. + Any failure emitting the thick shell falls back to the bare face — never lose + the plate.""" + import ada.geom.surfaces as geo_su + from ada.cadit.ifc.write.geom.surfaces import create_closed_shell + + geom = None + try: + geom = plate.solid_geom() + except Exception as e: # noqa: BLE001 - degenerate face data + logger.warning("PlateCurved %r: solid_geom failed (%s); writing the bare face", plate.name, e) + if geom is not None and isinstance(geom.geometry, geo_su.ClosedShell): + try: + return f.create_entity("IfcAdvancedBrep", Outer=create_closed_shell(geom.geometry, f)) + except Exception as e: # noqa: BLE001 - unsupported surface/curve in this shell + logger.warning( + "PlateCurved %r: thick-shell IFC emit failed (%s); falling back to the bare face", plate.name, e + ) + return advanced_face(plate.geom.geometry, f) + + def write_ifc_plate_curved(ifc_store: IfcStore, plate: PlateCurved): if plate.parent is None: raise ValueError("Ifc element cannot be built without any parent element") @@ -67,7 +117,7 @@ def write_ifc_plate_curved(ifc_store: IfcStore, plate: PlateCurved): plate_placement = f.create_entity("IfcLocalPlacement", PlacementRelTo=None, RelativePlacement=axis2placement) - solid = advanced_face(plate.geom.geometry, f) + solid = _plate_curved_body(plate, f) body = f.createIfcShapeRepresentation(ifc_store.get_context("Body"), "Body", "SolidModel", [solid]) product_shape = f.create_entity("IfcProductDefinitionShape", None, None, [body]) diff --git a/src/ada/cadit/ngeom/__init__.py b/src/ada/cadit/ngeom/__init__.py index a91b7635d..54188063f 100644 --- a/src/ada/cadit/ngeom/__init__.py +++ b/src/ada/cadit/ngeom/__init__.py @@ -1,7 +1,7 @@ """NGEOM — neutral-geometry interchange between adapy (ada.geom) and adacpp. Pure-Python serializer of ``ada.geom`` into the NGEOM binary buffer (see the spec at -dap/plan/v3/spec_neutral_geometry_schema.md). adapy has NO dependency on adacpp; the buffer +the neutral-geometry schema spec). adapy has NO dependency on adacpp; the buffer is the only contract. adacpp decodes the buffer into its neutral geometry layer and tessellates it via the libtess2 or ifcopenshell-taxonomy pipelines. """ diff --git a/src/ada/cadit/ngeom/deserialize.py b/src/ada/cadit/ngeom/deserialize.py index 22defe208..084ef3dd9 100644 --- a/src/ada/cadit/ngeom/deserialize.py +++ b/src/ada/cadit/ngeom/deserialize.py @@ -1,6 +1,6 @@ """Decode an NGEOM buffer back into ``ada.geom`` geometry — the exact inverse of ``serialize.py``. -Wire format (see ``serialize.py`` / dap/plan/v3/spec_neutral_geometry_schema.md): a header +Wire format (see ``serialize.py`` / the neutral-geometry schema spec): a header ``b"ADANGEOM"`` + ``i32 version`` + ``i32 num_records``, then ``num_records`` records each ``i32 tag, i32 nbytes, payload``, then a roots trailer ``i32 root_count`` followed by ``i32 geom_record_index, i32 id_len, id_utf8`` per root. Records reference earlier records by their diff --git a/src/ada/cadit/ngeom/export.py b/src/ada/cadit/ngeom/export.py new file mode 100644 index 000000000..1f92d4dbe --- /dev/null +++ b/src/ada/cadit/ngeom/export.py @@ -0,0 +1,254 @@ +"""Native NGEOM-record export: Assembly -> STEP / IFC via adacpp's C++ emitters. + +Walks the assembly, serializes each physical object's ``solid_geom()`` to a per-object NGEOM +blob (the same neutral wire the stream tessellator consumes) and hands ``(name, blob, color, +transforms, paths)`` records to ``adacpp.cad.stream_ngeom_to_step`` / ``stream_ngeom_to_ifc``. +This replaces the per-entity Python writers (ifcopenshell / ap242_stream, ~ms/face) with the +~µs/face C++ emitters — on a hull model with thickened curved shells the emit drops from tens +of seconds to seconds. + +Coverage rule ("no geometry left behind"): if ANY object fails to serialize, the export raises +:class:`NativeExportUnsupported` and the caller falls back WHOLESALE to the Python writer — a +partially-native file silently missing solids is never produced. ``MassPoint`` (no geometry) +is exempt. The serialize phase collects all records up front (per-object blobs are compact — +KBs each) so the fallback decision is made before any output file exists; the C++ side then +streams them one at a time at bounded memory. + +Preserved: object names, presentation colours, units (header SI length unit), and the part +hierarchy — as a NEXT_ASSEMBLY_USAGE_OCCURRENCE product tree (STEP) / nested IfcSpatialZone +tree (IFC). IFC GlobalIds are writer-generated (deterministic per run), not the ada guids — +same property as the existing ``blobs_to_ifc`` native writer. +""" + +from __future__ import annotations + +import pathlib +from typing import TYPE_CHECKING + +from ada.config import Config, logger + +if TYPE_CHECKING: + from ada.api.spatial.part import Part + + +class NativeExportUnsupported(Exception): + """The model (or environment) can't take the native NGEOM export path; fall back wholesale.""" + + +def native_ngeom_writers_available() -> bool: + """True if adacpp's NGEOM-record emitters (stream_ngeom_to_step/ifc) are importable.""" + try: + import adacpp + + return hasattr(adacpp.cad, "stream_ngeom_to_step") and hasattr(adacpp.cad, "stream_ngeom_to_ifc") + except Exception: + return False + + +def native_export_enabled() -> bool: + """The config switch for the converter legs (env ``ADA_CAD_NATIVE_NGEOM_EXPORT``).""" + return bool(Config().cad_native_ngeom_export) + + +def native_mesh_writers_available() -> bool: + """True if adacpp's NGEOM-record mesh emitters (stream_ngeom_to_glb / stream_ngeom_to_mesh) + are importable.""" + try: + import adacpp + + return hasattr(adacpp.cad, "stream_ngeom_to_glb") and hasattr(adacpp.cad, "stream_ngeom_to_mesh") + except Exception: + return False + + +def _object_color_rgba(obj) -> list[float] | None: + col = getattr(obj, "color", None) + if col is None: + return None + try: + r, g, b, a = tuple(col) + except Exception: + return None + return [float(r), float(g), float(b), float(a)] + + +def collect_ngeom_records(part: Part) -> tuple[list[tuple], dict]: + """Serialize every physical object under ``part`` to one NGEOM record. + + Returns ``(records, stats)`` where records are ``(name, blob, rgba|None, None, [path])`` + tuples for the adacpp emitters and stats counts the walk. Raises + :class:`NativeExportUnsupported` if any geometric object can't be serialized (listing the + per-type reasons), so the caller can fall back wholesale to the Python writer. + """ + from ada.api.mass import MassPoint + from ada.cadit.ngeom.serialize import _Encoder, _Unsupported + + records: list[tuple] = [] + unsupported: dict[str, int] = {} + part_ids: dict[int, int] = {} # id(Part) -> stable per-run int for the path rep-id slot + n_objects = 0 + + def _path_for(obj) -> list[tuple[int, str]]: + # Root-first (rep_id, name) levels; the last level is the object's own (leaf) product. + chain = [] + p = getattr(obj, "parent", None) + while p is not None: + chain.append(p) + p = getattr(p, "parent", None) + levels: list[tuple[int, str]] = [] + for prt in reversed(chain): + pid = part_ids.setdefault(id(prt), len(part_ids) + 1) + levels.append((pid, str(prt.name))) + leaf_id = len(part_ids) + 1000000 + len(records) # unique, disjoint from part ids + levels.append((leaf_id, str(obj.name))) + return levels + + for obj in part.get_all_physical_objects(pipe_to_segments=True): + if isinstance(obj, MassPoint): + continue # no geometry — the Python writers skip these too + n_objects += 1 + try: + geom = obj.solid_geom() + except Exception as ex: # no parametric solid_geom (e.g. raw-OCC import) + unsupported[f"{type(obj).__name__}: {type(ex).__name__}: {ex}"] = ( + unsupported.get(f"{type(obj).__name__}: {type(ex).__name__}: {ex}", 0) + 1 + ) + continue + enc = _Encoder() + try: + idx = enc.root(geom) + except _Unsupported as ex: + inner = getattr(geom, "geometry", geom) + key = f"{type(inner).__name__}: {ex}" + unsupported[key] = unsupported.get(key, 0) + 1 + continue + blob = enc.finish([(idx, str(obj.name))]) + records.append((str(obj.name), blob, _object_color_rgba(obj), None, [_path_for(obj)])) + + if unsupported: + n_bad = sum(unsupported.values()) + raise NativeExportUnsupported( + f"native NGEOM export: {n_bad}/{n_objects} object(s) not serializable — falling back " + f"to the Python writer. Reasons: {unsupported}" + ) + if not records: + raise NativeExportUnsupported("native NGEOM export: model has no serializable physical objects") + stats = {"objects": n_objects, "records": len(records)} + return records, stats + + +def _unit_scale(part: Part) -> float: + from ada.base.units import Units + + return 0.001 if part.units == Units.MM else 1.0 + + +def native_to_stp(part: Part, out_path: str | pathlib.Path) -> dict: + """Write ``part`` to AP242 STEP via adacpp's NGEOM-record emitter. Returns the audit dict.""" + if not native_ngeom_writers_available(): + raise NativeExportUnsupported("adacpp with stream_ngeom_to_step is not importable") + import adacpp + + out_path = pathlib.Path(out_path) + out_path.parent.mkdir(parents=True, exist_ok=True) + records, walk = collect_ngeom_records(part) + stats = adacpp.cad.stream_ngeom_to_step(records, str(out_path), unit_scale=_unit_scale(part)) + stats.update(walk) + if stats.get("solids_skipped"): + raise NativeExportUnsupported( + f"native STEP emit skipped {stats['solids_skipped']} solid(s): {stats.get('drop_reasons')}" + ) + logger.info("native NGEOM->STEP: %s", stats) + return stats + + +def _check_mesh_emit_stats(stats: dict, what: str) -> None: + """No geometry left behind: any solid the tessellator skipped, or any dropped face + (the [GEOMHEALTH-JSON] counter), fails the native path so the caller falls back + WHOLESALE to the Python writer instead of shipping a silently-partial mesh.""" + if stats.get("solids_skipped"): + raise NativeExportUnsupported( + f"native {what} emit skipped {stats['solids_skipped']} solid(s): {stats.get('drop_reasons')}" + ) + if stats.get("faces_dropped"): + raise NativeExportUnsupported( + f"native {what} emit dropped {stats['faces_dropped']} face(s): {stats.get('drop_reasons')}" + ) + + +def _stream_tess_params() -> dict: + """Tessellation-density knobs for the record emitters, sourced from the SAME single source of + truth as the Python scene/stream path (``ada.cad.registry.stream_tess_defaults`` + + ``stream_tess_model_scale``) — so the native route produces the same density the Python + ``to_gltf`` / ``to_trimesh_scene`` legs would have, and one nominal config can't mean + different densities on different call paths.""" + from ada.cad.registry import stream_tess_defaults, stream_tess_model_scale + + deflection, angular_deg = stream_tess_defaults() + return {"deflection": deflection, "angular_deg": angular_deg, "model_scale": stream_tess_model_scale()} + + +def native_to_glb( + part: Part, out_path: str | pathlib.Path, *, meshopt: bool = True, pipeline: str = "libtess2" +) -> dict: + """Write ``part`` to a viewer-structured GLB via adacpp's NGEOM-record tessellate+emit core + (merge-by-colour materials, per-solid draw ranges, inline EXT_meshopt when ``meshopt``). + Output is metres regardless of the model units. Returns the audit dict.""" + if not native_mesh_writers_available(): + raise NativeExportUnsupported("adacpp with stream_ngeom_to_glb is not importable") + import adacpp + + out_path = pathlib.Path(out_path) + out_path.parent.mkdir(parents=True, exist_ok=True) + records, walk = collect_ngeom_records(part) + stats = adacpp.cad.stream_ngeom_to_glb( + records, + str(out_path), + meshopt=meshopt, + pipeline=pipeline, + unit_scale=_unit_scale(part), + **_stream_tess_params(), + ) + stats.update(walk) + _check_mesh_emit_stats(stats, "GLB") + logger.info("native NGEOM->GLB: %s", stats) + return stats + + +def native_to_mesh(part: Part, out_path: str | pathlib.Path, fmt: str, *, pipeline: str = "libtess2") -> dict: + """Write ``part`` to a binary STL / welded OBJ (``fmt``: ``"stl"`` | ``"obj"``) via adacpp's + NGEOM-record tessellate+emit core. Output is metres regardless of the model units. + Returns the audit dict.""" + if not native_mesh_writers_available(): + raise NativeExportUnsupported("adacpp with stream_ngeom_to_mesh is not importable") + import adacpp + + out_path = pathlib.Path(out_path) + out_path.parent.mkdir(parents=True, exist_ok=True) + records, walk = collect_ngeom_records(part) + stats = adacpp.cad.stream_ngeom_to_mesh( + records, str(out_path), fmt, pipeline=pipeline, unit_scale=_unit_scale(part), **_stream_tess_params() + ) + stats.update(walk) + _check_mesh_emit_stats(stats, fmt.upper()) + logger.info("native NGEOM->%s: %s", fmt.upper(), stats) + return stats + + +def native_to_ifc(part: Part, out_path: str | pathlib.Path, schema: str = "IFC4X3_ADD2") -> dict: + """Write ``part`` to IFC via adacpp's NGEOM-record emitter. Returns the audit dict.""" + if not native_ngeom_writers_available(): + raise NativeExportUnsupported("adacpp with stream_ngeom_to_ifc is not importable") + import adacpp + + out_path = pathlib.Path(out_path) + out_path.parent.mkdir(parents=True, exist_ok=True) + records, walk = collect_ngeom_records(part) + stats = adacpp.cad.stream_ngeom_to_ifc(records, str(out_path), schema=schema, unit_scale=_unit_scale(part)) + stats.update(walk) + if stats.get("solids_skipped"): + raise NativeExportUnsupported( + f"native IFC emit skipped {stats['solids_skipped']} solid(s): {stats.get('drop_reasons')}" + ) + logger.info("native NGEOM->IFC: %s", stats) + return stats diff --git a/src/ada/cadit/ngeom/serialize.py b/src/ada/cadit/ngeom/serialize.py index 87e7f3ac7..812c718e0 100644 --- a/src/ada/cadit/ngeom/serialize.py +++ b/src/ada/cadit/ngeom/serialize.py @@ -1,7 +1,7 @@ """Serialize ``ada.geom`` geometry into the NGEOM binary buffer (spec v1). The buffer is the contract with adacpp's neutral geometry layer (no adacpp import here). See -dap/plan/v3/spec_neutral_geometry_schema.md for the wire format and tag catalog. +the neutral-geometry schema spec for the wire format and tag catalog. """ from __future__ import annotations @@ -28,6 +28,14 @@ FACE_STATS: Counter = Counter() FACE_DROP_REASONS: Counter = Counter() +# Root-level (whole-geometry) drops. ``connected_face_set`` counts individual faces, but an +# entire root geometry whose top-level type isn't mappable (``_dispatch`` -> _Unsupported) was +# skipped SILENTLY in ``serialize_geometries`` with no counter — a whole solid could leave the +# stream with nothing recorded, the same silent-drop class the per-face counters exist to expose. +# Keys: "total", "built", "dropped"; reasons tallied by (geometry type, exception). +ROOT_STATS: Counter = Counter() +ROOT_DROP_REASONS: Counter = Counter() + def consume_face_stats() -> dict[str, int]: """Return and reset this process's per-face serialization counters.""" @@ -43,6 +51,20 @@ def consume_face_drop_reasons() -> dict[str, int]: return out +def consume_root_stats() -> dict[str, int]: + """Return and reset this process's per-root (whole-geometry) serialization counters.""" + out = dict(ROOT_STATS) + ROOT_STATS.clear() + return out + + +def consume_root_drop_reasons() -> dict[str, int]: + """Return and reset the per-reason tally of root geometries this path could not serialize.""" + out = dict(ROOT_DROP_REASONS) + ROOT_DROP_REASONS.clear() + return out + + # Arrays shorter than this serialize via per-scalar ``struct.pack`` (faster than building a numpy # array); longer arrays go through ``numpy.tobytes()`` (the B-spline / polyline bulk path). _BULK_MIN = 16 @@ -505,23 +527,33 @@ def face_surface(self, f: su.Face) -> int: body += b"".join(self.i32(b) for b in bounds) # 1-2 bounds/face: inline (per-face hot path) return self._add(_FACE_SURFACE, body) + def _count_mapped_face(self, f, build) -> int | None: + """Map one face to a record index via ``build``, tallying the outcome on the + shared FACE_STATS/FACE_DROP_REASONS counters. Skipping a face that can't be + mapped stays the behaviour; being SILENT about it does not — every face-set + path (connected_face_set / face_based / shell_based) routes through here so a + dropped OR merely un-counted face can never hide under a green coverage check. + Tally by (surface type, exception) rather than logging per face — a bad file + can carry thousands, and the caller summarizes once per run.""" + FACE_STATS["total"] += 1 + try: + idx = build(f) + FACE_STATS["built"] += 1 + return idx + except Exception as ex: # noqa: BLE001 - skip any face that can't be mapped (robustness) + FACE_STATS["dropped"] += 1 + surf = getattr(f, "face_surface", None) + FACE_DROP_REASONS[f"{type(surf).__name__}: {type(ex).__name__}: {ex}"] += 1 + return None + def connected_face_set(self, cfs) -> int: # ConnectedFaceSet / ClosedShell / OpenShell all expose ``cfs_faces`` (FaceSurface or # the structurally-identical AdvancedFace). Skip any face that can't be mapped. faces = [] for f in cfs.cfs_faces: - FACE_STATS["total"] += 1 - try: - faces.append(self.face_surface(f)) - FACE_STATS["built"] += 1 - except Exception as ex: # noqa: BLE001 - skip any face that can't be mapped (robustness) - # Skipping stays the behaviour; being SILENT about it does not. Tally by - # (surface type, exception) rather than logging per face — a bad file can carry - # thousands, and the caller summarizes once per run. - FACE_STATS["dropped"] += 1 - surf = getattr(f, "face_surface", None) - FACE_DROP_REASONS[f"{type(surf).__name__}: {type(ex).__name__}: {ex}"] += 1 - continue + idx = self._count_mapped_face(f, self.face_surface) + if idx is not None: + faces.append(idx) return self._add(_CONNECTED_FACE_SET, self.i32(len(faces)) + self._i32_raw(faces)) # --- solids ------------------------------------------------------------------------ @@ -546,12 +578,15 @@ def _to3(p): return _composite_curve_loop_points(curve) segs = getattr(curve, "segments", None) - if segs and any(isinstance(s, cu.ArcLine) for s in segs): + if segs and any(isinstance(s, (cu.ArcLine, cu.BSplineCurveWithKnots)) for s in segs): pts: list[tuple[float, float, float]] = [] for s in segs: if isinstance(s, cu.ArcLine): arc = _sample_arc(s.start, s.midpoint, s.end) pts.extend(_to3(p) for p in arc[:-1]) # end repeats the next segment's start + elif isinstance(s, cu.BSplineCurveWithKnots): # analytic spline edge -> polyline + sp = s.sample(max(16, int(s.degree) * 8)) + pts.extend(_to3(p) for p in sp[:-1]) # end repeats the next segment's start else: # Edge / straight segment pts.append(_to3(s.start)) return pts @@ -891,10 +926,9 @@ def face_based_surface_model(self, fbsm) -> int: faces = [] for cfs in fbsm.fbsm_faces: for f in getattr(cfs, "cfs_faces", []): - try: - faces.append(self._any_face(f)) - except Exception: # noqa: BLE001 - continue + idx = self._count_mapped_face(f, self._any_face) + if idx is not None: + faces.append(idx) return self._add(_CONNECTED_FACE_SET, self.i32(len(faces)) + self._i32_raw(faces)) def shell_based_surface_model(self, sbsm) -> int: @@ -904,10 +938,9 @@ def shell_based_surface_model(self, sbsm) -> int: faces = [] for shell in sbsm.sbsm_boundary: for f in getattr(shell, "cfs_faces", []): - try: - faces.append(self._any_face(f)) - except Exception: # noqa: BLE001 - continue + idx = self._count_mapped_face(f, self._any_face) + if idx is not None: + faces.append(idx) return self._add(_CONNECTED_FACE_SET, self.i32(len(faces)) + self._i32_raw(faces)) def boolean_result(self, br) -> int: @@ -1189,8 +1222,16 @@ def serialize_geometries(items: Iterable[tuple[str, object]]) -> bytes: enc = _Encoder() roots: list[tuple[int, str]] = [] for rid, geom in items: + ROOT_STATS["total"] += 1 try: roots.append((enc.root(geom), rid)) - except _Unsupported: + ROOT_STATS["built"] += 1 + except _Unsupported as ex: + # A whole geometry that can't be mapped is skipped for robustness — but not + # silently: tally it (by geometry type) so the run summary flags an unmapped + # solid instead of it vanishing from the stream under a green check. + ROOT_STATS["dropped"] += 1 + inner = getattr(geom, "geometry", geom) + ROOT_DROP_REASONS[f"{type(inner).__name__}: {ex}"] += 1 continue return enc.finish(roots) diff --git a/src/ada/cadit/sat/read/advanced_face.py b/src/ada/cadit/sat/read/advanced_face.py index a9614d333..6d6ec5095 100644 --- a/src/ada/cadit/sat/read/advanced_face.py +++ b/src/ada/cadit/sat/read/advanced_face.py @@ -65,15 +65,39 @@ def _bboxes_disjoint(a, b, tol: float = 1e-3) -> bool: def get_face_bound(acis_record: AcisRecord) -> list[geo_su.FaceBound]: - """Gets the edge loop from the SAT object data.""" - - loop_rec = acis_record.sat_store.get(acis_record.chunks[7]) - edges = [] - - for edge in iter_loop_coedges(loop_rec): - edges.append(edge) - - return [geo_su.FaceBound(bound=geo_cu.EdgeLoop(edges), orientation=True)] + """Gets the outer edge loop from the SAT object data. + + A face's loops are a linked list — ``loop.chunks[6]`` points to the next loop, ``chunks[7]`` + to the loop's first coedge, and ``chunks[16]`` is the loop kind (``periphery`` = outer boundary, + ``hole`` = inner). The outer boundary is usually first, but ACIS sometimes orders a *degenerate* + hole loop ahead of it: a single zero-length, curve-less coedge marking a surface singularity + (its two vertices are the same point). ``iter_loop_coedges`` correctly steps over that coedge, so + reading only the face's first loop then yields an empty wire and the whole plate fails to build + (``build_advanced_face: wire build failed``) — dropping a valid plate. + + Walk the chain and take the outer boundary: the ``periphery`` loop if one is marked, else the + first loop that actually carries edges. Inner holes are not represented here (the downstream + planar/advanced-face builders take a single bound), matching the prior single-loop behaviour. + """ + loop_ptr = acis_record.chunks[7] + seen: set[str] = set() + first_nonempty: list | None = None + + while loop_ptr and loop_ptr != "$-1" and loop_ptr not in seen: + seen.add(loop_ptr) + loop_rec = acis_record.sat_store.get(loop_ptr) + if loop_rec is None: + break + edges = list(iter_loop_coedges(loop_rec)) + if edges: + if first_nonempty is None: + first_nonempty = edges + # Prefer the periphery (outer) loop over any non-degenerate hole loop. + if len(loop_rec.chunks) > 16 and loop_rec.chunks[16] == "periphery": + return [geo_su.FaceBound(bound=geo_cu.EdgeLoop(edges), orientation=True)] + loop_ptr = loop_rec.chunks[6] + + return [geo_su.FaceBound(bound=geo_cu.EdgeLoop(first_nonempty or []), orientation=True)] def get_face_surface(face_record: AcisRecord) -> geo_su.SURFACE_GEOM_TYPES | geo_su.Plane: diff --git a/src/ada/cadit/sat/read/faces.py b/src/ada/cadit/sat/read/faces.py index e2c246a80..54dbc2399 100644 --- a/src/ada/cadit/sat/read/faces.py +++ b/src/ada/cadit/sat/read/faces.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING +from ada.api.curves import ArcEdge, PlateEdgeCurve, SplineEdge from ada.cadit.sat.exceptions import ACISInsufficientPointsError from ada.cadit.sat.read.sat_entities import AcisRecord from ada.config import Config, logger @@ -45,14 +46,14 @@ def get_face_name_and_points(self, acis_record: AcisRecord) -> tuple[str, list[t edges = self._drop_whisker_coedges(edges) try: - points = self.get_points(edges) + points, edge_curves = self.get_points(edges) except ACISInsufficientPointsError as e: logger.warning(f"face: '{name}' failed to get points due to {e}. Skipping...") return None points = remove_near_collinear_points(points) - return name, points + return name, points, edge_curves def _loop_type(self, loop: AcisRecord) -> str | None: for token in loop.chunks: @@ -134,71 +135,45 @@ def get_points(self, edges: list[AcisRecord]) -> list[tuple[float]]: if coedge_first_direction == "reversed": points.reverse() - # Curved boundary edges (intcurve/ellipse) otherwise collapse to the chord between their two - # corners — a deck plate meeting a curved hull skin renders straight while the skin beside it - # curves (measured on OP1_v1007_hullskin FACE00004482: a 0.072 m bulge flattened out of a - # 1.4 m plate). Splice the sampled curve into the FINISHED corner polygon, between the two - # corners that edge joins. Done here, after the corner sequence is complete, precisely because - # `edges` is NOT in geometric chain order — the loop above works by collecting far-endpoints - # and deduping, not by walking the chain, so there is no "insert before edge i" position to - # write into. A straight-only loop splices nothing and keeps its exact historic point list. + # Curved boundary edges (circle/ellipse/spline) otherwise collapse to the chord between their + # two corners — a deck plate meeting a curved hull skin renders straight while the skin beside + # it curves (measured on a hull-skin model FACE00004482: a 0.072 m bulge flattened out of a + # 1.4 m plate). Carry each analytically as an ArcEdge / SplineEdge keyed on its two corners; + # CurvePoly2d.build_edge_segments turns them into real ArcSegment/SplineSegment (arc exact in + # IFC/STEP; spline analytic in OCC/IFC, discretized in NGEOM/STEP). The corner point list is + # unchanged — no sampling into it — so a straight-only loop keeps its exact historic points. + edge_curves: list[PlateEdgeCurve] = [] if Config().sat_plate_curved_edges: - points = self._splice_curved_edges(points, edges) + edge_curves = self._collect_edge_curves(edges) - return points + return points, edge_curves - def _splice_curved_edges(self, points: list[tuple[float]], edges: list[AcisRecord]) -> list[tuple[float]]: - """Insert each curved edge's sampled interior between the two corners it joins. + def _collect_edge_curves(self, edges: list[AcisRecord]) -> list[PlateEdgeCurve]: + """Analytic ``ArcEdge`` / ``SplineEdge`` specs for the curved boundary edges (keyed by corner). - Purely additive: every original corner stays, in its original order. An edge whose two - corners aren't adjacent in `points` (or which we couldn't sample) is skipped and keeps its - chord — so the worst case is exactly today's output. + Best-effort by design: any edge we can't read is simply absent, and the caller keeps that + edge's straight chord. A curved boundary that silently stays straight is the bug we're fixing; + one that fails to read is only as bad as the status quo. """ - curved = self._curved_edge_interiors(edges) - if not curved: - return points + from ada.cadit.sat.read.plate_edge_curves import edge_curve_descriptor - pts = list(points) - for i, coedge in enumerate(edges): - interior = curved.get(i) - if not interior: - continue - p1, p2 = self.get_points_from_edge(coedge) - near, far = (p1, p2) if str(coedge.chunks[-4]) == "forward" else (p2, p1) - n = len(pts) - for k in range(n): - a, b = pts[k], pts[(k + 1) % n] - if a == near and b == far: - pts[k + 1 : k + 1] = interior - break - if a == far and b == near: - pts[k + 1 : k + 1] = list(reversed(interior)) - break - else: - logger.debug(f"curved edge {i}: corners not adjacent in the outline; keeping its chord") - return pts - - def _curved_edge_interiors(self, edges: list[AcisRecord]) -> dict[int, list[tuple[float, float, float]]]: - """{edge index -> interior points, ordered near->far along the coedge's own direction}. - - Best-effort by design: any edge we can't read or sample simply isn't in the dict, and the - caller keeps today's chord for it. A curved boundary that silently stays straight is the bug - we're fixing; a curved boundary that fails to sample is only as bad as the status quo. - """ - from ada.cadit.sat.read.plate_edge_curves import edge_interior_points - - out: dict[int, list[tuple[float, float, float]]] = {} + specs: list[PlateEdgeCurve] = [] for i, coedge in enumerate(edges): try: p1, p2 = self.get_points_from_edge(coedge) near, far = (p1, p2) if str(coedge.chunks[-4]) == "forward" else (p2, p1) - pts = edge_interior_points(coedge, self.sat_store, near, far) + desc = edge_curve_descriptor(coedge, self.sat_store, near, far) except Exception as exc: # noqa: BLE001 - never let a curve read drop a whole plate - logger.debug(f"curved edge sample failed on coedge {i}: {exc}") + logger.debug(f"curved edge read failed on coedge {i}: {exc}") + continue + if desc is None: continue - if pts: - out[i] = pts - return out + kind, payload = desc + if kind == "arc": + specs.append(ArcEdge(a=near, b=far, midpoint=tuple(payload))) + elif kind == "spline": + specs.append(SplineEdge(a=near, b=far, curve=payload)) + return specs def get_edges(self, face_data_list: list[str]) -> list[AcisRecord]: loop = self._get_primary_loop(face_data_list) diff --git a/src/ada/cadit/sat/read/plate_edge_curves.py b/src/ada/cadit/sat/read/plate_edge_curves.py index c516f3362..36cf7605f 100644 --- a/src/ada/cadit/sat/read/plate_edge_curves.py +++ b/src/ada/cadit/sat/read/plate_edge_curves.py @@ -6,7 +6,7 @@ ENDPOINTS. Whatever the edge's curve does between its two vertices is discarded, so a plate whose boundary follows a spline (e.g. a deck plate meeting a curved hull skin) comes out as a straight chord between its corners. Measured on -``OP1_v1007_hullskin.xml`` face ``FACE00004482``: 4 coedges — 1 straight, 2 +``a hull-skin Genie-XML model`` face ``FACE00004482``: 4 coedges — 1 straight, 2 ``intcurve``, 1 ``ellipse`` — collapsed to a 4-point polygon. The loss is doubled by the target type: ``CurvePoly2d`` (the ``Plate`` outline) is @@ -23,7 +23,7 @@ so the two faces still do not share edge points and the seam is still not watertight — it just looks right. A real fix is B-spline edge support in ``Plate`` / ``CurvePoly2d``, or routing curved-boundary flat plates through ``AdvancedFace`` -like the curved shells. See dap ``plan/v3/notes_plate_bspline_edges.md``. +like the curved shells. See the internal design notes. """ from __future__ import annotations @@ -36,8 +36,6 @@ # remove_near_collinear_points (tol 1e-8*scale^2), so over-sampling a nearly # straight edge costs nothing; under-sampling a tight one is visible. DEFAULT_CURVE_SAMPLES = 24 -# Ring resolution for ellipse/circle sampling before clipping to the edge's arc. -_ELLIPSE_RING_SAMPLES = 2048 def _de_boor(x: float, knots, cp, deg: int): @@ -60,42 +58,28 @@ def _de_boor(x: float, knots, cp, deg: int): def _bspline_points(curve, n: int) -> list[tuple[float, float, float]] | None: - """Sample a BSplineCurveWithKnots at n params across its knot span.""" + """Sample a ``BSplineCurveWithKnots`` at n params across its knot span, or None if unsamplable. + + Thin wrapper over :meth:`ada.geom.curves.BSplineCurveWithKnots.sample` (the single home of the de + Boor evaluator) that keeps this module's "unreadable curve => keep the chord" contract by returning + None instead of raising. + """ try: - cp = np.asarray([list(p)[:3] for p in curve.control_points_list], dtype=float) - # SAT/IFC store knots + multiplicities separately; de Boor wants them expanded. - knots = np.repeat(np.asarray(curve.knots, dtype=float), np.asarray(curve.knot_multiplicities, dtype=int)) - deg = int(curve.degree) - if deg < 1 or len(cp) <= deg or len(knots) != len(cp) + deg + 1: - # Malformed (or a form we don't model) — fall back to the chord. - return None - # Rational curves: weight the control points, then divide through after evaluation. - w = getattr(curve, "weights_data", None) or getattr(curve, "weights", None) - if w is not None and len(w) == len(cp): - wa = np.asarray(w, dtype=float).reshape(-1, 1) - cp = np.hstack([cp * wa, wa]) - lo, hi = float(knots[deg]), float(knots[len(knots) - deg - 1]) - if not np.isfinite([lo, hi]).all() or hi <= lo: - return None - out = [] - for x in np.linspace(lo, hi, n): - p = _de_boor(float(x), knots, cp, deg) - if p.shape[0] == 4: # rational: de-homogenize - if p[3] == 0: - return None - p = p[:3] / p[3] - out.append(tuple(float(v) for v in p[:3])) - arr = np.asarray(out) - if not np.isfinite(arr).all(): - return None - return out + return curve.sample(n) except Exception as exc: # noqa: BLE001 - a curve we can't sample falls back to the chord logger.debug(f"bspline sample failed: {exc}") return None -def _ellipse_points(curve, n: int) -> list[tuple[float, float, float]] | None: - """Sample a full Ellipse/Circle. Trimming to the edge happens in _clip_to_endpoints.""" +def _ellipse_arc_points(curve, a, b, n: int) -> list[tuple[float, float, float]] | None: + """Interior points along the Ellipse/Circle ARC from vertex `a` to vertex `b`. + + Analytic clip: a point on the ellipse is ``p = c + ra*cos(t)*x + rb*sin(t)*y``, so each vertex's + parameter angle ``t`` is recovered by projecting ``p - c`` onto the (x, y) frame. We then sample + the SHORT arc between the two angles directly — ~n points on the actual edge — instead of the old + 2048-point full ring that ``_clip_to_endpoints`` immediately threw all but ~n of away (~27s of the + hull-skin import). Endpoints are dropped: the caller already holds the exact vertices. + """ try: c = np.asarray(list(curve.position.location)[:3], dtype=float) z = np.asarray(list(curve.position.axis)[:3], dtype=float) @@ -107,14 +91,24 @@ def _ellipse_points(curve, n: int) -> list[tuple[float, float, float]] | None: rb = float(getattr(curve, "semi_axis2", getattr(curve, "radius", 0.0))) if ra <= 0 or rb <= 0: return None - # Sample the FULL ring densely, not n*4: the edge is usually a small arc of it (measured: - # a 1.46 m edge spanning ~5 of 96 ring samples), so ring density must be high enough that - # the clipped arc still carries ~n points. 2048 vectorised points is free. - t = np.linspace(0.0, 2.0 * np.pi, _ELLIPSE_RING_SAMPLES, endpoint=False) - pts = c + np.outer(ra * np.cos(t), x) + np.outer(rb * np.sin(t), y) - return [tuple(float(v) for v in p) for p in pts] + + def _angle(p) -> float: + d = np.asarray(p, dtype=float) - c + return float(np.arctan2(float(d @ y) / rb, float(d @ x) / ra)) + + ta = _angle(a) + # The edge is the short arc: sweep the signed angular delta wrapped into (-pi, pi]. A plate + # boundary edge is always the shorter way round its ring (measured: ~5 of 96 samples), and for + # such small sweeps short-angle == short-arc-length for an ellipse too, so this matches the + # old arclength-based clip without sampling the full ring. + delta = (_angle(b) - ta + np.pi) % (2.0 * np.pi) - np.pi + if abs(delta) < 1e-12: + return [] + ts = ta + delta * np.linspace(0.0, 1.0, n + 2)[1:-1] + pts = c + np.outer(ra * np.cos(ts), x) + np.outer(rb * np.sin(ts), y) + return [tuple(p) for p in pts.tolist()] except Exception as exc: # noqa: BLE001 - logger.debug(f"ellipse sample failed: {exc}") + logger.debug(f"ellipse arc sample failed: {exc}") return None @@ -157,11 +151,16 @@ def arclen(idx): return [tuple(float(v) for v in arr[i]) for i in inner] -def edge_interior_points(coedge, sat_store, a, b, n: int = DEFAULT_CURVE_SAMPLES) -> list[tuple[float, float, float]]: - """Interior 3D points along `coedge`'s curve, ordered from vertex `a` to vertex `b`. +def edge_curve_descriptor(coedge, sat_store, a, b, n: int = DEFAULT_CURVE_SAMPLES): + """Analytic descriptor for a curved coedge from vertex `a` to vertex `b`, or None. - Returns [] for straight edges, unreadable curves, or anything we can't sample — - the caller then keeps today's chord, so this can only add detail, never lose a plate. + - ``("arc", midpoint)`` — circle/ellipse: the point on the arc halfway between `a` and `b`. + - ``("spline", curve)`` — B-spline: the analytic ``ada.geom.curves.BSplineCurveWithKnots`` itself. + - ``None`` — straight, unreadable, or unsupported: the caller keeps the chord. + + The caller carries the payload as an :class:`~ada.api.curves.ArcEdge` / ``SplineEdge`` so the plate + keeps a real analytic segment (arc exact in IFC/STEP; spline analytic in OCC/IFC, discretized in + NGEOM/STEP) instead of being sampled into straight outline points at read time. """ # Local import: ada.cadit.sat.read.curves imports from this package's siblings. from ada.cadit.sat.read.curves import get_edge @@ -169,22 +168,27 @@ def edge_interior_points(coedge, sat_store, a, b, n: int = DEFAULT_CURVE_SAMPLES try: oe = get_edge(coedge) except Exception as exc: # noqa: BLE001 - unreadable curve => keep the chord - logger.debug(f"edge_interior_points: get_edge failed: {exc}") - return [] + logger.debug(f"edge_curve_descriptor: get_edge failed: {exc}") + return None curve = getattr(oe, "edge_element", None) or getattr(oe, "edge_geometry", None) or oe curve = getattr(curve, "edge_geometry", curve) from ada.geom.curves import BSplineCurveWithKnots, Circle, Ellipse, Line if isinstance(curve, Line): - return [] - pts = None - closed = False + return None + if isinstance(curve, (Ellipse, Circle)): + mid = _ellipse_arc_points(curve, a, b, 1) # n=1 -> the single arc midpoint + if not mid: + return None + return ("arc", mid[0]) if isinstance(curve, BSplineCurveWithKnots): - pts = _bspline_points(curve, max(n * 4, 32)) # open: sampled across the knot span - elif isinstance(curve, (Ellipse, Circle)): - pts = _ellipse_points(curve, n) - closed = True # sampled as a full ring; the edge is an arc of it - if not pts: - return [] - return _clip_to_endpoints(pts, a, b, n, closed) + # Sanity-check the spec is samplable before committing to the analytic edge; else keep the chord. + try: + curve.sample(2) + except Exception as exc: # noqa: BLE001 + logger.debug(f"edge_curve_descriptor: unsamplable b-spline ({exc})") + return None + return ("spline", curve) + return None + return [] diff --git a/src/ada/cadit/sat/store.py b/src/ada/cadit/sat/store.py index edfb63cb9..d65ae9eda 100644 --- a/src/ada/cadit/sat/store.py +++ b/src/ada/cadit/sat/store.py @@ -348,7 +348,7 @@ def face_has_curved_edge(self, face_record: AcisRecord) -> bool: loop = self.sat_store.get(loop.chunks[6]) return False - def iter_flat_plates(self) -> Iterable[tuple[str, list[tuple[float, float, float]]]]: + def iter_flat_plates(self) -> Iterable[tuple[str, list[tuple[float, float, float]], list]]: for face_record in self.iter_faces(): # face_surface = self.sat_store.get(face_record.chunks[10]) # if face_surface.type == "spline-surface": diff --git a/src/ada/cadit/step/read/geom/curves.py b/src/ada/cadit/step/read/geom/curves.py index 5523f1f5c..941ec50a6 100644 --- a/src/ada/cadit/step/read/geom/curves.py +++ b/src/ada/cadit/step/read/geom/curves.py @@ -1,6 +1,6 @@ """Back-compat re-export. The OCC implementation moved to `ada.occ.step.geom.curves` (the pythonocc CAD backend's home); this shim keeps the historical import path working without -itself importing OCC. See dap plan/v3 Phase 2 (STEP-IO relocation).""" +itself importing OCC. See the internal design notes Phase 2 (STEP-IO relocation).""" from ada.occ.step.geom.curves import get_wires_from_face, process_wire diff --git a/src/ada/cadit/step/read/geom/geom_reader.py b/src/ada/cadit/step/read/geom/geom_reader.py index 77610f970..8f33d6115 100644 --- a/src/ada/cadit/step/read/geom/geom_reader.py +++ b/src/ada/cadit/step/read/geom/geom_reader.py @@ -1,6 +1,6 @@ """Back-compat re-export. The OCC implementation moved to `ada.occ.step.geom.geom_reader` (the pythonocc CAD backend's home); this shim keeps the historical import path working without -itself importing OCC. See dap plan/v3 Phase 2 (STEP-IO relocation).""" +itself importing OCC. See the internal design notes Phase 2 (STEP-IO relocation).""" from ada.occ.step.geom.geom_reader import import_geometry_from_step_geom diff --git a/src/ada/cadit/step/read/geom/helpers.py b/src/ada/cadit/step/read/geom/helpers.py index 073529957..632ae89f8 100644 --- a/src/ada/cadit/step/read/geom/helpers.py +++ b/src/ada/cadit/step/read/geom/helpers.py @@ -1,6 +1,6 @@ """Back-compat re-export. The OCC implementation moved to `ada.occ.step.geom.helpers` (the pythonocc CAD backend's home); this shim keeps the historical import path working without -itself importing OCC. See dap plan/v3 Phase 2 (STEP-IO relocation).""" +itself importing OCC. See the internal design notes Phase 2 (STEP-IO relocation).""" from ada.occ.step.geom.helpers import ( array1_to_int_list, diff --git a/src/ada/cadit/step/read/geom/surfaces.py b/src/ada/cadit/step/read/geom/surfaces.py index efa7a5726..36a782e28 100644 --- a/src/ada/cadit/step/read/geom/surfaces.py +++ b/src/ada/cadit/step/read/geom/surfaces.py @@ -1,6 +1,6 @@ """Back-compat re-export. The OCC implementation moved to `ada.occ.step.geom.surfaces` (the pythonocc CAD backend's home); this shim keeps the historical import path working without -itself importing OCC. See dap plan/v3 Phase 2 (STEP-IO relocation).""" +itself importing OCC. See the internal design notes Phase 2 (STEP-IO relocation).""" from ada.occ.step.geom.surfaces import ( get_bsplinesurface_with_knots, diff --git a/src/ada/cadit/step/read/reader_utils.py b/src/ada/cadit/step/read/reader_utils.py index 3cc7c2c26..e6f9b5a46 100644 --- a/src/ada/cadit/step/read/reader_utils.py +++ b/src/ada/cadit/step/read/reader_utils.py @@ -1,6 +1,6 @@ """Back-compat re-export. The OCC implementation moved to `ada.occ.step.reader_utils` (the pythonocc CAD backend's home); this shim keeps the historical import path working without -itself importing OCC. See dap plan/v3 Phase 2 (STEP-IO relocation).""" +itself importing OCC. See the internal design notes Phase 2 (STEP-IO relocation).""" from ada.occ.step.reader_utils import ( iter_children, diff --git a/src/ada/cadit/step/read/stream_reader.py b/src/ada/cadit/step/read/stream_reader.py index e50df6675..3a4b7db29 100644 --- a/src/ada/cadit/step/read/stream_reader.py +++ b/src/ada/cadit/step/read/stream_reader.py @@ -39,8 +39,9 @@ # (reclaimable, file-backed), the spilled id/offset index tmpfiles, anonymous # Python heap, plus a sizing of the ``colour_map`` / ``tmap`` dicts. This is the # measurement that decides where the ~2.1 GB parent peak actually lives before -# we optimise (see dap plan: cut peak parent RSS of STEP->GLB). +# we optimise (see internal notes: cut peak parent RSS of STEP->GLB). # --------------------------------------------------------------------------- +import math import os as _os import re import threading @@ -205,6 +206,7 @@ def _mem_probe(label: str, *, step_path=None, idx_paths=(), sized=None) -> None: ) from ada.geom.surfaces import ( # noqa: E402 AdvancedFace, + ArbitraryProfileDef, BSplineSurfaceForm, BSplineSurfaceWithKnots, ClosedShell, @@ -711,8 +713,11 @@ def _world_matrices(rep_id: int, _seen: frozenset) -> list: } # Both arg forms occur in the wild: SI_UNIT(.MILLI.,.METRE.) and SI_UNIT(.METRE.). -_SI_LEN_RE = re.compile(r"SI_UNIT\(\s*(?:(\.\w+\.|\$)\s*,\s*)?\.METRE\.\s*\)") -_CONV_NAME_RE = re.compile(r"CONVERSION_BASED_UNIT\(\s*'([^']*)'") +# The ``\s*`` before ``(`` matters: some exporters pretty-print the record as +# ``SI_UNIT ( .MILLI., .METRE. )`` (space between the keyword and its arg list), and +# without it an mm file falls through to the ``assuming metres`` default — a 1000x scale error. +_SI_LEN_RE = re.compile(r"SI_UNIT\s*\(\s*(?:(\.\w+\.|\$)\s*,\s*)?\.METRE\.\s*\)") +_CONV_NAME_RE = re.compile(r"CONVERSION_BASED_UNIT\s*\(\s*'([^']*)'") # Chunk size for the ``os.pread`` scan that locates the LENGTH_UNIT record. Module-level @@ -860,6 +865,109 @@ def _representation_length_scale(pool_get, rep_id: int) -> float | None: return None +# Radians per named plane-angle unit — the fallback when a CONVERSION_BASED_UNIT names its +# angle unit but its MEASURE_WITH_UNIT factor can't be resolved (the factor is normally read +# straight off the referenced PLANE_ANGLE_MEASURE_WITH_UNIT record, which is exact). +_PLANE_ANGLE_NAME_SCALE = { + "DEGREE": math.pi / 180.0, + "DEGREES": math.pi / 180.0, + "ARC_DEGREE": math.pi / 180.0, + "RADIAN": 1.0, + "RADIANS": 1.0, + "GRAD": math.pi / 200.0, + "GRADIAN": math.pi / 200.0, + "GON": math.pi / 200.0, +} + + +def _unit_plane_angle_scale(pool_get, unit_id: int) -> float | None: + """Radians-per-unit of a plane-angle unit entity, or None if it isn't one. + + STEP tags every plane angle (notably ``CONICAL_SURFACE.semi_angle``) in the unit the + owning ``GLOBAL_UNIT_ASSIGNED_CONTEXT`` declares. ``ada.geom`` (and both the OCC and + adacpp kernels) want radians, so a file whose context is ``CONVERSION_BASED_UNIT('DEGREE')`` + must have its angles multiplied by ``pi/180``. The plain SI form is the radian (factor 1). + """ + rec = pool_get(unit_id) + if rec is None: + return None + if rec.type == _COMPLEX and isinstance(rec.args, dict): + if "PLANE_ANGLE_UNIT" not in rec.args: + return None + if rec.args.get("SI_UNIT") is not None: # SI_UNIT($, .RADIAN.) — the base radian + return 1.0 + cbu = rec.args.get("CONVERSION_BASED_UNIT") + if cbu: + for v in cbu: # exact factor from the referenced PLANE_ANGLE_MEASURE_WITH_UNIT + if isinstance(v, _Ref): + mrec = pool_get(v.id) + if mrec is not None and mrec.type == "PLANE_ANGLE_MEASURE_WITH_UNIT" and mrec.args: + try: + return float(mrec.args[0]) + except (TypeError, ValueError): + pass + if isinstance(cbu[0], str): # fall back to the unit name + return _PLANE_ANGLE_NAME_SCALE.get(cbu[0].strip().upper()) + return None + if rec.type == "SI_UNIT": # bare SI_UNIT($, .RADIAN.) + if rec.args and _enum_name(rec.args[-1]).strip(".").upper() == "RADIAN": + return 1.0 + return None + + +def _context_plane_angle_scale(pool_get, ctx_rec) -> float | None: + """Radians-per-unit of the plane-angle unit assigned by a (possibly complex) context + record's ``GLOBAL_UNIT_ASSIGNED_CONTEXT`` unit list, or None.""" + if ctx_rec is None: + return None + units = None + if ctx_rec.type == _COMPLEX and isinstance(ctx_rec.args, dict): + gua = ctx_rec.args.get("GLOBAL_UNIT_ASSIGNED_CONTEXT") + if gua and isinstance(gua[0], (list, tuple)): + units = gua[0] + elif ctx_rec.type == "GLOBAL_UNIT_ASSIGNED_CONTEXT" and ctx_rec.args and isinstance(ctx_rec.args[0], (list, tuple)): + units = ctx_rec.args[0] + if not units: + return None + for u in units: + if isinstance(u, _Ref): + s = _unit_plane_angle_scale(pool_get, u.id) + if s is not None: + return s + return None + + +def _representation_plane_angle_scale(pool_get, rep_id: int) -> float | None: + """Radians-per-unit of the plane-angle unit of a representation's own context (its + trailing context ``_Ref``), mirroring :func:`_representation_length_scale`.""" + rec = pool_get(rep_id) + if rec is None or not isinstance(rec.args, list): + return None + for v in reversed(rec.args): # the context is the last _Ref arg (after the items list) + if isinstance(v, _Ref): + return _context_plane_angle_scale(pool_get, pool_get(v.id)) + return None + + +def _detect_plane_angle_scale(pool_get, rep_ids=(), all_recs=None) -> float: + """The file's radians-per-unit plane-angle factor (1.0 radian, ~0.01745 degree). + + Resolved from a representation's context first (works for the lazy offset pool, which + can't be enumerated), then — dict pool only — by scanning every record for any + ``GLOBAL_UNIT_ASSIGNED_CONTEXT``. Defaults to 1.0 (radians) when undetectable, so files + that omit the unit (or the adapy emitter's own SI output) are unchanged.""" + for rid in rep_ids: + s = _representation_plane_angle_scale(pool_get, rid) + if s is not None: + return s + if all_recs is not None: + for rec in all_recs: + s = _context_plane_angle_scale(pool_get, rec) + if s is not None: + return s + return 1.0 + + _HEADER_RE = re.compile(r"^\s*#(\d+)\s*=\s*([A-Z0-9_]+)\s*\(", re.S) _COMPLEX_RE = re.compile(r"^\s*#(\d+)\s*=\s*\(", re.S) # #id=(NAME(..)NAME(..)..) complex record _COMPLEX = "__COMPLEX__" @@ -985,9 +1093,13 @@ class _Resolver: """Resolves instance ids into adapy geom objects against an entity pool, memoizing within a single solid so shared points/edges are built once.""" - def __init__(self, pool: dict[int, _Rec]): + def __init__(self, pool: dict[int, _Rec], angle_scale: float = 1.0): self._pool = pool self._cache: dict[int, object] = {} + # Radians per file plane-angle unit — applied to plane angles the file tags in its + # declared angle unit (e.g. CONICAL_SURFACE.semi_angle in a DEGREE context). 1.0 for + # a radian context (the default), ~0.01745 for degrees. + self.angle_scale = angle_scale def reset_cache(self): self._cache = {} @@ -1094,12 +1206,24 @@ def _trim_value(r: _Resolver, item): def _b_trimmed_curve(r: _Resolver, a: list) -> TrimmedCurve: # TRIMMED_CURVE('', #basis_curve, (trim_1), (trim_2), sense_agreement, master_repr) - t1 = a[2][0] if a[2] else 0.0 - t2 = a[3][0] if a[3] else 1.0 + basis = r.deref(a[1]) + t1 = _trim_value(r, a[2][0] if a[2] else 0.0) + t2 = _trim_value(r, a[3][0] if a[3] else 1.0) + # A PARAMETER_VALUE trim on a conic basis (circle / ellipse) is an angle in the file's + # plane-angle unit, so a DEGREE-context arc trimmed at "90" means 90 deg, not 90 rad. Scale + # numeric trims to radians (the serializer's arc sampler treats them as radians). Line and + # b-spline bases parameterize by length / knot value — the angle unit must not touch those, + # and CARTESIAN_POINT trims (Point objects) carry no unit here. + angle_scale = getattr(r, "angle_scale", 1.0) + if angle_scale != 1.0 and isinstance(basis, (Circle, Ellipse)): + if isinstance(t1, float): + t1 *= angle_scale + if isinstance(t2, float): + t2 *= angle_scale return TrimmedCurve( - basis_curve=r.deref(a[1]), - trim1=_trim_value(r, t1), - trim2=_trim_value(r, t2), + basis_curve=basis, + trim1=t1, + trim2=t2, sense_agreement=_enum_true(a[4]), master_representation=_enum_name(a[5]) if len(a) > 5 else "PARAMETER", ) @@ -1185,8 +1309,13 @@ def _b_cylindrical_surface(r: _Resolver, a: list) -> CylindricalSurface: def _b_conical_surface(r: _Resolver, a: list) -> ConicalSurface: - # CONICAL_SURFACE('', #position, radius, semi_angle) - return ConicalSurface(position=r.deref(a[1]), radius=float(a[2]), semi_angle=float(a[3])) + # CONICAL_SURFACE('', #position, radius, semi_angle). semi_angle is a plane angle in the + # file's declared angle unit — scale it to the radians ada.geom/adacpp expect. Without this + # a DEGREE-context cone (e.g. semi_angle 1.5 -> read as 1.5 rad ~= 86 deg) becomes a + # near-degenerate flat cone that libtess2 meshes to zero triangles, dropping the face. + return ConicalSurface( + position=r.deref(a[1]), radius=float(a[2]), semi_angle=float(a[3]) * getattr(r, "angle_scale", 1.0) + ) def _b_spherical_surface(r: _Resolver, a: list) -> SphericalSurface: @@ -1344,6 +1473,16 @@ def _b_torus(r: _Resolver, a: list) -> Torus: return Torus(position=r.deref(a[1]), major_radius=float(a[2]), minor_radius=float(a[3])) +def _b_arbitrary_closed_profile_def(r: _Resolver, a: list) -> ArbitraryProfileDef: + # ARBITRARY_CLOSED_PROFILE_DEF(.AREA.,'',#outer_curve) — the profile form the adacpp + # native STEP writer (stream_ngeom_to_step / stream_step_to_step) emits for rigid + # EXTRUDED_AREA_SOLIDs; the Python ap242 writer only ever emitted baked B-reps, so the + # reader never needed it before. + from ada.geom.surfaces import ProfileType + + return ArbitraryProfileDef(profile_type=ProfileType.from_str(_enum_name(a[0])), outer_curve=r.deref(a[2])) + + def _b_extruded_area_solid(r: _Resolver, a: list) -> ExtrudedAreaSolid: # EXTRUDED_AREA_SOLID('', #swept_area, #position, #extruded_direction, depth) return ExtrudedAreaSolid( @@ -1720,6 +1859,7 @@ def _build_complex(r: _Resolver, subs: dict): "RIGHT_CIRCULAR_CONE": _b_right_circular_cone, "SPHERE": _b_sphere, "TORUS": _b_torus, + "ARBITRARY_CLOSED_PROFILE_DEF": _b_arbitrary_closed_profile_def, "EXTRUDED_AREA_SOLID": _b_extruded_area_solid, "REVOLVED_AREA_SOLID": _b_revolved_area_solid, "BOOLEAN_RESULT": _b_boolean_result, @@ -2023,7 +2163,8 @@ def _read_two_pass_dict(filepath: Path, *, tolerant: bool, skipped, on_total=Non ) if on_total is not None: on_total(len(root_ids)) - resolver = _Resolver(pool) + angle_scale = _detect_plane_angle_scale(pool.get, absr_ids, all_recs=pool.values()) + resolver = _Resolver(pool, angle_scale=angle_scale) n_solids = 0 for rid in root_ids: rec = pool[rid] @@ -2265,11 +2406,22 @@ class StreamIndex: "tmap", "prod_names", "tolerant", + "angle_scale", "_owns", ) def __init__( - self, step_path, idx_ids_path, idx_offs_path, file_size, roots, colour_map, tmap, prod_names, tolerant + self, + step_path, + idx_ids_path, + idx_offs_path, + file_size, + roots, + colour_map, + tmap, + prod_names, + tolerant, + angle_scale=1.0, ): self.step_path = str(step_path) self.idx_ids_path = idx_ids_path @@ -2280,6 +2432,7 @@ def __init__( self.tmap = tmap self.prod_names = prod_names self.tolerant = tolerant + self.angle_scale = angle_scale self._owns = True # the creating process unlinks the tempfiles; pickled copies don't def __getstate__(self): @@ -2305,7 +2458,7 @@ def open_pool(self): ids_mm = np.empty(0, dtype=np.int64) offs_mm = np.empty(0, dtype=np.int64) pool = _OffsetPool(ids_mm, offs_mm, fd=fd, file_size=self.file_size, owns_fd=True) - return pool, _Resolver(pool) + return pool, _Resolver(pool, angle_scale=self.angle_scale) def close(self): """Unlink the spilled index tempfiles (creating process only).""" @@ -2392,6 +2545,7 @@ def prepare_stream_index(filepath, *, tolerant: bool, on_total=None) -> StreamIn idx_paths=idx_paths, sized={"tmap": tmap, "colour_map": colour_map}, ) + angle_scale = _detect_plane_angle_scale(pool.get, absr) if on_total is not None: on_total(len(roots)) # Drop the prepare-local memmaps + pool; consumers rebind fresh ones via open_pool(). @@ -2400,7 +2554,9 @@ def prepare_stream_index(filepath, *, tolerant: bool, on_total=None) -> StreamIn if mm is not None: # early error before the post-scan munmap mm.close() fh.close() # the prepare fd; consumers reopen the file in open_pool() - return StreamIndex(filepath, p_i, p_o, file_size, roots, colour_map, tmap, prod_names, tolerant) + return StreamIndex( + filepath, p_i, p_o, file_size, roots, colour_map, tmap, prod_names, tolerant, angle_scale=angle_scale + ) def build_one_solid(idx: StreamIndex, pool, resolver, rid: int, seq: int, *, skipped): diff --git a/src/ada/cadit/step/store.py b/src/ada/cadit/step/store.py index 59519296e..4f7e48e7f 100644 --- a/src/ada/cadit/step/store.py +++ b/src/ada/cadit/step/store.py @@ -1,6 +1,6 @@ """Back-compat re-export. The OCC implementation moved to `ada.occ.step.store` (the pythonocc CAD backend's home); this shim keeps the historical import path working without -itself importing OCC. See dap plan/v3 Phase 2 (STEP-IO relocation).""" +itself importing OCC. See the internal design notes Phase 2 (STEP-IO relocation).""" from ada.occ.step.store import EntityProps, StepStore diff --git a/src/ada/cadit/step/write/ap242_stream.py b/src/ada/cadit/step/write/ap242_stream.py index 348876faa..67d2324d0 100644 --- a/src/ada/cadit/step/write/ap242_stream.py +++ b/src/ada/cadit/step/write/ap242_stream.py @@ -36,10 +36,11 @@ class Seg: """One boundary segment in 2D profile coordinates.""" - kind: str # "line" | "arc" + kind: str # "line" | "arc" | "spline" start: tuple end: tuple mid: tuple | None = None # on-curve midpoint, required for kind == "arc" + spline: object | None = None # a 2D-profile BSplineCurveWithKnots, required for kind == "spline" @dataclass @@ -360,6 +361,36 @@ def _bspline_curve(self, c): return self._id return self._w(f"B_SPLINE_CURVE_WITH_KNOTS('',{c.degree},{cps},.{form}.,{closed},{si},{mult},{kn},.{spec}.)") + def _bspline_basis_from_cps(self, cps3d, spline): + """B_SPLINE_CURVE_WITH_KNOTS from ALREADY-LIFTED world 3D control points (unlike ``_bspline_curve`` + which lifts 2D profile points via the active-instance offset). The basis for a swept boundary + edge and its SURFACE_OF_LINEAR_EXTRUSION side surface.""" + cps = "(" + ",".join(f"#{self._pt(p)}" for p in cps3d) + ")" + form = spline.curve_form.value + closed = ".T." if spline.closed_curve else ".F." + si = ".T." if spline.self_intersect else ".F." + spec = spline.knot_spec.value + mult, kn = self._ilist(spline.knot_multiplicities), self._rlist(spline.knots) + weights = getattr(spline, "weights_data", None) + if weights: + body = ( + f"BOUNDED_CURVE()B_SPLINE_CURVE({spline.degree},{cps},.{form}.,{closed},{si})" + f"B_SPLINE_CURVE_WITH_KNOTS({mult},{kn},.{spec}.)CURVE()GEOMETRIC_REPRESENTATION_ITEM()" + f"RATIONAL_B_SPLINE_CURVE({self._rlist(weights)})REPRESENTATION_ITEM('')" + ) + self._id += 1 + self.fh.write(f"#{self._id}=({body});\n") + return self._id + return self._w( + f"B_SPLINE_CURVE_WITH_KNOTS('',{spline.degree},{cps},.{form}.,{closed},{si},{mult},{kn},.{spec}.)" + ) + + def _bspline_edge(self, v0, v1, cps3d, spline): + """EDGE_CURVE on a B-spline basis (same_sense .T. — the segment is oriented start->end, and the + spline was reversed by _reverse when the loop winding demanded it).""" + basis = self._bspline_basis_from_cps(cps3d, spline) + return self._w(f"EDGE_CURVE('',#{v0},#{v1},#{basis},.T.)") + def _axis1(self, loc, axis): return self._w(f"AXIS1_PLACEMENT('',#{self._pt(loc)},#{self._dir(axis)})") @@ -412,9 +443,7 @@ def begin(self): fh.write("DATA;\n") app = self._w(f"APPLICATION_CONTEXT('{sch['app_context']}')") - self._w( - "APPLICATION_PROTOCOL_DEFINITION('international standard'," f"'{sch['protocol']}',{sch['year']},#{app})" - ) + self._w(f"APPLICATION_PROTOCOL_DEFINITION('international standard','{sch['protocol']}',{sch['year']},#{app})") self._prod_ctx = self._w(f"PRODUCT_CONTEXT('',#{app},'mechanical')") self._pd_ctx = self._w(f"PRODUCT_DEFINITION_CONTEXT('part definition',#{app},'design')") @@ -463,7 +492,7 @@ def _emit_single_rep(self): axis = self._identity_axis() items = [axis, *self._solids] rep = self._w( - f"ADVANCED_BREP_SHAPE_REPRESENTATION('{self.product_name}'," f"{self._refs(items)},#{self._geom_ctx})" + f"ADVANCED_BREP_SHAPE_REPRESENTATION('{self.product_name}',{self._refs(items)},#{self._geom_ctx})" ) product = self._w(f"PRODUCT('{self.product_name}','{self.product_name}','',(#{self._prod_ctx}))") self._w(f"PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#{product}))") @@ -536,7 +565,7 @@ def _emit_assembly_root(self): idx += 1 # -- the main entry point ----------------------------------------------- # - def add_extrusion(self, ext: Extrusion) -> int: + def add_extrusion(self, ext: Extrusion, *, parent_path=None) -> int: if not self._began or self._ended: raise RuntimeError("add_extrusion() must be called between begin() and end()") @@ -574,7 +603,7 @@ def to3d_top(p2): self._emit_color(brep, ext.color) if self.assembly: - self._emit_component(brep, name) + self._emit_component(brep, name, parent_path=parent_path) else: self._solids.append(brep) return brep @@ -664,10 +693,84 @@ def add_brep(self, g, *, name="shape", color=None, translate=(0.0, 0.0, 0.0), tr return item def _emit_brep_geometry(self, g, nm, color): - """Emit just the B-rep geometry item (+ optional colour) under the active - instance transform ``self._tf``; return its id or None if unsupported. No - product/assembly wrapping — shared by the flat ``add_brep`` and the - instanced ``add_solid_instances``.""" + """Emit a B-rep geometry item (+ optional colour) under the active instance + transform ``self._tf``; return its id or None if unsupported. No product/ + assembly wrapping — shared by the flat ``add_brep`` and the instanced + ``add_solid_instances``. + + Analytic tiers, tried in order — a CSG primitive (Sphere/Cone) that isn't a + shell has an EXACT analytic B-rep, so tessellation is the last resort, never + the response to "not directly a shell": + + 1. ``g`` is already an analytic shell/face → emit it directly. + 2. ``g`` is a CSG primitive → pure-Python primitive→B-rep. + 3. → adacpp-native primitive→B-rep. + 4. genuinely non-analytic swept solid → faceted (tessellated) B-rep. + + Every tier is buffered (a fresh ``StringIO`` swapped in, rolled back on + failure) so a partial write from a tier whose Nth face is unsupported is + discarded before the next tier runs.""" + import ada.geom.solids as so + + item = self._emit_shell_buffered(g, nm, color) + if item is not None: + return item + + # CSG primitive solids (not shells): build their exact analytic B-rep shell — + # pure-Python first (kernel-free / wasm), then the adacpp-native track — and + # emit that. NEVER tessellate a shape that has an analytic form. + from ada.geom.primitive_brep import ( + native_primitive_to_analytic_shell, + primitive_to_analytic_shell, + ) + + for converter in (primitive_to_analytic_shell, native_primitive_to_analytic_shell): + try: + shell = converter(g) + except Exception as exc: # noqa: BLE001 - a converter must never sink the file + logger.warning("ap242 primitive->brep converter %s failed for %r: %s", converter.__name__, nm, exc) + shell = None + if shell is None: + continue + item = self._emit_shell_buffered(shell, nm, color) + if item is not None: + return item + + # No analytic AP242 B-rep form at all (e.g. an alignment + # IfcFixedReferenceSweptAreaSolid swept over an IfcGradientCurve: the clothoid + # + vertical-gradient directrix has no STEP analytic curve). Rather than leave + # it behind, tessellate via the validated NGEOM path and emit the triangle + # mesh as one faceted MANIFOLD_SOLID_BREP. + if isinstance(g, so.FixedReferenceSweptAreaSolid): + return self._emit_faceted_brep(g, nm, color) + return None + + def _emit_shell_buffered(self, g, nm, color): + """Emit ``g`` (a ClosedShell / ConnectedFaceSet / OpenShell / + ShellBasedSurfaceModel / AdvancedFace / FaceSurface) into a fresh buffer so a + partial write (a shell whose Nth face uses an unsupported surface writes the + earlier faces before returning None) is rolled back wholesale. Returns the + emitted item id, or None when ``g`` is not an analytic shell/face or a face is + unsupported.""" + real_fh, saved_id = self.fh, self._id + self.fh = io.StringIO() + try: + item = self._emit_analytic_shell(g, nm, color) + except Exception: # noqa: BLE001 - discard the partial buffer, reclaim ids + self.fh, self._id = real_fh, saved_id + raise + if item is None: + self.fh, self._id = real_fh, saved_id + return None + buffered = self.fh.getvalue() + self.fh = real_fh + real_fh.write(buffered) + return item + + def _emit_analytic_shell(self, g, nm, color): + """Emit an analytic B-rep shell/face (no primitive conversion, no buffering, + no faceting) — the raw writer for the shapes ``_brep_surface`` covers. Returns + the item id, or None if ``g`` is not such a shape or a face is unsupported.""" import ada.geom.surfaces as su self._vcache = {} # coord -> VERTEX_POINT id (shared across all faces) @@ -705,15 +808,6 @@ def _emit_brep_geometry(self, g, nm, color): shell = self._w(f"OPEN_SHELL('',{self._refs(faces)})") item = self._w(f"SHELL_BASED_SURFACE_MODEL('{nm}',(#{shell}))") else: - # No analytic AP242 B-rep form (e.g. an alignment - # IfcFixedReferenceSweptAreaSolid swept over an IfcGradientCurve: the - # clothoid + vertical-gradient directrix has no STEP analytic curve). - # Rather than leave it behind, tessellate via the validated NGEOM path - # and emit the triangle mesh as one faceted MANIFOLD_SOLID_BREP. - import ada.geom.solids as so - - if isinstance(g, so.FixedReferenceSweptAreaSolid): - return self._emit_faceted_brep(g, nm, color) return None if color is not None: @@ -859,7 +953,7 @@ def add_solid_instances(self, g, *, name="shape", color=None, instances=()): self._instances.append((pd, sr, nm, parent_rep, tuple(tf) if tf is not None else None)) return len(instances) - def add_baked_instances(self, g, *, name="shape", color=None, transforms=()) -> int: + def add_baked_instances(self, g, *, name="shape", color=None, transforms=(), parent_path=None) -> int: """Emit one component per world 4x4 in ``transforms``, each BAKED into a faceted MANIFOLD_SOLID_BREP whose planar faces are recomputed from the transformed points — so ANY affine (including the non-uniform-scale mapped-item transforms an IfcMappedItem carries) @@ -885,7 +979,7 @@ def add_baked_instances(self, g, *, name="shape", color=None, transforms=()) -> if item is None: continue if self.assembly: - self._emit_component(item, nm) + self._emit_component(item, nm, parent_path=parent_path) else: self._solids.append(item) emitted += 1 @@ -917,6 +1011,7 @@ def _brep_faces(self, faces): return out or None def _brep_face(self, face): + import ada.geom.curves as cu import ada.geom.surfaces as su # AdvancedFace and FaceSurface are structurally identical (same @@ -932,7 +1027,10 @@ def _brep_face(self, face): loop = self._brep_loop(fb.bound) if loop is None: return None - kw = "FACE_OUTER_BOUND" if i == 0 else "FACE_BOUND" + # A single-vertex (pole) loop is never an outer wire bound — a whole + # sphere's spherical face carries just a FACE_BOUND(VERTEX_LOOP). + is_vertex = isinstance(fb.bound, cu.VertexLoop) + kw = "FACE_OUTER_BOUND" if (i == 0 and not is_vertex) else "FACE_BOUND" bounds.append(self._w(f"{kw}('',#{loop},{'.T.' if fb.orientation else '.F.'})")) if not bounds: return None @@ -981,6 +1079,10 @@ def _brep_surface(self, s): def _brep_loop(self, loop): import ada.geom.curves as cu + if isinstance(loop, cu.VertexLoop): + # A single-vertex loop — the degenerate boundary of a fully-closed + # periodic face (a whole sphere), anchoring its pole point. + return self._w(f"VERTEX_LOOP('',#{self._vfor(loop.loop_vertex)})") if isinstance(loop, cu.EdgeLoop): oriented = [] for oe in loop.edge_list: @@ -1191,6 +1293,21 @@ def _build_loop(self, segs, is_outer, to3d_base, to3d_top, normal, xdir): et[i] = self._arc_edge(tv[i], bpos_top[i], tv[j], bpos_top[j], c3_top, xdir, radius, ccw, normal) surf = self._cylinder(c3_base, normal, xdir, radius) face_sense = is_outer + elif seg.kind == "spline": + # Analytic B-spline side face: the boundary spline (base + top edges) swept along the + # extrusion vector as a SURFACE_OF_LINEAR_EXTRUSION. The spline is clamped with its end + # control points snapped to the corners, so cps_base[0]/[-1] coincide with bv[i]/bv[j]. + sp = seg.spline + cps_base = [to3d_base(_xy(cp)) for cp in sp.control_points_list] + cps_top = [to3d_top(_xy(cp)) for cp in sp.control_points_list] + eb[i] = self._bspline_edge(bv[i], bv[j], cps_base, sp) + et[i] = self._bspline_edge(tv[i], tv[j], cps_top, sp) + delta = _sub(to3d_top((0.0, 0.0)), to3d_base((0.0, 0.0))) # = depth_vec + mag = math.sqrt(_dot(delta, delta)) + swept = self._bspline_basis_from_cps(cps_base, sp) + vec = self._w(f"VECTOR('',#{self._dir(_unit(delta))},{self._r(mag)})") + surf = self._w(f"SURFACE_OF_LINEAR_EXTRUSION('',#{swept},#{vec})") + face_sense = is_outer else: raise ValueError(f"unknown segment kind {seg.kind!r}") @@ -1282,8 +1399,32 @@ def _signed_area(segs): return 0.5 * a +def _reverse_bspline(c): + """Reverse a clamped B-spline's parametrization: reverse control points/weights and mirror the + knot vector, so the curve now runs end->start with an unchanged shape.""" + from ada.geom.curves import RationalBSplineCurveWithKnots + + total = c.knots[0] + c.knots[-1] + common = dict( + degree=c.degree, + control_points_list=list(reversed(c.control_points_list)), + curve_form=c.curve_form, + closed_curve=c.closed_curve, + self_intersect=c.self_intersect, + knot_multiplicities=list(reversed(c.knot_multiplicities)), + knots=[total - k for k in reversed(c.knots)], + knot_spec=c.knot_spec, + ) + if isinstance(c, RationalBSplineCurveWithKnots): + return RationalBSplineCurveWithKnots(weights_data=list(reversed(c.weights_data)), **common) + return type(c)(**common) + + def _reverse(segs): - return [Seg(s.kind, s.end, s.start, s.mid) for s in reversed(segs)] + return [ + Seg(s.kind, s.end, s.start, s.mid, spline=_reverse_bspline(s.spline) if s.spline is not None else None) + for s in reversed(segs) + ] def _orient(segs, *, ccw): @@ -1294,7 +1435,13 @@ def _orient(segs, *, ccw): def _curve_to_segs(curve, *, is_outer): """Translate one adapy 2D profile curve into a closed list[Seg], or None.""" - from ada.geom.curves import ArcLine, Circle, Edge, IndexedPolyCurve + from ada.geom.curves import ( + ArcLine, + BSplineCurveWithKnots, + Circle, + Edge, + IndexedPolyCurve, + ) if isinstance(curve, Circle): return circle_loop(_xy(curve.position.location), float(curve.radius), ccw=is_outer) @@ -1306,6 +1453,13 @@ def _curve_to_segs(curve, *, is_outer): segs.append(Seg("line", _xy(seg.start), _xy(seg.end))) elif isinstance(seg, ArcLine): segs.append(Seg("arc", _xy(seg.start), _xy(seg.end), mid=_xy(seg.midpoint))) + elif isinstance(seg, BSplineCurveWithKnots): + # Analytic B-spline boundary edge: a B_SPLINE_CURVE_WITH_KNOTS EDGE_CURVE with a + # SURFACE_OF_LINEAR_EXTRUSION side face (built in _build_loop). The profile spline is + # clamped with its endpoints snapped to the corners, so its first/last control points + # ARE the edge vertices. Validated by an OCC round-trip (test_write_step_stream). + cps = seg.control_points_list + segs.append(Seg("spline", _xy(cps[0]), _xy(cps[-1]), spline=seg)) else: logger.warning("unhandled poly-curve segment %s", type(seg).__name__) return None @@ -1553,6 +1707,12 @@ def _iter_stream_objects(part, merge_strategy=None): yield _AnalyticShell(shell, f"{part.name or 'model'}_analytic") for p in part.get_all_parts_in_assembly(include_self=True): fused = _part_fuses_from_fem(p) + if fused: + # The analytic face engine covers SHELL elements only — LINE (beam) elements have + # no shell faces and were silently dropped here (a beam+plate FEM exported to STEP + # with the plate alone; parity flagged the bbox loss). Fuse beams the same way the + # non-analytic branch does; plates stay with the analytic shell above. + yield from p.iter_objects_from_fem(beams=True, plates=False, detached=True) for o in p.get_all_physical_objects(sub_elements_only=True, pipe_to_segments=True): if not fused or not isinstance(o, (Beam, Plate)): yield o @@ -1625,6 +1785,9 @@ def write_step_stream( geom, name, color, translate = _object_geom_meta(obj) done = False if geom is not None: + # Assembly breadcrumb (owning Part chain up to the root) so each + # streamed object lands under its real part, not flat under the root. + parent_path = _object_parent_path(obj, part) # transforms ride obj.geom (the mapped-item mesh-level instances); solid_geom() # strips them. World order (matching the tessellation path + the ifcopenshell oracle) # is placement @ transform[k] @ local — so bake the LOCAL geometry (obj.geom.geometry, @@ -1649,11 +1812,15 @@ def write_step_stream( W = P @ np.asarray(m, dtype=float) placed = _transform_extrusion(base_ext, W) if base_ext is not None else None if placed is not None: - writer.add_extrusion(placed) + writer.add_extrusion(placed, parent_path=parent_path) n_ok += 1 elif ( writer.add_baked_instances( - obj_geom.geometry, name=name, color=color, transforms=[tuple(W.ravel())] + obj_geom.geometry, + name=name, + color=color, + transforms=[tuple(W.ravel())], + parent_path=parent_path, ) > 0 ): @@ -1665,9 +1832,14 @@ def write_step_stream( geom, name=name, color=color, translate=translate ) or _primitive_to_extrusion(geom, name=name, color=color, translate=translate) if ext is not None: - writer.add_extrusion(ext) + writer.add_extrusion(ext, parent_path=parent_path) done = True - elif writer.add_brep(geom.geometry, name=name, color=color, translate=translate) is not None: + elif ( + writer.add_brep( + geom.geometry, name=name, color=color, translate=translate, parent_path=parent_path + ) + is not None + ): # B-rep fallback: imported shapes, pure shells, analytic faces done = True emitted += 1 if done else 0 @@ -1704,6 +1876,24 @@ def _edge_curve_value_sig(g): return (tname, loc) +def _object_parent_path(obj, root): + """Root-first ``(rep_id, name)`` breadcrumb of an object's owning Part chain, + up to but excluding the ``root`` product (which the writer emits as the assembly + root). ``None`` when the object sits directly under the root. ``rep_id`` is the + Part's ``id()`` — a stable key for the duration of one streamed write, enough for + ``_register_asm_path`` to rebuild the nested PRODUCT / NAUO tree. Names are left + unescaped; ``_register_asm_path`` escapes them.""" + levels: list = [] + node = getattr(obj, "parent", None) + while node is not None and node is not root: + rep_id = id(node) + name = getattr(node, "name", None) or f"asm_{rep_id}" + levels.append((rep_id, name)) + node = getattr(node, "parent", None) + levels.reverse() + return levels or None + + def _object_geom_meta(obj): """(geom, name, color, translate) for a physical object, or (None, ...) if it has no usable solid geometry. Shared by the extrusion and B-rep emit paths.""" diff --git a/src/ada/cadit/step/write/writer.py b/src/ada/cadit/step/write/writer.py index 5bf8c3c21..ec5d8d17b 100644 --- a/src/ada/cadit/step/write/writer.py +++ b/src/ada/cadit/step/write/writer.py @@ -1,6 +1,6 @@ """Back-compat re-export. The OCC implementation moved to `ada.occ.step.writer` (the pythonocc CAD backend's home); this shim keeps the historical import path working without -itself importing OCC. See dap plan/v3 Phase 2 (STEP-IO relocation).""" +itself importing OCC. See the internal design notes Phase 2 (STEP-IO relocation).""" from ada.occ.step.writer import StepSchema, StepWriter diff --git a/src/ada/cadit/visual_parity.py b/src/ada/cadit/visual_parity.py index 1f2f7b86a..84d5a28ff 100644 --- a/src/ada/cadit/visual_parity.py +++ b/src/ada/cadit/visual_parity.py @@ -1,21 +1,35 @@ """Cross-format visual-parity validation. -The same model exported to different structure-preserving formats (GLB, IFC, -Genie XML, STEP) and rendered must show the *same number of visualized -elements*. A divergence means a converter silently dropped, merged, or invented -geometry on the way through that format — exactly the class of audit failure -that a count-only smoke test misses (e.g. an empty scene, an IFC that imports no +The same model exported to different formats (GLB, IFC, Genie XML, STEP) must +carry the *same geometry*. A divergence means a converter silently dropped, +merged, or invented geometry on the way through that format — exactly the class +of audit failure a smoke test misses (an empty scene, an IFC that imports no geometry, a STEP that loses solids). -The metric is the number of renderable scene entries built with ``merge_meshes`` -disabled, so each physical object maps to one entry (placeholder point clouds -that the converter seeds for empty scenes are not counted). Mesh-only formats -(STL/OBJ/PLY) are intentionally excluded: they carry no per-object identity and -always collapse to a single mesh soup, so they cannot preserve an element count. +The AUDIT path (:func:`parity_from_produced_files`, driven by the worker) reads +the ALREADY-PRODUCED output blobs — the ones the audit converted+uploaded with +the production analytic (``cylinder``) strategy — and compares a FORMAT-AGNOSTIC +GEOMETRY INVARIANT: bounding-box extent (strict gate) plus a coarse surface-area +floor. It re-derives nothing, so it validates exactly what ships and does zero +extra conversion. + +This replaced an earlier count-based design (re-derive with +``merge_strategy=None``, compare per-object element counts) which had two bugs: +it validated an UNMERGED model production never ships (~71k plates instead of a +handful of analytic cylinder faces), and the ``None`` re-derivation wrote ~1 GB +of temp files per model, stalling the audit worker on nvme write-contention. +Entity-count equality also cannot work under the analytic model — one physical +tube is a single CYLINDRICAL_SURFACE in STEP but several SAT faces in Genie XML — +so a geometry measure, not a count, is the correct invariant. + +STEP *sources* keep a separate streaming instance-count fast path +(:func:`parity_for_step_file`); they were never the memory/temp-file problem. """ from __future__ import annotations +import json +import statistics from dataclasses import dataclass, field from pathlib import Path from typing import TYPE_CHECKING, Callable @@ -110,10 +124,17 @@ def assembly_element_count(assembly: "Assembly") -> int: @dataclass class ParityResult: - counts: dict[str, int] # format label -> visualized element count ("source" is the baseline) - expected: int # the baseline (source) count - consistent: bool # True iff every format matches the baseline - mismatches: dict[str, int] = field(default_factory=dict) # format -> count, for the ones that differ + # format label -> the per-format measure. Two shapes coexist: the legacy + # count-based paths (STEP/whole-model) store an int element count; the + # geometry-invariant path (:func:`parity_from_produced_files`) stores a small + # dict ``{"area": .., "bbox": .., "tris": ..}`` — a format-agnostic measure of + # the geometry that actually shipped. ``summary`` renders both. + counts: dict[str, "int | dict"] + expected: int # baseline: legacy = source element count; geometry path = reference triangle count + consistent: bool # True iff every compared format matches the baseline / consensus + # format -> the diverging value (legacy: the mismatching count; geometry path: + # a short "area/bbox vs ref" reason string). Non-empty => inconsistent. + mismatches: dict[str, "int | str"] = field(default_factory=dict) errors: dict[str, str] = field( default_factory=dict ) # format -> error message when that format failed to round-trip @@ -124,7 +145,16 @@ class ParityResult: def summary(self) -> str: status = "OK" if self.consistent and not self.errors else "MISMATCH" - parts = [f"{k}={v}" for k, v in self.counts.items()] + + def _fmt(v) -> str: + if isinstance(v, dict): + # geometry measure: show area (the primary invariant) compactly + if "area" in v: + return f"{v['area']:.4g}m2" + return str(v) + return str(v) + + parts = [f"{k}={_fmt(v)}" for k, v in self.counts.items()] if self.errors: parts += [f"{k}=ERR" for k in self.errors] if self.skipped: @@ -170,6 +200,380 @@ def load_assembly_auto(path: str | Path) -> "Assembly": raise ValueError(f"visual_parity: no loader for source suffix {ext!r}") +# ── Geometry-invariant parity over ALREADY-PRODUCED output files ───────────── +# +# The audit converts each source to its production outputs (step/ifc/xml with the +# analytic ``cylinder`` strategy, glb via to_gltf) and uploads them. This path +# reads those SAME blobs back and compares a FORMAT-AGNOSTIC GEOMETRY MEASURE — +# it never re-derives, so it validates exactly what ships and does zero extra +# conversion. This replaces the old "re-derive with merge_strategy=None, compare +# entity counts" design, which (a) validated an unmerged model production never +# ships and (b) wrote ~1 GB of temp files per model, stalling on nvme contention. +# +# Entity-count equality cannot work under the analytic model: one physical tube is +# a single CYLINDRICAL_SURFACE in STEP but several SAT faces in Genie XML, so the +# counts legitimately differ while the geometry is identical. +# +# INVARIANT CHOICE (validated locally — see the commit): the BOUNDING-BOX EXTENT is +# the strict cross-format gate; total surface AREA is a coarse secondary floor only. +# Absolute area is NOT reliably comparable across adapy's writers because they use +# different DIMENSIONAL representations of the same object: the STEP stream writer +# emits a plate as a single mid-surface (area = one face) while Genie-XML/IFC can +# emit it as a thin solid (area ~= both faces + edges = ~2x), and the shipped glb +# renders FEM beams as zero-area *lines*. The bounding box is invariant to all of +# that — a mid-surface, a thin solid and a line span the same extent — so a dropped +# solid / region / empty output (the "geometry left behind" failure modes) shows as +# a shrunk or zero bbox in every representation, with no false positive from the +# solid-vs-surface split. +_BBOX_REL_TOL = 0.02 # bbox diagonal extent within 2 % of the consensus (largest) +# Secondary floor: a CAD/structural format retaining less than this fraction of the +# largest CAD area has grossly dropped geometry (near-empty). Set well below the +# legitimate solid-vs-mid-surface ratio (~0.5) so that representation difference +# never trips it — only a real, large loss does. +_AREA_GROSS_FLOOR = 0.34 + +# Extensions measured DIRECTLY as a mesh (already tessellated). Everything else is +# a CAD/structural format that we tessellate through the production ``to_gltf`` +# path before measuring. +_MESH_MEASURE_EXTS = frozenset({".glb", ".gltf", ".obj", ".stl", ".ply", ".off"}) + +# Formats whose area feeds the coarse area floor. The analytic CAD/structural trio +# render solids/surfaces (comparable up to the ~2x solid-vs-surface factor). Mesh +# formats (glb/obj/stl) are excluded from the area floor entirely: their beams are +# zero-area lines, so their absolute area is not comparable — they are validated on +# bbox alone (which DOES include the beam lines, see _measure_scene). +_AREA_FLOOR_FORMATS = frozenset({"step", "stp", "ifc", "xml"}) + + +@dataclass +class _GeomMeasure: + area: float # total tessellated surface area (native model units, squared) + bbox: float # bounding-box diagonal length (native model units) + tris: int # triangle count (secondary; representation-dependent) + empty: bool # True when the format produced no renderable geometry at all + + +def _measure_scene(scene) -> "_GeomMeasure": + """Geometry measure of a trimesh scene: + + * bbox diagonal — from ``scene.bounds``, which spans ALL geometry including + Path3D line entities (FEM beams render as lines), so a line-beam and a + solid-beam of the same member measure the same extent. Scene-graph transforms + are applied, so translated/rotated instances measure correctly. + * surface area + triangle count — from the concatenated mesh geometry only + (lines have no area); transforms baked in via ``dump(concatenate=True)``. + + ``empty`` is True only when the scene has no spatial extent at all (no meshes + AND no lines) — a total geometry loss.""" + import numpy as np + + bounds = getattr(scene, "bounds", None) + if bounds is None: + return _GeomMeasure(0.0, 0.0, 0, True) + d = np.asarray(bounds[1], dtype=float) - np.asarray(bounds[0], dtype=float) + bbox = float(np.sqrt(float((d * d).sum()))) + + # Concatenate the mesh geometry (transforms baked in) for area/tris. Prefer the + # newer ``to_geometry`` and fall back to ``dump(concatenate=True)`` on older + # trimesh — lines have no area, so a line-only scene yields no mesh here. + dumped = None + if hasattr(scene, "to_geometry"): + try: + dumped = scene.to_geometry() + except Exception: # noqa: BLE001 - only line/point geometry, or version quirk + dumped = None + if dumped is None: + try: + dumped = scene.dump(concatenate=True) + except Exception: # noqa: BLE001 - a scene with only line/point geometry + dumped = None + if dumped is None or not hasattr(dumped, "area") or len(getattr(dumped, "vertices", ())) == 0: + # No meshes, but a finite bbox (e.g. a line-only export) is not "empty". + return _GeomMeasure(0.0, bbox, 0, bbox <= 0.0) + area = float(dumped.area) + tris = int(len(getattr(dumped, "faces", ()))) + return _GeomMeasure(area, bbox, tris, bbox <= 0.0) + + +_MEASURE_TIMEOUT_S = 300 # per-format CAD measurement wall cap (fork-isolated) + + +class _MeasureUnavailable(Exception): + """A produced blob could not be measured because its tessellation hard-crashed + (a native SIGSEGV that no ``try/except`` can catch) or timed out. Raised from a + forked measurement subprocess so the fault fails only that ONE format, not the + whole parity cell — the formats that DO measure still decide the verdict.""" + + +def _measure_cad_scene(path: Path) -> "_GeomMeasure": + """In-process CAD/structural measurement: load the blob + tessellate via the + production ``to_gltf`` path + measure. The tessellation can hard-crash on some + produced analytic geometry, so this is only ever called inside a forked + subprocess (see :func:`_measure_cad_isolated`).""" + import io as _io + + import trimesh + + asm = load_assembly_auto(path) + # A concept format with no physical objects (e.g. FEM→ifc/xml/step of a + # solid-only mesh — solids have no shell/beam concepts) exports nothing; + # measure it as empty instead of letting to_gltf raise on an empty scene. + if not any(True for _ in asm.get_all_physical_objects()): + return _GeomMeasure(0.0, 0.0, 0, True) + buf = _io.BytesIO() + try: + asm.to_gltf(buf, merge_meshes=True) + except ValueError as ex: # trimesh: "Can't export empty scenes!" + if "empty scen" in str(ex).lower(): + return _GeomMeasure(0.0, 0.0, 0, True) + raise + buf.seek(0) + scene = trimesh.load(buf, file_type="glb", process=False) + return _measure_scene(scene) + + +# Isolation runs a FRESH interpreter (subprocess), not os.fork(): forking after +# adacpp/numpy have started threads can deadlock in the child on a lock the parent +# held at fork time (this hung the parity test suite). A clean subprocess has no +# inherited lock state; a native crash surfaces as a negative return code. +_MEASURE_MARKER = "__PARITY_MEASURE__" +_MEASURE_SCRIPT = ( + "import sys, json; from pathlib import Path;" + " from ada.cadit.visual_parity import _measure_cad_scene;" + " m = _measure_cad_scene(Path(sys.argv[1]));" + f" print('{_MEASURE_MARKER}' + json.dumps(" + "{'area': m.area, 'bbox': m.bbox, 'tris': m.tris, 'empty': m.empty}))" +) + + +def _measure_cad_isolated(path: Path) -> "_GeomMeasure": + """Measure a CAD/structural blob in an isolated subprocess so a native + tessellation crash (SIGSEGV) is contained: the child dies with a negative + return code, the parent raises :class:`_MeasureUnavailable`, and the parity + keeps the formats that DID measure.""" + import subprocess + import sys + + try: + proc = subprocess.run( + [sys.executable, "-c", _MEASURE_SCRIPT, str(path)], + capture_output=True, + text=True, + timeout=_MEASURE_TIMEOUT_S, + ) + except subprocess.TimeoutExpired as ex: + raise _MeasureUnavailable(f"measurement timed out (>{_MEASURE_TIMEOUT_S}s)") from ex + if proc.returncode != 0: + tail = ((proc.stderr or "").strip().splitlines() or [""])[-1][:200] + raise _MeasureUnavailable( + f"measurement subprocess died (exit {proc.returncode} — likely a native tessellation crash): {tail}" + ) + for line in proc.stdout.splitlines(): + if line.startswith(_MEASURE_MARKER): + d = json.loads(line[len(_MEASURE_MARKER) :]) + return _GeomMeasure(d["area"], d["bbox"], d["tris"], d["empty"]) + raise _MeasureUnavailable(f"measurement subprocess produced no result (exit {proc.returncode})") + + +def _measure_produced_file(fmt: str, path: Path) -> "_GeomMeasure": + """Load one produced output blob and measure its geometry. + + Mesh formats (glb/obj/stl/…) are measured directly (cheap, no native crash + path). CAD/structural formats (step/ifc/xml) are tessellated through the SAME + ``to_gltf`` path production ships — but that adacpp tessellation can hard-crash + on some produced analytic geometry, so it is run FORK-ISOLATED so the fault + fails only that format. All formats derive from one source, so no unit + conversion is injected (which could manufacture a false mismatch).""" + import trimesh + + ext = path.suffix.lower() + if ext in _MESH_MEASURE_EXTS: + if ext == ".glb" and _glb_is_meshopt_packed(path): + # Production GLBs ship EXT_meshopt_compression, which trimesh's glTF loader cannot + # decode (it slices the fallback-buffer layout and IndexErrors). Unpack to a plain + # GLB first; a missing codec is "unmeasurable", not a geometry mismatch. + import tempfile + + from ada.visit.gltf.meshopt import meshopt_decompress_glb + + with tempfile.TemporaryDirectory(prefix="adapy-parity-meshopt-") as td: + unpacked = Path(td) / "unpacked.glb" + try: + meshopt_decompress_glb(path, unpacked) + except Exception as ex: # noqa: BLE001 - codec missing / unsupported filter + raise _MeasureUnavailable(f"meshopt-packed glb undecodable: {ex}") from ex + scene = trimesh.load(str(unpacked), file_type="glb", process=False) + if isinstance(scene, trimesh.Trimesh): + scene = trimesh.Scene(scene) + return _measure_scene(scene) + scene = trimesh.load(str(path), file_type=ext.lstrip("."), process=False) + if isinstance(scene, trimesh.Trimesh): + scene = trimesh.Scene(scene) + return _measure_scene(scene) + + return _measure_cad_isolated(path) + + +def _glb_is_meshopt_packed(path: Path) -> bool: + """True when the GLB's JSON chunk declares EXT_meshopt_compression (header-only sniff).""" + import json + import struct + + try: + with open(path, "rb") as f: + magic, _ver, _total = struct.unpack(" ParityResult: + """Cross-format geometry-invariant parity over already-produced output blobs. + + ``produced`` maps each compared format (``step``/``ifc``/``xml``/``glb``) to the + local path of its produced blob, or ``None`` when that format's conversion + failed or was skipped (recorded, never re-derived). For each present format we + measure bbox diagonal + surface area + triangle count, then flag divergence: + + * BBOX (strict gate, all formats, ``_BBOX_REL_TOL``): reference = the largest + bbox diagonal. A format that dropped a solid / region / everything — including + the shipped glb — shrinks (or zeroes) its bbox and is flagged. This is the + representation-independent invariant (mid-surface, thin solid and line-beam of + one member all span the same extent), so it never false-positives on the + solid-vs-surface representation split the way absolute area would. + * AREA (coarse secondary floor, CAD/structural trio only): a format retaining + less than ``_AREA_GROSS_FLOOR`` of the largest CAD area has grossly dropped + geometry (near-empty) — a backstop for a gross loss that somehow preserved the + bbox. The floor sits well below the legitimate ~0.5 solid-vs-mid-surface ratio + so that representation difference never trips it. + + ``expected`` (the persisted baseline) is the reference format's triangle count — + an always-positive integer stand-in for the old element count. The per-format + ``{"area","bbox","tris"}`` measures go in ``counts``.""" + measures: dict[str, _GeomMeasure] = {} + errors: dict[str, str] = {} + skipped: dict[str, str] = {} + + for fmt in sorted(produced): + path = produced[fmt] + if path is None: + skipped[fmt] = "no produced blob (conversion failed or was skipped)" + continue + try: + measures[fmt] = _measure_produced_file(fmt, Path(path)) + except _MeasureUnavailable as ex: + # A native tessellation crash / timeout measuring this blob (contained + # in a forked subprocess): not a geometry mismatch, just un-measurable. + # The formats that DO measure still decide the verdict; recorded so the + # crash is visible, not silently green. + skipped[fmt] = f"unmeasurable ({ex})" + logger.warning(f"parity_from_produced_files: {fmt} unmeasurable: {ex}") + except Exception as ex: # noqa: BLE001 - record and continue with the other formats + errors[fmt] = f"{type(ex).__name__}: {ex}" + logger.warning(f"parity_from_produced_files: measuring {fmt} failed: {ex}") + + counts: dict[str, dict] = { + fmt: {"area": round(m.area, 3), "bbox": round(m.bbox, 4), "tris": m.tris} for fmt, m in measures.items() + } + + mismatches: dict[str, str] = {} + expected = 0 + + # A structure-preserving concept format (step/ifc/xml) that carries no geometry + # is NOT a drop when the source has no concepts to begin with: a solid-only / + # mesh-only FEM has no shells or beams to reconstruct, so those formats + # correctly export nothing (the glb still carries the element mesh). Record + # them as skipped rather than flagging every solid FEM as a mismatch. + for fmt, m in measures.items(): + if fmt in _AREA_FLOOR_FORMATS and m.empty: + skipped.setdefault(fmt, "source has no concept geometry (solid-only / mesh-only)") + + # Compare only formats that actually carry geometry. If NONE do, every format + # agrees there is nothing to render — consistent, not a mismatch. + live = {fmt: m for fmt, m in measures.items() if not m.empty and fmt not in skipped} + if live: + bbox_ref_fmt = max(live, key=lambda f: live[f].bbox) + bbox_ref = live[bbox_ref_fmt].bbox + # Reference for the gross-loss area floor: the MEDIAN concept area, not the + # max. One format legitimately carries several× the area of the others — + # Genie-XML emits thick solids that tessellate to ~5× the mid-surface area + # STEP/IFC carry — so using the max would let that outlier inflate the floor + # and falsely flag the (correct) mid-surface formats. Median is robust to a + # single inflated representation while still catching a near-empty format. + _concept_areas = [m.area for f, m in live.items() if f in _AREA_FLOOR_FORMATS] + area_ref = statistics.median(_concept_areas) if _concept_areas else 0.0 + expected = int(live[bbox_ref_fmt].tris) + + for fmt, m in live.items(): + reasons: list[str] = [] + if bbox_ref > 0 and m.bbox < (1.0 - _BBOX_REL_TOL) * bbox_ref: + reasons.append(f"bbox {m.bbox:.4g} vs ref {bbox_ref:.4g} ({(m.bbox / bbox_ref - 1) * 100:+.1f}%)") + if fmt in _AREA_FLOOR_FORMATS and area_ref > 0 and m.area < _AREA_GROSS_FLOOR * area_ref: + reasons.append(f"area {m.area:.4g} << ref {area_ref:.4g} (grossly dropped)") + if reasons: + mismatches[fmt] = "; ".join(reasons) + # A NON-concept format (glb, the shipped mesh) that is empty while concepts + # carry geometry is a genuine total loss — flag it. + for fmt, m in measures.items(): + if m.empty and fmt not in _AREA_FLOOR_FORMATS and fmt not in skipped: + mismatches[fmt] = "produced no renderable geometry" + + consistent = bool(measures) and not mismatches and not errors + return ParityResult( + counts=counts, + expected=expected, + consistent=consistent, + mismatches=mismatches, + errors=errors, + skipped=skipped, + ) + + +# Formats the audit compares geometry across (∩ with the source's viable targets). +# STEP/IFC/Genie-XML carry analytic solids; GLB is the mesh the viewer loads. OBJ/ +# STL are deliberately out — they add no representation the compared set lacks. +PARITY_GEOMETRY_FORMATS: tuple[str, ...] = ("step", "ifc", "xml", "glb") + + +def _derive_produced_for_parity(path: str | Path, formats: tuple[str, ...], work_dir: Path) -> dict[str, "Path | None"]: + """Offline fallback for :func:`parity_from_produced_files`: derive the same + outputs the AUDIT would have uploaded, using the PRODUCTION strategy (analytic + ``cylinder`` for step/ifc/xml, ``to_gltf`` for glb) — NOT the retired + ``merge_strategy=None`` unmerged model. Used by the offline + ``parity_for_source_file`` when no produced blobs are available (local `ada` + usage); the audit worker fetches the real blobs instead of calling this.""" + import ada + + p = Path(path) + is_fem = p.suffix.lower() in _FEM_PARITY_EXTS + produced: dict[str, Path | None] = {} + for fmt in formats: + out = work_dir / f"produced.{fmt}" + try: + asm = ada.from_fem(p) if is_fem else load_assembly_auto(p) + if fmt == "glb": + asm.to_gltf(out, merge_meshes=True) + elif fmt in ("step", "stp"): + asm.to_stp(out, writer="stream", merge_strategy="cylinder", fuse_fem=True) + elif fmt == "ifc": + asm.to_ifc(destination=str(out), streaming=True, merge_strategy="cylinder") + elif fmt == "xml": + asm.to_genie_xml(destination_xml=str(out), streaming=True, merge_strategy="cylinder") + else: + continue + produced[fmt] = out if out.exists() and out.stat().st_size > 0 else None + except Exception as ex: # noqa: BLE001 - a format that can't carry this source is recorded, not fatal + logger.warning(f"_derive_produced_for_parity: {fmt} derive failed: {ex}") + produced[fmt] = None + return produced + + def _count_curve_set_roots(path: str | Path, size_limit: int = 64_000_000) -> int: """Number of loose curve/geometric-set roots (one per placed curve body — e.g. an evaluated alignment reference curve) in a STEP file. The solid-only native reader skips these; each is a @@ -397,152 +801,66 @@ def parity_for_step_file( ) -def _concept_count(assembly: "Assembly") -> int: - """Number of structure-preserving concept objects (beams + plates + shapes) in an - assembly — the parity metric, counted directly instead of tessellating each.""" - n = 0 - for part in assembly.get_all_parts_in_assembly(include_self=True): - n += len(part.beams) + len(part.plates) + len(part.shapes) - return n - - -def _count_format_entities(fmt: str, path: str | Path) -> int: - """Count the structural elements in an exported file WITHOUT re-reading or - re-tessellating the whole model: a bounded text scan (ifc/xml) or the streaming - solid count (step). One entity per source concept on a clean round-trip.""" - if fmt == "step": - return _count_step_instances(path) - n = 0 - if fmt == "ifc": - # one IfcBeam / IfcPlate per concept; '(' after the type excludes IfcBeamType etc. - with open(path, errors="ignore") as f: - for line in f: - u = line.upper() - if "IFCBEAM(" in u or "IFCPLATE(" in u or "IFCBUILDINGELEMENTPROXY(" in u: - n += 1 - elif fmt == "xml": - # count the GEOMETRY-bearing elements specifically (each beam -> , - # each plate -> ); the generic wrapper is shared with - # masses / BCs, which ifc/step don't emit as countable entities. - with open(path, errors="ignore") as f: - for line in f: - n += line.count(" ParityResult: - """Count-based cross-format parity for a FEM source — rebuild concept objects - (the converter does the same), export each format, and count the OUTPUT entities - by a bounded text scan / solid count instead of re-reading + re-tessellating the - (100k+ plate) model 4×. The whole-model tessellation that timed out the parity is - gone; only the (unavoidable) concept rebuild + per-format exports remain. - """ - import tempfile - - import ada - from ada.fem import FEM - - tmp_ctx = None - if work_dir is None: - tmp_ctx = tempfile.TemporaryDirectory() - work_dir = tmp_ctx.name - work_dir = Path(work_dir) - - asm = ada.from_fem(path) - asm.create_objects_from_fem(merge=True) - # Drop the FEM mesh so the baseline counts the exported concept geometry, not the - # auxiliary mesh viz (mirrors load_assembly_auto). - for part in asm.get_all_parts_in_assembly(include_self=True): - if part.fem is not None and len(part.fem.elements) > 0: - part.fem = FEM(part.fem.name, parent=part) - - baseline = _concept_count(asm) - # ifc/xml: the STREAMING writers (merge_strategy=None streams the already-built - # concepts — bounded, count-matches the baseline). step: the OCC writer, which - # doesn't scale past tens of thousands of plates (malloc fail), so it's guarded - # by concept count below. - writers: dict[str, tuple] = { - "ifc": (lambda a, p: a.to_ifc(p, streaming=True, merge_strategy=None), ".ifc"), - "xml": (lambda a, p: a.to_genie_xml(p, streaming=True, merge_strategy=None), ".xml"), - "step": (lambda a, p: a.to_stp(p), ".step"), - } - counts: dict[str, int] = {"source": baseline} - errors: dict[str, str] = {} - skipped: dict[str, str] = {} - try: - for fmt in formats: - wsuf = writers.get(fmt) - if wsuf is None: - errors[fmt] = f"unknown format {fmt!r}" - continue - reason = _unrepresentable_reason(fmt, asm) - if reason is not None: - skipped[fmt] = reason - continue - if fmt == "step" and baseline > _STEP_OCC_CONCEPT_LIMIT: - skipped["step"] = f"OCC STEP writer does not scale to {baseline} concepts" - continue - writer, suffix = wsuf - out = work_dir / f"parity{suffix}" - try: - writer(asm, out) - counts[fmt] = _count_format_entities(fmt, out) - except Exception as ex: # noqa: BLE001 - record and continue with the other formats - errors[fmt] = f"{type(ex).__name__}: {ex}" - logger.warning(f"parity_for_fem_file: {fmt} round-trip failed: {ex}") - finally: - if tmp_ctx is not None: - tmp_ctx.cleanup() - - mismatches = {k: v for k, v in counts.items() if k != "source" and v != baseline} - return ParityResult( - counts=counts, - expected=baseline, - consistent=not mismatches and not errors, - mismatches=mismatches, - errors=errors, - skipped=skipped, - ) - +# NOTE: The old count-based FEM parity (parity_for_fem_file + its +# _streaming_baseline_count / _count_format_entities / _PARITY_MERGE_STRATEGY=None +# helpers) has been RETIRED. It re-derived the source with merge_strategy=None (one +# plate per FEM element) and compared entity counts for equality — validating an +# unmerged model production never ships, and writing ~1 GB of temp files per model +# (which stalled on nvme write-contention). The FEM parity now reads the +# already-produced analytic (cylinder) output blobs and compares a geometry +# invariant (parity_from_produced_files); the offline fallback derives those same +# production outputs via _derive_produced_for_parity. _FEM_PARITY_EXTS = (".fem", ".inp", ".sif", ".sin") def parity_for_source_file( path: str | Path, - formats: tuple[str, ...] = ("ifc", "xml", "step"), + formats: tuple[str, ...] = PARITY_GEOMETRY_FORMATS, *, work_dir: str | Path | None = None, ) -> ParityResult: - """Load a source model from disk and run :func:`cross_format_parity` on it. - - STEP and FEM sources take count-based fast paths (:func:`parity_for_step_file` / - :func:`parity_for_fem_file`) — bounded memory, no whole-model tessellation — since - those are exactly the multi-GB / 100k-plate models that OOM'd / timed out the - tessellation path. Each falls back to the whole-model path on any failure.""" + """OFFLINE cross-format parity for a source on disk — the fallback used when no + already-produced blobs are available (local `ada` usage). The audit worker does + NOT call this: it fetches the real produced blobs and calls + :func:`parity_from_produced_files` directly. + + For a FEM source this DERIVES the production outputs (analytic ``cylinder`` for + step/ifc/xml, ``to_gltf`` for glb — exactly what ships) and compares the + GEOMETRY INVARIANT, so local and audit agree. This replaces the retired + ``merge_strategy=None`` + entity-count design, which validated an unmerged model + production never ships and wrote ~1 GB of temp files per model. STEP sources keep + the streaming instance-count fast path (never a memory/temp-file problem).""" ext = Path(path).suffix.lower() - fast = None + # cross_format_parity is count-based over structure-preserving formats only (no + # glb); keep glb out of the formats it sees. + count_formats = tuple(f for f in formats if f in ("ifc", "xml", "step")) or ("ifc", "xml", "step") + if ext in (".step", ".stp"): - fast = parity_for_step_file - elif ext in _FEM_PARITY_EXTS: - fast = parity_for_fem_file - if fast is not None: try: - return fast(path, formats, work_dir=work_dir) + return parity_for_step_file(path, count_formats, work_dir=work_dir) except Exception as ex: # noqa: BLE001 - fall back to the whole-model path on any failure - logger.warning(f"parity_for_source_file: count-based parity failed ({ex}); using whole-model path") - return cross_format_parity(load_assembly_auto(path), formats, work_dir=work_dir) + logger.warning(f"parity_for_source_file: STEP fast-path failed ({ex}); using whole-model path") + return cross_format_parity(load_assembly_auto(path), count_formats, work_dir=work_dir) + + if ext in _FEM_PARITY_EXTS: + import tempfile + + geom_formats = tuple(f for f in formats if f in PARITY_GEOMETRY_FORMATS) or PARITY_GEOMETRY_FORMATS + tmp_ctx = None + if work_dir is None: + tmp_ctx = tempfile.TemporaryDirectory() + wd = Path(tmp_ctx.name) + else: + wd = Path(work_dir) + try: + produced = _derive_produced_for_parity(path, geom_formats, wd) + return parity_from_produced_files(str(path), produced) + finally: + if tmp_ctx is not None: + tmp_ctx.cleanup() + + return cross_format_parity(load_assembly_auto(path), count_formats, work_dir=work_dir) def cross_format_parity( @@ -638,4 +956,110 @@ def _unrepresentable_reason(fmt: str, assembly: "Assembly") -> str | None: if any(isinstance(o, (Beam, Plate)) for o in assembly.get_all_physical_objects()): return None + # A FEM source is streamed straight from the mesh (Part.iter_objects_from_fem): + # its Beam/Plate concepts are never materialised into the part containers, so the + # get_all_physical_objects() scan above sees none. A part carrying shell/line + # elements DOES stream to plates/beams, so it is representable in Genie XML. + for part in assembly.get_all_parts_in_assembly(include_self=True): + fem = getattr(part, "fem", None) + if fem is not None and (len(fem.elements.shell) or len(fem.elements.lines)): + return None return "Genie XML carries only Beam/Plate concepts, not generic solids" + + +# ── Genie-XML produced-files parity (count-based, zero re-derivation) ──────── +def _count_gxml_objects(path: "str | Path") -> int: + """Reader-visible object count of a Genie-XML file WITHOUT loading any geometry. + + Mirrors ``GxmlStore``'s iteration: one plate per ```` under each + ``flat_plate``/``curved_shell`` (inline ```` sheets when no faces), plus one + beam per ``straight_beam``/``curved_beam``. A pure ElementTree pass — the SAT blob is + never touched, so a 16 MB hull counts in well under a second.""" + import xml.etree.ElementTree as ET + + root = ET.parse(str(path)).getroot() + n = 0 + for tag in ("flat_plate", "curved_shell"): + for el in root.iter(tag): + faces = el.findall(".//face") + if faces: + n += len(faces) + else: + n += len(el.findall(".//polygon")) + for tag in ("straight_beam", "curved_beam"): + n += sum(1 for _ in root.iter(tag)) + return n + + +def _count_ifc_products(path: "str | Path") -> int: + """TYPED structural products in an IFC file via a bounded text scan. + + Products are one-per-line in SPF, so a line scan matches ``ifcopenshell`` counting at a + fraction of the parse cost (a 90 MB hull IFC scans in ~1 s). ``IfcBuildingElementProxy`` + is deliberately NOT counted: for a Genie-XML source the IFC writer emits every structural + object typed (IfcPlate/IfcBeam — including thick curved shells), while proxies carry + CONCEPT objects (point/ballast masses) that the xml face-count and the STEP solid-count + structurally cannot represent — counting them flagged every mass-carrying model as an + ifc-leg surplus. The shared cross-format basis is the structural set.""" + kinds = ("=IFCPLATE(", "=IFCBEAM(", "=IFCMEMBER(", "=IFCPIPESEGMENT(") + n = 0 + with open(path, "r", errors="ignore") as f: + for line in f: + u = line.upper() + if any(k in u for k in kinds): + n += 1 + return n + + +def parity_gxml_from_produced_files(source_key: str, produced: dict[str, "Path | None"]) -> ParityResult: + """Cross-format COUNT parity for a Genie-XML source over already-produced output blobs. + + Genie-XML kept the old re-derive path (load + export via parity's own Python writers + + reload) long after FEM sources moved to produced-files measurement — and with thickened + curved shells the re-derive tripled, dominating the sweep. This is the produced-files + fix, but count-based rather than geometry-based: the historical gxml invariant is the + per-object count (it caught e.g. an ifc leg silently dropping 9 of 5470 plates, which a + bbox gate can miss), and every produced format has a cheap counter — xml via a text/ET + structure scan, ifc via an SPF line scan, step via the native C++ stream index. No + loading, no export, no tessellation: the whole check is a few seconds. + + ``expected`` (the persisted baseline) is the produced-xml round-trip count when present + (the identity format), else the max across formats. Mesh formats (glb/obj/stl) carry no + product granularity and are recorded as skipped.""" + counts: dict[str, int] = {} + errors: dict[str, str] = {} + skipped: dict[str, str] = {} + + for fmt in sorted(produced): + path = produced[fmt] + if path is None: + skipped[fmt] = "no produced blob (conversion failed or was skipped)" + continue + try: + if fmt == "ifc": + counts[fmt] = _count_ifc_products(path) + elif fmt == "step": + n = _count_step_product_instances(path) + if n is None: + skipped[fmt] = "native step counter unavailable" + else: + counts[fmt] = n + elif fmt == "xml": + counts[fmt] = _count_gxml_objects(path) + else: # glb / obj / stl — merged meshes, no per-product granularity + skipped[fmt] = "mesh format has no product granularity" + except Exception as ex: # noqa: BLE001 - record and continue with the other formats + errors[fmt] = f"{type(ex).__name__}: {ex}" + logger.warning(f"parity_gxml_from_produced_files: counting {fmt} failed: {ex}") + + expected = counts.get("xml", max(counts.values(), default=0)) + mismatches: dict[str, "int | str"] = {f: c for f, c in counts.items() if c != expected} + consistent = not mismatches and bool(counts) + return ParityResult( + counts=dict(counts), + expected=int(expected), + consistent=consistent, + mismatches=mismatches, + errors=errors, + skipped=skipped, + ) diff --git a/src/ada/comms/rest/audit_issue.py b/src/ada/comms/rest/audit_issue.py index e0440a994..03180a062 100644 --- a/src/ada/comms/rest/audit_issue.py +++ b/src/ada/comms/rest/audit_issue.py @@ -1,5 +1,5 @@ """Pure helpers for the audit-run → issue-tracker bridge (M5 of -plan/v2/notes_admin_audit_panel.md). +the admin audit-panel design notes). Two responsibilities, both deterministic and free of I/O: diff --git a/src/ada/comms/rest/audit_perf.py b/src/ada/comms/rest/audit_perf.py index f2d0699c7..1373b9c67 100644 --- a/src/ada/comms/rest/audit_perf.py +++ b/src/ada/comms/rest/audit_perf.py @@ -1,5 +1,5 @@ """Pure helpers for the cross-conversion performance dashboard (M6 -of plan/v2/notes_admin_audit_panel.md). +of the admin audit-panel design notes). This module is intentionally I/O-free: the SQL aggregation lives in :mod:`db` and the REST plumbing lives in :mod:`app`. Here we just diff --git a/src/ada/comms/rest/converter.py b/src/ada/comms/rest/converter.py index 0397fc1fa..c65260d1d 100644 --- a/src/ada/comms/rest/converter.py +++ b/src/ada/comms/rest/converter.py @@ -704,6 +704,69 @@ def _gxml_face_streaming(source_ext: str, target_format: str, reconstruct_surfac return os.environ.get("ADA_GXML_STREAMING", "").strip().lower() not in _FALSE +def _native_ngeom_mesh_route( + model, + source_ext: str | None, + target_format: str, + out_path: pathlib.Path, + on_progress: ProgressFn, + *, + glb_tess_engine: str | None = None, +) -> pathlib.Path | None: + """Fully-native mesh leg for ada-object sources: serialize each object's ``solid_geom()`` to + an NGEOM record and let adacpp tessellate + write the GLB / OBJ / STL in C++ + (``stream_ngeom_to_glb`` / ``stream_ngeom_to_mesh``) — no Python scene assembly, no trimesh + writer (the hull's 137 s xml→obj becomes the same class as the native step→obj leg). + + Returns ``out_path`` on success, or ``None`` to fall back WHOLESALE to the Python path: + Genie-XML sources only (concept objects with parametric ``solid_geom()``), gated by the same + ``Config().cad_native_ngeom_export`` switch as the xml→step/ifc legs, and only when the + requested tessellation engine is an adacpp record track. A model carrying FEM mesh content + falls back too — the Python ``to_gltf`` renders the FEM mesh itself (shell faces, beam + lines, the beam_solids sidecar), which the concept-object record walk cannot express. The + zero-renderable-object case also falls back (``collect_ngeom_records`` raises), preserving + the Python path's seeded empty-scene output. + """ + if source_ext is None or source_ext.lower() != ".xml": + return None + # Engine choice must resolve to an adacpp record-stream track; occ-builtin / the taxonomy + # kernels (occ/cgal/hybrid) mean the user asked for a different tessellator — honour it. + stream = _glb_engine_stream_value(glb_tess_engine) + if stream not in ("libtess2", "cdt"): + return None + from ada.cadit.ngeom.export import ( + NativeExportUnsupported, + native_export_enabled, + native_mesh_writers_available, + native_to_glb, + native_to_mesh, + ) + from ada.config import logger + + if not (native_export_enabled() and native_mesh_writers_available()): + return None + try: + # Renderable FEM = elements. Bare nodes (Genie support points / mass nodes) don't + # produce mesh geometry, so they must not disqualify the native route. + if any(len(p.fem.elements) > 0 for p in model.get_all_parts_in_assembly(include_self=True)): + return None + except Exception: # noqa: BLE001 - a malformed FEM container must not kill the conversion + return None + try: + on_progress("native-ngeom-tessellating", 0.55) + if target_format == "glb": + native_to_glb(model, out_path, pipeline=stream) + else: + native_to_mesh(model, out_path, target_format, pipeline=stream) + on_progress("ready", 1.0) + return out_path + except NativeExportUnsupported as exc: + logger.warning("native xml->%s route unavailable (%s); using the Python writer", target_format, exc) + except Exception as exc: # noqa: BLE001 - wholesale fallback: never fail the job on the fast path + logger.warning("native xml->%s route failed (%s); using the Python writer", target_format, exc) + return None + + def _export_with_ada( model, target_format: str, @@ -742,6 +805,15 @@ def _export_with_ada( merge_env = (_os.environ.get("ADA_GLB_MERGE_MESHES") or "").strip().lower() merge_meshes = merge_env not in {"0", "false", "no", "off"} + # Fully-native record path (Genie-XML sources): adacpp tessellates and writes the GLB + # itself — no Python scene assembly. merge_meshes=False is the one-node-per-object debug + # layout, which the merge-by-colour native writer can't express; that stays on Python. + if merge_meshes: + native_out = _native_ngeom_mesh_route( + model, source_ext, "glb", out_path, on_progress, glb_tess_engine=glb_tess_engine + ) + if native_out is not None: + return native_out # FEM beam (line) elements render as line geometry by default; the solid (swept- # profile) representation is delivered as a separate beam_solids sidecar the viewer # lazy-loads when the "show beams as solid" toggle is on (mirrors the FEA-results path). @@ -795,6 +867,35 @@ def _export_with_ada( return buf.getvalue() if target_format == "ifc": on_progress("writing-ifc", 0.55) + # The DEFAULT xml->ifc path is the streaming writer below (model.to_ifc(streaming=True)): + # it emits TYPED products (IfcBeam/IfcPlate with parametric round-trip) while curved and + # spline-boundary plates stream their heavy B-rep body graphs through adacpp's + # ngeom_to_ifc_body_spf C++ fragment emitter (~µs/face vs the per-entity ifcopenshell + # writer's ~ms/face that made the thickened-curved-shell hull take ~34 s). + # ADA_CAD_NATIVE_NGEOM_EXPORT_IFC=true remains a GEOMETRY-ONLY opt-in: the fully-native + # record-stream writer wraps every solid in an IfcBuildingElementProxy (no typed + # products), acceptable only for geometry handoff. The STEP leg stays native by default: + # STEP products carry name-only semantics either way, so nothing is lost there. + if source_ext is not None and source_ext.lower() == ".xml": + import os as _os + + from ada.cadit.ngeom.export import ( + NativeExportUnsupported, + native_export_enabled, + native_ngeom_writers_available, + native_to_ifc, + ) + + _ifc_opt_in = _os.environ.get("ADA_CAD_NATIVE_NGEOM_EXPORT_IFC", "").strip().lower() in ("1", "true") + if _ifc_opt_in and native_export_enabled() and native_ngeom_writers_available(): + try: + native_to_ifc(model, out_path) + on_progress("ready", 1.0) + return out_path + except NativeExportUnsupported as exc: + from ada.config import logger + + logger.warning("native xml->ifc route unavailable (%s); using the Python writer", exc) # Memory-bounded writer is the default: it hand-authors Plate solids as # SPF text instead of holding the whole ifcopenshell.file, ~halving peak # RSS on large FEM→IFC and clearing the worker OOM cap. The admin "Stream @@ -823,13 +924,19 @@ def _export_with_ada( recon = bool(reconstruct_surfaces) if reconstruct_surfaces is not None else False if source_ext is not None and _gxml_face_streaming(source_ext, target_format, recon): # Object-free path: plates stream from the vectorized FEM-shell face - # source (no Plate objects, no DOM). merge_fem_objects -> strategy. - merge = True if merge_fem_objects is None else bool(merge_fem_objects) - model.to_genie_xml( - destination_xml=str(out_path), - streaming=True, - merge_strategy=("coplanar" if merge else "none"), - ) + # source (no Plate objects, no DOM). Default is the analytic auto-detect + # ("cylinder": tubular members -> over an embedded SAT + # body, flat panels -> merged ), matching FEM->STEP/IFC and + # collapsing a tube's shell facets instead of emitting thousands of + # coplanar polygons; a string merge_fem_objects overrides verbatim, + # False opts out to 1:1. + if isinstance(merge_fem_objects, str): + ms = merge_fem_objects + elif merge_fem_objects is False: + ms = "none" + else: + ms = "cylinder" + model.to_genie_xml(destination_xml=str(out_path), streaming=True, merge_strategy=ms) else: model.to_genie_xml(destination_xml=str(out_path)) else: @@ -1610,7 +1717,7 @@ def _brep_writer_is_python(serializer: str | None) -> bool: def _default_glb_tess_engine() -> str: """Default engine for the non-STEP →GLB (scene) path: ``libtess2`` when adacpp is importable, else the OCC BatchTessellator. OCC's prism tessellation of curved B-spline plates is - NON-MANIFOLD — it drops the viewer's per-plate edge outlines (hullskin elev13 plates) — so + NON-MANIFOLD — it drops the viewer's per-plate edge outlines (hull-skin plates) — so libtess2 (manifold; non-NGEOM-serializable geom still falls back to OCC per-object) is preferred wherever it can run. Evaluated at conversion time so a slim/adacpp-less pool still gets OCC.""" @@ -2101,22 +2208,26 @@ def _via_ada_to_trimesh( source_ext: str, target_ext: str, on_progress: ProgressFn, -) -> bytes: - """Ada-loadable source → trimesh mesh export (``.stl`` / ``.obj``). - - Bridges the same ada-loadable formats ``_via_ada`` handles to - trimesh's mesh-only export targets. We go through - :meth:`Part.to_trimesh_scene` so tessellation honours adapy's - geom-repr / merge-meshes conventions; trimesh just serialises the - resulting scene. - - No native STL/OBJ ada exporter is needed — trimesh's own writers - are mature and the GLB pipeline already proves the round-trip - works. +) -> bytes | pathlib.Path: + """Ada-loadable source → mesh export (``.stl`` / ``.obj``). + + Genie-XML sources take the fully-native NGEOM-record route when available + (:func:`_native_ngeom_mesh_route`): adacpp tessellates and writes the OBJ/STL in C++, + returning the file path (ownership transfers to the caller). Everything else — and any + native fallback — bridges the same ada-loadable formats ``_via_ada`` handles to trimesh's + mesh-only export targets via :meth:`Part.to_trimesh_scene`, so tessellation honours adapy's + geom-repr / merge-meshes conventions and trimesh serialises the resulting scene. """ on_progress("parsing", 0.15) model = _load_with_ada(src_path, source_ext) + fmt = target_ext.lstrip(".").lower() + if fmt in ("obj", "stl"): + native_path = pathlib.Path(tempfile.mkstemp(suffix=f".{fmt}")[1]) + native_out = _native_ngeom_mesh_route(model, source_ext, fmt, native_path, on_progress) + if native_out is not None: + return native_out + native_path.unlink(missing_ok=True) # fell back: drop the unused temp slot on_progress("tessellating", 0.55) scene = model.to_trimesh_scene() _strip_unexportable_for(scene, target_ext) @@ -2281,6 +2392,25 @@ def _via_ada_to_step( if skipped: logger.warning(f"streaming STEP writer skipped {skipped} non-extrudable object(s)") else: + # Genie-XML fast path (default on, ADA_CAD_NATIVE_NGEOM_EXPORT=false to opt + # out): NGEOM records -> adacpp's C++ AP242 writer instead of the OCC XCAF / + # per-entity Python writers. Wholesale fallback below when adacpp is absent + # or any object fails to serialize (mirrors the xml->ifc leg). + if source_ext.lower() == ".xml": + from ada.cadit.ngeom.export import ( + NativeExportUnsupported, + native_export_enabled, + native_ngeom_writers_available, + ) + + if native_export_enabled() and native_ngeom_writers_available(): + try: + model.to_stp(str(out_path), writer="native") + on_progress("ready", 1.0) + returned_path = True + return out_path + except NativeExportUnsupported as exc: + logger.warning("native xml->step route unavailable (%s); using the OCC writer", exc) model.to_stp(str(out_path)) if not _step_has_solids(out_path): # The OCC/adacpp writer emitted no solid root — e.g. an alignment diff --git a/src/ada/comms/rest/db.py b/src/ada/comms/rest/db.py index 576c34faa..43693d01a 100644 --- a/src/ada/comms/rest/db.py +++ b/src/ada/comms/rest/db.py @@ -2033,13 +2033,19 @@ async def audit_log_history_for_cell( ) -> list[dict]: """Historic ``audit_log`` results for one ``(source key, target_format)`` cell across every run — newest first. Powers the per-cell 'show history' - table so an operator can spot run-to-run regressions for one conversion.""" + table so an operator can spot run-to-run regressions for one conversion. + + Only rows tied to an audit run (``audit_run_id IS NOT NULL``) are returned: + history is a cross-run comparison, so on-demand / viewer-triggered + re-conversions (e.g. a GLB re-tessellation on file open, ``audit_run_id`` + NULL) are excluded — otherwise a heavily-viewed source floods its glb cell + with standalone rows that push the actual per-run results past ``limit``.""" rows = await pool.fetch( """ SELECT id, ts, status, error, duration_ms, peak_rss_kb, worker_image_tag, audit_run_id FROM audit_log - WHERE key = $1 AND target_format = $2 + WHERE key = $1 AND target_format = $2 AND audit_run_id IS NOT NULL ORDER BY id DESC LIMIT $3 """, diff --git a/src/ada/comms/rest/migrations/007_corpora.sql b/src/ada/comms/rest/migrations/007_corpora.sql index dfcc5ad29..d5dd7900c 100644 --- a/src/ada/comms/rest/migrations/007_corpora.sql +++ b/src/ada/comms/rest/migrations/007_corpora.sql @@ -11,7 +11,7 @@ -- dispatch. The ``corpora`` table itself owns the metadata -- (slug → human name + description); per-file metadata (e.g. the -- ``expected_to_fail`` annotation called out in --- plan/v2/notes_admin_audit_panel.md) lives as a sidecar JSON in +-- the admin audit-panel design notes) lives as a sidecar JSON in -- the bucket alongside the file, so adding it doesn't require a -- table migration. -- diff --git a/src/ada/comms/rest/queue.py b/src/ada/comms/rest/queue.py index 3a8849965..6bbe358d6 100644 --- a/src/ada/comms/rest/queue.py +++ b/src/ada/comms/rest/queue.py @@ -665,7 +665,7 @@ async def pull_subscribe(self, capability: str | None = None): forced every worker to NAK messages from other pools — that NAK loop burned the per-message delivery budget and surfaced as ``worker exceeded 3 delivery attempts`` errors on perfectly - valid jobs (see plan/v2 audit-pool routing notes). + valid jobs (see the internal notes). Idempotent: ``pull_subscribe`` matches an existing durable by name if the config is compatible, so multiple pods in the diff --git a/src/ada/comms/rest/source_cache.py b/src/ada/comms/rest/source_cache.py new file mode 100644 index 000000000..1d4c49581 --- /dev/null +++ b/src/ada/comms/rest/source_cache.py @@ -0,0 +1,296 @@ +"""Cross-job source-blob cache for the conversion worker. + +An audit sweep converts one source to many targets (glb/obj/stl/step/ifc/ +xml/parity/...), and each of those jobs used to stream the SAME source +object from storage again — a 778 MB source at ~15 MB/s costs 30-60 s of +pure re-download per job, times one job per target. This module keeps the +downloaded (already-decompressed) source bytes on worker-local disk keyed +by (scope, key, version) so every job after the first pays ~0 fetch time. + +Design: + +* **Versioned keys** — the cache filename is a SHA-256 over the scope + prefix, the object key, and a version token from ``Storage.head`` + (etag when the backend reports one, else size + last_modified). A + changed blob therefore hashes to a DIFFERENT entry name and can never + be served stale; the superseded entry ages out via LRU. The head call + is a real object-store metadata request (obstore ``head_async``), not + an HTTP HEAD on a presigned GET URL — the latter is method-bound and + 403s on Garage. +* **Bounded, LRU** — ``ADA_WORKER_SOURCE_CACHE_MB`` caps the on-disk + size (default 4096 MiB; ``0`` disables the cache entirely). Oldest- + mtime entries are evicted before a new blob is written; a hit touches + the entry's mtime. +* **Atomic entries** — blobs are streamed to a dot-prefixed temp name in + the cache dir and ``os.replace``d into place, then a sidecar ``.meta`` + file records the byte size. An entry is only ever served when the meta + matches the blob's on-disk size, so a torn write / external truncation + reads as a miss, never as a corrupt source. +* **Read-only hand-off** — the job's own temp path is hard-linked to the + cache entry (falling back to a copy across filesystems), so the job's + post-conversion ``unlink`` drops its link without touching the cache, + and an eviction mid-job can't yank the inode out from under a running + conversion. Converters treat the source as read-only input (outputs and + co-downloaded sidecars land at *sibling* paths next to the job's temp + name, not next to the cache entry). +* **Fail-open** — any cache-machinery error falls back to the plain + direct download. The only exception allowed through is + ``FileNotFoundError`` from the actual download, which the worker + already handles as "source missing". + +Multiple worker pods each keep an independent cache (worker-local disk); +that costs one download per pod per source version, which is fine. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import logging +import os +import pathlib +import shutil +import tempfile + +logger = logging.getLogger(__name__) + +_CACHE_MB_ENV = "ADA_WORKER_SOURCE_CACHE_MB" +_CACHE_DIR_ENV = "ADA_WORKER_SOURCE_CACHE_DIR" +_DEFAULT_CACHE_MB = 4096 + +# Fetch-mode strings recorded in the audit row's convert_meta. +MODE_HIT = "cache-hit" +MODE_MISS = "cache-miss" +MODE_DIRECT = "direct" + + +def _cap_bytes_from_env() -> int: + raw = os.environ.get(_CACHE_MB_ENV, "").strip() + if raw: + try: + return max(0, int(float(raw))) * (1 << 20) + except ValueError: + logger.warning("source-cache: bad %s=%r; using default %d MiB", _CACHE_MB_ENV, raw, _DEFAULT_CACHE_MB) + return _DEFAULT_CACHE_MB * (1 << 20) + + +def _dir_from_env() -> pathlib.Path: + raw = os.environ.get(_CACHE_DIR_ENV, "").strip() + if raw: + return pathlib.Path(raw) + # Same filesystem as the worker's mkstemp source paths by default, so + # the hard-link hand-off works and hits cost no byte copy at all. + return pathlib.Path(tempfile.gettempdir()) / "ada_worker_source_cache" + + +class SourceBlobCache: + """Disk LRU of downloaded source blobs, keyed by (scope, key, version).""" + + def __init__(self, cache_dir: pathlib.Path, cap_bytes: int) -> None: + self.cache_dir = pathlib.Path(cache_dir) + self.cap_bytes = int(cap_bytes) + # Per-entry locks: jobs are processed sequentially today, but the + # worker is an asyncio program — this keeps a future concurrent + # pull loop from double-downloading (or torn-reading) one entry. + self._locks: dict[str, asyncio.Lock] = {} + + @classmethod + def from_env(cls) -> "SourceBlobCache": + return cls(_dir_from_env(), _cap_bytes_from_env()) + + @property + def enabled(self) -> bool: + return self.cap_bytes > 0 + + async def fetch(self, storage, scope, key: str, dest_path: pathlib.Path) -> str: + """Materialise object ``key`` (decompressed) at ``dest_path``. + + Returns the fetch mode: ``cache-hit`` / ``cache-miss`` / ``direct`` + (cache disabled or bypassed after an internal error). Raises + ``FileNotFoundError`` when the object doesn't exist — same contract + as ``Storage.stream_to_path``. + """ + dest_path = pathlib.Path(dest_path) + if not self.enabled: + await storage.stream_to_path(scope, key, dest_path) + return MODE_DIRECT + + try: + version, stored_size = await self._version_token(storage, scope, key) + entry = self._entry_path(scope, key, version) + except Exception: + logger.exception("source-cache: version probe failed for %s; direct download", key) + entry, stored_size = None, 0 + if entry is None: + # No metadata (missing key or backend hiccup) — the direct + # stream raises a clean FileNotFoundError for truly-absent keys. + await storage.stream_to_path(scope, key, dest_path) + return MODE_DIRECT + + lock = self._locks.setdefault(entry.name, asyncio.Lock()) + async with lock: + try: + if self._entry_valid(entry): + os.utime(entry) # LRU touch + self._materialize(entry, dest_path) + logger.info("source-cache: hit %s (%d bytes) -> %s", key, entry.stat().st_size, dest_path.name) + return MODE_HIT + except Exception: + logger.exception("source-cache: hit-path failed for %s; refetching", key) + + tmp = entry.with_name(f".{entry.name}.{os.getpid()}.tmp") + try: + self.cache_dir.mkdir(parents=True, exist_ok=True) + # Pre-evict on the stored-size estimate (a gzip-at-rest blob + # decompresses larger; the post-write pass below settles it). + self._evict(incoming=stored_size) + try: + await storage.stream_to_path(scope, key, tmp) + except FileNotFoundError: + raise # source genuinely missing — worker handles this + disk_size = tmp.stat().st_size + os.replace(tmp, entry) + self._write_meta(entry, disk_size) + self._materialize(entry, dest_path) + if disk_size > self.cap_bytes: + # Single blob over the whole cap: serve it, don't keep it. + self._remove_entry(entry, reason="over-cap") + else: + self._evict(exclude=entry.name) + logger.info("source-cache: miss %s (%d bytes cached)", key, disk_size) + return MODE_MISS + except FileNotFoundError: + raise + except Exception: + logger.exception("source-cache: caching %s failed; direct download", key) + await storage.stream_to_path(scope, key, dest_path) + return MODE_DIRECT + finally: + try: + tmp.unlink(missing_ok=True) + except OSError: + pass + + # -- internals ----------------------------------------------------- + + async def _version_token(self, storage, scope, key: str) -> tuple[str | None, int]: + """(version discriminator, stored-size estimate) from the storage + layer's metadata call. + + ``Storage.head`` wraps obstore ``head_async`` — a real metadata + request against the store API (NOT an HTTP HEAD on a presigned GET + URL, which is method-bound and 403s on Garage). Prefers the etag; + size + last_modified is the fallback for backends without one. + """ + meta = await storage.head(scope, key) + if meta is None: + return None, 0 + size = int(meta.get("size") or 0) + etag = meta.get("e_tag") + if etag: + return f"etag:{etag}", size + return f"sz:{size}:lm:{meta.get('last_modified')}", size + + def _entry_path(self, scope, key: str, version: str | None) -> pathlib.Path | None: + if version is None: + return None + digest = hashlib.sha256(f"{scope.prefix()}\n{key}\n{version}".encode()).hexdigest() + suffix = pathlib.PurePosixPath(key).suffix.lower() + return self.cache_dir / f"{digest}{suffix}" + + def _entry_valid(self, entry: pathlib.Path) -> bool: + """True iff the blob exists AND its byte size matches the meta + sidecar written after the atomic rename — a torn/truncated entry + (or a blob whose meta never landed) reads as a miss.""" + meta = entry.with_name(entry.name + ".meta") + try: + expected = int(meta.read_text().strip()) + except (OSError, ValueError): + return False + try: + return entry.stat().st_size == expected + except OSError: + return False + + def _write_meta(self, entry: pathlib.Path, size: int) -> None: + meta = entry.with_name(entry.name + ".meta") + tmp = meta.with_name(f".{meta.name}.{os.getpid()}.tmp") + tmp.write_text(f"{size}\n") + os.replace(tmp, meta) + + def _materialize(self, entry: pathlib.Path, dest_path: pathlib.Path) -> None: + """Hand the cached blob to the job at its own temp path. + + Hard-link when possible (same filesystem): zero-copy, and the job's + eventual ``unlink`` only drops its own link. Converters never write + into the source path (outputs + sidecars are sibling files of the + job's temp name), so sharing the inode is safe. Cross-device falls + back to a plain copy. + """ + try: + dest_path.unlink(missing_ok=True) + os.link(entry, dest_path) + except OSError: + shutil.copyfile(entry, dest_path) + + def _entries(self) -> list[pathlib.Path]: + try: + return [ + p + for p in self.cache_dir.iterdir() + if p.is_file() and not p.name.startswith(".") and not p.name.endswith(".meta") + ] + except OSError: + return [] + + def _remove_entry(self, entry: pathlib.Path, *, reason: str) -> None: + size = 0 + try: + size = entry.stat().st_size + except OSError: + pass + for p in (entry, entry.with_name(entry.name + ".meta")): + try: + p.unlink(missing_ok=True) + except OSError: + pass + logger.info("source-cache: evict %s (%d bytes, %s)", entry.name, size, reason) + + def _evict(self, *, incoming: int = 0, exclude: str | None = None) -> None: + """Drop oldest-mtime entries until total + incoming fits the cap.""" + entries = [] + total = 0 + for p in self._entries(): + try: + st = p.stat() + except OSError: + continue + entries.append((st.st_mtime, st.st_size, p)) + total += st.st_size + if total + incoming <= self.cap_bytes: + return + entries.sort() # oldest mtime first + for _mtime, size, p in entries: + if total + incoming <= self.cap_bytes: + break + if exclude is not None and p.name == exclude: + continue + self._remove_entry(p, reason="lru") + total -= size + + +_default_cache: SourceBlobCache | None = None + + +def default_cache() -> SourceBlobCache: + """Process-wide cache instance, env-configured on first use.""" + global _default_cache + if _default_cache is None: + _default_cache = SourceBlobCache.from_env() + logger.info( + "source-cache: dir=%s cap=%d MiB%s", + _default_cache.cache_dir, + _default_cache.cap_bytes >> 20, + "" if _default_cache.enabled else " (disabled)", + ) + return _default_cache diff --git a/src/ada/comms/rest/storage.py b/src/ada/comms/rest/storage.py index a2545522e..8d9e5b8a5 100644 --- a/src/ada/comms/rest/storage.py +++ b/src/ada/comms/rest/storage.py @@ -597,7 +597,7 @@ async def presigned_put_url(self, scope: Scope, key: str, expires_in_seconds: in ``supports_presigned_uploads`` first. """ if not self.supports_presigned_uploads: - raise NotImplementedError("presigned uploads require an HTTP object store; " "LocalStore is not supported") + raise NotImplementedError("presigned uploads require an HTTP object store; LocalStore is not supported") return await obs.sign_async( self._presign_store, "PUT", @@ -622,9 +622,7 @@ async def presigned_get_url( range-stream read), where the public ingress would be a hairpin. """ if not self.supports_presigned_uploads: - raise NotImplementedError( - "presigned downloads require an HTTP object store; " "LocalStore is not supported" - ) + raise NotImplementedError("presigned downloads require an HTTP object store; LocalStore is not supported") return await obs.sign_async( self._store if internal else self._presign_store, "GET", @@ -633,9 +631,13 @@ async def presigned_get_url( ) async def head(self, scope: Scope, key: str) -> dict | None: - """Return ``{size, last_modified}`` for a key, or None if missing. - Used after a direct upload to confirm the object actually - landed before we audit a "ok" row.""" + """Return ``{size, last_modified, e_tag}`` for a key, or None if + missing. Used after a direct upload to confirm the object actually + landed before we audit a "ok" row, and by the worker's source-blob + cache as a cheap version discriminator (``e_tag`` when the backend + reports one — S3/Garage always do, obstore's LocalStore synthesises + an inode/mtime/size one — else None and callers fall back to + size + last_modified).""" try: meta = await self._store.head_async(self._full_key(scope, key)) except FileNotFoundError: @@ -643,7 +645,8 @@ async def head(self, scope: Scope, key: str) -> dict | None: size = int(meta["size"]) if isinstance(meta, dict) else int(getattr(meta, "size", 0)) lm = meta.get("last_modified") if isinstance(meta, dict) else getattr(meta, "last_modified", None) lm_iso = lm.isoformat() if hasattr(lm, "isoformat") else (str(lm) if lm else None) - return {"size": size, "last_modified": lm_iso} + etag = meta.get("e_tag") if isinstance(meta, dict) else getattr(meta, "e_tag", None) + return {"size": size, "last_modified": lm_iso, "e_tag": str(etag) if etag else None} async def exists(self, scope: Scope, key: str) -> bool: try: diff --git a/src/ada/comms/rest/worker.py b/src/ada/comms/rest/worker.py index 1b34f4540..3e5214c0b 100644 --- a/src/ada/comms/rest/worker.py +++ b/src/ada/comms/rest/worker.py @@ -34,6 +34,7 @@ from ada.config import logger from . import db as db_module +from . import source_cache from .config import load_settings from .converter import LEGACY_CONVERT_EXTS, ConverterRegistry, convert from .queue import JOB_STATUS_DONE, JOB_STATUS_ERROR, JOB_STATUS_RUNNING, Job, JobQueue @@ -711,22 +712,42 @@ async def _run_fea_meta_compute( await _audit_done(db_pool, job_id, "done", None, started_at) -def _parity_child(src_path, source_key, target_format, on_progress, *, formats=()): +def _parity_child(src_path, source_key, target_format, on_progress, *, produced=None): """``convert_fn``-shaped wrapper that runs the cross-format parity check and returns its result as JSON bytes. - Parity re-derives the source to ifc/xml/step and reloads each — easily a - multi-GB peak on a large model. Running it through ``run_isolated_convert`` - (this function in the forked child) instead of a worker threadpool means an - OOM is SIGKILLed by the per-job memory watchdog and fails the cell, rather - than taking the whole worker pod down.""" + ``produced`` maps each compared format (step/ifc/xml/glb) to the local path of + the blob the audit ALREADY produced+uploaded with the production strategy (or + None when that conversion failed/was skipped). The check reads those blobs and + compares a format-agnostic geometry invariant — it re-derives nothing, so it + validates exactly what ships. It still tessellates step/ifc/xml to measure them; + running through ``run_isolated_convert`` (this function in the forked child) + means an OOM there is SIGKILLed by the per-job memory watchdog and fails the + cell, rather than taking the whole worker pod down. + + Falls back to the offline re-derive path (``parity_for_source_file``) only when + no produced blobs were passed — never the case on the audit worker.""" import json as _json import pathlib as _pl - from ada.cadit.visual_parity import parity_for_source_file + from ada.cadit.visual_parity import ( + parity_for_source_file, + parity_from_produced_files, + parity_gxml_from_produced_files, + ) on_progress("parity", 0.2) - res = parity_for_source_file(_pl.Path(src_path), tuple(formats)) + if produced: + pmap = {fmt: (_pl.Path(p) if p else None) for fmt, p in produced.items()} + if str(source_key).lower().endswith(".xml"): + # Genie-XML: cheap per-format COUNT comparison over the produced blobs (the + # historical gxml invariant — catches a leg silently dropping N of M objects, + # which a bbox gate can miss) — zero re-derivation, zero tessellation. + res = parity_gxml_from_produced_files(source_key, pmap) + else: + res = parity_from_produced_files(source_key, pmap) + else: + res = parity_for_source_file(_pl.Path(src_path)) on_progress("ready", 1.0) return _json.dumps( { @@ -735,6 +756,7 @@ def _parity_child(src_path, source_key, target_format, on_progress, *, formats=( "consistent": res.consistent, "mismatches": res.mismatches, "errors": res.errors, + "skipped": res.skipped, "summary": res.summary(), } ).encode("utf-8") @@ -745,6 +767,7 @@ async def _run_parity_validation( job: Job, src_path: pathlib.Path, scope, + storage: "Storage", queue: "JobQueue", db_pool: "asyncpg.Pool | None", started_at: float, @@ -753,24 +776,31 @@ async def _run_parity_validation( ) -> None: """Cross-format visual-parity validation for one source (target_format=='parity'). - Re-derives the source to each structure-preserving format, reloads, and - compares the visualized-element count (ada.cadit.visual_parity). Produces no - derived blob: the structured per-format result goes to the ``audit_parity`` - table and the cell is audited done/error (a mismatch maps to ``error`` so it - surfaces in the run's failed cells). Never raises. + Reads the source's ALREADY-PRODUCED output blobs (step/ifc/xml/glb, converted + + uploaded earlier in the run with the production strategy) and compares a + format-agnostic GEOMETRY INVARIANT — surface area + bbox extent (see + ada.cadit.visual_parity.parity_from_produced_files). It re-derives nothing, so + it validates exactly what ships and does zero extra conversion; the parity cells + are only enqueued after every conversion cell for the source has landed, so the + blobs already exist. A format whose conversion failed/was skipped is recorded + (its blob is absent), never re-derived. Produces no derived blob: the structured + per-format result goes to the ``audit_parity`` table and the cell is audited + done/error (a mismatch maps to ``error`` so it surfaces in the run's failed + cells). Never raises. Runs in the same memory-capped forked child the convert path uses - (``run_isolated_convert``): re-deriving + reloading several formats can spike + (``run_isolated_convert``): tessellating step/ifc/xml to measure them can spike RAM, and a blow-up must die in isolation (cell fails as OOM) rather than - OOM-killing the worker pod. Re-deriving (rather than reading the stored GLB) - is deliberate: the stored GLB is mesh-merged, so its scene-entry count isn't - the object count — the check reloads with merging off. + OOM-killing the worker pod. """ import json + from ada.cadit.visual_parity import PARITY_GEOMETRY_FORMATS + + from .converter import derived_key_for + job_id = job.job_id suffix = pathlib.PurePosixPath(job.source_key).suffix.lower() - formats = tuple(t for t in ("ifc", "xml", "step") if t in ConverterRegistry.targets_for(suffix)) async def _cancel_check() -> bool: if db_pool is None: @@ -780,13 +810,55 @@ async def _cancel_check() -> bool: except Exception: return False + # FEM sources take the produced-files geometry-invariant path: fetch each already- + # produced output blob to a worker-local tempfile BEFORE forking (the child can't + # reach async storage; the fork shares the filesystem, so the child reads these + # paths). A missing blob (conversion failed/skipped) maps to None — recorded by + # parity_from_produced_files, never re-derived. This is the fix: it validates what + # actually ships (the analytic cylinder model) and does zero re-conversion, so it + # no longer stalls on nvme write-contention writing ~1 GB of temp files. + # + # Genie-XML sources take a produced-files COUNT path (parity_gxml_from_produced_files): + # the old re-derive (load + export via parity's own Python writers + reload) tripled + # once curved shells thicken by default and dominated the sweep; every produced format + # has a cheap counter instead (xml structure scan, ifc SPF line scan, native C++ step + # stream index) so the whole check is seconds and validates exactly what shipped. + # + # Non-gxml CAD (STEP/IFC/SAT) sources keep the streaming/whole-model re-derive path + # (produced left empty -> the child calls parity_for_source_file). Those were never + # the hang; and their Genie-XML output is legitimately empty for a raw-solid source + # (no Beam/Plate concept), which parity_for_source_file correctly SKIPS rather than + # flagging as dropped geometry. + _FEM_PARITY_SUFFIXES = (".fem", ".inp", ".sif", ".sin") + _PRODUCED_PARITY_SUFFIXES = _FEM_PARITY_SUFFIXES + (".xml",) + produced_dir = pathlib.Path(tempfile.mkdtemp(prefix="adapy-parity-")) + produced: dict[str, str | None] = {} + if suffix in _PRODUCED_PARITY_SUFFIXES: + targets = set(ConverterRegistry.targets_for(suffix)) + compare_formats = tuple(f for f in PARITY_GEOMETRY_FORMATS if f in targets) + for fmt in compare_formats: + try: + dkey = derived_key_for(job.source_key, fmt) + except Exception: + produced[fmt] = None + continue + dpath = produced_dir / f"produced.{fmt}" + try: + await storage.stream_to_path(scope, dkey, dpath) + produced[fmt] = str(dpath) + except FileNotFoundError: + produced[fmt] = None + except Exception: + logger.exception("worker: parity fetch of produced %s failed for %s", fmt, job.source_key) + produced[fmt] = None + try: iresult: IsolatedConvertResult = await run_isolated_convert( _parity_child, src_path, job.source_key, "parity", - convert_kwargs={"formats": list(formats)}, + convert_kwargs={"produced": produced}, on_progress=_on_progress, timeout_s=timeout_s, cancel_check=_cancel_check, @@ -796,6 +868,10 @@ async def _cancel_check() -> bool: await queue.update(job_id, status=JOB_STATUS_ERROR, stage="parity", error=str(exc)) await _audit_done(db_pool, job_id, "error", str(exc), started_at, traceback=tb_module.format_exc()) return + finally: + # The forked child has read the produced blobs by the time the call returns + # (or raises); drop the fetched copies either way. + shutil.rmtree(produced_dir, ignore_errors=True) metrics = dict(iresult.final_metrics) @@ -1323,6 +1399,11 @@ async def _process_one( # entirely. glb is the only registry target for ``.sin`` (the FEA-result # route); None falls back to the full stream below. sin_source_uri: str | None = None + # How the source landed on disk: "cache-hit" / "cache-miss" / "direct" + # (None for the SIF-reduced / SIN-stream special paths). Recorded in + # convert_meta so audit timing analysis can see the cache working — + # fetch_ms drops to ~0 on hits. + source_fetch_mode: str | None = None fetch_t0 = time.monotonic() try: try: @@ -1331,7 +1412,12 @@ async def _process_one( elif src_suffix.lower() == ".sin" and job.target_format == "glb": sin_source_uri = await _try_sin_stream_uri(storage, scope, job.source_key) if not sif_reduced and sin_source_uri is None: - await storage.stream_to_path(scope, job.source_key, src_path) + # Cross-job source cache: an audit sweep converts the same + # source to many targets, and re-downloading a multi-hundred- + # MB source per target costs 30-60 s each time. Falls back to + # a plain stream on any cache error (never fails the job) and + # still raises FileNotFoundError for a missing source. + source_fetch_mode = await source_cache.default_cache().fetch(storage, scope, job.source_key, src_path) except FileNotFoundError as exc: logger.warning("worker: source %s missing for job %s", job.source_key, job_id) await queue.update(job_id, status=JOB_STATUS_ERROR, stage="loading", error=str(exc)) @@ -1659,6 +1745,7 @@ async def _maybe_upload_log_bytes(log_bytes: bytes | None) -> str | None: job=job, src_path=src_path, scope=scope, + storage=storage, queue=queue, db_pool=db_pool, started_at=started_at, @@ -1699,6 +1786,10 @@ async def _maybe_upload_log_bytes(log_bytes: bytes | None) -> str | None: convert_meta["fetch_ms"] = fetch_ms if fetch_bytes is not None: convert_meta["fetch_bytes"] = fetch_bytes + if source_fetch_mode is not None: + # "cache-hit" explains a ~0 fetch_ms; "direct" marks a cache + # bypass (disabled or fell back after a cache error). + convert_meta["source_fetch"] = source_fetch_mode if sin_source_uri is not None: # No local copy — the child range-fetches pages on demand, so the # download cost shows up inside convert_ms, not fetch_ms. diff --git a/src/ada/config.py b/src/ada/config.py index df867e58e..5ed87d6a4 100644 --- a/src/ada/config.py +++ b/src/ada/config.py @@ -101,7 +101,7 @@ class Config: # pcurves are redundant — CAD consumers recompute them from the # 3D geometry on import — and OCCT's write.surfacecurve.mode=0 # roughly halves both STEP file size AND write time on plate-heavy - # models (validated ~-49% size / -60% time on Ship1T1). Enable for + # models (validated ~-49% size / -60% time on a large ship model). Enable for # the rare strict consumer that needs pcurves written explicitly. ConfigEntry("occ_step_write_pcurves", bool, False), ConfigEntry("add_trace_to_exception", bool, False), @@ -170,7 +170,7 @@ class Config: # On => sample the curve and carry the samples as extra outline points, so the plate # follows the curve. STOPGAP: Plate/CurvePoly2d has only line+arc segments, so the # samples are ours, not the neighbour's, and the seam still isn't shared. Off => - # the pre-2026-07-14 chord. See dap plan/v3/notes_plate_bspline_edges.md. + # the pre-2026-07-14 chord. See the internal design notes. ConfigEntry("plate_curved_edges", bool, True), ], ), @@ -188,6 +188,34 @@ class Config: # decompress copy per hydration/fast-path access — trades the zero-copy # property for a smaller resident floor. ConfigEntry("shape_store_compress", bool, False, required=False), + # Native NGEOM-record export (adacpp stream_ngeom_to_step/ifc): serialize + # each object's solid_geom() to an NGEOM blob and emit STEP/IFC through the + # C++ writers instead of the per-entity Python ones (~ms/face -> ~µs/face; + # a 5.5k-curved-shell hull's thickened to_ifc drops 31 s -> seconds). Gates + # the Genie-XML converter legs (xml -> step/ifc); the Python writers remain + # the wholesale fallback whenever adacpp is absent or any object fails to + # serialize. Env: ADA_CAD_NATIVE_NGEOM_EXPORT. + ConfigEntry("native_ngeom_export", bool, True, required=False), + ], + ), + ConfigSection( + "geom", + [ + # Where the modeled reference surface sits within the thickness when a + # plate/shell is thickened to a solid (flat Plate extrusions AND the + # thickened PlateCurved shells alike). "as_is" = base at the modeled + # surface, material extruded along +normal (the historical behaviour — + # flat-plate output stays byte-identical); "flipped" = material extruded + # along -normal (base translated by -t*normal); "centerline" = the + # modeled surface is the mid-surface (base translated by -t/2*normal). + # Env: ADA_GEOM_THICKNESS_ANCHOR. + ConfigEntry("thickness_anchor", str, "as_is"), + # Export curved shells (PlateCurved) as real thickness-t solids — an + # analytic ClosedShell built kernel-free from the shell face (bottom = + # the modeled surface, top = the same surface translated t along the + # sense-corrected normal, one side face per boundary edge). Off => the + # historical zero-thickness bare face. Env: ADA_GEOM_THICKEN_CURVED_SHELLS. + ConfigEntry("thicken_curved_shells", bool, True), ], ), ConfigSection( diff --git a/src/ada/core/clash_check.py b/src/ada/core/clash_check.py index fe2816da3..93e8ee26b 100644 --- a/src/ada/core/clash_check.py +++ b/src/ada/core/clash_check.py @@ -301,7 +301,7 @@ def _classify_connection( def find_edge_connected_perpendicular_plates(plates: list[Plate]) -> PlateConnections: """Find all plates that are connected at an edge and are perpendicular to that edge.""" # OCC-backend solid build/distance — imported lazily so this module stays - # importable under a non-OCC CAD backend (e.g. adacpp). See dap plan/v3 Phase 1. + # importable under a non-OCC CAD backend (e.g. adacpp). See the internal design notes. from ada.occ.geom.cache import get_solid_occ from ada.occ.occ_clash_check import plates_min_distance diff --git a/src/ada/fem/formats/code_aster/write/helper_utils.py b/src/ada/fem/formats/code_aster/write/helper_utils.py index b945bef51..1e0e0ccf0 100644 --- a/src/ada/fem/formats/code_aster/write/helper_utils.py +++ b/src/ada/fem/formats/code_aster/write/helper_utils.py @@ -7,7 +7,7 @@ def resolve_ids_in_multiple(tags, tags_data, is_elem): """Find elements shared by multiple sets. - Hot path on the JackethybridFEM → Code_Aster (.med) conversion + Hot path on a jacket FEM model → Code_Aster (.med) conversion (~530 s of a ~660 s job before tuning). Three O(N²) issues in the original loop: diff --git a/src/ada/fem/formats/mesh_faces.py b/src/ada/fem/formats/mesh_faces.py index e971714c4..ed71e2866 100644 --- a/src/ada/fem/formats/mesh_faces.py +++ b/src/ada/fem/formats/mesh_faces.py @@ -1076,6 +1076,7 @@ def iter_fem_analytic_faces( reconstruct_curved: bool = False, skip_cylinders: bool = False, drop_on_tube=None, + with_meta: bool = False, ): """Yield analytic ``ada.geom`` faces for every FEM shell mesh under ``part``, auto- detecting each region-grown patch's primitive: a **cylinder** patch → analytic @@ -1097,6 +1098,13 @@ def iter_fem_analytic_faces( with the walls (no penetration, no gap). Leave it False unless a caller specifically wants the B-spline reduction on clean grid-topology hull panels. + ``with_meta`` (default False) yields ``(face, material, thickness)`` tuples instead of bare + faces — the (material, thickness) of the patch each face came from. The strong flat merge + buckets by (material, thickness, normal), so a merged planar face has a single well-defined + pair; a cylinder patch is tagged with its seed facet's. This is what the Genie-XML analytic + writer needs to give each ```` / ```` a thickness_ref/material_ref + while reusing this strong merge. STEP/IFC call it without meta and are unaffected. + Never worse than the plain coplanar merge (non-reconstructed regions fall through to it) and collapses a tube's thousands of shell facets to a handful of exact cylinders.""" parts = part.get_all_parts_in_assembly(include_self=True) if hasattr(part, "get_all_parts_in_assembly") else [part] @@ -1114,6 +1122,11 @@ def iter_fem_analytic_faces( patches = list(_surface_patches(prims, angle_tol, ndigits)) patch_cls = [(pt, classify_patch(prims, pt) if len(pt) >= min_patch_quads else "planar") for pt in patches] + elid_to_prim = {_elid_of(prims.names[j]): j for j in range(len(prims))} if with_meta else None + + def _tag(face, ref_j): + return (face, prims.mats[ref_j], float(prims.ts[ref_j])) if with_meta else face + # Curved-panel B-spline pass over each QUAD block's structured grid (cylinder patches # excluded, kept analytic). Consumes only the curved rectangles; everything else falls to # the cylinder / planar / facet emit below, which skips the consumed elements. @@ -1126,7 +1139,7 @@ def iter_fem_analytic_faces( for face, panel_elids in _reconstruct_curved_panels( blk, cyl_elids, ndigits, angle_tol, min_patch_quads ): - yield face + yield _tag(face, elid_to_prim[panel_elids[0]]) if with_meta else face consumed.update(panel_elids) # Cylinders emit as analytic tubes (region-grow finds them by swept normals); their prims @@ -1143,7 +1156,8 @@ def iter_fem_analytic_faces( # exact joint-cut trim only when asked (breaks adacpp meshing, see above); # otherwise the viz-safe full tube with flat circular ends. trimmed = cylinder_trim_faces(prims, patch, cf, ndigits) if trim_cylinders else None - yield from (trimmed if trimmed is not None else cylinder_fit_to_faces(cf)) + for face in trimmed if trimmed is not None else cylinder_fit_to_faces(cf): + yield _tag(face, patch[0]) # Flat plates: group every remaining facet (not a cylinder, not consumed by a curved B-spline # panel) by plane bucket — (material, thickness, normal) + edge-connected components — and @@ -1160,14 +1174,15 @@ def iter_fem_analytic_faces( flat = [j for j in flat if not drop_on_tube(prims.outline(j).mean(axis=0))] for comp in _plane_bucket_components(prims, flat, ndigits, plane_digits=3): if len(comp) == 1: - yield _facet_flat_face(prims, comp[0]) + yield _tag(_facet_flat_face(prims, comp[0]), comp[0]) continue faces = _flat_faces_with_holes(prims, comp, ndigits) if faces: - yield from faces + for face in faces: + yield _tag(face, comp[0]) else: # boundary wouldn't resolve → per-facet (never lose geometry) for j in comp: - yield _facet_flat_face(prims, j) + yield _tag(_facet_flat_face(prims, j), j) def _analytic_face_data( diff --git a/src/ada/fem/formats/sesam/read/reader.py b/src/ada/fem/formats/sesam/read/reader.py index dcc54b8ad..6075f3646 100644 --- a/src/ada/fem/formats/sesam/read/reader.py +++ b/src/ada/fem/formats/sesam/read/reader.py @@ -98,7 +98,7 @@ def _build_array_fem(part, coords, node_ids, by_type, mass_elem, spring_elem, ex # ``from_id`` against the still-internal store). The object path stays correct for # free because its sets hold Node/Elem objects whose ``.id`` is renumbered in place; # the array path must remap the captured ids explicitly or every NSET/ELSET member - # resolves to a now-missing id (e.g. JacketHybrid elset member 787 -> external 3052). + # resolves to a now-missing id (e.g. a jacket model elset member 787 -> external 3052). _remap_id_backed_sets(fem, node_map, ext_map) diff --git a/src/ada/fem/formats/sesam/write/write_elements.py b/src/ada/fem/formats/sesam/write/write_elements.py index d088526fc..99c4da04b 100644 --- a/src/ada/fem/formats/sesam/write/write_elements.py +++ b/src/ada/fem/formats/sesam/write/write_elements.py @@ -28,7 +28,7 @@ def _is_writable_to_sesam(el: Elem) -> bool: GELMNT1 with eltyp=40 without the matching MSHGLSP would produce a Sestra-incomplete deck — skipping is the honest choice. Cross-format roundtrip uses an MPC / kinematic- - coupling representation instead (see plan/v2 follow-up). + coupling representation instead (see the internal notes). * **Unsectioned elements** (``fem_sec is None``): the Sesam writer's GELREF1 emitter needs a section / material binding @@ -133,7 +133,7 @@ def write_elem(el: Elem, thick_map) -> str: # MTRSOL records define the solid material's orientation; # those are absent today and the analysis program assumes a # default isotropic alignment. That's a real coverage gap - # for anisotropic solids; flagged in plan/v2. + # for anisotropic solids; flagged in the internal notes. sec_id = 0 else: raise ValueError(f'Unsupported elem type "{fem_sec.type}"') diff --git a/src/ada/fem/formats/utils.py b/src/ada/fem/formats/utils.py index cc1654d92..0eaea4306 100644 --- a/src/ada/fem/formats/utils.py +++ b/src/ada/fem/formats/utils.py @@ -566,66 +566,65 @@ def convert_shell_elem_to_plates( return plates -def convert_part_shell_elements_to_plates(p: Part) -> Plates: - """Shell elements -> Plates, with the per-element orientation math vectorized. - - A gather pass mirrors :func:`convert_shell_elem_to_plates` exactly (section - guards + warnings, material consolidation, coplanarity split of warped - quads, the tri-branch material quirk and its try/except), producing one - entry per output plate in element order. The geometry then runs once per - polygon arity through :meth:`CurvePoly2d.from_fem_shells_batch` — the same - floating-point ops as ``from_fem_shell``, batched — which on a 100k-shell - model removes ~1/3 of the FEM->concept wall time. Rows the bulk math - escapes (degenerate corners/edges) fall back to the scalar constructor. - """ +def shell_elem_to_plate_entries(elem: Elem, part: Part, mat_dict: dict) -> list[tuple]: + """Gather the plate build-entries for one shell element, mirroring + :func:`convert_shell_elem_to_plates` exactly (section guards + warnings, material + consolidation via ``mat_dict``, coplanarity split of warped quads, and the + tri-branch material quirk / try/except). Returns ``(name, pts (k,3), thickness, + material, catch_errors)`` tuples — one per output plate, in element order — or + ``[]`` to skip the element. The geometry is deferred so a caller can batch the + orientation math per arity (:func:`build_plates_from_entries`).""" import numpy as np - from ada import Plate - from ada.api.curves import CurvePoly2d from ada.base.types import GeomRepr from ada.core.vector_utils import is_coplanar - # One shared material cache for every shell in the part — see the note - # in convert_shell_elem_to_plates on why a per-element dict was O(N²). - mat_dict: dict = {} + fem_sec = elem.fem_sec + if fem_sec is None or fem_sec.material is None: + logger.warning(f"Shell element {elem.id} has no section/material; skipping plate conversion") + return [] + if fem_sec.type == GeomRepr.SOLID or getattr(fem_sec, "thickness", None) is None: + logger.warning(f"Shell-shaped element {elem.id} has a solid/thickness-less section; not a plate, skipping") + return [] + fem_sec.material.parent = part + new_mat = mat_dict.get(fem_sec.material.name, None) + if new_mat is None: + new_mat = part.materials.add(fem_sec.material.copy_to(fem_sec.material.name, parent=part)) + mat_dict[fem_sec.material.name] = new_mat - # (name, pts (k,3), thickness, material, catch_errors) — one per output plate. - entries: list[tuple] = [] - for elem in p.fem.elements.shell: - fem_sec = elem.fem_sec - if fem_sec is None or fem_sec.material is None: - logger.warning(f"Shell element {elem.id} has no section/material; skipping plate conversion") - continue - if fem_sec.type == GeomRepr.SOLID or getattr(fem_sec, "thickness", None) is None: - logger.warning(f"Shell-shaped element {elem.id} has a solid/thickness-less section; not a plate, skipping") - continue - fem_sec.material.parent = p - new_mat = mat_dict.get(fem_sec.material.name, None) - if new_mat is None: - new_mat = p.materials.add(fem_sec.material.copy_to(fem_sec.material.name, parent=p)) - mat_dict[fem_sec.material.name] = new_mat - - t = fem_sec.thickness - nodes = elem.nodes - if len(nodes) == 4: - n0, n1, n2, n3 = nodes[0].p, nodes[1].p, nodes[2].p, nodes[3].p - if is_coplanar(*n0, *n1, *n2, *n3): - entries.append((f"sh{elem.id}", np.array((n0, n1, n2, n3), dtype=float), t, new_mat, False)) - else: - entries.append((f"sh{elem.id}", np.array((n0, n1, n2), dtype=float), t, new_mat, False)) - entries.append((f"sh{elem.id}_1", np.array((n0, n2, n3), dtype=float), t, new_mat, False)) - else: - # The scalar path binds the tri branch to the raw (unconsolidated) - # section material and wraps it in try/except — both preserved. - entries.append((f"sh{elem.id}", np.array([n.p for n in nodes], dtype=float), t, fem_sec.material, True)) + t = fem_sec.thickness + nodes = elem.nodes + if len(nodes) == 4: + n0, n1, n2, n3 = nodes[0].p, nodes[1].p, nodes[2].p, nodes[3].p + if is_coplanar(*n0, *n1, *n2, *n3): + return [(f"sh{elem.id}", np.array((n0, n1, n2, n3), dtype=float), t, new_mat, False)] + return [ + (f"sh{elem.id}", np.array((n0, n1, n2), dtype=float), t, new_mat, False), + (f"sh{elem.id}_1", np.array((n0, n2, n3), dtype=float), t, new_mat, False), + ] + # The scalar path binds the tri branch to the raw (unconsolidated) section + # material and wraps it in try/except — both preserved. + return [(f"sh{elem.id}", np.array([n.p for n in nodes], dtype=float), t, fem_sec.material, True)] + + +def build_plates_from_entries(entries: list[tuple], part: Part, detached: bool = False) -> list: + """Build the Plates for a list of :func:`shell_elem_to_plate_entries` entries with + the orientation/projection math vectorized once per polygon arity + (:meth:`CurvePoly2d.from_fem_shells_batch` — same floating-point ops as the scalar + ``from_fem_shell``, batched — removing ~1/3 of the FEM->concept wall time on large + shell meshes). Rows the bulk math escapes (degenerate corners/edges) fall back to + the scalar constructor. Result order matches ``entries``.""" + import numpy as np + + from ada import Plate + from ada.api.curves import CurvePoly2d - # Vectorized geometry per polygon arity, results realigned to entry order. polys: list = [None] * len(entries) by_k: dict[int, list[int]] = {} for i, e in enumerate(entries): by_k.setdefault(e[1].shape[0], []).append(i) for _k, idxs in by_k.items(): - batch = CurvePoly2d.from_fem_shells_batch(np.stack([entries[i][1] for i in idxs]), parent=p) + batch = CurvePoly2d.from_fem_shells_batch(np.stack([entries[i][1] for i in idxs]), parent=part) for i, poly in zip(idxs, batch): polys[i] = poly @@ -633,17 +632,32 @@ def convert_part_shell_elements_to_plates(p: Part) -> Plates: for (name, pts, t, mat, catch_errors), poly in zip(entries, polys): try: if poly is None: - pl = Plate.from_fem_shell(name, [q for q in pts], t, mat=mat, parent=p) + pl = Plate.from_fem_shell(name, [q for q in pts], t, mat=mat, parent=part, detached=detached) else: - pl = Plate(name, poly, t, mat=mat, parent=p) + pl = Plate(name, poly, t, mat=mat, parent=part, detached=detached) except BaseException as e: if not catch_errors: raise logger.error(f"Unable to convert {name} to plate due to {e}") continue plates.append(pl) + return plates + + +def convert_part_shell_elements_to_plates(p: Part) -> Plates: + """Shell elements -> Plates, with the per-element orientation math vectorized. - return Plates(plates, parent=p) + A gather pass mirrors :func:`convert_shell_elem_to_plates` exactly (section + guards + warnings, material consolidation, coplanarity split of warped quads, the + tri-branch material quirk and its try/except), producing one entry per output + plate in element order; the geometry then runs once per polygon arity through + :meth:`CurvePoly2d.from_fem_shells_batch`. + """ + mat_dict: dict = {} # shared cache; a per-element dict was O(N²) — see the note above + entries: list[tuple] = [] + for elem in p.fem.elements.shell: + entries.extend(shell_elem_to_plate_entries(elem, p, mat_dict)) + return Plates(build_plates_from_entries(entries, p), parent=p) def convert_part_elem_bm_to_beams(p: Part) -> Beams: diff --git a/src/ada/fem/sections.py b/src/ada/fem/sections.py index e198df682..9f8e48f0f 100644 --- a/src/ada/fem/sections.py +++ b/src/ada/fem/sections.py @@ -74,7 +74,7 @@ def __hash__(self): # Tuple hash of the underlying attrs — was previously # ``hash(f"{self.name}{self.id}")`` which paid for an # f-string format AND went through the ``name`` / ``id`` - # property descriptors on every call. The JackethybridFEM + # property descriptors on every call. A jacket FEM model # → Genie XML conversion calls this 5.8 BILLION times # during ``consolidate_materials`` (set membership checks # on the materials' ref lists). The format/descriptor diff --git a/src/ada/geom/curves.py b/src/ada/geom/curves.py index 7c2b65adf..d38bc9a0b 100644 --- a/src/ada/geom/curves.py +++ b/src/ada/geom/curves.py @@ -2,7 +2,6 @@ from dataclasses import dataclass, field from enum import Enum -from itertools import chain from typing import TYPE_CHECKING, Iterable, Union import numpy as np @@ -127,6 +126,39 @@ class CompositeCurveSegment: transition: str = "CONTINUOUS" +def _sample_circle_arc(circle, t1, t2, sense_agreement, n: int = 24) -> list[tuple[float, float]]: + """Sample a 2D arc of ``circle`` between cartesian trims ``t1``/``t2`` (full ring if either is None). + + Natural parametrization is CCW about the placement axis with x = ``ref_direction``; ``sense_agreement`` + False sweeps CW instead. Used by :meth:`CompositeCurve.to_points2d` for the rare arc-in-composite.""" + center = np.asarray(circle.position.location, dtype=float)[:2] + ref = np.asarray(circle.position.ref_direction, dtype=float)[:2] + rn = np.linalg.norm(ref) + xdir = ref / rn if rn else np.array([1.0, 0.0]) + ydir = np.array([-xdir[1], xdir[0]]) # +90deg (CCW natural direction of an IfcCircle) + r = float(circle.radius) + + def _ang(p): + d = np.asarray(p, dtype=float)[:2] - center + return float(np.arctan2(float(d @ ydir), float(d @ xdir))) + + if t1 is None or t2 is None: + a0, a1 = 0.0, 2.0 * np.pi + else: + a0, a1 = _ang(t1), _ang(t2) + if sense_agreement: + while a1 <= a0: + a1 += 2.0 * np.pi + else: + while a1 >= a0: + a1 -= 2.0 * np.pi + out = [] + for a in np.linspace(a0, a1, n): + p = center + r * np.cos(a) * xdir + r * np.sin(a) * ydir + out.append((float(p[0]), float(p[1]))) + return out + + @dataclass(slots=True) class CompositeCurve: """ @@ -139,6 +171,43 @@ class CompositeCurve: segments: list[CompositeCurveSegment] self_intersect: bool = False + def to_points2d(self) -> list[tuple[float, float]]: + """Ordered, de-duplicated 2D outline points — line endpoints, sampled arcs/B-splines. + + Lets a spline-bearing plate profile (an ``IfcCompositeCurve``, which ``IfcIndexedPolyCurve`` + can't carry) be read back as a point-based ``Plate`` outline. The IFC file itself stays + analytic (the composite carries the real ``IfcBSplineCurveWithKnots``); only this reader-side + outline samples the curve.""" + + def _parent_points(c) -> list: + if isinstance(c, PolyLine): + return list(c.points) + if isinstance(c, BSplineCurveWithKnots): + return c.sample(max(16, int(c.degree) * 8)) + if isinstance(c, Edge): + return [c.start, c.end] + if isinstance(c, TrimmedCurve) and isinstance(c.basis_curve, BSplineCurveWithKnots): + # Parameter-trimmed over the full knot span (how the writer wraps a spline segment). + return c.basis_curve.sample(max(16, int(c.basis_curve.degree) * 8)) + if isinstance(c, TrimmedCurve) and isinstance(c.basis_curve, Circle): + return _sample_circle_arc(c.basis_curve, c.trim1, c.trim2, c.sense_agreement) + if isinstance(c, Circle): + return _sample_circle_arc(c, None, None, True) + raise NotImplementedError(f"CompositeCurve.to_points2d: unsupported parent curve {type(c).__name__}") + + pts: list[tuple[float, float]] = [] + for seg in self.segments: + seq = _parent_points(seg.parent_curve) + if not seg.same_sense: + seq = list(reversed(seq)) + for p in seq: + xy = (float(p[0]), float(p[1])) + if not pts or abs(pts[-1][0] - xy[0]) > 1e-9 or abs(pts[-1][1] - xy[1]) > 1e-9: + pts.append(xy) + if len(pts) > 2 and abs(pts[0][0] - pts[-1][0]) < 1e-9 and abs(pts[0][1] - pts[-1][1]) < 1e-9: + pts.pop() # drop the closing duplicate + return pts + @dataclass(slots=True) class Clothoid: @@ -252,11 +321,26 @@ def get_points(self): return points def get_unique_points_and_segment_indices(self) -> tuple[np.ndarray, list[list[int]]]: - points = list(chain.from_iterable([list(segment) for segment in self.segments])) - points_tuple = [tuple(x) for x in chain.from_iterable([list(segment) for segment in self.segments])] - unique_pts, pts_index = np.unique(points, axis=0, return_index=False, return_inverse=True) - indices = [[int(pts_index[points_tuple.index(tuple(s))]) + 1 for s in segment] for segment in self.segments] - + # Per-segment point sequence: Edge -> 2 pts (line), ArcLine -> 3 pts (arc). A B-spline can't be + # indexed analytically here (IfcIndexedPolyCurve only has line/arc index), so it is sampled into + # a polyline -> a single IfcLineIndex of k>2 points. The analytic path routes spline outlines to + # an IfcCompositeCurve instead (see indexed_poly_curve_or_composite); this keeps every other + # get_unique consumer (e.g. the streaming IFC writer) working with a faceted-but-valid spline. + seg_point_lists = [ + ( + [tuple(p) for p in segment.sample(max(16, int(segment.degree) * 8))] + if isinstance(segment, BSplineCurveWithKnots) + else [tuple(p) for p in segment] + ) + for segment in self.segments + ] + all_pts = [p for lst in seg_point_lists for p in lst] + unique_pts, inv = np.unique(all_pts, axis=0, return_inverse=True) + inv = np.asarray(inv).reshape(-1) # np>=2 returns shape (n,1); flatten for indexing + indices, cursor = [], 0 + for lst in seg_point_lists: + indices.append([int(inv[cursor + j]) + 1 for j in range(len(lst))]) + cursor += len(lst) return unique_pts, indices def to_points2d(self): @@ -416,6 +500,48 @@ class BSplineCurveWithKnots: knots: list[float] knot_spec: KnotType + def sample(self, n: int) -> list[tuple[float, float, float]]: + """Discretize the curve into ``n`` points evenly across its knot span (endpoints included). + + Vectorised de Boor (numpy only — scipy is not a core dep); the subclass + :class:`RationalBSplineCurveWithKnots` is de-homogenised via ``weights_data``. This is where a + B-spline plate/section edge is turned into a polyline for tessellation, so the analytic curve + can be carried unsampled everywhere upstream and discretized only here. Raises ``ValueError`` + on a malformed spec (degree/knot/control-point-count mismatch or a zero rational weight). + """ + cp = np.asarray([list(p)[:3] for p in self.control_points_list], dtype=float) + # SAT/IFC store knots + multiplicities separately; de Boor wants them expanded. + knots = np.repeat(np.asarray(self.knots, dtype=float), np.asarray(self.knot_multiplicities, dtype=int)) + deg = int(self.degree) + if deg < 1 or len(cp) <= deg or len(knots) != len(cp) + deg + 1: + raise ValueError("malformed B-spline (degree / knot / control-point-count mismatch)") + weights = getattr(self, "weights_data", None) + if weights is not None and len(weights) == len(cp): + wa = np.asarray(weights, dtype=float).reshape(-1, 1) + cp = np.hstack([cp * wa, wa]) # homogeneous; divided through after evaluation + lo, hi = float(knots[deg]), float(knots[len(knots) - deg - 1]) + if not np.isfinite([lo, hi]).all() or hi <= lo: + raise ValueError("degenerate B-spline knot span") + xs = np.linspace(lo, hi, n) + ks = np.clip(np.searchsorted(knots, xs, side="right") - 1, deg, len(cp) - 1) + d = [cp[j + ks - deg] for j in range(deg + 1)] # d[j][i] = j-th active ctrl pt for sample i + for r in range(1, deg + 1): + for j in range(deg, r - 1, -1): + lo_ = knots[j + ks - deg] + hi_ = knots[j + 1 + ks - r] + den = hi_ - lo_ + a = np.divide(xs - lo_, den, out=np.zeros_like(den), where=den != 0)[:, None] + d[j] = (1.0 - a) * d[j - 1] + a * d[j] + pts = d[deg] + if pts.shape[1] == 4: # rational: de-homogenize + w_last = pts[:, 3] + if np.any(w_last == 0): + raise ValueError("zero rational weight in B-spline evaluation") + pts = pts[:, :3] / w_last[:, None] + if not np.isfinite(pts).all(): + raise ValueError("non-finite point in B-spline evaluation") + return [tuple(p) for p in pts.tolist()] + @dataclass(slots=True) class PCurve: @@ -591,6 +717,19 @@ class EdgeLoop: edge_list: list[OrientedEdge] +@dataclass(slots=True) +class VertexLoop: + """ + IFC4x3 (https://standards.buildingsmart.org/IFC/RELEASE/IFC4_3/HTML/lexical/IfcVertexLoop.htm) + STEP (https://www.steptools.com/stds/stp_aim/html/t_vertex_loop.html) + + A loop of a single vertex — the degenerate boundary a fully-closed periodic + surface (a whole sphere's spherical face) uses to anchor its one pole point. + """ + + loop_vertex: Point + + # Concrete tuple of bare-curve geometry classes (CURVE_GEOM_TYPES is a Union of forward-ref # strings, so it can't be used with isinstance). Used to detect a Geometry that carries a curve # rather than a surface/solid — e.g. a sectionless SAT wire body that must render as glTF line diff --git a/src/ada/geom/primitive_brep.py b/src/ada/geom/primitive_brep.py new file mode 100644 index 000000000..89b3eb34c --- /dev/null +++ b/src/ada/geom/primitive_brep.py @@ -0,0 +1,896 @@ +"""Analytic B-rep shells for the CSG solid primitives (Sphere / Cone / ...). + +The streaming STEP writer authors AP242 B-rep straight from ``ada.geom`` analytic +faces (Plane / Cylindrical / Conical / Spherical / Toroidal / B-spline surfaces), +but the CSG *primitive* solids — ``ada.geom.solids.Sphere``, ``.Cone`` — are not +shells, so they were dropped by the analytic path and (per the "no geometry left +behind" rule) must NOT fall back to a tessellated facet soup: a sphere has an +exact one-face spherical B-rep, a cone an exact conical-surface + planar-cap +B-rep. + +This module builds those exact analytic shells kernel-free (pure Python, so it +runs under wasm/pyodide) as ``ada.geom`` :class:`ClosedShell`\\ s of +:class:`AdvancedFace`\\ s, which the existing ``_emit_analytic_brep`` / +``_brep_surface`` writer path then emits — and which the IFC face-surface writer +consumes too, so both emitters share one converter. + +Box and Cylinder already emit as extrusions on the streaming path, so they are +intentionally not built here (the extrusion form is the proven watertight one). +""" + +from __future__ import annotations + +import dataclasses +import math +from bisect import bisect_right + +import ada.geom.curves as cu +import ada.geom.solids as so +import ada.geom.surfaces as su +from ada.geom.direction import Direction +from ada.geom.placement import Axis2Placement3D +from ada.geom.points import Point + + +def _unit(v) -> tuple[float, float, float]: + x, y, z = float(v[0]), float(v[1]), float(v[2]) + n = math.sqrt(x * x + y * y + z * z) + if n == 0.0: + return (0.0, 0.0, 1.0) + return (x / n, y / n, z / n) + + +def _cross(a, b) -> tuple[float, float, float]: + return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]) + + +def _right_hand(axis) -> tuple[tuple, tuple]: + """A ref (local x) + local y orthonormal to ``axis`` (local z).""" + z = _unit(axis) + seed = (1.0, 0.0, 0.0) if abs(z[0]) < 0.9 else (0.0, 1.0, 0.0) + x = _unit(_cross(seed, z)) + y = _unit(_cross(z, x)) + return x, y + + +def _placement(location, axis, ref) -> Axis2Placement3D: + return Axis2Placement3D( + location=Point(*location), + axis=Direction(*_unit(axis)), + ref_direction=Direction(*_unit(ref)), + ) + + +def sphere_to_shell(sphere: so.Sphere) -> su.ClosedShell: + """A whole sphere as one spherical ``AdvancedFace`` bounded by a single pole + ``VertexLoop`` — the canonical fully-periodic-surface B-rep (matches the OCC / + adacpp sphere: one SPHERICAL_SURFACE, one ADVANCED_FACE).""" + center = (float(sphere.center[0]), float(sphere.center[1]), float(sphere.center[2])) + r = float(sphere.radius) + axis = (0.0, 0.0, 1.0) + ref = (1.0, 0.0, 0.0) + surface = su.SphericalSurface(position=_placement(center, axis, ref), radius=r) + pole = Point(center[0], center[1], center[2] + r) + face = su.AdvancedFace( + bounds=[su.FaceBound(bound=cu.VertexLoop(loop_vertex=pole), orientation=True)], + face_surface=surface, + same_sense=True, + ) + return su.ClosedShell(cfs_faces=[face]) + + +def cone_to_shell(cone: so.Cone) -> su.ClosedShell | None: + """A right circular cone as a conical lateral ``AdvancedFace`` (seam generatrix + + base circle, split into two semicircle arcs so the shared base edge's sense is + unambiguous) plus a planar bottom cap. Analytic — one CONICAL_SURFACE, one PLANE + — never tessellated. Returns None for a degenerate cone.""" + r = float(cone.bottom_radius) + h = float(cone.height) + if r <= 0.0 or h <= 0.0: + return None + + pos = cone.position + base_c = (float(pos.location[0]), float(pos.location[1]), float(pos.location[2])) + axis = _unit(pos.axis) if pos.axis is not None else (0.0, 0.0, 1.0) + ref = _unit(pos.ref_direction) if pos.ref_direction is not None else _right_hand(axis)[0] + + apex = tuple(base_c[i] + h * axis[i] for i in range(3)) + p0 = tuple(base_c[i] + r * ref[i] for i in range(3)) # base circle at param 0 (+ref) + p1 = tuple(base_c[i] - r * ref[i] for i in range(3)) # param pi (-ref) + semi_angle = math.atan2(r, h) + + conical = su.ConicalSurface(position=_placement(base_c, axis, ref), radius=r, semi_angle=semi_angle) + plane = su.Plane(position=_placement(base_c, axis, ref)) + circ = cu.Circle(position=_placement(base_c, axis, ref), radius=r) + + def _pt(p): + return Point(*p) + + # shared edges (one EdgeCurve object each -> the writer shares the EDGE_CURVE and + # derives each ORIENTED_EDGE's sense from the loop traversal direction): + seam = cu.EdgeCurve(start=_pt(p0), end=_pt(apex), edge_geometry=None, same_sense=True) # generatrix line + arc_u = cu.EdgeCurve(start=_pt(p0), end=_pt(p1), edge_geometry=circ, same_sense=True) # +y half + arc_l = cu.EdgeCurve(start=_pt(p1), end=_pt(p0), edge_geometry=circ, same_sense=True) # -y half + + def _oe(a, b, ec): + return cu.OrientedEdge(start=_pt(a), end=_pt(b), edge_element=ec, orientation=True) + + # lateral face: down the seam, around the base (P0->P1->P0), back up the seam. + lateral_loop = cu.EdgeLoop( + edge_list=[_oe(apex, p0, seam), _oe(p0, p1, arc_u), _oe(p1, p0, arc_l), _oe(p0, apex, seam)] + ) + lateral = su.AdvancedFace( + bounds=[su.FaceBound(bound=lateral_loop, orientation=True)], + face_surface=conical, + same_sense=True, + ) + # cap: the base circle traversed the opposite way, planar face flipped so its + # normal points out of the solid (down the -axis). + cap_loop = cu.EdgeLoop(edge_list=[_oe(p0, p1, arc_l), _oe(p1, p0, arc_u)]) + cap = su.AdvancedFace( + bounds=[su.FaceBound(bound=cap_loop, orientation=True)], + face_surface=plane, + same_sense=False, + ) + return su.ClosedShell(cfs_faces=[lateral, cap]) + + +def primitive_to_analytic_shell(geometry) -> su.ClosedShell | None: + """Pure-Python analytic B-rep shell for a CSG primitive solid, or None when the + primitive has no kernel-free converter here (the caller then tries the adacpp + native track). Kernel-free, so it works under wasm/pyodide.""" + if isinstance(geometry, so.Sphere): + return sphere_to_shell(geometry) + if isinstance(geometry, so.Cone): + return cone_to_shell(geometry) + return None + + +# Primitive solids the pure-Python track above cannot yet build a kernel-free +# analytic B-rep for, but which the CAD backend (adacpp / OCC) CAN build exactly — +# so the native track is worth trying before any faceting. Box/Cylinder/Sphere/Cone +# are covered by the extrusion or pure-Python paths, so this is Torus + friends. +_NATIVE_PRIMITIVES = (so.Torus, so.Cone, so.Sphere, so.Cylinder, so.Box) + + +def native_primitive_to_analytic_shell(geometry) -> su.ClosedShell | None: + """Analytic B-rep shell for a CSG primitive built by the CAD backend (adacpp + preferred, its bundled OCCT) and read back kernel-free as ``ada.geom`` analytic + faces via the streaming STEP reader — so the emitted faces are exact analytic + surfaces (CONICAL/SPHERICAL/TOROIDAL/PLANE), never a facet mesh. Best-effort: + returns None when no backend is available, the build fails, or the reader yields + no bounded analytic shell. Used only as a fallback after the pure-Python track.""" + if not isinstance(geometry, _NATIVE_PRIMITIVES): + return None + import tempfile + + from ada.geom import Geometry + + try: + from ada.cad import active_backend + except Exception: # noqa: BLE001 - no CAD backend at all (slim worker) + return None + try: + backend = active_backend() + except Exception: # noqa: BLE001 + return None + build = getattr(backend, "build", None) + write_step = getattr(backend, "write_step", None) + if build is None or write_step is None: + return None + + path = tempfile.mktemp(suffix=".stp") + try: + shape = build(Geometry(id="prim", geometry=geometry)) + write_step([shape], ["prim"], [(0.6, 0.6, 0.6)], path, "m", "AP242") + except Exception: # noqa: BLE001 - backend can't build/write this primitive + return None + + from ada.cadit.step.read.stream_reader import stream_read_step + + shell = None + try: + for geom in stream_read_step(path, local_pool=False, tolerant=True): + g = getattr(geom, "geometry", None) + faces = getattr(g, "cfs_faces", None) + # keep the first shell whose faces all carry a boundary the writer can + # re-emit (a bound-less face — e.g. adacpp's whole-sphere VERTEX_LOOP the + # reader drops — would be silently lost, so reject it here). + if faces and all(getattr(f, "bounds", None) for f in faces): + shell = g + break + except Exception: # noqa: BLE001 - reader can't parse this STEP kernel-free + return None + finally: + try: + import os + + os.remove(path) + except OSError: + pass + return shell + + +# ── extruded boundary-loop shells (analytic plate B-rep) ───────────────────── +def _vsub(a, b): + return (float(a[0]) - float(b[0]), float(a[1]) - float(b[1]), float(a[2]) - float(b[2])) + + +def _vadd(a, b): + return (float(a[0]) + float(b[0]), float(a[1]) + float(b[1]), float(a[2]) + float(b[2])) + + +def _vscale(a, s): + return (float(a[0]) * s, float(a[1]) * s, float(a[2]) * s) + + +def _vdot(a, b): + return float(a[0]) * float(b[0]) + float(a[1]) * float(b[1]) + float(a[2]) * float(b[2]) + + +def _circumcenter(p1, m, p2): + """Center of the circle through three 3D points, or None if collinear.""" + u = _vsub(m, p1) + v = _vsub(p2, p1) + w = _cross(u, v) + w2 = _vdot(w, w) + if w2 < 1e-24: + return None + # c = p1 + (|v|^2 (w x u) + |u|^2 (v x w)) / (2 |w|^2) + t = _vadd(_vscale(_cross(w, u), _vdot(v, v)), _vscale(_cross(v, w), _vdot(u, u))) + return _vadd(p1, _vscale(t, 0.5 / w2)) + + +def _reversed_bspline(c: cu.BSplineCurveWithKnots) -> cu.BSplineCurveWithKnots: + """Reverse a B-spline's parametrization (reverse control points/weights, mirror knots).""" + total = c.knots[0] + c.knots[-1] + common = dict( + degree=c.degree, + control_points_list=list(reversed(c.control_points_list)), + curve_form=c.curve_form, + closed_curve=c.closed_curve, + self_intersect=c.self_intersect, + knot_multiplicities=list(reversed(c.knot_multiplicities)), + knots=[total - k for k in reversed(c.knots)], + knot_spec=c.knot_spec, + ) + if isinstance(c, cu.RationalBSplineCurveWithKnots): + return cu.RationalBSplineCurveWithKnots(weights_data=list(reversed(c.weights_data)), **common) + return cu.BSplineCurveWithKnots(**common) + + +def _translated_bspline(c: cu.BSplineCurveWithKnots, dvec) -> cu.BSplineCurveWithKnots: + common = dict( + degree=c.degree, + control_points_list=[Point(*_vadd(p, dvec)) for p in c.control_points_list], + curve_form=c.curve_form, + closed_curve=c.closed_curve, + self_intersect=c.self_intersect, + knot_multiplicities=c.knot_multiplicities, + knots=c.knots, + knot_spec=c.knot_spec, + ) + if isinstance(c, cu.RationalBSplineCurveWithKnots): + return cu.RationalBSplineCurveWithKnots(weights_data=list(c.weights_data), **common) + return cu.BSplineCurveWithKnots(**common) + + +def _extruded_bspline_surface(c: cu.BSplineCurveWithKnots, dvec) -> su.BSplineSurfaceWithKnots: + """The EXACT linear extrusion of a B-spline curve: u follows the curve (same degree/knots), + v is linear across the extrusion vector — two control rows per curve control point.""" + grid = [[Point(*p), Point(*_vadd(p, dvec))] for p in c.control_points_list] + common = dict( + u_degree=int(c.degree), + v_degree=1, + control_points_list=grid, + surface_form=su.BSplineSurfaceForm.UNSPECIFIED, + u_closed=bool(c.closed_curve), + v_closed=False, + self_intersect=False, + u_multiplicities=list(c.knot_multiplicities), + v_multiplicities=[2, 2], + u_knots=list(c.knots), + v_knots=[0.0, 1.0], + knot_spec=c.knot_spec, + ) + weights = getattr(c, "weights_data", None) + if weights: + return su.RationalBSplineSurfaceWithKnots(weights_data=[[float(w), float(w)] for w in weights], **common) + return su.BSplineSurfaceWithKnots(**common) + + +def extruded_loop_to_shell( + segments3d: list, extrude_dir, depth: float, base_offset: float = 0.0 +) -> su.ClosedShell | None: + """Analytic ``ClosedShell`` of ``AdvancedFace``s for a plate extruded from a boundary loop that + carries analytic curved segments (``ArcSegment`` / ``SplineSegment``). + + ``IfcExtrudedAreaSolid`` cannot carry a B-spline boundary through the tools that matter — + ``IfcIndexedPolyCurve`` is line/arc-only and ifcopenshell's engine won't build a wire from a + B-spline ``IfcCompositeCurve`` segment — so a spline-boundary plate is emitted as an + ``IfcAdvancedBrep`` instead: planar caps + planar/cylindrical side faces, and the spline side + face as the EXACT degree-1-in-v B-spline surface of the linear extrusion. Topology mirrors the + (OCC-round-trip-proven) streaming AP242 STEP emitter: shared base/top boundary edges, vertical + connector edges, 4-edge quad side loops. Returns None for loops this builder cannot express. + + ``base_offset`` shifts the extrusion BASE by ``base_offset * extrude_dir`` from the modeled + loop (the global thickness-anchor control; 0.0 keeps the historical output). + """ + from ada.api.curves import ArcSegment, LineSegment, SplineSegment + + ez = _unit(extrude_dir) + dvec = _vscale(ez, float(depth)) + ovec = _vscale(ez, float(base_offset)) + n = len(segments3d) + if n < 3: + return None + + # Normalize the loop to CCW about the extrusion axis so every face's same_sense is fixed. + area2 = (0.0, 0.0, 0.0) + for seg in segments3d: + area2 = _vadd(area2, _cross(tuple(map(float, seg.p1[:3])), tuple(map(float, seg.p2[:3])))) + if _vdot(area2, ez) < 0.0: + rev = [] + for seg in reversed(segments3d): + if isinstance(seg, SplineSegment): + rev.append(SplineSegment(seg.p2, seg.p1, curve=_reversed_bspline(seg.curve))) + elif isinstance(seg, ArcSegment): + rev.append(ArcSegment(seg.p2, seg.p1, midpoint=seg.midpoint)) + else: + rev.append(LineSegment(seg.p2, seg.p1)) + segments3d = rev + + base = [Point(*_vadd(tuple(map(float, seg.p1[:3])), ovec)) for seg in segments3d] + top = [Point(*_vadd(b, dvec)) for b in base] + + def line_ec(pa: Point, pb: Point) -> cu.EdgeCurve: + return cu.EdgeCurve(pa, pb, edge_geometry=cu.Line(pa, Direction(*_unit(_vsub(pb, pa)))), same_sense=True) + + # Vertical connector edges, shared between adjacent side faces. + vert = [line_ec(base[i], top[i]) for i in range(n)] + + eb: list = [None] * n + et: list = [None] * n + faces: list[su.AdvancedFace] = [] + + for i, seg in enumerate(segments3d): + j = (i + 1) % n + if isinstance(seg, SplineSegment): + base_curve = _translated_bspline(seg.curve, ovec) if base_offset else seg.curve + eb[i] = cu.EdgeCurve(base[i], base[j], edge_geometry=base_curve, same_sense=True) + et[i] = cu.EdgeCurve(top[i], top[j], edge_geometry=_translated_bspline(base_curve, dvec), same_sense=True) + surf = _extruded_bspline_surface(base_curve, dvec) + same_sense = True # CCW loop: du x dv = tangent x extrude = outward + elif isinstance(seg, ArcSegment): + c = _circumcenter(seg.p1, seg.midpoint, seg.p2) + if c is None: + return None + r = math.sqrt(_vdot(_vsub(seg.p1, c), _vsub(seg.p1, c))) + # Axis such that travelling CCW about it from p1 passes the midpoint before p2. + axis = _unit(_cross(_vsub(seg.p1, c), _vsub(seg.midpoint, c))) + ref = _unit(_vsub(seg.p1, c)) + c_base = _vadd(c, ovec) + circle = cu.Circle(_placement(c_base, axis, ref), r) + eb[i] = cu.EdgeCurve(base[i], base[j], edge_geometry=circle, same_sense=True) + c_top = _vadd(c_base, dvec) + et[i] = cu.EdgeCurve( + top[i], top[j], edge_geometry=cu.Circle(_placement(c_top, axis, ref), r), same_sense=True + ) + surf = su.CylindricalSurface(position=_placement(c_base, ez, ref), radius=r) + # Cylinder normals point radially outward; a convex (outward-bulging) arc has its + # material inside the circle, so radial == outward. Concave: flipped. + chord_out = _cross(_unit(_vsub(seg.p2, seg.p1)), ez) + same_sense = _vdot(_vsub(seg.midpoint, c), chord_out) > 0.0 + else: + eb[i] = line_ec(base[i], base[j]) + et[i] = line_ec(top[i], top[j]) + tangent = _unit(_vsub(base[j], base[i])) + out_n = _cross(tangent, ez) + surf = su.Plane(position=_placement(base[i], out_n, tangent)) + same_sense = True + + loop = cu.EdgeLoop( + edge_list=[ + cu.OrientedEdge(eb[i].start, eb[i].end, edge_element=eb[i], orientation=True), + cu.OrientedEdge(vert[j].start, vert[j].end, edge_element=vert[j], orientation=True), + cu.OrientedEdge(et[i].start, et[i].end, edge_element=et[i], orientation=False), + cu.OrientedEdge(vert[i].start, vert[i].end, edge_element=vert[i], orientation=False), + ] + ) + faces.append( + su.AdvancedFace( + bounds=[su.FaceBound(bound=loop, orientation=True)], face_surface=surf, same_sense=same_sense + ) + ) + + xdir, _ = _right_hand(ez) + top_loop = cu.EdgeLoop(edge_list=[cu.OrientedEdge(e.start, e.end, edge_element=e, orientation=True) for e in et]) + faces.append( + su.AdvancedFace( + bounds=[su.FaceBound(bound=top_loop, orientation=True)], + face_surface=su.Plane(position=_placement(top[0], ez, xdir)), + same_sense=True, + ) + ) + bot_loop = cu.EdgeLoop( + edge_list=[cu.OrientedEdge(e.start, e.end, edge_element=e, orientation=False) for e in reversed(eb)] + ) + faces.append( + su.AdvancedFace( + bounds=[su.FaceBound(bound=bot_loop, orientation=True)], + face_surface=su.Plane(position=_placement(base[0], _vscale(ez, -1.0), xdir)), + same_sense=True, + ) + ) + return su.ClosedShell(cfs_faces=faces) + + +# ── thickened curved-shell B-rep (PlateCurved thickness export) ────────────── +# +# A gxml/SAT curved shell carries only its reference face (typically a trimmed +# B-spline patch) plus a thickness ``t`` in the XML. ``face_to_thick_shell`` +# turns that face into an analytic thickness-``t`` ``ClosedShell`` KERNEL-FREE: +# bottom = the reference surface (translated per the thickness anchor), top = +# the SAME surface translated by ``t * direction`` (exact — every B-spline +# control point / placement location moves rigidly), and one side face per +# boundary edge (plane / cylinder / ruled B-spline / surface-of-linear- +# extrusion). Topology follows ``extruded_loop_to_shell``: shared EdgeCurve +# objects between adjacent faces, 4-edge quad side loops, outward same_sense. +# +# NOTE: the translated top surface is a rigid copy, not a normal-offset — the +# wall thickness measured along the local surface normal varies with the +# surface's slope relative to ``direction``. For the gently curved hull shells +# this feeds (and for how Genie itself displays thickness) that is the intended +# semantic; a true offset surface is not expressible for the downstream writers. + +THICKNESS_ANCHORS = ("as_is", "flipped", "centerline") + + +def thickness_anchor_base_offset(anchor: str, t: float) -> float: + """Signed offset of the extrusion BASE from the modeled reference surface, + along the (sense-corrected) thickness direction, for a given anchor. + + ``as_is``: base at the modeled surface, material on the +direction side (0). + ``flipped``: material on the -direction side (base at ``-t``). + ``centerline``: modeled surface is the mid-surface (base at ``-t/2``). + """ + if anchor == "as_is": + return 0.0 + if anchor == "flipped": + return -float(t) + if anchor == "centerline": + return -0.5 * float(t) + raise ValueError(f"unknown thickness anchor {anchor!r} (expected one of {THICKNESS_ANCHORS})") + + +def _is_zero_vec(v) -> bool: + return float(v[0]) == 0.0 and float(v[1]) == 0.0 and float(v[2]) == 0.0 + + +def _translated_placement(pos: Axis2Placement3D, dvec) -> Axis2Placement3D: + return Axis2Placement3D( + location=Point(*_vadd(pos.location, dvec)), + axis=pos.axis, + ref_direction=pos.ref_direction, + ) + + +def _translated_curve(c, dvec): + """A rigid translated copy of a curve, or None when the type isn't supported. + Returns the original object for a zero vector (instance sharing is deliberate).""" + if _is_zero_vec(dvec): + return c + if isinstance(c, cu.BSplineCurveWithKnots): # incl. Rational subclass + return _translated_bspline(c, dvec) + if isinstance(c, cu.Line): + return dataclasses.replace(c, pnt=Point(*_vadd(c.pnt, dvec))) + if isinstance(c, (cu.Circle, cu.Ellipse)): + return dataclasses.replace(c, position=_translated_placement(c.position, dvec)) + if isinstance(c, cu.PolyLine): + return dataclasses.replace(c, points=[Point(*_vadd(p, dvec)) for p in c.points]) + if isinstance(c, cu.TrimmedCurve): + basis = _translated_curve(c.basis_curve, dvec) + if basis is None: + return None + trims = [] + for trim in (c.trim1, c.trim2): + trims.append(Point(*_vadd(trim, dvec)) if isinstance(trim, Point) else trim) + return dataclasses.replace(c, basis_curve=basis, trim1=trims[0], trim2=trims[1]) + if isinstance(c, cu.SurfaceCurve): + c3d = _translated_curve(c.curve_3d, dvec) + if c3d is None: + return None + # pcurves are UV-space images; a rigid translation of surface + curve keeps them valid. + return dataclasses.replace(c, curve_3d=c3d) + return None + + +def _translated_surface(s, dvec): + """A rigid translated copy of a surface, or None when the type isn't supported.""" + if _is_zero_vec(dvec): + return s + if isinstance(s, su.BSplineSurfaceWithKnots): # incl. Rational subclass (weights copied by replace) + return dataclasses.replace( + s, control_points_list=[[Point(*_vadd(p, dvec)) for p in row] for row in s.control_points_list] + ) + if isinstance(s, (su.Plane, su.CylindricalSurface, su.ConicalSurface, su.SphericalSurface, su.ToroidalSurface)): + return dataclasses.replace(s, position=_translated_placement(s.position, dvec)) + return None + + +# -- kernel-free B-spline surface evaluation (normal probe) -------------------- +def _full_knots(knots, mults) -> list[float]: + return [float(k) for k, m in zip(knots, mults) for _ in range(int(m))] + + +def _deboor_pt(x: float, knots: list[float], cps: list[list[float]], deg: int) -> list[float]: + """de Boor point evaluation on a full (repeated) knot vector; pure Python.""" + k = bisect_right(knots, x) - 1 + k = max(deg, min(k, len(cps) - 1)) + d = [list(cps[j + k - deg]) for j in range(deg + 1)] + for r in range(1, deg + 1): + for j in range(deg, r - 1, -1): + lo = knots[j + k - deg] + hi = knots[j + 1 + k - r] + a = 0.0 if hi == lo else (x - lo) / (hi - lo) + d[j] = [(1.0 - a) * p + a * q for p, q in zip(d[j - 1], d[j])] + return d[deg] + + +def _bspline_surface_point(s: su.BSplineSurfaceWithKnots, u: float, v: float) -> tuple[float, float, float]: + """Exact point on a (rational) B-spline surface via tensor-product de Boor.""" + uk = _full_knots(s.u_knots, s.u_multiplicities) + vk = _full_knots(s.v_knots, s.v_multiplicities) + weights = getattr(s, "weights_data", None) + rows_h = [] + for i, row in enumerate(s.control_points_list): + hrow = [] + for j, p in enumerate(row): + w = float(weights[i][j]) if weights else 1.0 + hrow.append([float(p[0]) * w, float(p[1]) * w, float(p[2]) * w, w]) + rows_h.append(hrow) + # de Boor along v for each u-row, then along u across the results. + col = [_deboor_pt(v, vk, hrow, int(s.v_degree)) for hrow in rows_h] + r = _deboor_pt(u, uk, col, int(s.u_degree)) + w = r[3] if r[3] != 0.0 else 1.0 + return (r[0] / w, r[1] / w, r[2] / w) + + +def _bspline_mid_normal(s: su.BSplineSurfaceWithKnots) -> tuple[float, float, float] | None: + """Surface normal at the parametric mid-point (central differences), or None if degenerate.""" + uk = _full_knots(s.u_knots, s.u_multiplicities) + vk = _full_knots(s.v_knots, s.v_multiplicities) + du_deg, dv_deg = int(s.u_degree), int(s.v_degree) + u0, u1 = uk[du_deg], uk[len(uk) - du_deg - 1] + v0, v1 = vk[dv_deg], vk[len(vk) - dv_deg - 1] + if u1 <= u0 or v1 <= v0: + return None + # Probe at the middle and, if degenerate there, at an off-centre fallback. + for fu, fv in ((0.5, 0.5), (0.35, 0.65), (0.65, 0.35)): + um, vm = u0 + fu * (u1 - u0), v0 + fv * (v1 - v0) + eu, ev = 1e-4 * (u1 - u0), 1e-4 * (v1 - v0) + pu1 = _bspline_surface_point(s, min(um + eu, u1), vm) + pu0 = _bspline_surface_point(s, max(um - eu, u0), vm) + pv1 = _bspline_surface_point(s, um, min(vm + ev, v1)) + pv0 = _bspline_surface_point(s, um, max(vm - ev, v0)) + n = _cross(_vsub(pu1, pu0), _vsub(pv1, pv0)) + if _vdot(n, n) > 1e-30: + return _unit(n) + return None + + +def face_mid_normal(face: su.AdvancedFace | su.FaceSurface) -> tuple[float, float, float] | None: + """The face's own oriented normal (surface normal at a representative parameter, + flipped by ``same_sense``), or None when the surface type has no cheap kernel-free + probe (conical/spherical/toroidal etc. — the caller falls back to the bare face).""" + s = face.face_surface + n = None + if isinstance(s, su.Plane): + n = _unit(s.position.axis) if s.position.axis is not None else (0.0, 0.0, 1.0) + elif isinstance(s, su.BSplineSurfaceWithKnots): + try: + n = _bspline_mid_normal(s) + except Exception: # noqa: BLE001 - malformed knot/cp data -> no thickening + n = None + if n is None: + return None + if not face.same_sense: + n = _vscale(n, -1.0) + return n + + +def _circle_arc_bspline(circle: cu.Circle, p_start, p_end, same_sense: bool) -> cu.RationalBSplineCurveWithKnots | None: + """The EXACT rational quadratic B-spline of the circular arc from ``p_start`` to + ``p_end`` along the circle's parametric direction (reversed when ``same_sense`` is + False) — the standard piecewise-Bezier conic (<=90 deg segments, w = cos(half-angle)). + + Returns None for a degenerate frame or a (near-)full circle, where the arc between + two coincident endpoints is ambiguous — callers keep the periodic surface form then. + """ + pos = circle.position + if pos.axis is None: + return None + z = _unit(pos.axis) + x = _unit(pos.ref_direction) if pos.ref_direction is not None else _right_hand(z)[0] + y = _cross(z, x) + c = tuple(float(v) for v in pos.location) + r = float(circle.radius) + if r <= 0.0: + return None + + def _theta(p): + d = _vsub(p, c) + return math.atan2(_vdot(d, y), _vdot(d, x)) + + th_s, th_e = _theta(p_start), _theta(p_end) + if same_sense: + sweep = (th_e - th_s) % (2.0 * math.pi) + else: + sweep = -((th_s - th_e) % (2.0 * math.pi)) + if abs(sweep) < 1e-9 or abs(sweep) > 2.0 * math.pi - 1e-9: + return None # coincident endpoints: ambiguous (full circle) — keep the periodic surface + + n_seg = max(1, int(math.ceil(abs(sweep) / (0.5 * math.pi)))) + delta = sweep / n_seg + half = 0.5 * abs(delta) + w_mid = math.cos(half) + if w_mid < 1e-9: + return None + + def _pt(theta, rad=r): + return Point(*(c[i] + rad * (math.cos(theta) * x[i] + math.sin(theta) * y[i]) for i in range(3))) + + cps: list[Point] = [_pt(th_s)] + weights: list[float] = [1.0] + for k in range(n_seg): + a0 = th_s + k * delta + mid = a0 + 0.5 * delta + cps.append(_pt(mid, rad=r / w_mid)) + cps.append(_pt(a0 + delta)) + weights.extend([w_mid, 1.0]) + + return cu.RationalBSplineCurveWithKnots( + degree=2, + control_points_list=cps, + curve_form=cu.BSplineCurveFormEnum.UNSPECIFIED, + closed_curve=False, + self_intersect=False, + knot_multiplicities=[3] + [2] * (n_seg - 1) + [3], + knots=[k / n_seg for k in range(n_seg + 1)], + knot_spec=cu.KnotType.UNSPECIFIED, + weights_data=weights, + ) + + +def _vkey(p) -> tuple[float, float, float]: + return (round(float(p[0]), 9), round(float(p[1]), 9), round(float(p[2]), 9)) + + +def _line_geom(a: Point, b: Point) -> cu.Line | None: + d = _vsub(b, a) + if _vdot(d, d) < 1e-24: + return None + return cu.Line(a, Direction(*_unit(d))) + + +def face_to_thick_shell( + advanced_face: su.AdvancedFace | su.FaceSurface, + direction, + t: float, + anchor: str = "as_is", + direction_agrees_with_face: bool = True, +) -> su.ClosedShell | None: + """Thickness-``t`` analytic ``ClosedShell`` for a bounded face, kernel-free. + + ``direction`` is the shell's sense-corrected thickness direction (unit); material + spans the anchor-dependent slab along it (see :func:`thickness_anchor_base_offset`). + ``direction_agrees_with_face`` says whether ``direction`` equals the face's own + oriented normal (surface normal x same_sense) — for a gxml ``curved_shell`` this is + the authored ``sense_flag``. + + Faces: bottom/top = rigid translated copies of the input face (outward normals + -direction / +direction), one side face per boundary edge of every loop (outer AND + hole loops — a hole grows an inner side tube automatically). Shared EdgeCurve / + vertex objects follow the ``extruded_loop_to_shell`` conventions so downstream + writers keep the shell topologically closed. Returns None for anything unbuildable + (unsupported surface/curve type, degenerate edge/loop) — callers keep today's bare + face, never lose geometry. + """ + t = float(t) + if t <= 0.0: + return None + bounds = getattr(advanced_face, "bounds", None) + if not bounds: + return None + for fb in bounds: + if not isinstance(fb, su.FaceBound) or not isinstance(fb.bound, cu.EdgeLoop): + return None + if not fb.bound.edge_list: + return None + for oe in fb.bound.edge_list: + if not isinstance(oe, cu.OrientedEdge) or not isinstance(oe.edge_element, cu.EdgeCurve): + return None + + dirn = _unit(direction) + base_off = thickness_anchor_base_offset(anchor, t) + ovec = _vscale(dirn, base_off) + tvec = _vscale(dirn, t) + topvec = _vadd(ovec, tvec) + agree = bool(direction_agrees_with_face) + + surf_bot = _translated_surface(advanced_face.face_surface, ovec) + surf_top = _translated_surface(advanced_face.face_surface, topvec) + if surf_bot is None or surf_top is None: + return None + + def _edge_copy(ec: cu.EdgeCurve, dvec, memo: dict): + hit = memo.get(id(ec)) + if hit is not None: + return hit + if _is_zero_vec(dvec): + memo[id(ec)] = ec + return ec + geom = _translated_curve(ec.edge_geometry, dvec) if ec.edge_geometry is not None else None + if ec.edge_geometry is not None and geom is None: + return None # untranslatable curve type -> shell unbuildable + copy = cu.EdgeCurve( + Point(*_vadd(ec.start, dvec)), Point(*_vadd(ec.end, dvec)), edge_geometry=geom, same_sense=ec.same_sense + ) + memo[id(ec)] = copy + return copy + + memo_bot: dict = {} + memo_top: dict = {} + + def _face_at(surface, offset_vec, memo, flip: bool) -> su.AdvancedFace | None: + new_bounds = [] + for fb in bounds: + oes = [] + edge_list = fb.bound.edge_list if not flip else list(reversed(fb.bound.edge_list)) + for oe in edge_list: + ec = _edge_copy(oe.edge_element, offset_vec, memo) + if ec is None: + return None + orientation = bool(oe.orientation) if not flip else not oe.orientation + a, b = (ec.start, ec.end) if orientation else (ec.end, ec.start) + oes.append( + cu.OrientedEdge( + a, + b, + edge_element=ec, + orientation=orientation, + pcurve=oe.pcurve, + t_start=oe.t_start, + t_end=oe.t_end, + ) + ) + new_bounds.append(su.FaceBound(bound=cu.EdgeLoop(edge_list=oes), orientation=fb.orientation)) + same_sense = bool(advanced_face.same_sense) if not flip else not advanced_face.same_sense + return su.AdvancedFace(bounds=new_bounds, face_surface=surface, same_sense=same_sense) + + # Bottom face's outward normal is -direction, top's +direction. The input face's + # oriented normal is +direction when ``agree`` — so bottom flips iff agree. + bot_face = _face_at(surf_bot, ovec, memo_bot, flip=agree) + top_face = _face_at(surf_top, topvec, memo_top, flip=not agree) + if bot_face is None or top_face is None: + return None + + faces: list[su.AdvancedFace] = [bot_face, top_face] + connectors: dict = {} + + def _connector(key, p_bot: Point, p_top: Point) -> cu.EdgeCurve: + conn = connectors.get(key) + if conn is None: + conn = cu.EdgeCurve(p_bot, p_top, edge_geometry=cu.Line(p_bot, Direction(*dirn)), same_sense=True) + connectors[key] = conn + return conn + + for fb in bounds: + items = [(oe, oe.edge_element, bool(oe.orientation)) for oe in fb.bound.edge_list] + # The loop as listed with orientation=True is CCW about the FACE normal; the side + # faces are built around a loop that is CCW about +direction. + if bool(fb.orientation) != agree: + items = [(oe, ec, not fwd) for (oe, ec, fwd) in reversed(items)] + for src_oe, ec, fwd in items: + bec = _edge_copy(ec, ovec, memo_bot) + tec = _edge_copy(ec, topvec, memo_top) + if bec is None or tec is None: + return None + a_b, b_b = (bec.start, bec.end) if fwd else (bec.end, bec.start) + a_t, b_t = (tec.start, tec.end) if fwd else (tec.end, tec.start) + ka = _vkey(ec.start if fwd else ec.end) + kb = _vkey(ec.end if fwd else ec.start) + conn_a = _connector(ka, a_b, a_t) + conn_b = _connector(kb, b_b, b_t) + + geom0 = ec.edge_geometry + bgeom = bec.edge_geometry + if isinstance(geom0, cu.SurfaceCurve): + geom0 = geom0.curve_3d + if isinstance(bgeom, cu.SurfaceCurve): + bgeom = bgeom.curve_3d + + straight = ( + geom0 is None + or isinstance(geom0, cu.Line) + or (isinstance(geom0, cu.PolyLine) and len(geom0.points) == 2) + ) + # Does the traversal follow the underlying curve's own parametric direction? + # (fwd = traversal from edge start to edge end; same_sense = the curve runs + # start->end.) The swept side surfaces below have du = the curve's parametric + # tangent, so their same_sense is anchored to this, not to fwd alone. + along_param = fwd == bool(ec.same_sense) + + arc = None + if isinstance(geom0, cu.Circle): + # EXACT rational-B-spline ruled surface of the ARC between the edge's + # vertices. Preferred over CylindricalSurface / SurfaceOfLinearExtrusion + # because the periodic forms are untrimmable for the stream tessellation + # kernel (a boundary arc's side face then meshes as the FULL tube); the + # arc patch's natural bounds ARE the ribbon, so it is robust everywhere. + arc = _circle_arc_bspline(bgeom, bec.start, bec.end, bool(bec.same_sense)) + + if straight: + chord = _vsub(b_b, a_b) + if _vdot(chord, chord) < 1e-24: + return None # degenerate straight edge + tangent = _unit(chord) + out_n = _cross(tangent, dirn) + if _vdot(out_n, out_n) < 1e-24: + return None # edge parallel to the thickness direction + surf = su.Plane(position=_placement(a_b, out_n, tangent)) + ssense = True + elif isinstance(geom0, cu.BSplineCurveWithKnots): + # Exact ruled surface of the linear extrusion (degree 1 in v). + surf = _extruded_bspline_surface(bgeom, tvec) + ssense = along_param + elif arc is not None: + # The arc curve already runs start->end (same_sense folded in), so its + # parametric tangent follows the edge direction. + surf = _extruded_bspline_surface(arc, tvec) + ssense = fwd + elif ( + isinstance(geom0, cu.Circle) + and geom0.position.axis is not None + and abs(_vdot(_unit(geom0.position.axis), dirn)) > 1.0 - 1e-9 + ): + # Full-circle edge (coincident endpoints) with axis parallel to the + # thickness direction: the natural cylinder IS the side face. + dp = _vdot(_unit(geom0.position.axis), dirn) + ref = bgeom.position.ref_direction + ref = _unit(ref) if ref is not None else _right_hand(dirn)[0] + surf = su.CylindricalSurface( + position=_placement(bgeom.position.location, dirn, ref), radius=float(geom0.radius) + ) + # Cylinder normals point radially outward: outward-of-solid equals radial + # when the traversal follows the circle's own (CCW about its axis) + # parametrization AND the axis points along +direction. + ssense = along_param if dp > 0.0 else not along_param + else: + # Generic swept side face (ellipse / full circle off-axis / multi-segment + # polyline / trimmed curve). + surf = su.SurfaceOfLinearExtrusion( + swept_curve=bgeom, position=None, extrusion_direction=Direction(*dirn), depth=t + ) + ssense = along_param + + # The source coedge's parametric trim rides along: the translated copies keep the + # original curve's parametrization exactly, and without (t_start, t_end) a CLOSED + # edge geometry (a boundary arc on a full Circle) is untrimmable for the stream + # kernel — it would tessellate the side face over the whole cylinder. + ts, te = src_oe.t_start, src_oe.t_end + loop = cu.EdgeLoop( + edge_list=[ + cu.OrientedEdge(a_b, b_b, edge_element=bec, orientation=fwd, t_start=ts, t_end=te), + cu.OrientedEdge(b_b, b_t, edge_element=conn_b, orientation=True), + cu.OrientedEdge(b_t, a_t, edge_element=tec, orientation=not fwd, t_start=ts, t_end=te), + cu.OrientedEdge(a_t, a_b, edge_element=conn_a, orientation=False), + ] + ) + faces.append( + su.AdvancedFace( + bounds=[su.FaceBound(bound=loop, orientation=True)], face_surface=surf, same_sense=ssense + ) + ) + + return su.ClosedShell(cfs_faces=faces) diff --git a/src/ada/occ/backend.py b/src/ada/occ/backend.py index d9e4740d3..e6dcb7001 100644 --- a/src/ada/occ/backend.py +++ b/src/ada/occ/backend.py @@ -390,7 +390,7 @@ def vertex_points(self, shape: ShapeHandle) -> list[tuple[float, float, float]]: # Walk every vertex and return all coordinates as one list. The # per-vertex loop stays inside the backend (the abstraction boundary # must never land inside a per-vertex loop — see the perf guardrail in - # dap plan/v3 notes_occ_backend_abstraction Phase 1/5). + # the internal design notes Phase 1/5). exp = self._TopologyExplorer(shape) pts = [] for v in exp.vertices(): diff --git a/src/ada/occ/geom/__init__.py b/src/ada/occ/geom/__init__.py index e469750d6..434f9a95f 100644 --- a/src/ada/occ/geom/__init__.py +++ b/src/ada/occ/geom/__init__.py @@ -16,7 +16,7 @@ def geom_to_occ_geom(geom: Geometry) -> TopoDS_Shape | TopoDS_Solid: # OCC builders imported lazily so importing this package (e.g. its sibling # ada.occ.geom.cache, which routes through active_backend().build) does not # require pythonocc. geom_to_occ_geom is OccBackend's builder and naturally - # needs OCC only when actually called. See dap plan/v3 Phase 2. + # needs OCC only when actually called. See the internal design notes Phase 2. import ada.occ.geom.solids as geo_so import ada.occ.geom.surfaces as geo_su from ada.occ.geom.boolean import apply_geom_booleans diff --git a/src/ada/occ/geom/curves.py b/src/ada/occ/geom/curves.py index 8f02a0192..0097a9269 100644 --- a/src/ada/occ/geom/curves.py +++ b/src/ada/occ/geom/curves.py @@ -18,10 +18,34 @@ OCC_RADIUS_TOL = 1e-9 # geometric tolerance in model units +def _occ_bspline_curve_from_geom(curve_geom) -> Geom_BSplineCurve: + """Build an OCC ``Geom_BSplineCurve`` from an adapy ``BSplineCurveWithKnots`` (rational supported).""" + n_poles = len(curve_geom.control_points_list) + poles = TColgp_Array1OfPnt(1, n_poles) + for i, cp in enumerate(curve_geom.control_points_list, start=1): + poles.SetValue(i, point3d(cp)) + n_knots = len(curve_geom.knots) + knots = TColStd_Array1OfReal(1, n_knots) + mults = TColStd_Array1OfInteger(1, n_knots) + for i, (k, m) in enumerate(zip(curve_geom.knots, curve_geom.knot_multiplicities), start=1): + knots.SetValue(i, float(k)) + mults.SetValue(i, int(m)) + degree = int(curve_geom.degree) + if isinstance(curve_geom, geo_cu.RationalBSplineCurveWithKnots): + weights = TColStd_Array1OfReal(1, n_poles) + for i, w in enumerate(curve_geom.weights_data, start=1): + weights.SetValue(i, float(w)) + return Geom_BSplineCurve(poles, weights, knots, mults, degree, False) + return Geom_BSplineCurve(poles, knots, mults, degree, False) + + def make_edge_from_line(geom: geo_cu.Edge | geo_cu.ArcLine) -> TopoDS_Edge: if isinstance(geom, geo_cu.ArcLine): a_arc_of_circle = GC_MakeArcOfCircle(point3d(geom.start), point3d(geom.midpoint), point3d(geom.end)) return BRepBuilderAPI_MakeEdge(a_arc_of_circle.Value()).Edge() + elif isinstance(geom, (geo_cu.BSplineCurveWithKnots, geo_cu.RationalBSplineCurveWithKnots)): + # A plate-boundary spline spans exactly corner->corner, so the full-curve edge is the edge. + return BRepBuilderAPI_MakeEdge(_occ_bspline_curve_from_geom(geom)).Edge() else: return BRepBuilderAPI_MakeEdge(point3d(geom.start), point3d(geom.end)).Edge() @@ -330,7 +354,7 @@ def make_wire_from_circle(circle: geo_cu.Circle) -> TopoDS_Wire: if r <= OCC_RADIUS_TOL: raise ValueError( - f"Circle radius must be > {OCC_RADIUS_TOL}, got {r}. " f"Circle={circle}, origin={circle.position.location}" + f"Circle radius must be > {OCC_RADIUS_TOL}, got {r}. Circle={circle}, origin={circle.position.location}" ) # ---- Build OCC circle ---- diff --git a/src/ada/occ/geom/surfaces.py b/src/ada/occ/geom/surfaces.py index 2d4f6de48..b9ecb058c 100644 --- a/src/ada/occ/geom/surfaces.py +++ b/src/ada/occ/geom/surfaces.py @@ -1825,6 +1825,43 @@ def make_toroidal_surface_from_geom(torus: geo_su.ToroidalSurface) -> Geom_Toroi return Geom_ToroidalSurface(gp_Torus(ax3, torus.major_radius, torus.minor_radius)) +def make_surface_of_linear_extrusion_from_geom(sle: geo_su.SurfaceOfLinearExtrusion): + """Build a ``Geom_SurfaceOfLinearExtrusion`` from the swept curve + extrusion + direction (the thickened curved-shell side faces from + :func:`ada.geom.primitive_brep.face_to_thick_shell`). Supports Line / Circle / + Ellipse / (Rational)BSplineCurveWithKnots basis curves.""" + from OCC.Core.Geom import ( + Geom_Circle, + Geom_Ellipse, + Geom_Line, + Geom_SurfaceOfLinearExtrusion, + ) + from OCC.Core.gp import gp_Ax2, gp_Circ, gp_Elips + + from ada.occ.geom.curves import _occ_bspline_curve_from_geom + + c = sle.swept_curve + if isinstance(c, geo_cu.SurfaceCurve): + c = c.curve_3d + if isinstance(c, geo_cu.BSplineCurveWithKnots): + basis = _occ_bspline_curve_from_geom(c) + elif isinstance(c, geo_cu.Line): + basis = Geom_Line(gp_Pnt(*c.pnt), gp_Dir(*c.dir)) + elif isinstance(c, (geo_cu.Circle, geo_cu.Ellipse)): + pos = c.position + if pos.ref_direction is not None: + ax2 = gp_Ax2(gp_Pnt(*pos.location), gp_Dir(*pos.axis), gp_Dir(*pos.ref_direction)) + else: + ax2 = gp_Ax2(gp_Pnt(*pos.location), gp_Dir(*pos.axis)) + if isinstance(c, geo_cu.Circle): + basis = Geom_Circle(gp_Circ(ax2, float(c.radius))) + else: + basis = Geom_Ellipse(gp_Elips(ax2, float(c.semi_axis1), float(c.semi_axis2))) + else: + raise NotImplementedError(f"SurfaceOfLinearExtrusion swept curve {type(c)} not implemented") + return Geom_SurfaceOfLinearExtrusion(basis, gp_Dir(*sle.extrusion_direction)) + + def make_surface_from_geom(face_surface): """ Create an OCC surface from an adapy surface geometry definition. @@ -1847,6 +1884,8 @@ def make_surface_from_geom(face_surface): return make_toroidal_surface_from_geom(face_surface) elif type(face_surface) is geo_su.SurfaceOfRevolution: return make_surface_of_revolution_from_geom(face_surface) + elif type(face_surface) is geo_su.SurfaceOfLinearExtrusion: + return make_surface_of_linear_extrusion_from_geom(face_surface) elif type(face_surface) in (geo_su.BSplineSurfaceWithKnots, geo_su.RationalBSplineSurfaceWithKnots): return make_bspline_surface_with_knots(face_surface) else: diff --git a/src/ada/occ/tessellating.py b/src/ada/occ/tessellating.py index 87436106f..3907c4825 100644 --- a/src/ada/occ/tessellating.py +++ b/src/ada/occ/tessellating.py @@ -37,7 +37,7 @@ def _is_topods_shape(shape) -> bool: """True if ``shape`` is a raw pythonocc ``TopoDS_Shape``. Returns False when pythonocc isn't installed (e.g. the adacpp-only environment) so the tessellation dispatch falls through to the active backend's tessellate verb - rather than blowing up on the import. See dap plan/v3 Phase 2.""" + rather than blowing up on the import. See the internal design notes Phase 2.""" try: from OCC.Core.TopoDS import TopoDS_Shape except ModuleNotFoundError: @@ -694,8 +694,8 @@ def tessellate_occ_geom( # The kind (line vs triangle mesh) is passed in by the caller rather # than sniffed off the OCC handle type — an opaque ShapeHandle under - # a non-OCC backend can't be isinstance-checked. See dap plan/v3 - # notes_occ_backend_abstraction (Phase 1). + # a non-OCC backend can't be isinstance-checked. See the internal design notes + # the internal design notes (Phase 1). if mesh_type == MeshType.LINES: tess_shape = tessellate_edges(occ_geom) indices = tess_shape.indices @@ -863,6 +863,28 @@ def tessellate_geom( if stream_ms is not None: return stream_ms + # adacpp backend + multi-face B-rep shell (e.g. an IFC-reimported thickened + # curved plate): the backend's shell build sews the faces (BRepBuilderAPI_Sewing), + # and OCCT's sewing can wreck the pcurve trims of large B-spline patches — the + # sewn shape then meshes to a sliver of its true area. The stream kernel + # tessellates the same shell per-face OCC-free (no sew), so use it here even when + # ADA_STREAM_TESS_PIPELINE is unset. + if mesh_type != MeshType.LINES and active_backend().name == "adacpp": + import ada.geom.surfaces as _gsu + + g_root = geom.geometry + is_shell = isinstance(g_root, (_gsu.ClosedShell, _gsu.OpenShell, _gsu.ConnectedFaceSet)) or isinstance( + g_root, _gsu.ShellBasedSurfaceModel + ) + n_shell_faces = 0 + if is_shell: + shells = g_root.sbsm_boundary if isinstance(g_root, _gsu.ShellBasedSurfaceModel) else [g_root] + n_shell_faces = sum(len(getattr(sh, "cfs_faces", [])) for sh in shells) + if n_shell_faces > 1 and not getattr(geom, "bool_operations", None): + stream_ms = self._tessellate_geom_via_stream(geom, node_ref, force_pipeline="libtess2") + if stream_ms is not None: + return stream_ms + try: # Construction seam: build through the active CAD backend rather # than calling geom_to_occ_geom directly (= OccBackend.build under @@ -1069,13 +1091,19 @@ def batch_tessellate( except Exception: # noqa: BLE001 - no parametric geom → OCC prism path cng = None if cng is not None: + import ada.geom.surfaces as _geo_su + + # A thickened curved shell (face_to_thick_shell ClosedShell) is + # already a solid — tessellate it as-is. Only a BARE face still + # gets the mesh-level thickness offset below. + already_solid = isinstance(cng.geometry, _geo_su.ClosedShell) ms_cs = self._tessellate_geom_via_stream(cng, node_ref) if ms_cs is not None: # The stream tessellates only the bare curved face (a shell); # give it the plate thickness so it matches the OCC prism solid # (extrude_face_along_normal). t=0 (SurfaceCurved) stays a shell. t = getattr(obj, "t", None) - if t: + if t and not already_solid: pos2, idx2 = _thicken_face_mesh(ms_cs.position, ms_cs.indices, float(t)) ms_cs = MeshStore( ms_cs.index, @@ -1098,7 +1126,25 @@ def batch_tessellate( ) ms_curved = None try: - shape = obj.extruded_solid_occ() + # Thickened curved shell: build the SAME ada.geom ClosedShell through + # the backend's normal geom conversion. The OCC prism + # (extruded_solid_occ) remains only as the legacy fallback (config + # off / unthickenable face / shell build failure). + shape = None + thick_fn = getattr(obj, "_thick_shell_geom", None) + if callable(thick_fn): + thick_geom = thick_fn() + if thick_geom is not None: + try: + shape = active_backend().build(thick_geom) + except Exception as e: # noqa: BLE001 - backend can't build this shell + logger.debug( + "PlateCurved %r: thick-shell build failed (%s); using prism fallback", + getattr(ada_obj, "name", "?"), + e, + ) + if shape is None: + shape = obj.extruded_solid_occ() ms_curved = self.tessellate_occ_geom(shape, node_ref, obj.color) except UnableToCreateTesselationFromSolidOCCGeom as e: logger.error(e) @@ -1225,15 +1271,22 @@ def batch_tessellate( fallback_pts = getattr(ada_obj, "_flat_fallback_pts", None) if fallback_pts: try: - from ada import Plate from ada.cadit.gxml.read.helpers import ( - _project_to_best_fit_plane, + _fit_best_fit_plane, + _plate_from_face, + _project_edge_curves_onto_plane, + _project_onto_plane, ) - fb = Plate.from_3d_points( + plane = _fit_best_fit_plane(fallback_pts) + fb = _plate_from_face( getattr(ada_obj, "name", "fallback"), - _project_to_best_fit_plane(fallback_pts), + _project_onto_plane(fallback_pts, plane) if plane else list(fallback_pts), + _project_edge_curves_onto_plane( + getattr(ada_obj, "_flat_fallback_edge_curves", None), plane + ), getattr(ada_obj, "t", None) or 0.0, + None, mat=getattr(ada_obj, "material", None), metadata=dict( props=dict( @@ -1410,13 +1463,20 @@ def batch_tessellate( if not fallback_pts: continue try: - from ada import Plate - from ada.cadit.gxml.read.helpers import _project_to_best_fit_plane + from ada.cadit.gxml.read.helpers import ( + _fit_best_fit_plane, + _plate_from_face, + _project_edge_curves_onto_plane, + _project_onto_plane, + ) - fallback = Plate.from_3d_points( + plane = _fit_best_fit_plane(fallback_pts) + fallback = _plate_from_face( getattr(ada_obj, "name", "fallback"), - _project_to_best_fit_plane(fallback_pts), + _project_onto_plane(fallback_pts, plane) if plane else list(fallback_pts), + _project_edge_curves_onto_plane(getattr(ada_obj, "_flat_fallback_edge_curves", None), plane), getattr(ada_obj, "t", None) or 0.0, + None, mat=getattr(ada_obj, "material", None), metadata=dict( props=dict( diff --git a/src/ada/visit/gltf/meshopt.py b/src/ada/visit/gltf/meshopt.py index 714811241..8997f1a65 100644 --- a/src/ada/visit/gltf/meshopt.py +++ b/src/ada/visit/gltf/meshopt.py @@ -267,3 +267,93 @@ def _write_glb_streaming(out_path: Path, j: dict, bin_path: Path, bin_len: int) src, dst = sys.argv[1], sys.argv[2] res = meshopt_compress_glb(src, dst) print(json.dumps({"out": str(res), "ok": str(res) == dst})) + + +def meshopt_decompress_glb(in_path: str | Path, out_path: str | Path) -> Path: + """Unpack an ``EXT_meshopt_compression`` GLB into a plain uncompressed GLB. + + The inverse of :func:`meshopt_compress_glb` for CONSUMERS that cannot decode the + extension (trimesh's glTF loader hits ``IndexError`` on the fallback-buffer layout + — this is what broke the audit's parity measurement of production FEM GLBs). + Each compressed bufferView is decoded with the same codecs the packer verified + against, raw views are copied, and everything is re-laid-out sequentially into a + fresh BIN (accessor byteOffsets are view-relative, so a new view layout is valid). + + Raises on a missing codec or malformed input — the caller decides whether that is + an error or a skip.""" + import adacpp.cad as mo # EXT_meshopt_compression codecs (vendored meshoptimizer in adacpp) + + in_path = Path(in_path) + out_path = Path(out_path) + + with in_path.open("rb") as f: + magic, _ver, _total = struct.unpack(" int: + nonlocal off + o = off + fout.write(b) + off += len(b) + pad = _align4(off) - off + if pad: + fout.write(b"\x00" * pad) + off += pad + return o + + for bv in j.get("bufferViews", []): + ext = (bv.get("extensions") or {}).pop("EXT_meshopt_compression", None) + if ext is not None: + if ext.get("filter") not in (None, "NONE"): + raise ValueError(f"unsupported meshopt filter {ext['filter']!r}") + enc = bin_data[ext["byteOffset"] : ext["byteOffset"] + ext["byteLength"]] + dec = _MODE_DECODERS[ext.get("mode", "ATTRIBUTES")](enc, ext["count"], ext["byteStride"]) + if len(dec) != bv["byteLength"]: + raise ValueError("decoded bufferView length mismatch") + bv["byteOffset"] = _emit(dec) + else: + start = bv.get("byteOffset", 0) + bv["byteOffset"] = _emit(bin_data[start : start + bv["byteLength"]]) + bv["buffer"] = 0 + if not bv.get("extensions"): + bv.pop("extensions", None) + new_len = off + + j["buffers"] = [{"byteLength": new_len}] + j["extensionsUsed"] = sorted(set(j.get("extensionsUsed", [])) - {"EXT_meshopt_compression"}) + j["extensionsRequired"] = sorted(set(j.get("extensionsRequired", [])) - {"EXT_meshopt_compression"}) + if not j["extensionsUsed"]: + j.pop("extensionsUsed") + if not j["extensionsRequired"]: + j.pop("extensionsRequired") + + _write_glb_streaming(out_path, j, tmp_bin, new_len) + return out_path + finally: + try: + os.remove(tmp_bin) + except OSError: + pass diff --git a/src/ada/visit/rendering/render_pygfx.py b/src/ada/visit/rendering/render_pygfx.py index 19f5923f6..fdd246554 100644 --- a/src/ada/visit/rendering/render_pygfx.py +++ b/src/ada/visit/rendering/render_pygfx.py @@ -97,7 +97,7 @@ def _get_scene_meshes(self, scene: trimesh.Scene, tag: str) -> Iterable[gfx.Mesh def add_geom(self, geom: Geometry, name: str, guid: str, tag=create_guid(), metadata=None): # Lazy import: ada.occ.tessellating pulls OCC, which need not be present - # under a non-OCC CAD backend. See dap plan/v3 Phase 1. + # under a non-OCC CAD backend. See the internal design notes. from ada.occ.tessellating import BatchTessellator bt = BatchTessellator() diff --git a/src/ada/visit/scene_converter.py b/src/ada/visit/scene_converter.py index 5e5ff65a2..628038ca0 100644 --- a/src/ada/visit/scene_converter.py +++ b/src/ada/visit/scene_converter.py @@ -290,7 +290,9 @@ def _rewrite_lineage_placeholders(self, mapping: dict[int, int]) -> None: grp.members_buffer_view = mapping[pv] def tree_postprocessor(self, tree: OrderedDict): - for material in tree["materials"]: + # A material-less scene (e.g. line-only, or a re-tessellated import that + # carries no materials) has no "materials" key at all — don't KeyError. + for material in tree.get("materials") or []: material["doubleSided"] = True self._update_animations(tree) diff --git a/src/ada/visit/scene_handling/scene_from_step_stream.py b/src/ada/visit/scene_handling/scene_from_step_stream.py index b811d4c40..a7c2766ff 100644 --- a/src/ada/visit/scene_handling/scene_from_step_stream.py +++ b/src/ada/visit/scene_handling/scene_from_step_stream.py @@ -110,7 +110,11 @@ def _rebuild_stats(): for k, v in consume_face_coverage_stats().items(): stats[f"faces_{k}"] = stats.get(f"faces_{k}", 0) + v - from ada.cadit.ngeom.serialize import consume_face_drop_reasons, consume_face_stats + from ada.cadit.ngeom.serialize import ( + consume_face_drop_reasons, + consume_face_stats, + consume_root_drop_reasons, + ) for k, v in consume_face_stats().items(): stats[f"faces_{k}"] = stats.get(f"faces_{k}", 0) + v @@ -121,6 +125,11 @@ def _rebuild_stats(): for reason, n in consume_face_drop_reasons().items(): key = f"{_FACE_DROP_PREFIX}{reason}" stats[key] = stats.get(key, 0) + n + # Whole-geometry (root) drops ride the same reason channel, tagged ``root:`` so an + # unmapped solid surfaces in the run's drop_reasons/warning rather than vanishing. + for reason, n in consume_root_drop_reasons().items(): + key = f"{_FACE_DROP_PREFIX}root: {reason}" + stats[key] = stats.get(key, 0) + n return stats or None diff --git a/src/ada_cli/audit.py b/src/ada_cli/audit.py index 671735134..a62e9099e 100644 --- a/src/ada_cli/audit.py +++ b/src/ada_cli/audit.py @@ -71,10 +71,8 @@ # validated a wheel six releases older than the one it shipped. # Duplicated as a literal because it cannot be derived: ada_cli ships in a wheel that carries no # deploy/ tree. tests/core/test_deploy_pins.py fails if the two drift. -ADACPP_DEFAULT_IMAGE = "ghcr.io/krande/adacpp-wasm-base:0.16.1" -IFC_WASM_WHEEL = ( - "https://ifcopenshell.github.io/wasm-wheels/" "ifcopenshell-0.8.5-cp313-cp313-pyodide_2025_0_wasm32.whl" -) +ADACPP_DEFAULT_IMAGE = "ghcr.io/krande/adacpp-wasm-base:0.18.0" +IFC_WASM_WHEEL = "https://ifcopenshell.github.io/wasm-wheels/ifcopenshell-0.8.5-cp313-cp313-pyodide_2025_0_wasm32.whl" # ── config / http ──────────────────────────────────────────────────────── @@ -411,6 +409,39 @@ def cmd_fetch(args: argparse.Namespace) -> int: return 0 +def _repro_parity(src: pathlib.Path, source_key: str) -> int: + """Local re-run of a ``parity`` audit cell: produce every compared format with the + production converter, then run the same produced-files parity check the worker runs. + (The worker measures the run's already-uploaded blobs; the repro re-produces them + locally so the whole loop is debuggable on one machine.)""" + from ada.cadit.visual_parity import ( + PARITY_GEOMETRY_FORMATS, + parity_from_produced_files, + ) + from ada.comms.rest.converter import ConverterRegistry, convert, result_bytes + + targets = set(ConverterRegistry.targets_for(src.suffix.lower())) + produced: dict[str, pathlib.Path | None] = {} + for fmt in (f for f in PARITY_GEOMETRY_FORMATS if f in targets): + try: + result = convert(src, source_key, fmt, lambda s, f: None) + out_path = src.with_suffix(f".out.{fmt}") + out_path.write_bytes(result_bytes(result)) + produced[fmt] = out_path + print(f" {fmt}: produced {out_path.stat().st_size} bytes -> {out_path}") + except Exception as ex: # noqa: BLE001 - a failed leg is recorded, parity still runs + print(f" {fmt}: CONVERT FAILED {type(ex).__name__}: {_short(str(ex), 140)}") + produced[fmt] = None + + res = parity_from_produced_files(source_key, produced) + print(f"\n{res.summary()}") + for label, d in (("mismatches", res.mismatches), ("errors", res.errors), ("skipped", res.skipped)): + if d: + for k, v in d.items(): + print(f" {label[:-2]}: {k}: {v}") + return 0 if res.consistent and not res.errors else 1 + + def cmd_repro(args: argparse.Namespace) -> int: base, token = _config(args) src, meta = fetch(base, token, args.audit_id, pathlib.Path(args.out)) @@ -419,6 +450,9 @@ def cmd_repro(args: argparse.Namespace) -> int: print(f"repro audit_id={args.audit_id} src={src} target={target}") print(f"original status={meta.get('status')} error={_short(meta.get('error'), 160)!r}") + if target == "parity": + return _repro_parity(src, meta.get("source_key") or str(src)) + # Lazy import — keeps the rest of `ada audit` free of the FEM/CAD stack. from ada.comms.rest.converter import convert, result_bytes @@ -563,8 +597,7 @@ def _resolve_driver(args: argparse.Namespace) -> str: if fallback.exists(): return str(fallback) print( - "error: could not find tools/pyodide-test/wasm_sweep_driver.js — run from the " - "adapy checkout or pass --driver", + "error: could not find tools/pyodide-test/wasm_sweep_driver.js — run from the adapy checkout or pass --driver", file=sys.stderr, ) sys.exit(2) @@ -856,7 +889,7 @@ def cmd_wasm_sweep(args: argparse.Namespace) -> int: cells = cells[: args.limit] print( - f"run {run.get('id') or args.run_id}: {len(jobs)} jobs → " f"{len(cells)} wasm cells, {len(skipped)} skipped", + f"run {run.get('id') or args.run_id}: {len(jobs)} jobs → {len(cells)} wasm cells, {len(skipped)} skipped", file=sys.stderr, ) diff --git a/src/frontend/scripts/diagnose_userdata_roundtrip.mjs b/src/frontend/scripts/diagnose_userdata_roundtrip.mjs index 7a426b037..3ac5e0587 100644 --- a/src/frontend/scripts/diagnose_userdata_roundtrip.mjs +++ b/src/frontend/scripts/diagnose_userdata_roundtrip.mjs @@ -6,7 +6,7 @@ // 4. Parse the JSON chunk and dump sceneDef.extras // 5. Re-load via GLTFLoader and dump scene.userData + mesh.name // -// Run from /home/kristoffer/code/adapy/src/frontend: +// Run from the frontend directory: // node --experimental-fetch scripts/diagnose_userdata_roundtrip.mjs import * as THREE from "three" diff --git a/src/frontend/src/components/admin/AuditRunsTab.tsx b/src/frontend/src/components/admin/AuditRunsTab.tsx index 8d9c41a49..1875c15e4 100644 --- a/src/frontend/src/components/admin/AuditRunsTab.tsx +++ b/src/frontend/src/components/admin/AuditRunsTab.tsx @@ -10,7 +10,7 @@ const WASM_POOL = "wasm"; // Admin tab — kick off regression sweeps across the converter matrix // and drill into per-cell results. Layer 1 of the audit panel from -// plan/v2/notes_admin_audit_panel.md: +// the admin audit-panel design notes: // // * "Run audit" form — pick a scope (M3 will add a corpus picker) // and an optional worker pool, fire one POST to /admin/audit/runs. @@ -244,6 +244,9 @@ const RunGrid: React.FC<{ // Right-click (desktop) / long-press (touch) context menu for a cell. const [menu, setMenu] = useState<{x: number; y: number; file: string; target: string} | null>(null); + // Tapped quality-flag detail popover. The flag chip's ``title`` is a hover + // tooltip, invisible on touch — a tap opens this so mobile can read it. + const [flagInfo, setFlagInfo] = useState<{x: number; y: number; label: string; title: string} | null>(null); const longPress = useRef(null); const openMenu = (x: number, y: number, file: string, target: string) => setMenu({x, y, file, target}); @@ -275,6 +278,21 @@ const RunGrid: React.FC<{ }; }, [menu]); + // Close the flag-detail popover on any outside tap / scroll / Escape. + useEffect(() => { + if (!flagInfo) return; + const close = () => setFlagInfo(null); + const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setFlagInfo(null); }; + window.addEventListener("click", close); + window.addEventListener("scroll", close, true); + window.addEventListener("keydown", onKey); + return () => { + window.removeEventListener("click", close); + window.removeEventListener("scroll", close, true); + window.removeEventListener("keydown", onKey); + }; + }, [flagInfo]); + // Per-source cross-format parity result (the dispatcher emits one // ``parity`` cell per source). The "Validation" metric colours every cell // in a source's row by this verdict — done = formats agree, error = @@ -420,8 +438,15 @@ const RunGrid: React.FC<{ {sourceFlags(grid.cells, grid.targets, file).map((f) => ( { + // Tap opens the detail popover (mobile has no hover for ``title``). + e.stopPropagation(); + const r = (e.currentTarget as HTMLElement).getBoundingClientRect(); + const x = Math.max(4, Math.min(r.left, window.innerWidth - 288)); + setFlagInfo({x, y: r.bottom + 4, label: f.label, title: f.title}); + }} > {f.label} @@ -488,6 +513,16 @@ const RunGrid: React.FC<{ )} )} + {flagInfo && ( +
e.stopPropagation()} + > +
{flagInfo.label}
+
{flagInfo.title}
+
+ )} ); }; diff --git a/src/frontend/src/components/admin/CorpusTab.tsx b/src/frontend/src/components/admin/CorpusTab.tsx index 08c8fb6e5..db6f5ede5 100644 --- a/src/frontend/src/components/admin/CorpusTab.tsx +++ b/src/frontend/src/components/admin/CorpusTab.tsx @@ -12,7 +12,7 @@ import FolderPickerModal from "@/components/common/FolderPickerModal"; import {scopeUrlPart} from "@/state/scopeStore"; // Admin tab — manage proprietary regression corpora (M3 of the audit -// panel design in plan/v2/notes_admin_audit_panel.md). +// panel design in the admin audit-panel design notes). // // Each corpus is its own scope (``corpus:``) — the per-scope // /api/scopes/{scope}/files endpoints already exist, so file diff --git a/src/frontend/src/components/admin/IssueTargetTab.tsx b/src/frontend/src/components/admin/IssueTargetTab.tsx index c1e8f2b77..23341cdae 100644 --- a/src/frontend/src/components/admin/IssueTargetTab.tsx +++ b/src/frontend/src/components/admin/IssueTargetTab.tsx @@ -2,7 +2,7 @@ import React, {useCallback, useEffect, useState} from "react"; import {IssueTargetConfig, viewerApi} from "@/services/viewerApi"; // Admin tab — configure where the audit-bot publishes failure -// issues (M5 of plan/v2/notes_admin_audit_panel.md). +// issues (the admin audit-panel design notes). // // Token-store model: the actual API token lives in a k8s Secret // exposed to the API process as an environment variable. The DB diff --git a/src/frontend/src/components/admin/PerformanceTab.tsx b/src/frontend/src/components/admin/PerformanceTab.tsx index 273152398..81796109a 100644 --- a/src/frontend/src/components/admin/PerformanceTab.tsx +++ b/src/frontend/src/components/admin/PerformanceTab.tsx @@ -10,7 +10,7 @@ import { } from "@/services/viewerApi"; // Admin tab — cross-conversion performance dashboard (M6 of -// plan/v2/notes_admin_audit_panel.md). +// the admin audit-panel design notes). // // One row per (source_ext, target_format) cell aggregated over the // last N days of convert jobs (audit + prod combined by default). diff --git a/src/frontend/src/components/admin/SchedulesTab.tsx b/src/frontend/src/components/admin/SchedulesTab.tsx index 3f5b16425..d1a05a95f 100644 --- a/src/frontend/src/components/admin/SchedulesTab.tsx +++ b/src/frontend/src/components/admin/SchedulesTab.tsx @@ -2,7 +2,7 @@ import React, {useCallback, useEffect, useState} from "react"; import {AuditSchedule, Corpus, viewerApi} from "@/services/viewerApi"; // Admin tab — manage recurring audit schedules (M4 of the audit -// panel design in plan/v2/notes_admin_audit_panel.md). +// panel design in the admin audit-panel design notes). // // Each row pairs a cron expression with a (scope, worker_pool) sweep // target. The API's scheduler tick claims due rows and fires the diff --git a/src/frontend/src/components/info_box_scene/GroupsSection.tsx b/src/frontend/src/components/info_box_scene/GroupsSection.tsx index adc34ea5e..7bcdc1e10 100644 --- a/src/frontend/src/components/info_box_scene/GroupsSection.tsx +++ b/src/frontend/src/components/info_box_scene/GroupsSection.tsx @@ -57,7 +57,7 @@ const GroupsSection = () => { // single adaExtensionRef only ever held the LAST loaded model). // Subscribing to loadedSourceNames keeps the list live across // load/unload instead of snapshotting on mount. Some fixtures - // (e.g. ship1t1.fem via the legacy convert path) bake thousands + // (e.g. a large ship FEM via the legacy convert path) bake thousands // of element / node sets into ADA_EXT_data — building the array // is cheap; the combobox below virtualizes the render. useEffect(() => { diff --git a/src/frontend/src/utils/mesh_select/CustomBatchedMesh.ts b/src/frontend/src/utils/mesh_select/CustomBatchedMesh.ts index 8907a848f..a66f9ef2e 100644 --- a/src/frontend/src/utils/mesh_select/CustomBatchedMesh.ts +++ b/src/frontend/src/utils/mesh_select/CustomBatchedMesh.ts @@ -177,7 +177,7 @@ export class CustomBatchedMesh extends THREE.Mesh { // THREE renders one draw call PER geometry.group, regardless of // material count. The previous implementation added one group per - // drawRange unconditionally — for a Ship1T1.FEM-style mesh with + // drawRange unconditionally — for a large ship FEM-style mesh with // ~48k selectable elements that's 48k draw calls and ~10 FPS, // even with nothing selected. Coalesce consecutive same-material // ranges into one group: in the common case (nothing selected, diff --git a/tests/comms/rest/test_audit_run_routes.py b/tests/comms/rest/test_audit_run_routes.py index 28fa6c78b..8f4dd28df 100644 --- a/tests/comms/rest/test_audit_run_routes.py +++ b/tests/comms/rest/test_audit_run_routes.py @@ -396,9 +396,28 @@ def test_cell_history_newest_first(db): audit_run_id=r1["id"], ) ) + # An on-demand / viewer-triggered re-conversion (no audit_run_id) must NOT + # leak in: history is a cross-run comparison, so standalone rows are excluded. + run( + db_module.insert_audit( + pool, + user_sub=None, + scope_kind="shared", + scope_id=None, + action="convert", + key="models/a.step", + target_format="ifc", + status="done", + duration_ms=99, + audit_run_id=None, + ) + ) hist = run(db_module.audit_log_history_for_cell(pool, "models/a.step", "ifc")) assert [h["status"] for h in hist] == ["error", "done"] # newest first assert hist[0]["error"] == "boom" and hist[0]["duration_ms"] == 22 + # the standalone (audit_run_id NULL) row is filtered out + assert all(h["duration_ms"] != 99 for h in hist) + assert all(h["audit_run_id"] is not None for h in hist) # ── Routes (TestClient) ──────────────────────────────────────────── diff --git a/tests/comms/rest/test_converter_step_routing.py b/tests/comms/rest/test_converter_step_routing.py index af16143c3..88ef7bee0 100644 --- a/tests/comms/rest/test_converter_step_routing.py +++ b/tests/comms/rest/test_converter_step_routing.py @@ -96,7 +96,10 @@ def test_glb_serializer_tessellator_advertised_single_source(): # dependent tessellator: enum_by keyed by serializer, depends_on wired assert tess["depends_on"] == "serializer" assert set(tess["enum_by"]) == set(ser["enum"]) - assert tess["enum_by"]["cpp"] == ["native"] + # cpp offers whatever this env discovered: the pinned `native` token against a binding + # with no track selection, the discovered neutral adacpp tracks otherwise. + assert tess["enum_by"]["cpp"], "the cpp serializer must always offer a kernel" + assert set(tess["enum_by"]["cpp"]) <= set(tess["enum"]) assert tess["enum_by"]["wasm"] == ["wasm-native", "pyodide"] # the python serializer offers whatever this env discovered — never nothing, and every # token it offers must be describable (the SPA renders labels straight from this). diff --git a/tests/comms/rest/test_parity_isolation.py b/tests/comms/rest/test_parity_isolation.py index adba132fd..a46e9c6b3 100644 --- a/tests/comms/rest/test_parity_isolation.py +++ b/tests/comms/rest/test_parity_isolation.py @@ -28,7 +28,10 @@ def test_parity_child_runs_in_isolated_fork(fem_files): src, str(src), "parity", - convert_kwargs={"formats": formats}, + # No ``produced`` blobs -> the child takes the offline re-derive path + # (parity_for_source_file). For a Genie-XML source that is the count-based + # cross_format_parity; the audit worker instead passes produced blobs. + convert_kwargs={}, timeout_s=300, ) ) diff --git a/tests/comms/rest/test_source_cache.py b/tests/comms/rest/test_source_cache.py new file mode 100644 index 000000000..33543147f --- /dev/null +++ b/tests/comms/rest/test_source_cache.py @@ -0,0 +1,252 @@ +"""Cross-job source-blob cache (``source_cache.SourceBlobCache``). + +An audit sweep converts one source to many targets; the worker used to +re-download the source per target. These tests pin the cache contract +against a LocalStore-backed Storage — the same backend shape the other +storage tests use: versioned hits, no stale serves after an overwrite, +LRU eviction under the cap, torn-entry recovery, fail-open fallback, and +the FileNotFoundError contract for missing sources. +""" + +from __future__ import annotations + +import asyncio +import gzip +import pathlib + +from obstore.store import LocalStore + +from ada.comms.rest.scope import Scope +from ada.comms.rest.source_cache import ( + MODE_DIRECT, + MODE_HIT, + MODE_MISS, + SourceBlobCache, +) +from ada.comms.rest.storage import Storage + + +def _storage(tmp_path: pathlib.Path) -> Storage: + bucket = tmp_path / "bucket" + bucket.mkdir(exist_ok=True) + return Storage(LocalStore(str(bucket)), prefix="") + + +def _cache(tmp_path: pathlib.Path, cap_bytes: int = 64 << 20) -> SourceBlobCache: + return SourceBlobCache(tmp_path / "cache", cap_bytes) + + +def _count_downloads(storage: Storage) -> list: + """Instrument ``stream_to_path`` so a test can assert whether the + cache actually skipped the object-store download.""" + calls: list = [] + orig = storage.stream_to_path + + async def counting(scope, key, dest): + calls.append(key) + return await orig(scope, key, dest) + + storage.stream_to_path = counting # type: ignore[method-assign] + return calls + + +def test_miss_downloads_and_caches_then_hit_skips_download(tmp_path): + storage = _storage(tmp_path) + scope = Scope.shared() + cache = _cache(tmp_path) + payload = b"ISO-10303-21;" + b"x" * 50_000 + asyncio.run(storage.put_bytes(scope, "model.step", payload)) + calls = _count_downloads(storage) + + dest1 = tmp_path / "job1.step" + mode1 = asyncio.run(cache.fetch(storage, scope, "model.step", dest1)) + assert mode1 == MODE_MISS + assert dest1.read_bytes() == payload + assert len(calls) == 1 + + # Second job, fresh dest: served from cache — no second download. + dest2 = tmp_path / "job2.step" + mode2 = asyncio.run(cache.fetch(storage, scope, "model.step", dest2)) + assert mode2 == MODE_HIT + assert dest2.read_bytes() == payload + assert len(calls) == 1 + + +def test_job_unlinking_its_dest_does_not_break_the_cache(tmp_path): + # The worker unlinks its temp source path after every job; with a + # hard-linked hand-off that must only drop the job's own link. + storage = _storage(tmp_path) + scope = Scope.shared() + cache = _cache(tmp_path) + payload = b"solid content" * 1000 + asyncio.run(storage.put_bytes(scope, "part.stp", payload)) + + dest1 = tmp_path / "j1.stp" + asyncio.run(cache.fetch(storage, scope, "part.stp", dest1)) + dest1.unlink() + + dest2 = tmp_path / "j2.stp" + assert asyncio.run(cache.fetch(storage, scope, "part.stp", dest2)) == MODE_HIT + assert dest2.read_bytes() == payload + + +def test_version_change_is_never_served_stale(tmp_path): + storage = _storage(tmp_path) + scope = Scope.shared() + cache = _cache(tmp_path) + asyncio.run(storage.put_bytes(scope, "model.xml", b"" * 100)) + dest1 = tmp_path / "j1.xml" + asyncio.run(cache.fetch(storage, scope, "model.xml", dest1)) + + # Overwrite the source (different size so the version token changes on + # every backend, etag or not). + new_payload = b"" * 200 + asyncio.run(storage.put_bytes(scope, "model.xml", new_payload)) + + dest2 = tmp_path / "j2.xml" + mode = asyncio.run(cache.fetch(storage, scope, "model.xml", dest2)) + assert mode == MODE_MISS + assert dest2.read_bytes() == new_payload + + +def test_gzip_stored_source_is_cached_decompressed(tmp_path): + # stream_to_path inflates gzip-at-rest sources; the cache must keep the + # inflated form so hits hand the converter valid plain bytes. + storage = _storage(tmp_path) + scope = Scope.shared() + cache = _cache(tmp_path) + payload = b"" + b"a" * 200_000 + b"" + asyncio.run(storage.put_bytes(scope, "big.xml", gzip.compress(payload), pre_compressed=True)) + + dest1 = tmp_path / "j1.xml" + assert asyncio.run(cache.fetch(storage, scope, "big.xml", dest1)) == MODE_MISS + assert dest1.read_bytes() == payload + dest2 = tmp_path / "j2.xml" + assert asyncio.run(cache.fetch(storage, scope, "big.xml", dest2)) == MODE_HIT + assert dest2.read_bytes() == payload + + +def test_cap_eviction_drops_oldest_entry(tmp_path): + storage = _storage(tmp_path) + scope = Scope.shared() + cache = _cache(tmp_path, cap_bytes=150_000) + asyncio.run(storage.put_bytes(scope, "a.step", b"A" * 100_000)) + asyncio.run(storage.put_bytes(scope, "b.step", b"B" * 100_000)) + + asyncio.run(cache.fetch(storage, scope, "a.step", tmp_path / "ja.step")) + # Age entry A so the LRU order is deterministic regardless of fs + # timestamp resolution. + import os + + (entry_a,) = cache._entries() + os.utime(entry_a, (1, 1)) + + asyncio.run(cache.fetch(storage, scope, "b.step", tmp_path / "jb.step")) + names = {p.name for p in cache._entries()} + assert entry_a.name not in names # oldest evicted + assert len(names) == 1 + + # Evicted entry re-fetches as a miss and still serves correct bytes. + dest = tmp_path / "ja2.step" + assert asyncio.run(cache.fetch(storage, scope, "a.step", dest)) == MODE_MISS + assert dest.read_bytes() == b"A" * 100_000 + + +def test_blob_larger_than_cap_is_served_but_not_kept(tmp_path): + storage = _storage(tmp_path) + scope = Scope.shared() + cache = _cache(tmp_path, cap_bytes=10_000) + payload = b"Z" * 50_000 + asyncio.run(storage.put_bytes(scope, "huge.sat", payload)) + + dest = tmp_path / "j.sat" + assert asyncio.run(cache.fetch(storage, scope, "huge.sat", dest)) == MODE_MISS + assert dest.read_bytes() == payload + assert cache._entries() == [] # over-cap blob not retained + + +def test_truncated_cache_entry_is_a_miss_not_a_corrupt_source(tmp_path): + storage = _storage(tmp_path) + scope = Scope.shared() + cache = _cache(tmp_path) + payload = b"P" * 40_000 + asyncio.run(storage.put_bytes(scope, "m.ifc", payload)) + asyncio.run(cache.fetch(storage, scope, "m.ifc", tmp_path / "j1.ifc")) + + (entry,) = cache._entries() + entry.write_bytes(payload[: len(payload) // 2]) # simulate torn/partial write + + dest = tmp_path / "j2.ifc" + assert asyncio.run(cache.fetch(storage, scope, "m.ifc", dest)) == MODE_MISS + assert dest.read_bytes() == payload + + +def test_missing_meta_sidecar_is_a_miss(tmp_path): + storage = _storage(tmp_path) + scope = Scope.shared() + cache = _cache(tmp_path) + asyncio.run(storage.put_bytes(scope, "m.step", b"S" * 10_000)) + asyncio.run(cache.fetch(storage, scope, "m.step", tmp_path / "j1.step")) + + (entry,) = cache._entries() + entry.with_name(entry.name + ".meta").unlink() + + dest = tmp_path / "j2.step" + assert asyncio.run(cache.fetch(storage, scope, "m.step", dest)) == MODE_MISS + assert dest.read_bytes() == b"S" * 10_000 + + +def test_cap_zero_disables_cache_entirely(tmp_path): + storage = _storage(tmp_path) + scope = Scope.shared() + cache = _cache(tmp_path, cap_bytes=0) + asyncio.run(storage.put_bytes(scope, "m.step", b"D" * 1000)) + + dest = tmp_path / "j.step" + assert asyncio.run(cache.fetch(storage, scope, "m.step", dest)) == MODE_DIRECT + assert dest.read_bytes() == b"D" * 1000 + assert not cache.cache_dir.exists() + + +def test_missing_source_raises_file_not_found(tmp_path): + import pytest + + storage = _storage(tmp_path) + scope = Scope.shared() + cache = _cache(tmp_path) + with pytest.raises(FileNotFoundError): + asyncio.run(cache.fetch(storage, scope, "nope.step", tmp_path / "j.step")) + + +def test_cache_error_falls_back_to_direct_download(tmp_path): + # A broken cache must never fail the job: sabotage the version probe + # and the fetch must still deliver the bytes via the direct path. + storage = _storage(tmp_path) + scope = Scope.shared() + cache = _cache(tmp_path) + payload = b"F" * 5000 + asyncio.run(storage.put_bytes(scope, "m.step", payload)) + + async def broken_head(scope_, key_): + raise RuntimeError("metadata service down") + + storage.head = broken_head # type: ignore[method-assign] + dest = tmp_path / "j.step" + assert asyncio.run(cache.fetch(storage, scope, "m.step", dest)) == MODE_DIRECT + assert dest.read_bytes() == payload + + +def test_scopes_do_not_collide(tmp_path): + # Same key in two scopes must be two distinct cache entries. + storage = _storage(tmp_path) + cache = _cache(tmp_path) + scope_a, scope_b = Scope.user("alice"), Scope.user("bob") + asyncio.run(storage.put_bytes(scope_a, "m.step", b"alice-model")) + asyncio.run(storage.put_bytes(scope_b, "m.step", b"bob-model")) + + dest_a, dest_b = tmp_path / "ja.step", tmp_path / "jb.step" + asyncio.run(cache.fetch(storage, scope_a, "m.step", dest_a)) + asyncio.run(cache.fetch(storage, scope_b, "m.step", dest_b)) + assert dest_a.read_bytes() == b"alice-model" + assert dest_b.read_bytes() == b"bob-model" + assert len(cache._entries()) == 2 diff --git a/tests/core/api/plates/test_plate_curved_thick.py b/tests/core/api/plates/test_plate_curved_thick.py new file mode 100644 index 000000000..a1d6e129d --- /dev/null +++ b/tests/core/api/plates/test_plate_curved_thick.py @@ -0,0 +1,289 @@ +"""Thickened curved-shell export (PlateCurved -> analytic thickness-t ClosedShell). + +Covers the kernel-free builder (:func:`ada.geom.primitive_brep.face_to_thick_shell`), +the global thickness-anchor config, the IFC / STEP-stream / NGEOM writer wiring and +the bare-face fallback. The OCC-oracle checks are skipped automatically on the +adacpp backend env (no pythonocc).""" + +from __future__ import annotations + +import os +import tempfile +from collections import Counter + +import pytest + +import ada +import ada.geom.curves as cu +import ada.geom.surfaces as su +from ada.config import Config +from ada.geom import Geometry +from ada.geom.curves import KnotType +from ada.geom.direction import Direction +from ada.geom.points import Point +from ada.geom.primitive_brep import face_mid_normal, face_to_thick_shell + +T = 0.025 + + +def _arch_face() -> su.AdvancedFace: + """A quadratic-arch B-spline patch (apex z=0.15) with its natural 4-edge bound: + two B-spline edges (v=0 / v=1 isolines) and two straight edges.""" + surf = su.BSplineSurfaceWithKnots( + u_degree=2, + v_degree=1, + control_points_list=[ + [Point(0, 0, 0), Point(0, 1, 0)], + [Point(0.5, 0, 0.3), Point(0.5, 1, 0.3)], + [Point(1, 0, 0), Point(1, 1, 0)], + ], + surface_form=su.BSplineSurfaceForm.UNSPECIFIED, + u_closed=False, + v_closed=False, + self_intersect=False, + u_multiplicities=[3, 3], + v_multiplicities=[2, 2], + u_knots=[0.0, 1.0], + v_knots=[0.0, 1.0], + knot_spec=KnotType.UNSPECIFIED, + ) + + def spline(y: float) -> cu.BSplineCurveWithKnots: + return cu.BSplineCurveWithKnots( + degree=2, + control_points_list=[Point(0, y, 0), Point(0.5, y, 0.3), Point(1, y, 0)], + curve_form=cu.BSplineCurveFormEnum.UNSPECIFIED, + closed_curve=False, + self_intersect=False, + knot_multiplicities=[3, 3], + knots=[0.0, 1.0], + knot_spec=KnotType.UNSPECIFIED, + ) + + p00, p10, p11, p01 = Point(0, 0, 0), Point(1, 0, 0), Point(1, 1, 0), Point(0, 1, 0) + e0 = cu.EdgeCurve(p00, p10, edge_geometry=spline(0.0), same_sense=True) + e1 = cu.EdgeCurve(p10, p11, edge_geometry=cu.Line(p10, Direction(0, 1, 0)), same_sense=True) + e2 = cu.EdgeCurve(p01, p11, edge_geometry=spline(1.0), same_sense=True) # traversed reversed + e3 = cu.EdgeCurve(p01, p00, edge_geometry=cu.Line(p01, Direction(0, -1, 0)), same_sense=True) + loop = cu.EdgeLoop( + edge_list=[ + cu.OrientedEdge(p00, p10, edge_element=e0, orientation=True), + cu.OrientedEdge(p10, p11, edge_element=e1, orientation=True), + cu.OrientedEdge(p11, p01, edge_element=e2, orientation=False), + cu.OrientedEdge(p01, p00, edge_element=e3, orientation=True), + ] + ) + return su.AdvancedFace(bounds=[su.FaceBound(bound=loop, orientation=True)], face_surface=surf, same_sense=True) + + +def _patch_z_levels(shell: su.ClosedShell) -> list[list[float]]: + zs = [] + for f in shell.cfs_faces: + s = f.face_surface + if isinstance(s, su.BSplineSurfaceWithKnots): + zs.append(sorted({round(float(p[2]), 6) for row in s.control_points_list for p in row})) + return zs + + +def test_face_mid_normal_probe(): + n = face_mid_normal(_arch_face()) + assert n is not None + assert abs(n[0]) < 1e-9 and abs(n[1]) < 1e-9 and abs(n[2] - 1.0) < 1e-9 + + +@pytest.mark.parametrize( + "anchor, lo", + [("as_is", 0.0), ("flipped", -T), ("centerline", -T / 2)], +) +def test_thickness_anchor_offsets(anchor, lo): + shell = face_to_thick_shell(_arch_face(), (0, 0, 1), T, anchor=anchor, direction_agrees_with_face=True) + assert shell is not None and len(shell.cfs_faces) == 6 + zs = _patch_z_levels(shell) + assert sorted({round(lo, 6), round(lo + 0.3, 6)}) in zs # bottom patch control net + assert sorted({round(lo + T, 6), round(lo + T + 0.3, 6)}) in zs # top patch control net + + +def test_sense_flag_flips_material_side(): + shell = face_to_thick_shell(_arch_face(), (0, 0, -1), T, anchor="as_is", direction_agrees_with_face=False) + assert shell is not None + zs = _patch_z_levels(shell) + assert sorted({round(-T, 6), round(-T + 0.3, 6)}) in zs # material grew on the -z side + + +def test_shell_is_topologically_shared(): + shell = face_to_thick_shell(_arch_face(), (0, 0, 1), T) + use = Counter() + for f in shell.cfs_faces: + for fb in f.bounds: + for oe in fb.bound.edge_list: + use[id(oe.edge_element)] += 1 + # every edge (boundary + connector) is used by exactly two faces -> closed 2-manifold + assert use and all(v == 2 for v in use.values()) + + +def test_unbuildable_input_returns_none(): + poly_bound = su.FaceBound( + bound=cu.PolyLoop(polygon=[Point(0, 0, 0), Point(1, 0, 0), Point(1, 1, 0)]), orientation=True + ) + bad = su.AdvancedFace(bounds=[poly_bound], face_surface=_arch_face().face_surface, same_sense=True) + assert face_to_thick_shell(bad, (0, 0, 1), T) is None + assert face_to_thick_shell(_arch_face(), (0, 0, 1), 0.0) is None + + +def _thick_assembly() -> tuple[ada.Assembly, ada.PlateCurved]: + a = ada.Assembly("A") / (ada.Part("P") / ada.PlateCurved("curved1", Geometry("synthpl", _arch_face(), None), t=T)) + pl = next(o for o in a.get_all_physical_objects() if isinstance(o, ada.PlateCurved)) + return a, pl + + +def test_plate_curved_solid_geom_is_thick_shell_and_config_off_restores_bare_face(): + _, pl = _thick_assembly() + assert isinstance(pl.solid_geom().geometry, su.ClosedShell) + os.environ["ADA_GEOM_THICKEN_CURVED_SHELLS"] = "false" + try: + Config().reload_config() + pl._thick_shell_cache = None + assert isinstance(pl.solid_geom().geometry, su.AdvancedFace) + finally: + os.environ.pop("ADA_GEOM_THICKEN_CURVED_SHELLS") + Config().reload_config() + + +def test_ngeom_serializes_thick_shell(): + from ada.cadit.ngeom.serialize import serialize_geometries + + _, pl = _thick_assembly() + blob = serialize_geometries([("g", pl.solid_geom())]) + assert len(blob) > 100 + + +def test_ifc_export_validates_and_tessellates(): + import ifcopenshell + import ifcopenshell.geom + from ifcopenshell.validate import json_logger, validate + + a, _ = _thick_assembly() + ifc_path = tempfile.mktemp(suffix=".ifc") + a.to_ifc(ifc_path, validate=False) + f = ifcopenshell.open(ifc_path) + assert len(f.by_type("IfcAdvancedBrep")) == 1 + + lg = json_logger() + validate(f, lg, express_rules=True) + assert lg.statements == [] + + it = ifcopenshell.geom.iterator(ifcopenshell.geom.settings(), f) + assert it.initialize() + verts = it.get().geometry.verts + zs = [verts[i] for i in range(2, len(verts), 3)] + # bezier apex is 0.15; the top face must reach apex + t + assert max(zs) > 0.15 + T * 0.9 + + +def test_step_stream_roundtrip_volume(): + a, _ = _thick_assembly() + stp_path = tempfile.mktemp(suffix=".stp") + a.to_stp(stp_path, writer="stream") + data = open(stp_path, "rb").read() + assert b"CLOSED_SHELL" in data + + from ada.cad import active_backend + + be = active_backend() + if be.name != "pythonocc-core": + pytest.skip("OCC read-back oracle runs on the pythonocc backend only") + shape = be.read_step_bytes(data) + assert be.is_valid(shape) + vol = be.volume(shape) + if vol > 1.0: # reader kept the file's mm units + vol *= 1e-9 + # rigid-translation sweep volume = t * projected area (= 1.0 m^2 here), exact + assert abs(vol - T * 1.0) / (T * 1.0) < 0.01 + + +def test_flat_plate_anchor_offsets_extrusion_base(): + pl = ada.Plate("pl", [(0, 0), (1, 0), (1, 1), (0, 1)], T) + base_as_is = pl.solid_geom().geometry.position.location[2] + os.environ["ADA_GEOM_THICKNESS_ANCHOR"] = "centerline" + try: + Config().reload_config() + base_center = pl.solid_geom().geometry.position.location[2] + os.environ["ADA_GEOM_THICKNESS_ANCHOR"] = "flipped" + Config().reload_config() + base_flipped = pl.solid_geom().geometry.position.location[2] + finally: + os.environ.pop("ADA_GEOM_THICKNESS_ANCHOR") + Config().reload_config() + assert base_as_is == pytest.approx(0.0) + assert base_center == pytest.approx(-T / 2) + assert base_flipped == pytest.approx(-T) + + +def test_ifc_surface_of_linear_extrusion_write_read_roundtrip(): + """The IfcSurfaceOfLinearExtrusion writer arm (2D profile in the Position frame, + WR12-compliant) and its reader inverse. Side faces normally prefer the ruled + B-spline form, so this covers the generic-swept fallback (ellipse / full circle + off-axis / polyline) directly at the surface level.""" + import ifcopenshell + import numpy as np + + from ada.cadit.ifc.read.geom.surfaces import ( + surface_of_linear_extrusion as read_sole, + ) + from ada.cadit.ifc.write.geom.surfaces import create_surface_of_linear_extrusion + from ada.geom.placement import Axis2Placement3D + + circle = cu.Circle( + position=Axis2Placement3D( + location=Point(1.0, 2.0, 3.0), axis=Direction(0.0, 1.0, 0.0), ref_direction=Direction(0.0, 0.0, 1.0) + ), + radius=2.5, + ) + sole = su.SurfaceOfLinearExtrusion( + swept_curve=circle, position=None, extrusion_direction=Direction(0.0, 0.0, 1.0), depth=0.02 + ) + + f = ifcopenshell.file(schema="IFC4") + ent = create_surface_of_linear_extrusion(sole, f) + assert ent.is_a("IfcSurfaceOfLinearExtrusion") + # WR12: the profile curve must be 2D + assert ent.SweptCurve.Curve.Position.is_a("IfcAxis2Placement2D") + + back = read_sole(ent) + assert isinstance(back.swept_curve, cu.Circle) + assert back.swept_curve.radius == pytest.approx(2.5) + assert np.allclose(list(back.swept_curve.position.location), [1.0, 2.0, 3.0]) + assert np.allclose(list(back.swept_curve.position.axis), [0.0, 1.0, 0.0]) + assert np.allclose(list(back.extrusion_direction), [0.0, 0.0, 1.0]) + assert back.depth == pytest.approx(0.02) + + +def test_circle_arc_side_face_is_exact_ruled_patch(): + """A circular-arc boundary edge grows a ruled RATIONAL B-spline side face whose + control net lies exactly on the arc's cylinder (periodic cylinder/SOLE forms are + untrimmable for the stream kernel).""" + import math + + from ada.geom.placement import Axis2Placement3D + from ada.geom.primitive_brep import _circle_arc_bspline + + circle = cu.Circle( + position=Axis2Placement3D(location=Point(0, 0, 0), axis=Direction(0, 0, 1), ref_direction=Direction(1, 0, 0)), + radius=2.0, + ) + a, b = Point(2, 0, 0), Point(0, 2, 0) + arc = _circle_arc_bspline(circle, a, b, True) + assert isinstance(arc, cu.RationalBSplineCurveWithKnots) + # 90 deg arc: single conic segment, w_mid = cos(45 deg) + assert len(arc.control_points_list) == 3 + assert arc.weights_data[1] == pytest.approx(math.cos(math.pi / 4)) + # endpoints exact, mid control point at r/w on the bisector + assert list(arc.control_points_list[0]) == pytest.approx([2, 0, 0]) + assert list(arc.control_points_list[2]) == pytest.approx([0, 2, 0]) + assert list(arc.control_points_list[1]) == pytest.approx([2.0, 2.0, 0.0]) + # sampled arc points lie on the circle + for x, y, z in arc.sample(9): + assert math.hypot(x, y) == pytest.approx(2.0, abs=1e-9) + assert z == pytest.approx(0.0, abs=1e-12) + # full circle (coincident endpoints) is ambiguous -> None + assert _circle_arc_bspline(circle, a, Point(2, 0, 0), True) is None diff --git a/tests/core/api/test_plate_from_segments.py b/tests/core/api/test_plate_from_segments.py new file mode 100644 index 000000000..26939a261 --- /dev/null +++ b/tests/core/api/test_plate_from_segments.py @@ -0,0 +1,206 @@ +"""``Plate.from_segments`` / ``CurvePoly2d.from_segments`` build a plate outline from an ordered list of +line/arc/spline segments, carrying analytic curved edges verbatim instead of sampling them into points. + +This is the path the ACIS/SAT plate reader uses for circle/ellipse boundary edges: it emits +``PlateEdgeCurve`` specs, ``build_edge_segments`` turns ordered corners + specs into real segments, and +``from_segments`` keeps them analytic (exact in IFC/STEP, discretized only downstream at tessellation). +""" + +from __future__ import annotations + +import numpy as np +import pytest + +from ada import Plate +from ada.api.curves import ( + ArcEdge, + ArcSegment, + CurvePoly2d, + LineSegment, + SplineEdge, + SplineSegment, +) +from ada.geom.curves import ArcLine, BSplineCurveWithKnots, IndexedPolyCurve + +_SQUARE = [(0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0)] +_ARC = ArcEdge(a=(1.0, 0.0, 0.0), b=(1.0, 1.0, 0.0), midpoint=(1.1, 0.5, 0.0)) + + +def test_build_edge_segments_marks_only_the_arc_edge(): + segs = CurvePoly2d.build_edge_segments(_SQUARE, [_ARC]) + assert [type(s).__name__ for s in segs] == ["LineSegment", "ArcSegment", "LineSegment", "LineSegment"] + arc = segs[1] + assert np.allclose(arc.midpoint, (1.1, 0.5, 0.0)) + # loop is closed: each segment's end is the next segment's start + for i in range(len(segs)): + assert np.allclose(segs[i].p2, segs[(i + 1) % len(segs)].p1) + + +def test_build_edge_segments_is_winding_agnostic(): + """A spec whose a/b are reversed relative to the corner order still matches its edge.""" + reversed_spec = ArcEdge(a=(1.0, 1.0, 0.0), b=(1.0, 0.0, 0.0), midpoint=(1.1, 0.5, 0.0)) + segs = CurvePoly2d.build_edge_segments(_SQUARE, [reversed_spec]) + assert sum(isinstance(s, ArcSegment) for s in segs) == 1 + + +def test_from_segments_keeps_the_arc_analytic_in_2d_and_3d(): + poly = CurvePoly2d.from_segments(CurvePoly2d.build_edge_segments(_SQUARE, [_ARC])) + assert sum(isinstance(s, ArcSegment) for s in poly.segments3d) == 1 + assert sum(isinstance(s, ArcSegment) for s in poly.segments) == 1 + assert np.allclose( + poly.segments3d[[isinstance(s, ArcSegment) for s in poly.segments3d].index(True)].midpoint, (1.1, 0.5, 0.0) + ) + # seg_index inserts the arc midpoint => one segment has 3 indices, the rest 2 + assert sorted(len(ix) for ix in poly.seg_index) == [2, 2, 2, 3] + + +def test_from_segments_curve_geom_emits_an_arcline(): + poly = CurvePoly2d.from_segments(CurvePoly2d.build_edge_segments(_SQUARE, [_ARC])) + cg = poly.curve_geom(use_3d_segments=False) + assert isinstance(cg, IndexedPolyCurve) + assert any(isinstance(s, ArcLine) for s in cg.segments) + + +def test_plate_from_segments_builds_an_extruded_solid(): + plate = Plate.from_segments("p", CurvePoly2d.build_edge_segments(_SQUARE, [_ARC]), 0.01) + sg = plate.solid_geom() + assert type(sg.geometry).__name__ == "ExtrudedAreaSolid" + assert sum(isinstance(s, ArcSegment) for s in plate.poly.segments3d) == 1 + + +def test_plate_from_segments_flip_normal_reverses_the_plate_normal(): + segs = CurvePoly2d.build_edge_segments(_SQUARE, [_ARC]) + n0 = np.asarray(Plate.from_segments("p", segs, 0.01).poly.normal, dtype=float) + n1 = np.asarray(Plate.from_segments("p", segs, 0.01, flip_normal=True).poly.normal, dtype=float) + assert np.allclose(n0, -n1) + + +def test_from_segments_requires_at_least_three_segments(): + a, b = (0.0, 0.0, 0.0), (1.0, 0.0, 0.0) + with pytest.raises(ValueError): + CurvePoly2d.from_segments([LineSegment(a, b), LineSegment(b, a)]) + + +def test_build_edge_segments_carries_a_spline_edge(): + spline = BSplineCurveWithKnots( + degree=1, + control_points_list=[(1.0, 0.0, 0.0), (1.0, 1.0, 0.0)], + curve_form=None, + closed_curve=False, + self_intersect=False, + knot_multiplicities=[2, 2], + knots=[0.0, 1.0], + knot_spec=None, + ) + spec = SplineEdge(a=(1.0, 0.0, 0.0), b=(1.0, 1.0, 0.0), curve=spline) + segs = CurvePoly2d.build_edge_segments(_SQUARE, [spec]) + spline_segs = [s for s in segs if isinstance(s, SplineSegment)] + assert len(spline_segs) == 1 + assert spline_segs[0].curve_geom() is spline + + +def _bulge_spline() -> BSplineCurveWithKnots: + """A quadratic B-spline edge from (1,0,0) to (1,1,0) bulging out in +x.""" + from ada.geom.curves import BSplineCurveFormEnum, KnotType + + return BSplineCurveWithKnots( + degree=2, + control_points_list=[(1.0, 0.0, 0.0), (1.3, 0.5, 0.0), (1.0, 1.0, 0.0)], + curve_form=BSplineCurveFormEnum.UNSPECIFIED, + closed_curve=False, + self_intersect=False, + knot_multiplicities=[3, 3], + knots=[0.0, 1.0], + knot_spec=KnotType.UNSPECIFIED, + ) + + +def _spline_plate() -> Plate: + segs = CurvePoly2d.build_edge_segments( + _SQUARE, [SplineEdge(a=(1.0, 0.0, 0.0), b=(1.0, 1.0, 0.0), curve=_bulge_spline())] + ) + return Plate.from_segments("sp", segs, 0.05) + + +def test_from_segments_keeps_the_spline_analytic_in_curve_geom(): + cg = _spline_plate().poly.curve_geom(use_3d_segments=False) + assert isinstance(cg, IndexedPolyCurve) + splines = [s for s in cg.segments if isinstance(s, BSplineCurveWithKnots)] + assert len(splines) == 1 + # 2D-local (extruded profile plane), endpoints snapped to the corners so the outline wire closes. + assert np.allclose(splines[0].control_points_list[0], (1.0, 0.0)) or np.allclose( + splines[0].control_points_list[0], (1.0, 0.0, 0.0) + ) + + +def test_spline_plate_ifc_outer_curve_is_a_composite_with_a_bspline(): + import ifcopenshell + + from ada.cadit.ifc.write.geom.surfaces import arbitrary_profile_def + + f = ifcopenshell.file(schema="IFC4X3") + oc = arbitrary_profile_def(_spline_plate().solid_geom().geometry.swept_area, f).OuterCurve + assert oc.is_a("IfcCompositeCurve") + kinds = [s.ParentCurve.is_a() for s in oc.Segments] + assert "IfcBSplineCurveWithKnots" in kinds + + +def test_spline_plate_builds_an_occ_solid_that_bulges(): + from ada.cad import active_backend + + be = active_backend() + solid = _spline_plate().solid_occ() + assert be.is_valid(solid) + # A flat unit square x 0.05 thick = 0.05; the +x bulge must add volume (analytic curve, not chord). + assert be.volume(solid) > 0.05 + 1e-4 + + +def test_spline_plate_get_unique_samples_the_spline_to_a_polyline(): + cg = _spline_plate().poly.curve_geom(use_3d_segments=False) + _pts, seg_idx = cg.get_unique_points_and_segment_indices() + # Three straight edges (2 indices each) + one sampled spline (a >3-index polyline). + assert sorted(len(i) for i in seg_idx)[-1] > 3 + + +def _degree1_spline(p0=(0.0, 0.0, 0.0), p1=(2.0, 0.0, 0.0)) -> BSplineCurveWithKnots: + return BSplineCurveWithKnots( + degree=1, + control_points_list=[p0, p1], + curve_form=None, + closed_curve=False, + self_intersect=False, + knot_multiplicities=[2, 2], + knots=[0.0, 1.0], + knot_spec=None, + ) + + +def test_bspline_curve_sample_is_the_sampler_home(): + """A degree-1 B-spline between two points samples to evenly spaced points along the line.""" + pts = np.asarray(_degree1_spline().sample(5)) + assert pts.shape == (5, 3) + assert np.allclose(pts[:, 0], np.linspace(0.0, 2.0, 5)) + assert np.allclose(pts[:, 1:], 0.0) + + +def test_spline_segment_sample_delegates_to_the_curve(): + spline = _degree1_spline() + seg = SplineSegment((0.0, 0.0, 0.0), (2.0, 0.0, 0.0), curve=spline) + pts = seg.sample(5) + assert len(pts) == 5 + assert np.allclose([p[0] for p in pts], np.linspace(0.0, 2.0, 5)) + + +def test_bspline_sample_raises_on_malformed_spec(): + bad = BSplineCurveWithKnots( + degree=3, # degree >= number of control points => malformed + control_points_list=[(0.0, 0.0, 0.0), (1.0, 0.0, 0.0)], + curve_form=None, + closed_curve=False, + self_intersect=False, + knot_multiplicities=[2, 2], + knots=[0.0, 1.0], + knot_spec=None, + ) + with pytest.raises(ValueError): + bad.sample(5) diff --git a/tests/core/cadit/gxml/test_analytic_xml.py b/tests/core/cadit/gxml/test_analytic_xml.py new file mode 100644 index 000000000..3c0f48dfd --- /dev/null +++ b/tests/core/cadit/gxml/test_analytic_xml.py @@ -0,0 +1,109 @@ +"""FEM-shell → Genie-XML analytic export. + +A curved shell mesh (a tubular member) folds into a handful of analytic +```` faces backed by an embedded ACIS body — the same compact +model FEM→STEP/IFC emit — instead of thousands of coplanar ```` +polygons. These cover the writer end to end: it emits curved shells with SAT +face references, the result is smaller than the coplanar polygon export, and it +round-trips back to curved (spline-surface) plates without dropping a face. +""" + +from __future__ import annotations + +import numpy as np + +import ada +import ada.geom.surfaces as geo_su +from ada.api.plates import PlateCurved +from ada.fem.formats.mesh_faces import iter_fem_analytic_faces + + +def _tube(nseg=24, nrows=10, r=0.5, length=4.0, t=0.01): + """A quad-meshed cylinder along z — a synthetic tubular member.""" + from ada import Node + + p = ada.Part("tube") + mat = ada.Material("S355") + grid: dict = {} + nid = 1 + for iz in range(nrows + 1): + for ia in range(nseg): + ang = 2.0 * np.pi * ia / nseg + grid[(ia, iz)] = Node([r * np.cos(ang), r * np.sin(ang), length * iz / nrows], nid) + p.fem.nodes.add(grid[(ia, iz)]) + nid += 1 + eid = 1 + quad = ada.fem.Elem.EL_TYPES.SHELL_SHAPES.QUAD + for iz in range(nrows): + for ia in range(nseg): + a_, b_ = grid[(ia, iz)], grid[((ia + 1) % nseg, iz)] + c_, d_ = grid[((ia + 1) % nseg, iz + 1)], grid[(ia, iz + 1)] + el = p.fem.add_elem(ada.fem.Elem(eid, [a_, b_, c_, d_], quad, el_formulation_override="S4")) + el.fem_sec = p.fem.add_section( + ada.fem.FemSection(f"S{eid}", "shell", ada.fem.FemSet(f"s{eid}", [el]), mat, thickness=t) + ) + eid += 1 + a = ada.Assembly() / p + p.fem.sections.merge_by_properties() + return a + + +def test_analytic_xml_emits_curved_shell_with_sat(tmp_path): + out = _tube().to_genie_xml(tmp_path / "cyl.xml", streaming=True, merge_strategy="cylinder") + txt = out.read_text() + assert "= 1 + # the cylinders come back as curved (spline) surfaces, not flattened + assert all(isinstance(c.geom.geometry.face_surface, geo_su.BSplineSurfaceWithKnots) for c in curved) + + +def test_roundtripped_cylinder_tessellates_curved(tmp_path): + """The read-back curved shell tessellates to a real curved surface (its + pcurves survive the round trip), not a degenerate flat sliver.""" + from ada.occ.tessellating import BatchTessellator + + out = _tube().to_genie_xml(tmp_path / "cyl.xml", streaming=True, merge_strategy="cylinder") + curved = list(ada.from_genie_xml(out).get_all_physical_objects(by_type=PlateCurved)) + assert curved + + bt = BatchTessellator() + area = 0.0 + for ms in bt.batch_tessellate(curved): + pos = np.asarray(ms.get_position3(), dtype=float) + idx = np.asarray(ms.get_indices3(), dtype=int) + area += sum(0.5 * np.linalg.norm(np.cross(pos[t[1]] - pos[t[0]], pos[t[2]] - pos[t[0]])) for t in idx) + # the tube's one-sided lateral area is ~12.6 m^2; a collapsed sliver would be + # near zero. A comfortably positive area proves the curvature tessellated. + assert area > 5.0 diff --git a/tests/core/cadit/ifc/roundtripping/test_roundtrip_advanced_face.py b/tests/core/cadit/ifc/roundtripping/test_roundtrip_advanced_face.py index e03bd50a0..00cb259e3 100644 --- a/tests/core/cadit/ifc/roundtripping/test_roundtrip_advanced_face.py +++ b/tests/core/cadit/ifc/roundtripping/test_roundtrip_advanced_face.py @@ -7,10 +7,13 @@ curved-plate geometry instead of crashing or losing it. """ +import os + import ada import ada.geom.surfaces as geo_su from ada.api.plates.base_pl import PlateCurved from ada.cadit.sat.store import SatReaderFactory +from ada.config import Config def _curved_plate_from_sat(sat_path) -> PlateCurved: @@ -20,18 +23,43 @@ def _curved_plate_from_sat(sat_path) -> PlateCurved: def test_advanced_face_plate_roundtrip(example_files, tmp_path): + """Bare-face round-trip (curved-shell thickening OFF): the body is a single + IfcAdvancedFace, which adapy re-imports as a PlateCurved.""" + os.environ["ADA_GEOM_THICKEN_CURVED_SHELLS"] = "false" + try: + Config().reload_config() + pc = _curved_plate_from_sat(example_files / "sat_files/curved_plate.sat") + assert isinstance(pc.geom.geometry, geo_su.AdvancedFace) + + fp = (ada.Assembly() / (ada.Part("P") / pc)).to_ifc(tmp_path / "curved.ifc", file_obj_only=True) + b = ada.from_ifc(fp) + + curved = [o for o in b.get_all_physical_objects() if isinstance(o, PlateCurved)] + assert len(curved) == 1 + geom = curved[0].geom.geometry + assert isinstance(geom, geo_su.AdvancedFace) + assert isinstance(geom.face_surface, geo_su.BSplineSurfaceWithKnots) + assert len(geom.bounds) == 1 + finally: + os.environ.pop("ADA_GEOM_THICKEN_CURVED_SHELLS", None) + Config().reload_config() + + +def test_advanced_face_plate_thick_roundtrip_keeps_geometry(example_files, tmp_path): + """Default (thickening ON): the body is an IfcAdvancedBrep of the thickness-t + shell. Re-import yields a generic B-rep shape (not a PlateCurved — the thickness + parameterisation is baked into the brep), but the geometry is preserved and + renders — no geometry left behind.""" pc = _curved_plate_from_sat(example_files / "sat_files/curved_plate.sat") - assert isinstance(pc.geom.geometry, geo_su.AdvancedFace) + fp = (ada.Assembly() / (ada.Part("P") / pc)).to_ifc(tmp_path / "curved_thick.ifc", file_obj_only=True) + assert len(fp.by_type("IfcAdvancedBrep")) == 1 - fp = (ada.Assembly() / (ada.Part("P") / pc)).to_ifc(tmp_path / "curved.ifc", file_obj_only=True) b = ada.from_ifc(fp) - - curved = [o for o in b.get_all_physical_objects() if isinstance(o, PlateCurved)] - assert len(curved) == 1 - geom = curved[0].geom.geometry - assert isinstance(geom, geo_su.AdvancedFace) - assert isinstance(geom.face_surface, geo_su.BSplineSurfaceWithKnots) - assert len(geom.bounds) == 1 + objs = list(b.get_all_physical_objects()) + assert len(objs) == 1 + scene = b.to_trimesh_scene() + faces = sum(g.faces.shape[0] for g in scene.geometry.values()) + assert faces > 0 def test_advanced_face_plate_renders(example_files): diff --git a/tests/core/cadit/ifc/test_ifc_validity.py b/tests/core/cadit/ifc/test_ifc_validity.py new file mode 100644 index 000000000..3bf503fbc --- /dev/null +++ b/tests/core/cadit/ifc/test_ifc_validity.py @@ -0,0 +1,105 @@ +"""adapy-written IFC must pass schema + EXPRESS where-rule validation (the offline pillars of the +official buildingSMART validation service — see scripts/validate_ifc.py / the ifc-validate task). + +Guards two writer bugs the validator caught: +* a beam used to get TWO IfcRelAssociatesMaterial (the bare per-material rel on top of its + IfcMaterialProfileSetUsage), violating IfcBuiltElement.MaxOneMaterialAssociation; +* an unused material's eagerly-created rel used to keep an empty RelatedObjects set. +""" + +from __future__ import annotations + +import ifcopenshell +import pytest +from ifcopenshell.validate import json_logger, validate + +import ada + + +def _model(): + p = ada.Part("p") + p.add_plate(ada.Plate("pl", [(0, 0), (1, 0), (1, 1), (0, 1)], 0.02)) + p.add_beam(ada.Beam("b", (0, 0, 0), (1, 0, 0), "IPE200")) + return ada.Assembly("A") / p + + +@pytest.mark.parametrize("streaming", [False, True], ids=["normal", "streaming"]) +def test_written_ifc_passes_express_validation(tmp_path, streaming): + dest = tmp_path / "m.ifc" + _model().to_ifc(dest, streaming=streaming) + + f = ifcopenshell.open(str(dest)) + lg = json_logger() + validate(f, lg, express_rules=True) + assert lg.statements == [], [s.get("message") for s in lg.statements] + + # exactly ONE material association per element: the beam's is its profile-set usage + for elem in (*f.by_type("IfcBeam"), *f.by_type("IfcPlate")): + mat_rels = [r for r in elem.HasAssociations if r.is_a("IfcRelAssociatesMaterial")] + assert len(mat_rels) == 1, f"{elem.is_a()} '{elem.Name}' has {len(mat_rels)} material associations" + (beam_rel,) = [r for r in f.by_type("IfcBeam")[0].HasAssociations if r.is_a("IfcRelAssociatesMaterial")] + assert beam_rel.RelatingMaterial.is_a("IfcMaterialProfileSetUsage") + + +@pytest.mark.parametrize("streaming", [False, True], ids=["normal", "streaming"]) +def test_beam_material_roundtrips_via_profile_set_usage(tmp_path, streaming): + dest = tmp_path / "m.ifc" + _model().to_ifc(dest, streaming=streaming) + b = ada.from_ifc(dest).get_by_name("b") + assert b.material.name == "S355" + assert b.section.name == "IPE200" + + +def _curved_boundary_plate() -> ada.Plate: + """Square plate with one B-spline edge (bulging +x) and one arc edge (bulging -x).""" + from ada.api.curves import ArcEdge, CurvePoly2d, SplineEdge + from ada.geom.curves import BSplineCurveFormEnum, BSplineCurveWithKnots, KnotType + + sp = BSplineCurveWithKnots( + degree=2, + control_points_list=[(1, 0, 0), (1.3, 0.5, 0), (1, 1, 0)], + curve_form=BSplineCurveFormEnum.UNSPECIFIED, + closed_curve=False, + self_intersect=False, + knot_multiplicities=[3, 3], + knots=[0.0, 1.0], + knot_spec=KnotType.UNSPECIFIED, + ) + specs = [SplineEdge(a=(1, 0, 0), b=(1, 1, 0), curve=sp), ArcEdge(a=(0, 1, 0), b=(0, 0, 0), midpoint=(-0.1, 0.5, 0))] + segs = CurvePoly2d.build_edge_segments([(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)], specs) + return ada.Plate.from_segments("curved_pl", segs, 0.05) + + +def test_spline_plate_advanced_brep_is_valid_renders_and_roundtrips(tmp_path): + """A spline-boundary plate's body is an analytic IfcAdvancedBrep: valid IFC, tessellatable by + ifcopenshell's OWN engine (what third-party viewers use), and reconstructed as a parametric Plate + on read-back — line, arc AND spline segments intact.""" + import ifcopenshell + import ifcopenshell.geom as ifc_geom + import numpy as np + + from ada.api.curves import ArcSegment, SplineSegment + + dest = tmp_path / "m.ifc" + (ada.Assembly("A") / (ada.Part("p") / _curved_boundary_plate())).to_ifc(dest) + + f = ifcopenshell.open(str(dest)) + lg = json_logger() + validate(f, lg, express_rules=True) + assert lg.statements == [], [s.get("message") for s in lg.statements] + assert len(f.by_type("IfcAdvancedBrep")) == 1 + assert len(f.by_type("IfcBSplineSurfaceWithKnots")) == 1 # exact extruded-spline side face + assert len(f.by_type("IfcCylindricalSurface")) == 1 # exact extruded-arc side face + + # renders in ifcopenshell's engine, and the curved edges actually bow out past the unit square + shp = ifc_geom.create_shape(ifc_geom.settings(), f.by_type("IfcPlate")[0]) + verts = np.array(shp.geometry.verts).reshape(-1, 3) + assert verts[:, 0].max() > 1.05 # spline bulge + assert verts[:, 0].min() < -0.05 # arc bulge + + # parametric round-trip: Plate with the analytic segment kinds reconstructed + pl = ada.from_ifc(dest).get_by_name("curved_pl") + assert isinstance(pl, ada.Plate) + assert pl.t == pytest.approx(0.05) + kinds = {type(s).__name__ for s in pl.poly.segments3d} + assert SplineSegment.__name__ in kinds and ArcSegment.__name__ in kinds diff --git a/tests/core/cadit/ifc/test_streaming_writer.py b/tests/core/cadit/ifc/test_streaming_writer.py index 2fd9e0662..036c309bd 100644 --- a/tests/core/cadit/ifc/test_streaming_writer.py +++ b/tests/core/cadit/ifc/test_streaming_writer.py @@ -1,5 +1,7 @@ """Tests for the memory-bounded streaming IFC writer (to_ifc(streaming=True)).""" +import pytest + import ada @@ -112,3 +114,169 @@ def test_streaming_falls_back_for_loaded_ifc(tmp_path): assert ret is not None # fell back to the in-memory writer (returns the file) g = ifcopenshell.open(str(tmp_path / "out.ifc")) assert len(g.by_type("IfcBeam")) == 1 and len(g.by_type("IfcPlate")) == 2 + + +def _spline_plate(): + from ada.api.curves import CurvePoly2d, SplineEdge + from ada.geom.curves import BSplineCurveFormEnum, BSplineCurveWithKnots, KnotType + + sp = BSplineCurveWithKnots( + degree=2, + control_points_list=[(1, 0, 0), (1.3, 0.5, 0), (1, 1, 0)], + curve_form=BSplineCurveFormEnum.UNSPECIFIED, + closed_curve=False, + self_intersect=False, + knot_multiplicities=[3, 3], + knots=[0.0, 1.0], + knot_spec=KnotType.UNSPECIFIED, + ) + segs = CurvePoly2d.build_edge_segments( + [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)], [SplineEdge(a=(1, 0, 0), b=(1, 1, 0), curve=sp)] + ) + return ada.Plate.from_segments("spline_pl", segs, 0.05) + + +def test_streaming_spline_plate_is_valid_analytic_ifc(tmp_path): + """A B-spline-boundary plate emits an analytic IfcAdvancedBrep body — streamed as a C++-emitted + SPF fragment under a typed IfcPlate wrapper when adacpp's ``ngeom_to_ifc_body_spf`` is present, + else via the normal ifcopenshell writer. Either way: valid IFC (schema + EXPRESS where-rules), + metre units, renders in ifcopenshell's own geometry engine, and round-trips through ada as a + parametric Plate whose OCC solid passes BRepCheck (ada's OCC harness, same as the STEP path).""" + import ifcopenshell + import ifcopenshell.geom as ifc_geom + from ifcopenshell.validate import json_logger, validate + + from ada.cad import active_backend + + dest = tmp_path / "spline_stream.ifc" + (ada.Assembly("A") / (ada.Part("p") / _spline_plate())).to_ifc(dest, streaming=True) + + f = ifcopenshell.open(str(dest)) + lg = json_logger() + validate(f, lg, express_rules=True) # schema + EXPRESS WHERE/global rules + assert lg.statements == [] # valid IFC + assert len(f.by_type("IfcAdvancedBrep")) == 1 # analytic B-rep, not a sampled extrusion + assert len(f.by_type("IfcBSplineSurfaceWithKnots")) == 1 # the exact extruded-spline side face + assert [u.Name for u in f.by_type("IfcSIUnit") if u.UnitType == "LENGTHUNIT"] == ["METRE"] + + # ifcopenshell's own engine renders it (this is what third-party IFC viewers use) + n_shapes = 0 + it = ifc_geom.iterator(ifc_geom.settings(), f) + if it.initialize(): + while True: + n_shapes += 1 + if not it.next(): + break + assert n_shapes == 1 + + # ada OCC harness: read back -> parametric Plate -> OCC solid -> BRepCheck valid + plate = ada.from_ifc(dest).get_by_name("spline_pl") + assert isinstance(plate, ada.Plate) + assert plate.t == pytest.approx(0.05) + assert active_backend().is_valid(plate.solid_occ()) + + +def _arch_plate_curved() -> ada.PlateCurved: + """A synthetic curved shell: quadratic-arch B-spline patch with its natural 4-edge bound + (mirrors tests/core/api/plates/test_plate_curved_thick.py).""" + import ada.geom.curves as cu + import ada.geom.surfaces as su + from ada.geom import Geometry + from ada.geom.curves import KnotType + from ada.geom.direction import Direction + from ada.geom.points import Point + + surf = su.BSplineSurfaceWithKnots( + u_degree=2, + v_degree=1, + control_points_list=[ + [Point(0, 0, 0), Point(0, 1, 0)], + [Point(0.5, 0, 0.3), Point(0.5, 1, 0.3)], + [Point(1, 0, 0), Point(1, 1, 0)], + ], + surface_form=su.BSplineSurfaceForm.UNSPECIFIED, + u_closed=False, + v_closed=False, + self_intersect=False, + u_multiplicities=[3, 3], + v_multiplicities=[2, 2], + u_knots=[0.0, 1.0], + v_knots=[0.0, 1.0], + knot_spec=KnotType.UNSPECIFIED, + ) + + def spline(y: float) -> cu.BSplineCurveWithKnots: + return cu.BSplineCurveWithKnots( + degree=2, + control_points_list=[Point(0, y, 0), Point(0.5, y, 0.3), Point(1, y, 0)], + curve_form=cu.BSplineCurveFormEnum.UNSPECIFIED, + closed_curve=False, + self_intersect=False, + knot_multiplicities=[3, 3], + knots=[0.0, 1.0], + knot_spec=KnotType.UNSPECIFIED, + ) + + p00, p10, p11, p01 = Point(0, 0, 0), Point(1, 0, 0), Point(1, 1, 0), Point(0, 1, 0) + e0 = cu.EdgeCurve(p00, p10, edge_geometry=spline(0.0), same_sense=True) + e1 = cu.EdgeCurve(p10, p11, edge_geometry=cu.Line(p10, Direction(0, 1, 0)), same_sense=True) + e2 = cu.EdgeCurve(p01, p11, edge_geometry=spline(1.0), same_sense=True) + e3 = cu.EdgeCurve(p01, p00, edge_geometry=cu.Line(p01, Direction(0, -1, 0)), same_sense=True) + loop = cu.EdgeLoop( + edge_list=[ + cu.OrientedEdge(p00, p10, edge_element=e0, orientation=True), + cu.OrientedEdge(p10, p11, edge_element=e1, orientation=True), + cu.OrientedEdge(p11, p01, edge_element=e2, orientation=False), + cu.OrientedEdge(p01, p00, edge_element=e3, orientation=True), + ] + ) + face = su.AdvancedFace(bounds=[su.FaceBound(bound=loop, orientation=True)], face_surface=surf, same_sense=True) + return ada.PlateCurved("curved1", Geometry("synthpl", face, None), t=0.025) + + +def _typed_mix_model() -> ada.Assembly: + """Beam + flat plate + spline-boundary plate + thick curved shell — one of each writer path.""" + p = ada.Part("P") + p.add_beam(ada.Beam("bm1", (0, 0, 2), (1, 0, 2), "IPE200")) + p.add_plate(ada.Plate("flat1", [(0, 0), (1, 0), (1, 1), (0, 1)], 20e-3)) + p.add_plate(_spline_plate()) + p.add_plate(_arch_plate_curved()) + return ada.Assembly("A") / p + + +def test_streaming_typed_roundtrip_matches_python_writer(tmp_path): + """The typed round-trip acceptance bar: the streamed file (C++ B-rep body fragments when adacpp + is present) re-imports every object as the SAME ada class the normal ifcopenshell writer yields — + Beam with section+material preserved, parametric Plates for flat and spline-boundary plates, and + the thick curved shell as whatever the Python writer's file gives (generic shape with full + geometry). Nothing may come back as a plain proxy-wrapped downgrade relative to the baseline.""" + import ifcopenshell + from ifcopenshell.validate import json_logger, validate + + streamed, normal = tmp_path / "typed_stream.ifc", tmp_path / "typed_normal.ifc" + _typed_mix_model().to_ifc(streamed, streaming=True) + _typed_mix_model().to_ifc(normal) + + fs = ifcopenshell.open(str(streamed)) + assert len(fs.by_type("IfcBeam")) == 1 + assert len(fs.by_type("IfcPlate")) == 3 + assert not fs.by_type("IfcBuildingElementProxy") # typed products, never proxies + + # zero writer-attributable validation issues (the synthetic model has no rational + # B-spline surfaces, so even the upstream IfcSurfaceWeightsPositive crash is absent) + lg = json_logger() + validate(fs, lg, express_rules=True) + assert lg.statements == [] + + a_s, a_n = ada.from_ifc(streamed), ada.from_ifc(normal) + for name in ("bm1", "flat1", "spline_pl", "curved1"): + obj_s, obj_n = a_s.get_by_name(name), a_n.get_by_name(name) + assert obj_s is not None and obj_n is not None, name + assert type(obj_s) is type(obj_n), f"{name}: streamed {type(obj_s)} != normal {type(obj_n)}" + + bm = a_s.get_by_name("bm1") + assert isinstance(bm, ada.Beam) + assert bm.section.name == "IPE200" + assert bm.material.name == a_n.get_by_name("bm1").material.name + assert isinstance(a_s.get_by_name("flat1"), ada.Plate) + assert isinstance(a_s.get_by_name("spline_pl"), ada.Plate) diff --git a/tests/core/cadit/sat/test_bspline_points_equivalence.py b/tests/core/cadit/sat/test_bspline_points_equivalence.py new file mode 100644 index 000000000..1c6d7cfd6 --- /dev/null +++ b/tests/core/cadit/sat/test_bspline_points_equivalence.py @@ -0,0 +1,99 @@ +"""Equivalence guard for the vectorised ``_bspline_points`` B-spline sampler. + +``_bspline_points`` (ada.cadit.sat.read.plate_edge_curves) is the hot path of the Genie-XML / SAT +plate-edge read (``_de_boor`` was ~4.7s of a single large Genie-XML import). It was a pure-Python +per-point de +Boor loop; the vectorised version must produce byte-for-byte-equivalent samples so plate geometry is +unchanged. This test pins the vectorised output against an independent scalar de Boor reference (a +copy of the original loop) plus a hand-computed quadratic Bézier golden. +""" + +import numpy as np + +from ada.cadit.sat.read.plate_edge_curves import _bspline_points, _de_boor +from ada.geom.curves import BSplineCurveWithKnots, RationalBSplineCurveWithKnots + + +def _Curve(degree, control_points, knots, mults, weights=None): + """A real ``BSplineCurveWithKnots`` (or rational subclass), whose ``sample`` ``_bspline_points`` wraps.""" + common = dict( + degree=degree, + control_points_list=control_points, + curve_form=None, + closed_curve=False, + self_intersect=False, + knot_multiplicities=mults, + knots=knots, + knot_spec=None, + ) + if weights is not None: + return RationalBSplineCurveWithKnots(weights_data=weights, **common) + return BSplineCurveWithKnots(**common) + + +def _scalar_bspline_points(curve, n): + """The original per-point scalar implementation, kept here as the reference.""" + cp = np.asarray([list(p)[:3] for p in curve.control_points_list], dtype=float) + knots = np.repeat(np.asarray(curve.knots, dtype=float), np.asarray(curve.knot_multiplicities, dtype=int)) + deg = int(curve.degree) + w = getattr(curve, "weights_data", None) or getattr(curve, "weights", None) + if w is not None and len(w) == len(cp): + wa = np.asarray(w, dtype=float).reshape(-1, 1) + cp = np.hstack([cp * wa, wa]) + lo, hi = float(knots[deg]), float(knots[len(knots) - deg - 1]) + out = [] + for x in np.linspace(lo, hi, n): + p = _de_boor(float(x), knots, cp, deg) + if p.shape[0] == 4: + p = p[:3] / p[3] + out.append(tuple(float(v) for v in p[:3])) + return out + + +# (name, curve, n) +_CASES = [ + ( + "bezier_deg2", + _Curve(2, [(0, 0, 0), (1, 2, 0), (2, 0, 0)], [0.0, 1.0], [3, 3]), + 7, + ), + ( + "clamped_deg2_4cp", + _Curve(2, [(0, 0, 0), (1, 3, 1), (3, 3, -1), (4, 0, 0.5)], [0.0, 1.0, 2.0], [3, 1, 3]), + 13, + ), + ( + "clamped_deg3_5cp", + _Curve(3, [(0, 0, 0), (1, 2, 0), (2, -1, 1), (3, 2, 0), (4, 0, 0)], [0.0, 0.5, 1.0], [4, 1, 4]), + 17, + ), + ( + "rational_deg2_quarter_circle", + _Curve(2, [(1, 0, 0), (1, 1, 0), (0, 1, 0)], [0.0, 1.0], [3, 3], weights=[1.0, np.sqrt(0.5), 1.0]), + 11, + ), +] + + +def test_vectorised_matches_scalar_reference(): + for name, curve, n in _CASES: + got = _bspline_points(curve, n) + ref = _scalar_bspline_points(curve, n) + assert got is not None, name + assert np.allclose(np.asarray(got), np.asarray(ref), rtol=0, atol=1e-9), name + + +def test_quadratic_bezier_golden(): + # Quadratic Bézier P0=(0,0,0) P1=(1,2,0) P2=(2,0,0). At t: (1-t)^2 P0 + 2t(1-t) P1 + t^2 P2. + curve = _Curve(2, [(0, 0, 0), (1, 2, 0), (2, 0, 0)], [0.0, 1.0], [3, 3]) + pts = _bspline_points(curve, 3) # t = 0, 0.5, 1 + expected = [(0.0, 0.0, 0.0), (1.0, 1.0, 0.0), (2.0, 0.0, 0.0)] + assert np.allclose(np.asarray(pts), np.asarray(expected), atol=1e-12) + + +def test_rational_weights_lie_on_unit_circle(): + # Standard rational quadratic Bézier for a quarter circle: every sample has radius 1. + curve = _Curve(2, [(1, 0, 0), (1, 1, 0), (0, 1, 0)], [0.0, 1.0], [3, 3], weights=[1.0, np.sqrt(0.5), 1.0]) + pts = np.asarray(_bspline_points(curve, 9)) + radii = np.linalg.norm(pts[:, :2], axis=1) + assert np.allclose(radii, 1.0, atol=1e-9) diff --git a/tests/core/cadit/sat/test_curved_plate_not_healed.py b/tests/core/cadit/sat/test_curved_plate_not_healed.py index ed431c8f5..e9fd7f6fc 100644 --- a/tests/core/cadit/sat/test_curved_plate_not_healed.py +++ b/tests/core/cadit/sat/test_curved_plate_not_healed.py @@ -21,7 +21,13 @@ from ada.occ.geom.surfaces import consume_param_rebuild_stats -def test_genie_curved_plates_build_without_healing(example_files): +def test_genie_curved_plates_build_without_healing(example_files, monkeypatch): + # This regression guards the BARE-FACE path: authored p-curves on the modeled surface must not + # be perturbed by the rational ShapeFix heal. The default thickened solid adds ruled side + # ribbons whose (legitimately) healed p-curves are not what this test is about — pin the config + # off so solid_geom() returns the bare face this test was written against. + monkeypatch.setenv("ADA_GEOM_THICKEN_CURVED_SHELLS", "false") + xml = (example_files / "fem_files/sesam/curved_plates.xml").resolve() if not xml.exists(): # fall back to a recursive search when the fixture layout differs hits = glob.glob("**/fem_files/sesam/curved_plates.xml", recursive=True) diff --git a/tests/core/cadit/sat/test_degenerate_first_loop.py b/tests/core/cadit/sat/test_degenerate_first_loop.py new file mode 100644 index 000000000..5ff6ee1cd --- /dev/null +++ b/tests/core/cadit/sat/test_degenerate_first_loop.py @@ -0,0 +1,51 @@ +"""A SAT face whose FIRST loop is a degenerate ``hole`` — a single zero-length, curve-less coedge +marking a surface singularity (both its vertices are the same point) — with the real ``periphery`` +boundary in the *next* loop of the face's loop chain. + +Regression for :func:`ada.cadit.sat.read.advanced_face.get_face_bound`, which used to read only the +face's first loop. When ACIS ordered the degenerate hole first, the wire came back empty and the +whole plate failed to build (``build_advanced_face: wire build failed``), silently dropping valid +hull-skin plates from Genie-XML -> STEP/IFC audits. The fix walks ``loop.next`` and takes the outer +(periphery) loop instead. + +Fixture ``degenerate_first_loop.sat`` is the minimal record closure of face ``FACE00000604`` from a +hull-skin Genie-XML SAT model (28 records). +""" + +import ada.geom.curves as geo_cu +import ada.geom.surfaces as geo_su +from ada.cadit.sat.read.advanced_face import get_face_bound, get_face_surface +from ada.cadit.sat.read.curves import iter_loop_coedges +from ada.cadit.sat.store import SatReaderFactory + +FIXTURE = "sat_files/degenerate_first_loop.sat" + + +def _face(example_files): + saf = SatReaderFactory(str(example_files / FIXTURE)) + saf.load_sat_data_from_file() + faces = [r for r in saf.sat_store.iter() if r.type == "face"] + assert len(faces) == 1, "fixture should carry exactly one face" + return saf, faces[0] + + +def test_first_loop_is_a_degenerate_hole(example_files): + """The bug's precondition: the face's FIRST loop is a hole that yields no usable edges.""" + saf, face = _face(example_files) + first_loop = saf.sat_store.get(face.chunks[7]) + assert first_loop.chunks[16] == "hole" + # iter_loop_coedges correctly steps over the single degenerate coedge -> empty. + assert list(iter_loop_coedges(first_loop)) == [] + + +def test_get_face_bound_follows_the_loop_chain_to_the_periphery(example_files): + """The fix: get_face_bound walks past the degenerate hole to the real periphery boundary.""" + saf, face = _face(example_files) + bounds = get_face_bound(face) + assert len(bounds) == 1 + edge_list = bounds[0].bound.edge_list + # The real rectangular plate boundary — 4 edges — not the empty hole loop. + assert len(edge_list) == 4 + assert all(isinstance(e, geo_cu.OrientedEdge) for e in edge_list) + # The face itself is planar; the periphery bounds it. + assert isinstance(get_face_surface(face), geo_su.Plane) diff --git a/tests/core/cadit/sat/test_plate_curved_edges.py b/tests/core/cadit/sat/test_plate_curved_edges.py index 5944dda42..d3debfcc9 100644 --- a/tests/core/cadit/sat/test_plate_curved_edges.py +++ b/tests/core/cadit/sat/test_plate_curved_edges.py @@ -3,18 +3,62 @@ ``PlateFactory.get_points`` keeps only edge ENDPOINTS, so a plate whose boundary follows a curve (a deck plate meeting a curved hull skin) rendered as a straight chord between its corners while the skin beside it curved. ``Config().sat_plate_curved_edges`` samples the curve into extra outline -points. See dap plan/v3/notes_plate_bspline_edges.md for why this is a stopgap. +points. See the internal design notes.md for why this is a stopgap. -Synthetic fixtures: the real reproducer (OP1_v1007_hullskin.xml, face FACE00004482 — a 0.072 m bulge +Synthetic fixtures: the real reproducer (a hull-skin Genie-XML model, face FACE00004482 — a 0.072 m bulge flattened out of a 1.4 m plate) is a client model and is not committed. """ from __future__ import annotations +from types import SimpleNamespace + import numpy as np import pytest -from ada.cadit.sat.read.plate_edge_curves import _clip_to_endpoints, _de_boor +from ada.cadit.sat.read.plate_edge_curves import ( + _clip_to_endpoints, + _de_boor, + _ellipse_arc_points, +) + + +def _circle(r=1.0, c=(0.0, 0.0, 0.0)): + pos = SimpleNamespace(location=c, axis=(0.0, 0.0, 1.0), ref_direction=(1.0, 0.0, 0.0)) + return SimpleNamespace(position=pos, radius=r) + + +def _ellipse(ra=2.0, rb=1.0, c=(0.0, 0.0, 0.0)): + pos = SimpleNamespace(location=c, axis=(0.0, 0.0, 1.0), ref_direction=(1.0, 0.0, 0.0)) + return SimpleNamespace(position=pos, semi_axis1=ra, semi_axis2=rb) + + +def test_ellipse_arc_is_analytic_short_arc_on_the_circle(): + """The analytic sampler emits exactly ~n points on the edge's SHORT arc — no full ring, no clip.""" + a, b = (1.0, 0.0, 0.0), (0.0, 1.0, 0.0) # a quarter turn apart + inner = _ellipse_arc_points(_circle(r=1.0), a, b, n=24) + assert inner is not None + arr = np.array(inner) + assert len(arr) == 24 # exactly n interior points, endpoints dropped + assert np.allclose(np.linalg.norm(arr[:, :2], axis=1), 1.0, atol=1e-9) # on the r=1 circle + assert (arr[:, 0] >= -1e-9).all() and (arr[:, 1] >= -1e-9).all() # first quadrant = short arc + walk = np.vstack([[a], arr, [b]]) + assert np.linalg.norm(np.diff(walk, axis=0), axis=1).sum() == pytest.approx(np.pi / 2, rel=2e-3) + + +def test_ellipse_arc_respects_reversed_direction(): + a, b = (0.0, 1.0, 0.0), (1.0, 0.0, 0.0) # reversed vs the test above + inner = np.array(_ellipse_arc_points(_circle(r=1.0), a, b, n=24)) + # First interior point must be nearer a than b — the walk runs a -> b. + assert np.linalg.norm(inner[0] - np.array(a)) < np.linalg.norm(inner[0] - np.array(b)) + + +def test_ellipse_arc_points_lie_on_the_ellipse(): + ra, rb = 2.0, 1.0 + a, b = (ra, 0.0, 0.0), (0.0, rb, 0.0) + arr = np.array(_ellipse_arc_points(_ellipse(ra, rb), a, b, n=24)) + # (x/ra)^2 + (y/rb)^2 == 1 for every sample. + assert np.allclose((arr[:, 0] / ra) ** 2 + (arr[:, 1] / rb) ** 2, 1.0, atol=1e-9) def _circle_ring(n=2048, r=1.0): diff --git a/tests/core/cadit/step/read/test_stream_plane_angle_units.py b/tests/core/cadit/step/read/test_stream_plane_angle_units.py new file mode 100644 index 000000000..2d2c11d02 --- /dev/null +++ b/tests/core/cadit/step/read/test_stream_plane_angle_units.py @@ -0,0 +1,147 @@ +"""STEP plane-angle unit handling in the kernel-free streaming reader. + +STEP tags every plane angle (notably ``CONICAL_SURFACE.semi_angle`` and conic +``TRIMMED_CURVE`` parameter trims) in the unit its ``GLOBAL_UNIT_ASSIGNED_CONTEXT`` +declares — commonly degrees. ``ada.geom`` and both kernels want radians, so the reader +must convert. A missed degree->radian conversion turns a shallow cone (semi_angle 1.5 deg) +into a near-degenerate 86 deg flat cone that libtess2 meshes to zero triangles, silently +dropping the face. These tests pin the unit detection and the builder scaling. +""" + +from __future__ import annotations + +import math + +import ada.geom.curves as gc +import ada.geom.surfaces as gs +from ada.cadit.step.read import stream_reader as sr + +_DEG = math.pi / 180.0 + + +class _Resolver: + """Minimal resolver stub carrying an angle scale, like the real ``_Resolver``.""" + + def __init__(self, angle_scale: float = 1.0): + self.angle_scale = angle_scale + + def deref(self, x): + return x + + +def _degree_unit_pool() -> dict[int, sr._Rec]: + """A pool mirroring a real ISO-10303 degree plane-angle unit assignment: + a GEOMETRIC_REPRESENTATION_CONTEXT whose GLOBAL_UNIT_ASSIGNED_CONTEXT lists a + length unit, a CONVERSION_BASED_UNIT('DEGREE') plane-angle unit, and a solid-angle unit.""" + return { + 10: sr._Rec( + sr._COMPLEX, + { + "GEOMETRIC_REPRESENTATION_CONTEXT": [3], + "GLOBAL_UNIT_ASSIGNED_CONTEXT": [[sr._Ref(20), sr._Ref(30), sr._Ref(40)]], + "REPRESENTATION_CONTEXT": ["id", "kind"], + }, + ), + 20: sr._Rec( + sr._COMPLEX, + {"LENGTH_UNIT": [], "NAMED_UNIT": [sr._STAR], "SI_UNIT": [sr._Enum("MILLI"), sr._Enum("METRE")]}, + ), + 30: sr._Rec( + sr._COMPLEX, + {"CONVERSION_BASED_UNIT": ["DEGREE", sr._Ref(31)], "NAMED_UNIT": [sr._Ref(32)], "PLANE_ANGLE_UNIT": []}, + ), + 31: sr._Rec("PLANE_ANGLE_MEASURE_WITH_UNIT", [0.0174532925, sr._Ref(33)]), + 40: sr._Rec(sr._COMPLEX, {"SI_UNIT": [sr._STAR, sr._Enum("STERADIAN")], "SOLID_ANGLE_UNIT": []}), + } + + +def _radian_unit_pool() -> dict[int, sr._Rec]: + return { + 10: sr._Rec( + sr._COMPLEX, + { + "GEOMETRIC_REPRESENTATION_CONTEXT": [3], + "GLOBAL_UNIT_ASSIGNED_CONTEXT": [[sr._Ref(20), sr._Ref(30)]], + "REPRESENTATION_CONTEXT": ["id", "kind"], + }, + ), + 20: sr._Rec( + sr._COMPLEX, + {"LENGTH_UNIT": [], "NAMED_UNIT": [sr._STAR], "SI_UNIT": [sr._Enum("MILLI"), sr._Enum("METRE")]}, + ), + 30: sr._Rec( + sr._COMPLEX, {"PLANE_ANGLE_UNIT": [], "NAMED_UNIT": [sr._STAR], "SI_UNIT": [sr._STAR, sr._Enum("RADIAN")]} + ), + } + + +# --- unit detection ------------------------------------------------------------------------ + + +def test_detect_degree_scale_from_context_scan(): + pool = _degree_unit_pool() + scale = sr._detect_plane_angle_scale(pool.get, all_recs=pool.values()) + assert scale == 0.0174532925 + + +def test_detect_degree_scale_from_representation(): + pool = _degree_unit_pool() + pool[100] = sr._Rec("ADVANCED_BREP_SHAPE_REPRESENTATION", ["name", [sr._Ref(200)], sr._Ref(10)]) + scale = sr._detect_plane_angle_scale(pool.get, rep_ids=[100]) + assert scale == 0.0174532925 + + +def test_detect_radian_scale_is_one(): + pool = _radian_unit_pool() + assert sr._detect_plane_angle_scale(pool.get, all_recs=pool.values()) == 1.0 + + +def test_detect_defaults_to_radian_when_absent(): + assert sr._detect_plane_angle_scale({}.get, all_recs=[]) == 1.0 + + +# --- builder scaling ----------------------------------------------------------------------- + + +def test_conical_surface_semi_angle_scaled_from_degrees(): + surf = sr._b_conical_surface(_Resolver(_DEG), ["", "POS", 5.0, 45.0]) + assert isinstance(surf, gs.ConicalSurface) + assert math.isclose(surf.semi_angle, math.pi / 4.0, rel_tol=1e-9) + + +def test_conical_surface_semi_angle_radian_unchanged(): + surf = sr._b_conical_surface(_Resolver(1.0), ["", "POS", 5.0, 1.2]) + assert math.isclose(surf.semi_angle, 1.2, rel_tol=1e-12) + + +def test_trimmed_conic_parameter_trims_scaled(): + basis = gc.Circle(gs.Axis2Placement3D(location=(0, 0, 0)), radius=2.0) + tc = sr._b_trimmed_curve(_Resolver(_DEG), ["", basis, [0.0], [90.0], sr._Enum("T"), sr._Enum("PARAMETER")]) + assert math.isclose(tc.trim2, math.pi / 2.0, rel_tol=1e-9) + + +def test_trimmed_line_parameter_trims_not_scaled(): + # A LINE trim is a length parameter, not an angle — the angle unit must not touch it. + basis = gc.Line((0, 0, 0), (1, 0, 0)) + tc = sr._b_trimmed_curve(_Resolver(_DEG), ["", basis, [0.0], [5.0], sr._Enum("T"), sr._Enum("PARAMETER")]) + assert tc.trim2 == 5.0 + + +# --- detection -> resolver -> builder wiring (the reader path) ------------------------------ + + +def test_resolver_applies_detected_degree_scale_to_cone(): + """Guards the reader wiring: the detected plane-angle scale reaches ``_Resolver`` and is + applied when a CONICAL_SURFACE is resolved — the same path ``_read_two_pass_dict`` takes.""" + pool = _degree_unit_pool() + pool[50] = sr._Rec("AXIS2_PLACEMENT_3D", ["", sr._Ref(51), sr._Ref(52), sr._Ref(53)]) + pool[51] = sr._Rec("CARTESIAN_POINT", ["", [0.0, 0.0, 0.0]]) + pool[52] = sr._Rec("DIRECTION", ["", [0.0, 0.0, 1.0]]) + pool[53] = sr._Rec("DIRECTION", ["", [1.0, 0.0, 0.0]]) + pool[60] = sr._Rec("CONICAL_SURFACE", ["", sr._Ref(50), 4.85, 30.0]) # 30 degrees + + angle_scale = sr._detect_plane_angle_scale(pool.get, all_recs=pool.values()) + resolver = sr._Resolver(pool, angle_scale=angle_scale) + surf = resolver.resolve(60) + assert isinstance(surf, gs.ConicalSurface) + assert math.isclose(surf.semi_angle, math.radians(30.0), rel_tol=1e-7) diff --git a/tests/core/cadit/step/read/test_stream_units.py b/tests/core/cadit/step/read/test_stream_units.py index bca887b92..e63e69406 100644 --- a/tests/core/cadit/step/read/test_stream_units.py +++ b/tests/core/cadit/step/read/test_stream_units.py @@ -89,6 +89,23 @@ def test_detect_unit_scale_pread_finds_unit_at_end(tmp_path): assert detect_step_length_unit_scale(metre) == 1.0 +def test_detect_unit_scale_pretty_printed_spaced_record(tmp_path): + # Some exporters pretty-print the unit record with a space between the keyword and its + # arg list: ``SI_UNIT ( .MILLI., .METRE. )`` / ``CONVERSION_BASED_UNIT ( 'INCH', ... )``. + # The fast pread regex scan must tolerate that whitespace, else an mm file falls through + # to the ``assuming metres`` default and the whole model is read 1000x too large. + pad = 5 << 20 + mm = _step_with_unit_at_end( + tmp_path, "#9 = ( LENGTH_UNIT ( ) NAMED_UNIT ( * ) SI_UNIT ( .MILLI., .METRE. ) );", pad + ) + assert detect_step_length_unit_scale(mm) == 0.001 + + inch = _step_with_unit_at_end( + tmp_path, "#9 = ( CONVERSION_BASED_UNIT ( 'INCH', #10 ) LENGTH_UNIT ( ) NAMED_UNIT ( #11 ) );", pad + ) + assert detect_step_length_unit_scale(inch) == 0.0254 + + def test_detect_unit_scale_absent_returns_one(tmp_path): p = tmp_path / "no_unit.step" p.write_text("ISO-10303-21;\nDATA;\n#1=CARTESIAN_POINT('p',(0.,0.,0.));\nENDSEC;\n") diff --git a/tests/core/cadit/step/write/test_write_step_stream.py b/tests/core/cadit/step/write/test_write_step_stream.py index f34eb745b..c01299029 100644 --- a/tests/core/cadit/step/write/test_write_step_stream.py +++ b/tests/core/cadit/step/write/test_write_step_stream.py @@ -27,6 +27,56 @@ def _roundtrip_names(path): return {shp.name for shp in store.iter_all_shapes(True)} +def _hierarchy_edges(path): + """Parse the streamed STEP's assembly tree into (parent_name, child_name) edges + by resolving each NEXT_ASSEMBLY_USAGE_OCCURRENCE's parent/child PRODUCT_DEFINITION + back to its PRODUCT name.""" + import re + + txt = open(path).read() + prods = dict(re.findall(r"#(\d+)\s*=\s*PRODUCT\('([^']*)'", txt)) + pd_to_pdf = dict(re.findall(r"#(\d+)\s*=\s*PRODUCT_DEFINITION\('[^']*','[^']*',#(\d+)", txt)) + pdf_to_prod = dict(re.findall(r"#(\d+)\s*=\s*PRODUCT_DEFINITION_FORMATION\('[^']*','[^']*',#(\d+)\)", txt)) + + def name_of_pd(pd): + return prods.get(pdf_to_prod.get(pd_to_pdf.get(pd))) + + edges = set() + for m in re.finditer(r"NEXT_ASSEMBLY_USAGE_OCCURRENCE\('[^']*','[^']*','',#(\d+),#(\d+),", txt): + edges.add((name_of_pd(m.group(1)), name_of_pd(m.group(2)))) + return edges + + +def test_stream_writer_preserves_assembly_hierarchy(tmp_path): + # A nested Assembly/Part tree must round-trip its parent hierarchy through the + # streamed STEP (NEXT_ASSEMBLY_USAGE_OCCURRENCE tree), not land flat under the + # root — each member sits under its real owning part. + sub2 = ada.Part("sub2") / [ + Beam("bm2", (0, 0, 0), (0, 0, 3), Section("s2", from_str="IPE300")), + Plate("pl2", [(0, 0), (1, 0), (1, 1), (0, 1)], 0.02), + ] + sub1 = ada.Part("sub1") / [Beam("bm1", (1, 0, 0), (1, 0, 3), Section("s1", from_str="IPE300")), sub2] + a = ada.Assembly("root") / sub1 + + out = tmp_path / "hier.stp" + stats = a.to_stp(out, writer="stream") + assert stats == {"emitted": 3, "skipped": 0} + + # member products all present + names = _roundtrip_names(out) + assert {"bm1", "bm2", "pl2"}.issubset(names) + + # the exact nested hierarchy is carried in the STEP assembly tree + edges = _hierarchy_edges(out) + assert ("root", "sub1") in edges + assert ("sub1", "sub2") in edges + assert ("sub1", "bm1") in edges + assert ("sub2", "bm2") in edges + assert ("sub2", "pl2") in edges + # nothing lands flat directly under root except the top sub-assembly + assert {c for p, c in edges if p == "root"} == {"sub1"} + + def _model(): tub = Beam("tub", (0, 0, 0), (0, 0, 3), Section("tub", from_str="TUB300x20")) # hollow circle box = Beam("box", (1, 0, 0), (1, 0, 3), Section("box", from_str="BOX400x400x20x20")) @@ -105,9 +155,10 @@ def test_stream_writer_emits_brep_shapes(tmp_path): def test_stream_writer_box_and_cylinder_primitives(tmp_path): - # Box and Cylinder primitives are extrusions (rectangle / circle swept by a - # length) and emit as watertight solids; Cone (tapered) and Sphere (periodic) - # are not yet supported and are skipped. + # All four CSG primitives emit as watertight ANALYTIC solids: Box + Cylinder are + # extrusions (rectangle / circle swept by a length); Cone + Sphere have an exact + # analytic B-rep (a CONICAL_SURFACE + planar cap, a single SPHERICAL_SURFACE face) + # built kernel-free — never tessellated. a = ada.Assembly("m") / ( ada.Part("p") / [ @@ -120,8 +171,53 @@ def test_stream_writer_box_and_cylinder_primitives(tmp_path): out = tmp_path / "prims.stp" stats = a.to_stp(out, writer="stream") - assert stats == {"emitted": 2, "skipped": 2} # box + cylinder; cone + sphere skipped + assert stats == {"emitted": 4, "skipped": 0} # all four emit analytically n_solids, n_invalid = _roundtrip_solids(out) - assert n_solids == 2 + assert n_solids == 4 assert n_invalid == 0 + + txt = out.read_text() + # analytic surfaces, not a facet soup: a whole sphere is ONE spherical face + # bounded by a single pole vertex-loop; the cone carries a conical surface. + assert txt.count("SPHERICAL_SURFACE(") == 1 + assert txt.count("VERTEX_LOOP(") == 1 + assert txt.count("CONICAL_SURFACE(") == 1 + # the sphere never degrades to thousands of planar triangle faces + assert txt.count("ADVANCED_FACE(") < 40 + + +def test_stream_writer_bspline_plate_is_an_analytic_valid_solid(tmp_path): + """A plate whose boundary follows a B-spline emits a real SURFACE_OF_LINEAR_EXTRUSION side face + (an analytic swept B-spline, NOT sampled into a fan of planar facets), and OCC reads it back as a + single valid solid. This is the guard for the ap242 analytic B-spline edge path.""" + from ada.api.curves import CurvePoly2d, SplineEdge + from ada.geom.curves import BSplineCurveFormEnum, BSplineCurveWithKnots, KnotType + + spline = BSplineCurveWithKnots( + degree=2, + control_points_list=[(1, 0, 0), (1.3, 0.5, 0), (1, 1, 0)], # bulges out in +x + curve_form=BSplineCurveFormEnum.UNSPECIFIED, + closed_curve=False, + self_intersect=False, + knot_multiplicities=[3, 3], + knots=[0.0, 1.0], + knot_spec=KnotType.UNSPECIFIED, + ) + segs = CurvePoly2d.build_edge_segments( + [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)], [SplineEdge(a=(1, 0, 0), b=(1, 1, 0), curve=spline)] + ) + pl = Plate.from_segments("bspline_pl", segs, 0.05) + + out = tmp_path / "spline.stp" + stats = (ada.Assembly("root") / (ada.Part("p") / pl)).to_stp(out, writer="stream") + assert stats == {"emitted": 1, "skipped": 0} + + n_solids, n_invalid = _roundtrip_solids(out) + assert (n_solids, n_invalid) == (1, 0) + + txt = out.read_text() + assert txt.count("SURFACE_OF_LINEAR_EXTRUSION(") == 1 # one analytic swept side face for the spline + assert "B_SPLINE_CURVE_WITH_KNOTS(" in txt + # the spline never explodes into a sampled fan of planar side faces (3 lines + 1 spline + 2 caps) + assert txt.count("ADVANCED_FACE(") == 6 diff --git a/tests/core/cadit/test_visual_parity.py b/tests/core/cadit/test_visual_parity.py index 18e825988..ea8474f34 100644 --- a/tests/core/cadit/test_visual_parity.py +++ b/tests/core/cadit/test_visual_parity.py @@ -5,6 +5,8 @@ purely in-process (no audit stack) on a known-good 4-object assembly. """ +from pathlib import Path + import pytest import trimesh @@ -90,17 +92,24 @@ def test_visualized_element_count_excludes_placeholder(): assert visualized_element_count(scene) == 1 -def test_parity_fem_source_rebuilds_objects(fem_files): - """A FEM source must rebuild Beam/Plate concept objects (as the converter - does) before the round-trip; the writers emit concepts, not the raw mesh. - Previously parity exported an objectless assembly and read every format back - empty (a false "all geometry dropped"). After the fix the source and every - format agree.""" - r = visual_parity.parity_for_source_file(fem_files / "sesam/beamMassT1.FEM", ("ifc", "xml", "step")) - assert r.expected > 0 - assert r.counts["ifc"] == r.counts["xml"] == r.counts["step"] == r.expected +def test_parity_for_source_file_fem_offline_geometry(tmp_path): + """The offline FEM parity fallback derives the PRODUCTION outputs (analytic + cylinder for step/ifc/xml, to_gltf for glb) and compares the geometry invariant + — NOT the retired merge_strategy=None + entity-count design. A clean plate-only + FEM source round-trips consistently: every format spans the same bounding box. + + (The counts are now per-format ``{"area","bbox","tris"}`` geometry measures, so + the old ``counts[ifc] == counts[step]`` entity-count assertion is gone — the + formats use different dimensional representations, e.g. STEP emits a plate as a + mid-surface while Genie-XML emits it as a thin solid, so their areas legitimately + differ while the geometry is the same.)""" + inp = _fem_plate_inp(tmp_path) + r = visual_parity.parity_for_source_file(inp) assert r.errors == {} assert r.consistent is True + # bbox agrees across the produced formats (the representation-independent gate) + bboxes = [m["bbox"] for m in r.counts.values()] + assert max(bboxes) - min(bboxes) < 0.02 * max(bboxes) def test_parity_skips_xml_for_generic_solid_source(): @@ -192,3 +201,176 @@ def test_native_step_parity_single_parse_matches(tmp_path): assert r.counts["source"] == r.counts["ifc"] == r.counts["step"] == r.expected assert "xml" in r.skipped assert r.consistent is True + + +def _fem_plate_inp(tmp_path): + """Write a small plate-only FEM to Abaqus .inp and return its path. Plate-only + (no line beams) so every produced format — including the STEP stream writer — + carries the geometry, giving a clean cross-format consistency baseline.""" + import glob + + pl = Plate("P", [(0, 0), (2, 0), (2, 1), (0, 1)], 0.02) + part = ada.Part("pp") + part.fem = pl.to_fem_obj(0.5, "shell") + (ada.Assembly("A") / part).to_fem("m", "abaqus", scratch_dir=str(tmp_path), overwrite=True) + return glob.glob(str(tmp_path / "**" / "*.inp"), recursive=True)[0] + + +# ── Geometry-invariant parity over produced files ─────────────────────────── +# +# These pin the verdict logic deterministically by feeding crafted geometry +# measures (no heavy tessellation), then a couple of real-file smoke tests. + + +def _gm(area, bbox, tris=100, empty=False): + from ada.cadit.visual_parity import _GeomMeasure + + return _GeomMeasure(area=area, bbox=bbox, tris=tris, empty=empty) + + +def _run_verdict(monkeypatch, measures): + """Run parity_from_produced_files with _measure_produced_file stubbed to return + ``measures`` (a {fmt: _GeomMeasure} map).""" + from ada.cadit import visual_parity as vp + + monkeypatch.setattr(vp, "_measure_produced_file", lambda fmt, path: measures[fmt]) + produced = {fmt: Path(f"produced.{fmt}") for fmt in measures} + return vp.parity_from_produced_files("m.fem", produced) + + +def test_produced_verdict_consistent_across_representations(monkeypatch): + """The KEY design property: absolute AREA legitimately differs across formats + that use different dimensional representations (STEP mid-surface = 2, Genie-XML + thin solid = 4.12, glb mesh = 2) yet the model is identical. The bbox is the + same for all, and the area floor sits well below the ~0.5 solid-vs-surface + ratio — so this must NOT flag a mismatch.""" + r = _run_verdict( + monkeypatch, + {"step": _gm(2.0, 5.0, 2), "ifc": _gm(2.0, 5.0, 2), "xml": _gm(4.12, 5.0, 12), "glb": _gm(2.0, 5.0, 16)}, + ) + assert r.consistent is True + assert r.mismatches == {} and r.errors == {} + # counts carry the per-format geometry measure, not an entity count + assert r.counts["xml"] == {"area": 4.12, "bbox": 5.0, "tris": 12} + + +def test_produced_verdict_flags_shrunk_bbox(monkeypatch): + """A format that dropped a solid/region shrinks its bounding box — the strict, + representation-independent gate flags it (here the shipped glb, which the viewer + loads: a drop there is the worst case).""" + r = _run_verdict( + monkeypatch, + {"ifc": _gm(10.0, 10.0), "step": _gm(10.0, 10.0), "glb": _gm(6.0, 8.0)}, # glb bbox -20% + ) + assert r.consistent is False + assert "glb" in r.mismatches and "bbox" in r.mismatches["glb"] + assert "ifc" not in r.mismatches and "step" not in r.mismatches + + +def test_produced_verdict_area_floor_catches_gross_drop(monkeypatch): + """A CAD format that keeps its bbox but grossly loses surface area (near-empty) + is caught by the coarse area floor — a backstop below the bbox gate.""" + r = _run_verdict( + monkeypatch, + {"ifc": _gm(10.0, 10.0), "step": _gm(0.5, 10.0), "glb": _gm(8.0, 10.0)}, # step area 0.5 << 10 + ) + assert r.consistent is False + assert "step" in r.mismatches and "area" in r.mismatches["step"] + + +def test_produced_verdict_area_floor_tolerates_inflated_representation(monkeypatch): + """A single format legitimately carrying several× the area (Genie-XML emits + thick solids that tessellate to ~5× the mid-surface area STEP/IFC carry) must + NOT trip the gross-loss floor. The floor references the MEDIAN concept area, so + the inflated xml can't penalise the correct mid-surface formats. bbox agrees + across all four (mirrors a real shell-FEM parity cell).""" + r = _run_verdict( + monkeypatch, + { + "glb": _gm(5622.0, 59.33, 82006), + "ifc": _gm(6288.0, 59.39, 87976), + "step": _gm(4671.0, 59.33, 29662), + "xml": _gm(29462.0, 59.63, 352856), # ~5x — thick-solid representation + }, + ) + assert r.consistent is True, r.mismatches + assert r.mismatches == {} + + +def test_produced_verdict_skips_unmeasurable_crash(monkeypatch): + """A format whose measurement HARD-crashes (a native SIGSEGV contained in the + isolated measurement subprocess -> _MeasureUnavailable) is recorded as skipped, + NOT an error: the formats that DO measure still decide the verdict. This is the + class that failed the two largest FEM parity cells (step/ifc re-tessellation + SIGSEGV in OCC).""" + from ada.cadit import visual_parity as vp + + good = {"glb": _gm(100.0, 10.0), "xml": _gm(120.0, 10.05)} + + def fake_measure(fmt, path): + if fmt in ("step", "ifc"): + raise vp._MeasureUnavailable("measurement subprocess died (exit -11 — a native tessellation crash)") + return good[fmt] + + monkeypatch.setattr(vp, "_measure_produced_file", fake_measure) + produced = {f: Path(f"p.{f}") for f in ("glb", "xml", "step", "ifc")} + r = vp.parity_from_produced_files("m.fem", produced) + assert r.consistent is True + assert set(r.skipped) == {"step", "ifc"} and "unmeasurable" in r.skipped["step"] + assert r.mismatches == {} and r.errors == {} + + +def test_produced_verdict_flags_empty(monkeypatch): + """A format that produced no renderable geometry at all (empty scene / IFC that + imported nothing) is flagged.""" + r = _run_verdict(monkeypatch, {"ifc": _gm(10.0, 10.0), "glb": _gm(0.0, 0.0, 0, empty=True)}) + assert r.consistent is False + assert "glb" in r.mismatches and "no renderable geometry" in r.mismatches["glb"] + + +def test_produced_verdict_skips_solid_only_concepts(monkeypatch): + """A solid-only / mesh-only FEM has no shells or beams to reconstruct, so its + step/ifc/xml exports are legitimately EMPTY while the glb still carries the + element mesh. The empty concept formats are SKIPPED (not flagged as a drop), + and the source is consistent — this is the class that erroneously failed every + solid FEM parity cell.""" + r = _run_verdict( + monkeypatch, + { + "step": _gm(0.0, 0.0, 0, empty=True), + "ifc": _gm(0.0, 0.0, 0, empty=True), + "xml": _gm(0.0, 0.0, 0, empty=True), + "glb": _gm(1.2, 0.3, 6000), + }, + ) + assert r.consistent is True + assert r.mismatches == {} and r.errors == {} + assert set(r.skipped) == {"step", "ifc", "xml"} + + +def test_produced_verdict_records_missing_format_without_rederiving(monkeypatch): + """A format whose conversion failed/was skipped comes in as None: it is RECORDED + in ``skipped`` (never re-derived) and excluded from the verdict — the present + formats still decide consistency.""" + from ada.cadit import visual_parity as vp + + measures = {"ifc": _gm(10.0, 10.0), "glb": _gm(10.0, 10.0)} + monkeypatch.setattr(vp, "_measure_produced_file", lambda fmt, path: measures[fmt]) + produced = {"ifc": Path("produced.ifc"), "glb": Path("produced.glb"), "step": None} + r = vp.parity_from_produced_files("m.fem", produced) + assert r.consistent is True + assert "step" in r.skipped + assert "step" not in r.counts and "step" not in r.mismatches + + +def test_measure_produced_file_reads_glb_directly(tmp_path): + """Smoke test the mesh-direct measure path on a real GLB: a 2×1×3 box scene + reports the expected bbox diagonal and a positive area/triangle count.""" + from ada.cadit.visual_parity import _measure_produced_file + + p = tmp_path / "box.glb" + trimesh.Scene(trimesh.creation.box((2, 1, 3))).export(str(p)) + m = _measure_produced_file("glb", p) + assert m.tris > 0 and m.area > 0 + assert abs(m.bbox - (2**2 + 1**2 + 3**2) ** 0.5) < 1e-6 + assert m.empty is False diff --git a/tests/core/cadit/test_visual_parity_regressions.py b/tests/core/cadit/test_visual_parity_regressions.py index 67ee0c207..340e1021d 100644 --- a/tests/core/cadit/test_visual_parity_regressions.py +++ b/tests/core/cadit/test_visual_parity_regressions.py @@ -66,3 +66,100 @@ def test_empty_source_counts_zero_everywhere(example_files, tmp_path): out = tmp_path / "wire.step" a.to_stp(out) assert assembly_element_count(ada.from_step(out, reader="auto")) == 1 + + +def test_fem_step_cylinder_strategy_keeps_beams(tmp_path): + """Audit regression (a beam+plate FEM): the analytic ``cylinder`` merge strategy fused only + SHELL elements — LINE (beam) elements were silently dropped from the FEM->STEP export (neither + emitted nor counted skipped), so the STEP bbox lost the beam's extent and parity flagged it.""" + import ada + + a = ada.Assembly("A") / ( + ada.Part("p") + / [ + ada.Beam("bm", (0, 0, 0), (0, 0, 2), "IPE200"), + ada.Plate("pl", [(0, 0), (1, 0), (1, 1), (0, 1)], 0.01), + ] + ) + part = a.get_part("p") + part.fem = part.to_fem_obj(0.5) + + fem_dir = tmp_path / "fem" + a.to_fem("beam_plate", "abaqus", scratch_dir=fem_dir) + b = ada.from_fem(fem_dir / "beam_plate" / "beam_plate.inp") + + out = tmp_path / "out.step" + stats = b.to_stp(out, writer="stream", fuse_fem=True, merge_strategy="cylinder") + # the analytic shell (plate faces) AND the fused beam must both emit + assert stats["emitted"] >= 2, stats + txt = out.read_text() + assert "MANIFOLD_SOLID_BREP" in txt # the beam's extruded solid — absent before the fix + + +def test_meshopt_packed_glb_measures_like_uncompressed(tmp_path): + """Audit regression: production GLBs ship EXT_meshopt_compression, which trimesh cannot decode + (IndexError in _read_buffers) — 4 of 5 parity failures in one sweep were glb=ERR on otherwise + CONSISTENT geometry. The measurer must unpack meshopt before measuring.""" + pytest.importorskip("adacpp") # the meshopt codecs live in adacpp + import ada + from ada.cadit.visual_parity import _measure_produced_file + from ada.visit.gltf.meshopt import meshopt_compress_glb + + plain = tmp_path / "plain.glb" + (ada.Assembly("A") / (ada.Part("p") / ada.Plate("pl", [(0, 0), (1, 0), (1, 1), (0, 1)], 0.01))).to_gltf(plain) + + packed = tmp_path / "packed.glb" + assert meshopt_compress_glb(plain, packed, min_bytes=0) == packed # actually packed, no fallback + + m_plain = _measure_produced_file("glb", plain) + m_packed = _measure_produced_file("glb", packed) + assert m_packed.tris == m_plain.tris + assert m_packed.area == pytest.approx(m_plain.area, rel=1e-6) + assert m_packed.bbox == pytest.approx(m_plain.bbox, rel=1e-6) + + +def test_gxml_produced_files_count_parity(tmp_path): + """Genie-XML parity runs over the ALREADY-PRODUCED blobs with cheap per-format counters + (no re-derive, no tessellation — the old load+export+reload path tripled once curved + shells thicken by default). The count invariant must still catch a leg silently dropping + an object.""" + import ada + from ada.cadit.visual_parity import parity_gxml_from_produced_files + + a = ada.Assembly("A") / ( + ada.Part("p") + / [ + ada.Plate("pl1", [(0, 0), (1, 0), (1, 1), (0, 1)], 0.01), + ada.Plate("pl2", [(2, 0), (3, 0), (3, 1), (2, 1)], 0.01), + ada.Beam("bm", (0, 0, 1), (3, 0, 1), "IPE200"), + ] + ) + xml = tmp_path / "m.xml" + ifc = tmp_path / "m.ifc" + stp = tmp_path / "m.step" + a.to_genie_xml(xml) + a.to_ifc(ifc) + a.to_stp(stp, writer="stream") + + produced = {"xml": xml, "ifc": ifc, "step": stp, "glb": None} + res = parity_gxml_from_produced_files("cad/gxml/m.xml", produced) + assert res.consistent, res.summary() + assert res.expected == 3 + assert res.counts["ifc"] == res.counts["xml"] == 3 + # the step counter needs the native adacpp stream index; absent (pyocc-only env) it is + # recorded as skipped, never guessed + assert res.counts.get("step", 3) == 3 + + # a leg that silently drops an object must flag + doctored = tmp_path / "dropped.ifc" + lines = ifc.read_text().splitlines() + kept, removed = [], 0 + for ln in lines: + if not removed and "=IFCPLATE(" in ln.upper(): + removed += 1 + continue + kept.append(ln) + doctored.write_text("\n".join(kept)) + res2 = parity_gxml_from_produced_files("cad/gxml/m.xml", {**produced, "ifc": doctored}) + assert not res2.consistent + assert "ifc" in res2.mismatches and res2.mismatches["ifc"] == 2 diff --git a/tests/core/fem/test_mesh_faces.py b/tests/core/fem/test_mesh_faces.py index e8d008cc3..db718e79a 100644 --- a/tests/core/fem/test_mesh_faces.py +++ b/tests/core/fem/test_mesh_faces.py @@ -175,8 +175,11 @@ def test_analytic_cylinder_ifc_streaming_roundtrips(tmp_path, monkeypatch): b = ada.from_ifc(out) scene = b.to_trimesh_scene() tris = sum(g.faces.shape[0] for g in scene.geometry.values() if hasattr(g, "faces")) - # the re-imported cylinder tessellates to a real curved mesh, not a degenerate one - assert tris > 100 + # the re-imported cylinder tessellates to a real curved mesh, not a degenerate + # sliver: one trimmed CYLINDRICAL_SURFACE -> ~52 tris (OCC) / ~144 (adacpp); a + # dropped/degenerate face would be a handful. (Was >100 only because a + # since-fixed get_all_parts_in_assembly duplicate emitted two overlapping tubes.) + assert tris > 40 def _part_with_fem(fem_files): diff --git a/tests/core/fem/test_sesam_set_renumber.py b/tests/core/fem/test_sesam_set_renumber.py index b52cf1105..5b800af08 100644 --- a/tests/core/fem/test_sesam_set_renumber.py +++ b/tests/core/fem/test_sesam_set_renumber.py @@ -4,7 +4,7 @@ resolves to a now-missing id. This reproduced as ``ValueError: The elem id "787" is not found`` when exporting -JacketHybrid.FEM (its elset members referenced internal ids that had been renumbered to +a jacket Sesam FEM (its elset members referenced internal ids that had been renumbered to external ones). The object path stays correct for free because its sets hold Node/Elem objects whose ``.id`` is renumbered in place; only the array path needed the explicit remap. diff --git a/tests/core/geom/test_fem_shell_fast_path.py b/tests/core/geom/test_fem_shell_fast_path.py index 086681574..fe6ecd552 100644 --- a/tests/core/geom/test_fem_shell_fast_path.py +++ b/tests/core/geom/test_fem_shell_fast_path.py @@ -141,3 +141,65 @@ def test_fem_conversion_uses_fast_path(): for cpl in plates: g = cpl.solid_geom().geometry assert type(g).__name__ == "ExtrudedAreaSolid" + + +def test_from_fem_shells_batch_matches_scalar(): + """The vectorized batch builder is bitwise-identical to the per-element scalar + from_fem_shell — the streaming merge_strategy=None plate stream relies on this.""" + import numpy as np + + cases = list(CASES.values()) + from collections import defaultdict + + by_k = defaultdict(list) + for i, c in enumerate(cases): + by_k[len(c)].append(i) + batch = [None] * len(cases) + for _k, idxs in by_k.items(): + arr = np.stack([np.array(cases[i], dtype=float) for i in idxs]) + for i, poly in zip(idxs, CurvePoly2d.from_fem_shells_batch(arr)): + batch[i] = poly + + for i, c in enumerate(cases): + scalar = CurvePoly2d.from_fem_shell(c) + b = batch[i] + assert b is not None + assert np.array_equal(np.asarray(b.points2d), np.asarray(scalar.points2d)) + assert np.array_equal(np.asarray(b.points3d), np.asarray(scalar.points3d)) + assert np.array_equal(np.asarray(b.seg_global_points), np.asarray(scalar.seg_global_points)) + assert b.seg_index == scalar.seg_index + + +def test_streaming_merge_none_matches_scalar_convert(): + """iter_objects_from_fem(merge_strategy=None) now batches the CurvePoly2d build; + its plate outlines must stay identical to the per-element scalar + convert_shell_elem_to_plates path (mixed tri + quad + warped-quad-split mesh).""" + import numpy as np + + import ada + from ada import Plate + from ada.fem.formats.utils import convert_shell_elem_to_plates + + # a meshed plate -> exercises coplanar quads + material consolidation through the + # wired streaming path (the batch/scalar geometry equivalence for tri/quad/warped + # arities is covered directly by test_from_fem_shells_batch_matches_scalar). + pl = Plate("pl", [(0, 0), (2, 0), (2, 1), (0, 1)], 0.02) + part = ada.Part("pp") + part.fem = pl.to_fem_obj(0.4, "shell") + + streamed = list(part.iter_objects_from_fem(beams=False, plates=True, merge_strategy=None)) + + scalar_part = ada.Part("pp") + scalar_part.fem = part.fem # same mesh + mat_dict: dict = {} + scalar = [] + for elem in scalar_part.fem.elements.shell: + scalar.extend(convert_shell_elem_to_plates(elem, scalar_part, mat_dict)) + + assert len(streamed) == len(scalar) > 1 + for a, b in zip(streamed, scalar): + assert a.name == b.name + assert np.array_equal(np.asarray(a.poly.points2d), np.asarray(b.poly.points2d)) + assert np.array_equal(np.asarray(a.poly.points3d), np.asarray(b.poly.points3d)) + assert a.poly.seg_index == b.poly.seg_index + assert a.t == b.t diff --git a/tests/profiling/test_cad_backend_bench.py b/tests/profiling/test_cad_backend_bench.py index 363b52e34..3c23b0062 100644 --- a/tests/profiling/test_cad_backend_bench.py +++ b/tests/profiling/test_cad_backend_bench.py @@ -1,7 +1,7 @@ """CAD-backend performance baseline. Purpose: a regression gate for the OCC -> backend-abstraction migration -(see dap ``plan/v3/notes_occ_backend_abstraction.md``). Phase 1 reroutes +(see dap ``the internal design notes``). Phase 1 reroutes the tessellation pipeline through ``ada.cad.active_backend()``; this file captures the *pre-Phase-1* cost of the hot paths so any per-shape overhead the seam introduces shows up as a measurable delta rather than a guess.